Idempotência com Idempotency-Key
Atualizado em
Nesta página
Redes falham. Se a sua requisição de emissão der timeout, você não sabe se a nota foi aceita. O header Idempotency-Key resolve isso: reenvie com a mesma chave e a Emitra devolve a resposta original, sem criar outra nota.
Onde é obrigatório#
| Rota | Idempotency-Key |
|---|---|
POST /integracao/nfse (emitir) |
Obrigatório (400 se ausente) |
POST /integracao/nfse/{id}/substituir |
Obrigatório |
Demais POST/PATCH |
Não usado |
Como funciona#
POST /api/v1/integracao/nfse HTTP/1.1
Authorization: Bearer emt_test_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: pedido-2026-11-0001A Emitra guarda, por empresa, a chave, um hash do corpo e a resposta, por 24 horas:
| Situação | Resposta |
|---|---|
| Primeira vez | Processa e responde normalmente (202) |
| Mesma chave, mesmo corpo | Devolve a resposta guardada, sem processar de novo |
| Mesma chave, corpo diferente | 422 com code: "IDEMPOTENCY_KEY_REUSED" |
| Mais de 24 h depois | A chave é esquecida e a requisição é tratada como nova |
{
"status": "error",
"message": "Chave de idempotência já usada com outro corpo",
"code": "IDEMPOTENCY_KEY_REUSED",
"details": null
}Como escolher a chave#
Use um identificador do seu lado que represente a operação de negócio: o número do pedido, da fatura ou do contrato + competência. Até 120 caracteres.
| Bom | Ruim |
|---|---|
pedido-2026-11-0001 |
Um UUID novo a cada tentativa (anula a proteção) |
contrato-77:2026-11 |
Um timestamp |
fatura-A1B2C3 |
A mesma chave para notas diferentes |
Reenvio seguro#
# Reenviar o mesmo comando é seguro: a 2ª resposta é a mesma da 1ª.
curl --retry 3 --retry-all-errors https://emitra.com.br/api/v1/integracao/nfse \
-H "Authorization: Bearer $EMITRA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-2026-11-0001" \
-d @nota.jsonimport time, requests
def emitir(api, headers, corpo, chave, tentativas=4):
for i in range(tentativas):
try:
r = requests.post(f"{api}/nfse", json=corpo, timeout=30,
headers={**headers, "Idempotency-Key": chave})
if r.status_code < 500 and r.status_code != 429:
return r
except requests.RequestException:
pass
time.sleep(2 ** i)
raise RuntimeError("Falha ao emitir após várias tentativas")async function emitir(api, headers, corpo, chave, tentativas = 4) {
for (let i = 0; i < tentativas; i++) {
try {
const r = await fetch(`${api}/nfse`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": chave },
body: JSON.stringify(corpo),
});
if (r.status < 500 && r.status !== 429) return r;
} catch {}
await new Promise((ok) => setTimeout(ok, 2 ** i * 1000));
}
throw new Error("Falha ao emitir após várias tentativas");
}<?php
function emitir(string $api, array $headers, array $corpo, string $chave, int $tentativas = 4) {
for ($i = 0; $i < $tentativas; $i++) {
$ch = curl_init("$api/nfse");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => array_merge($headers, ["Idempotency-Key: $chave"]),
CURLOPT_POSTFIELDS => json_encode($corpo),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body !== false && $status < 500 && $status !== 429) return json_decode($body, true);
sleep(2 ** $i);
}
throw new RuntimeException("Falha ao emitir após várias tentativas");
}Não repita em 4xx (exceto 429): o erro é do conteúdo e vai se repetir.
Proteções que a Emitra aplica por conta própria#
A Idempotency-Key protege a sua chamada à Emitra. Do lado da Sefin, a Emitra tem outras camadas:
- O número da DPS é reservado e gravado antes do envio.
- Em timeout com a Sefin, a Emitra consulta pelo Id da DPS antes de reenviar. Se a nota já existe, ela é recuperada, não emitida de novo.
- Uma resposta
E0014(DPS duplicada) num reenvio é tratada como "já emitida".
Automação interna usa as mesmas regras: no lote, a chave é lote:linha; no gatilho do Asaas, asaas:{payment_id}; na recorrência, a unicidade é por agendamento e competência.
external_ref#
A Idempotency-Key expira em 24 h. Para encontrar a nota depois, envie também external_ref no corpo e consulte com GET /nfse?external_ref=pedido-2026-11-0001. Veja Listar NFS-e.