Idempotência, numa API de pagamento, é a garantia de que enviar a mesma requisição duas vezes não cobra nem paga duas vezes. Na API Pix da xFlow, você envia o header Idempotency-Key com um valor único por operação ao criar uma cobrança (POST /api/v1/charges) ou pedir um saque (POST /api/v1/transfers). Se a requisição for repetida com a mesma chave e o mesmo corpo, a API devolve a mesma cobrança ou o mesmo saque. Se a chave vier com um corpo diferente, a resposta é 409 idempotency_conflict.
Por que o retry pode duplicar um pagamento
Redes falham no pior momento. Imagine este fluxo:
- O seu servidor envia
POST /chargespara o pedido 1234. - A xFlow cria a cobrança e responde.
- A resposta se perde no caminho, ou o seu cliente HTTP desiste por timeout.
- O seu código não sabe se deu certo e tenta de novo.
Sem idempotência, o passo 4 cria uma segunda cobrança Pix para o mesmo pedido. O cliente pode receber dois códigos, pagar os dois, e você terá um estorno para fazer e um cliente insatisfeito para atender.
No saque, o risco é maior. Um retry sem proteção pode debitar o saldo duas vezes e mandar dinheiro duas vezes para a mesma chave Pix. Por isso a documentação recomenda o header nos dois casos e diz, sobre o saque, que no retry o mesmo saque é devolvido em vez de debitar o saldo de novo.
Como a xFlow trata cada operação
| Operação | Proteção contra duplicidade |
|---|---|
POST /api/v1/charges (criar cobrança) |
Header Idempotency-Key, opcional e recomendado |
POST /api/v1/transfers (saque Pix) |
Header Idempotency-Key, opcional e recomendado, único por saque |
POST /api/v1/charges/{id}/refund (estorno) |
Idempotente por cobrança: repetir devolve o mesmo estorno; um estorno failed pode ser pedido de novo |
GET (consultas e listagens) |
Só leem dados; repetir não muda nada |
| Webhooks (no seu servidor) | Cada evento pode ser reenviado até 3 vezes; o seu handler precisa tolerar repetição |
As três respostas possíveis
Com o header Idempotency-Key: pedido-1234:
- Primeira chamada. A cobrança é criada e a API responde
201. - Mesma chave, mesmo corpo. A API devolve a mesma cobrança, com o mesmo
ide o mesmopix.copyPaste. Nenhum Pix novo é gerado. - Mesma chave, corpo diferente. A API responde
409 idempotency_conflict. É um aviso de bug: a chave está sendo reaproveitada para outra operação.
curl -X POST https://app.xflowpayments.com/api/v1/charges \
-u "pk_live_...:sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-1234" \
-d '{
"amountCents": 1990,
"customer": { "name": "Cliente Exemplo", "email": "cliente@exemplo.com", "document": "00000000000" },
"description": "Pedido #1234",
"external_reference": "pedido-1234"
}'Rode o mesmo comando duas vezes: o id da cobrança será o mesmo nas duas respostas.
Como escolher a chave
- Derive a chave da operação, não da tentativa. Use o id do pedido (
pedido-1234) ou do saque (saque-987). A chave precisa ser a mesma em todas as tentativas da mesma operação. - Nunca gere uma chave aleatória a cada tentativa. Isso anula a proteção: cada retry vira uma operação nova.
- Monte o corpo uma vez. Se o corpo mudar entre tentativas (um timestamp, um campo recalculado), a segunda chamada recebe
409. - Operação nova, chave nova. Se o cliente desistiu e voltou com outro valor, ou se a cobrança anterior expirou, crie uma chave nova, como
pedido-1234-2.
Exemplo: retry seguro em Node.js
O exemplo repete a chamada em falha de conexão, timeout, 429 e erros 5xx, sempre com a mesma chave e o mesmo corpo. No 429, espera o tempo do header retry-after, como a documentação orienta.
const BASE = "https://app.xflowpayments.com/api/v1";
const cred = Buffer.from(
process.env.XFLOW_PUBLIC_KEY + ":" + process.env.XFLOW_SECRET_KEY,
).toString("base64");
const esperar = (ms) => new Promise((r) => setTimeout(r, ms));
export async function criarCobranca(pedido, tentativas = 4) {
const chave = `pedido-${pedido.id}`; // a mesma em todas as tentativas
const body = JSON.stringify({ // montado uma vez, fora do laço
amountCents: pedido.valorCentavos,
customer: pedido.cliente,
description: `Pedido #${pedido.id}`,
external_reference: chave,
});
for (let i = 1; i <= tentativas; i++) {
try {
const res = await fetch(`${BASE}/charges`, {
method: "POST",
headers: {
Authorization: "Basic " + cred,
"Content-Type": "application/json",
"Idempotency-Key": chave,
},
body,
signal: AbortSignal.timeout(15_000),
});
if (res.status === 429) {
await esperar(Number(res.headers.get("retry-after") ?? 1) * 1000);
continue;
}
if (res.status >= 500) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
if (!res.ok) throw Object.assign(new Error(json.error), { naoRepetir: true, json });
return json; // 201 na primeira vez, a mesma cobrança nas seguintes
} catch (err) {
if (err.naoRepetir || i === tentativas) throw err;
await esperar(2 ** i * 250); // backoff exponencial
}
}
throw new Error("Limite de tentativas atingido");
}Erros 4xx como 400 validation e 409 idempotency_conflict não são repetidos: repetir não muda o resultado. Guarde o request_id de cada resposta. Ele aparece em Integrações > Logs da API, com status e duração de cada chamada, e ajuda a reconstruir o que aconteceu num incidente.
Idempotência no saque Pix
No saque, a chave protege o seu saldo. Gere um valor único quando o saque é registrado no seu sistema e repita esse valor em todas as tentativas:
curl -X POST https://app.xflowpayments.com/api/v1/transfers \
-u "pk_live_...:sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: saque-1234" \
-d '{ "amount": 500, "pix_key": "12345678909", "pix_key_type": "CPF" }'Se a resposta não chegar, repita a mesma chamada com a mesma chave. A API devolve o mesmo saque, com o mesmo id, em vez de debitar o saldo de novo. Para conferir o resultado sem criar nada, consulte GET /api/v1/transfers/{id}, se já tiver o id, ou liste os saques do período com GET /api/v1/transfers.
Um detalhe evita retry desnecessário: quando o saque automático está desligado na conta, o saque nasce pendente de aprovação (pending_approval: true). Isso não é falha. O saque foi criado e aguarda a aprovação.
Idempotency-Key não é external_reference
Os dois campos parecem parecidos, mas resolvem problemas diferentes:
Idempotency-Key |
external_reference |
|
|---|---|---|
| Onde vai | Header HTTP | Corpo da cobrança |
| Precisa ser único | Sim, por operação | Não |
| Para que serve | Repetir a chamada sem duplicar | Ligar a cobrança ao pedido no seu sistema |
| Uso na API | Proteção na criação | Filtro: GET /api/v1/charges?external_reference=pedido-1234 |
Use os dois. O external_reference também ajuda quando você não sabe se uma cobrança chegou a ser criada: consulte a listagem por ele antes de decidir o que fazer.
Do outro lado: o seu webhook também precisa ser idempotente
A idempotência vale nos dois sentidos. A xFlow reenvia cada webhook até 3 vezes quando não recebe 2xx, e uma entrega pode chegar repetida. Antes de dar baixa, confira se o pedido já foi processado, usando o data.id da cobrança e o nome do event. O handler completo está em webhook Pix.
No saque, a regra é a mesma do pedido: uma chave por saque. Veja os detalhes na API de saque Pix. Para provar que o seu retry funciona antes de ir ao ar, use o sandbox da API Pix, onde a idempotência funciona igual. Chaves, logs e guias por linguagem estão em Desenvolvedores.
Perguntas frequentes
O que é Idempotency-Key?
É um header HTTP com um valor único por operação, como o id do pedido. Se a mesma requisição for enviada de novo com a mesma chave, a API devolve o resultado da primeira em vez de executar outra vez.
Quais endpoints da xFlow aceitam Idempotency-Key?
A documentação indica o header na criação de cobrança (POST /api/v1/charges) e na solicitação de saque (POST /api/v1/transfers). O estorno já é idempotente por cobrança: repetir o pedido devolve o mesmo estorno.
O que significa o erro 409 idempotency_conflict?
Que a Idempotency-Key já foi usada com um corpo diferente. A mesma chave só vale para repetir o mesmo pedido. Para uma operação nova, use uma chave nova.
Idempotência funciona no ambiente de teste?
Sim. O comportamento é igual com chave pk_test_, inclusive o 409 idempotency_conflict, então você pode testar o seu retry no sandbox.