Idempotência
Header obrigatório em todo POST financeiro — evita cobrança duplicada em retry de network.
Todo POST que cria recurso financeiro (PaymentIntent, Refund, Transfer, Subscription) deve receber o header Idempotency-Key.
Por quê
Em redes reais, um POST que parece "perdido" pode ter chegado e processado — você só não recebeu a resposta. Sem Idempotency-Key, retry duplica a cobrança.
[seu app] ──POST──→ [Zhex] ──cobra cartão──→ [adquirente] ✓
↓ resposta enviada
↓
[pacote perdido na rede]
[seu app] timeout → retry → [Zhex] cobra DE NOVO ✗Com Idempotency-Key, a 2ª request retorna a resposta cacheada da 1ª. Sem dupla cobrança.
Sintaxe
curl -X POST https://prometheus.zhex.io/v1/payment_intents \
-H "Authorization: Bearer $ZHEX_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d "amount=4990" -d "currency=brl"Comportamento
| Situação | Resultado |
|---|---|
| Mesmo header + mesmo body | Resposta cacheada (200 OK), sem nova cobrança |
| Mesmo header + body diferente | 422 idempotency_key_in_use + request_id original |
| Sem header em POST financeiro | Cada retry cria recurso novo (cobrança duplicada) |
| Header expirado (> 24h) | Tratado como request novo |
TTL: 24h. Após esse prazo, mesma chave pode ser reutilizada para nova operação.
Receita Node típica
import { randomUUID } from 'node:crypto';
import { setTimeout as sleep } from 'node:timers/promises';
async function chargeWithRetry(body: object) {
const key = randomUUID();
for (let attempt = 0; attempt < 3; attempt++) {
const res = await fetch('https://prometheus.zhex.io/v1/payment_intents', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZHEX_SECRET_KEY}`,
'Idempotency-Key': key, // mesma key em TODOS os retries
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (res.ok) return res.json();
const err = await res.json();
// Retentável apenas em api_error (5xx)
if (err.error.type === 'api_error' && attempt < 2) {
await sleep(2 ** attempt * 100);
continue;
}
throw new Error(`${err.error.code}: ${err.error.message}`);
}
}A mesma key em todas as tentativas é o ponto crítico — gera UUID fora do loop.
Casos de uso por entidade
| Entidade | Idempotency-Key típica |
|---|---|
PaymentIntent (cobrança one-shot) | order-{orderId} ou UUID |
PaymentIntent (assinatura recorrente) | sub-{customerSubId}-{cycleMonth} |
Refund | refund-{paymentIntentId}-{n} (parcial) |
Subscription | signup-{customerEmail} |
Customer | customer-{userIdInternal} |
Transfer | transfer-{orderId}-{sellerId} |
Geradores recomendados
import { randomUUID } from 'node:crypto'; // Node 14+ — recomendado
import { v4 as uuidv4 } from 'uuid'; // alternativa universal
import { ulid } from 'ulid'; // ordenável por tempo (bom para debug)
import { ksuid } from 'ksuid'; // similar a ULIDUse UUIDv4 (ou ULID/KSUID). Não use timestamp + userId — colisão entre devices reabre cobrança fechada. Um Idempotency-Key por operação lógica (não por retry).
Erros comuns
Reutilizar key entre operações diferentes
// ❌ ERRADO — mesma key em duas cobranças
const key = randomUUID();
await fetch(URL, { headers: { 'Idempotency-Key': key, ... }, body: JSON.stringify({ amount: 1000 }) });
await fetch(URL, { headers: { 'Idempotency-Key': key, ... }, body: JSON.stringify({ amount: 2000 }) });
// → 422 idempotency_key_in_use// ✅ CERTO — uma key por operação
await fetch(URL, { headers: { 'Idempotency-Key': randomUUID(), ... }, body: JSON.stringify({ amount: 1000 }) });
await fetch(URL, { headers: { 'Idempotency-Key': randomUUID(), ... }, body: JSON.stringify({ amount: 2000 }) });Gerar key dentro do retry loop
// ❌ ERRADO — cada retry cria key nova, idempotência não funciona
for (let i = 0; i < 3; i++) {
await fetch(URL, { headers: { 'Idempotency-Key': randomUUID(), ... }, body });
}// ✅ CERTO — key fora do loop
const key = randomUUID();
for (let i = 0; i < 3; i++) {
await fetch(URL, { headers: { 'Idempotency-Key': key, ... }, body });
}Trocar body no retry
// ❌ ERRADO — alterou amount no retry
const key = randomUUID();
await fetch(URL, { headers: { 'Idempotency-Key': key, ... }, body: JSON.stringify({ amount: 1000 }) });
// retry "corrigindo bug" no amount
await fetch(URL, { headers: { 'Idempotency-Key': key, ... }, body: JSON.stringify({ amount: 2000 }) });
// → 422 com request_id da primeira requestPara mudar o body, use nova key.
Endpoints sem necessidade
GETs e DELETEs não precisam de Idempotency-Key (são naturalmente idempotentes). Mas você pode passar — é ignorado.
PUTs em recursos financeiros (subscriptions.update, customers.update) também são idempotentes por natureza, mas aceitam o header para defensive coding.
Atualizado em