docs

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çãoResultado
Mesmo header + mesmo bodyResposta cacheada (200 OK), sem nova cobrança
Mesmo header + body diferente422 idempotency_key_in_use + request_id original
Sem header em POST financeiroCada 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

EntidadeIdempotency-Key típica
PaymentIntent (cobrança one-shot)order-{orderId} ou UUID
PaymentIntent (assinatura recorrente)sub-{customerSubId}-{cycleMonth}
Refundrefund-{paymentIntentId}-{n} (parcial)
Subscriptionsignup-{customerEmail}
Customercustomer-{userIdInternal}
Transfertransfer-{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 ULID

Use 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 request

Para 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.

Esta página foi útil?

Atualizado em

Nesta página