Conceitos
Idempotência
Como repetir um pedido que cria algo sem o criar duas vezes: a chave de idempotência.
Entre dois sistemas, um pedido pode falhar a meio: o servidor processou-o mas a resposta perdeu-se, ou o seu programa caiu antes de a ler. Repetir às cegas emitiria dois documentos. A chave de idempotência resolve isto: repetir o pedido com a mesma chave devolve o resultado do primeiro e nunca o efeito duas vezes.
#Como se usa
- 1Gere uma chave por intenção
Um valor único (um GUID) para cada coisa que quer criar: «esta encomenda», «este cliente». Não reutilize a chave para outra coisa.
- 2Guarde-a antes de enviar
Grave a chave e o corpo do pedido na sua base de dados antes do primeiro envio. É assim que, se o programa cair, sabe o que repetir e com que chave.
- 3Envie-a no cabeçalho
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7, com 1 a 100 caracteres (letras, dígitos,-e_). - 4Repita igual se não tiver resposta
Mesmo corpo, mesma chave. Se o primeiro tiver chegado, recebe o mesmo resultado; se não, é executado agora.
#O que a API devolve
| Situação | Resposta |
|---|---|
| Chave nova | O pedido executa e a resposta é a normal (201…). |
| Mesma chave, mesmo corpo | O resultado do primeiro (o mesmo id), com o cabeçalho Idempotency-Replayed: true. Nada se cria outra vez. |
| Mesma chave, corpo diferente | 409 IDEMPOTENCIA_CONFLITO: a chave pertence a outro pedido. Use uma chave nova. |
O primeiro pedido falhou (4xx/5xx) | A chave não fica consumida: repetir com a mesma chave executa de novo. |
| Dois pedidos iguais ao mesmo tempo | Um executa e o outro devolve o mesmo resultado: nunca dois efeitos. |
| Chave com formato inválido | 400 IDEMPOTENCIA_CHAVE_INVALIDA. |
A resposta a um pedido repetido
curl -X POST 'https://<endereço-da-API>/api/clientes' \
-H 'Idempotency-Key: b2a756b8-b776-4fd1-9af3-ba4e60f115f3' \
-H 'X-Api-Key: assw_v1_<chave>' \
-H 'Content-Type: application/json' \
-d '{
"nome": "Mercearia Cazenga Nova",
"endereco": "Rua 5, Cazenga, Luanda",
"numeroContribuinte": "5520879633",
"isConsumidorFinal": false,
"email": "compras@mercearia-cazenga.example",
"telefone": null,
"telemovel": "926000111",
"fax": null,
"website": null,
"municipioId": null,
"taxaRetencaoId": null,
"enviarAlertas": false,
"enviarDocumentos": true,
"observacao": null,
"especial": false,
"pvpPreferido": "Pvp1",
"condicaoPagamentoId": null,
"desconto": null,
"limiteCredito": null
}'
const resposta = await fetch('https://<endereço-da-API>/api/clientes', {
method: 'POST',
headers: {
"Idempotency-Key": "b2a756b8-b776-4fd1-9af3-ba4e60f115f3",
"X-Api-Key": "assw_v1_<chave>",
"Content-Type": "application/json"
},
body: JSON.stringify({
"nome": "Mercearia Cazenga Nova",
"endereco": "Rua 5, Cazenga, Luanda",
"numeroContribuinte": "5520879633",
"isConsumidorFinal": false,
"email": "compras@mercearia-cazenga.example",
"telefone": null,
"telemovel": "926000111",
"fax": null,
"website": null,
"municipioId": null,
"taxaRetencaoId": null,
"enviarAlertas": false,
"enviarDocumentos": true,
"observacao": null,
"especial": false,
"pvpPreferido": "Pvp1",
"condicaoPagamentoId": null,
"desconto": null,
"limiteCredito": null
}),
})
const dados = await resposta.json()
import requests
resposta = requests.post(
'https://<endereço-da-API>/api/clientes',
headers={
'Idempotency-Key': 'b2a756b8-b776-4fd1-9af3-ba4e60f115f3',
'X-Api-Key': 'assw_v1_<chave>',
'Content-Type': 'application/json'
},
json={
'nome': 'Mercearia Cazenga Nova',
'endereco': 'Rua 5, Cazenga, Luanda',
'numeroContribuinte': '5520879633',
'isConsumidorFinal': False,
'email': 'compras@mercearia-cazenga.example',
'telefone': None,
'telemovel': '926000111',
'fax': None,
'website': None,
'municipioId': None,
'taxaRetencaoId': None,
'enviarAlertas': False,
'enviarDocumentos': True,
'observacao': None,
'especial': False,
'pvpPreferido': 'Pvp1',
'condicaoPagamentoId': None,
'desconto': None,
'limiteCredito': None
},
)
dados = resposta.json()
using var cliente = new HttpClient { BaseAddress = new Uri("https://<endereço-da-API>") };
using var pedido = new HttpRequestMessage(HttpMethod.Post, "/api/clientes");
pedido.Headers.Add("Idempotency-Key", "b2a756b8-b776-4fd1-9af3-ba4e60f115f3");
pedido.Headers.Add("X-Api-Key", "assw_v1_<chave>");
pedido.Content = new StringContent("""
{
"nome": "Mercearia Cazenga Nova",
"endereco": "Rua 5, Cazenga, Luanda",
"numeroContribuinte": "5520879633",
"isConsumidorFinal": false,
"email": "compras@mercearia-cazenga.example",
"telefone": null,
"telemovel": "926000111",
"fax": null,
"website": null,
"municipioId": null,
"taxaRetencaoId": null,
"enviarAlertas": false,
"enviarDocumentos": true,
"observacao": null,
"especial": false,
"pvpPreferido": "Pvp1",
"condicaoPagamentoId": null,
"desconto": null,
"limiteCredito": null
}
""", Encoding.UTF8, "application/json");
using var resposta = await cliente.SendAsync(pedido);
var texto = await resposta.Content.ReadAsStringAsync();
201 Created Resposta
content-type: application/json; charset=utf-8
x-mucanda-api-version: 1
idempotency-replayed: true
location: /api/clientes/01a12f69-98f8-7688-85b9-8d829c101c8b
{
"id": "01a12f69-98f8-7688-85b9-8d829c101c8b"
}
A mesma chave com um corpo diferente
curl -X POST 'https://<endereço-da-API>/api/clientes' \
-H 'Idempotency-Key: b2a756b8-b776-4fd1-9af3-ba4e60f115f3' \
-H 'X-Api-Key: assw_v1_<chave>' \
-H 'Content-Type: application/json' \
-d '{
"nome": "Outro nome",
"endereco": "Rua 5, Cazenga, Luanda",
"numeroContribuinte": "5520879633",
"isConsumidorFinal": false,
"email": "compras@mercearia-cazenga.example",
"telefone": null,
"telemovel": "926000111",
"fax": null,
"website": null,
"municipioId": null,
"taxaRetencaoId": null,
"enviarAlertas": false,
"enviarDocumentos": true,
"observacao": null,
"especial": false,
"pvpPreferido": "Pvp1",
"condicaoPagamentoId": null,
"desconto": null,
"limiteCredito": null
}'
const resposta = await fetch('https://<endereço-da-API>/api/clientes', {
method: 'POST',
headers: {
"Idempotency-Key": "b2a756b8-b776-4fd1-9af3-ba4e60f115f3",
"X-Api-Key": "assw_v1_<chave>",
"Content-Type": "application/json"
},
body: JSON.stringify({
"nome": "Outro nome",
"endereco": "Rua 5, Cazenga, Luanda",
"numeroContribuinte": "5520879633",
"isConsumidorFinal": false,
"email": "compras@mercearia-cazenga.example",
"telefone": null,
"telemovel": "926000111",
"fax": null,
"website": null,
"municipioId": null,
"taxaRetencaoId": null,
"enviarAlertas": false,
"enviarDocumentos": true,
"observacao": null,
"especial": false,
"pvpPreferido": "Pvp1",
"condicaoPagamentoId": null,
"desconto": null,
"limiteCredito": null
}),
})
const dados = await resposta.json()
import requests
resposta = requests.post(
'https://<endereço-da-API>/api/clientes',
headers={
'Idempotency-Key': 'b2a756b8-b776-4fd1-9af3-ba4e60f115f3',
'X-Api-Key': 'assw_v1_<chave>',
'Content-Type': 'application/json'
},
json={
'nome': 'Outro nome',
'endereco': 'Rua 5, Cazenga, Luanda',
'numeroContribuinte': '5520879633',
'isConsumidorFinal': False,
'email': 'compras@mercearia-cazenga.example',
'telefone': None,
'telemovel': '926000111',
'fax': None,
'website': None,
'municipioId': None,
'taxaRetencaoId': None,
'enviarAlertas': False,
'enviarDocumentos': True,
'observacao': None,
'especial': False,
'pvpPreferido': 'Pvp1',
'condicaoPagamentoId': None,
'desconto': None,
'limiteCredito': None
},
)
dados = resposta.json()
using var cliente = new HttpClient { BaseAddress = new Uri("https://<endereço-da-API>") };
using var pedido = new HttpRequestMessage(HttpMethod.Post, "/api/clientes");
pedido.Headers.Add("Idempotency-Key", "b2a756b8-b776-4fd1-9af3-ba4e60f115f3");
pedido.Headers.Add("X-Api-Key", "assw_v1_<chave>");
pedido.Content = new StringContent("""
{
"nome": "Outro nome",
"endereco": "Rua 5, Cazenga, Luanda",
"numeroContribuinte": "5520879633",
"isConsumidorFinal": false,
"email": "compras@mercearia-cazenga.example",
"telefone": null,
"telemovel": "926000111",
"fax": null,
"website": null,
"municipioId": null,
"taxaRetencaoId": null,
"enviarAlertas": false,
"enviarDocumentos": true,
"observacao": null,
"especial": false,
"pvpPreferido": "Pvp1",
"condicaoPagamentoId": null,
"desconto": null,
"limiteCredito": null
}
""", Encoding.UTF8, "application/json");
using var resposta = await cliente.SendAsync(pedido);
var texto = await resposta.Content.ReadAsStringAsync();
409 Conflict Resposta
content-type: application/problem+json
x-mucanda-api-version: 1
{
"title": "Conflito",
"status": 409,
"detail": "Esta chave de idempotência já foi usada com um pedido diferente. Use uma chave nova para cada pedido novo.",
"codigo": "IDEMPOTENCIA_CONFLITO",
"traceId": "00-11ee5d18dce4b8aff91edd67d70cbeb9-57c4c9774844f370-01"
}
#Onde se aplica
| Operação | Idempotência |
|---|---|
| Emitir documentos, notas de crédito e recibos | Sim. Envie sempre a chave: uma emissão é fiscal. |
| Criar clientes, fornecedores e artigos | Sim. |
| Registar compras e saídas manuais de stock | Sim. |
| Lançar despesas e registar pagamentos | Sim. |
PUT, DELETE, activar e desactivar | Naturalmente idempotentes: aplicar duas vezes o mesmo estado dá o mesmo resultado. A chave é ignorada. |
| Enviar um documento por e-mail | Não: repetir envia outro e-mail. |
#Regras a conhecer
- Âmbito: a chave vale por empresa. Duas integrações da mesma empresa não devem usar a mesma chave (use GUIDs).
- Retenção: o servidor lembra-se de uma chave durante 72 horas. Depois disso a chave pode ser reutilizada (e criará um efeito novo).
- O corpo conta: a comparação é do corpo inteiro. Se o corpo tem um campo que muda sozinho entre tentativas (a data de hoje, por exemplo), as tentativas conflituam. Construa o corpo uma vez, grave-o com a chave e reenvie exactamente esse.
- Sem cabeçalho: o pedido executa normalmente, sem protecção. É sua a decisão de repetir.