Allya Payments
Conceitos

Idempotência

Como o externalId evita cobranças e assinaturas duplicadas em retentativas e chamadas concorrentes.

A Allya Payments aceita uma única criação para cada externalId dentro de um ambiente: tanto para pagamentos quanto para assinaturas. Retentativas, jobs duplicados ou concorrência no seu backend não criam recursos duplicados se você enviar o mesmo externalId em cada chamada.

externalId é obrigatório em POST /v1/payments e POST /v1/subscriptions. Chamadas sem o campo respondem 400 invalid_input. A obrigatoriedade existe porque idempotência sem chave externa é uma armadilha clássica de gateway: um timeout de rede leva o caller a fazer retry, e sem externalId cada retry vira uma cobrança nova no gateway.

Esta página cobre o mecanismo geral. Detalhes específicos:

Como funciona

A chave de idempotência é (environmentId, externalId). O environmentId vem implícito da API key (sk_test_… = sandbox, sk_live_… = production).

A unicidade é garantida no banco por @@unique([environmentId, externalId]) no Prisma, tanto para Payment quanto para Subscription.

Pagamentos

Quando você chama POST /v1/payments:

  • Se não existe pagamento com aquele externalId no ambiente → a Allya cria a cobrança, persiste o pagamento e devolve o objeto normalizado.
  • Se já existe pagamento com aquele externalId no ambiente → a Allya não cria uma nova cobrança no gateway. Devolve o pagamento existente, do jeito que estiver naquele momento (status pending, paid, etc.).

A resposta é a mesma em estrutura: a única diferença é que o pagamento devolvido pode ter qualquer status, não necessariamente pending.

Assinaturas

POST /v1/subscriptions segue exatamente a mesma regra com (environmentId, externalId). Reenviar a mesma requisição com o mesmo externalId retorna a assinatura existente, sem criar uma nova no gateway.

Recomendado para assinaturas: use um externalId que identifique unicamente o cliente + oferta, não a fatura/ciclo. Exemplo: assinatura_cliente_42_plano_pro_mensal. Cada ciclo individual aparece como um Payment vinculado por subscriptionId: esses sim têm externalId próprio se você quiser.

Quando usar

Use externalId sempre que o caller pode disparar a mesma operação mais de uma vez:

  • Retentativa por timeout de rede no seu próprio backend.
  • Reprocessamento de fila/cron que pode rodar duas vezes para o mesmo pedido.
  • Worker que cai logo após o POST /v1/payments e reprocessa o job na próxima execução.

O externalId natural costuma ser o identificador do pedido/assinatura no seu sistema:

externalId: `pedido_${order.id}`
externalId: `assinatura_${subscription.id}_${cicloAtual}`
externalId: `fatura_${invoice.id}`

Garanta que o valor é único por cobrança real. Se você cobrar a mesma assinatura todo mês, inclua o ciclo/competência: caso contrário, da segunda cobrança em diante a Allya devolve a primeira em vez de criar uma nova.

Chamadas concorrentes

Duas requisições idênticas chegando ao mesmo tempo são race-safe. O Prisma + Postgres garantem unicidade por (environmentId, externalId); uma das duas vence o INSERT e a outra recebe o recurso já criado pela vencedora: não dá erro 500, nem cria duplicata. Vale para Payment e Subscription.

Limitações

  • Idempotência é por ambiente. O mesmo externalId em sandbox e production cria duas cobranças separadas: uma em cada lado. Faz sentido: chaves diferentes, dados diferentes.
  • Payload diferente, mesmo externalId. Se você reenviar com amount ou customer diferentes mantendo o externalId, a Allya devolve o primeiro pagamento criado: o segundo payload é silenciosamente ignorado. Não há comparação semântica.
  • Sem expiração. A chave de idempotência fica registrada para sempre (não há janela de 24h como em alguns provedores). Se você quer "esquecer" um externalId, gere outro.
  • Sem Idempotency-Key em header. A Allya usa o campo externalId do body, não header HTTP separado.

Verificando depois

Para conferir se um externalId já gerou cobrança sem disparar POST de novo, use:

GET /v1/payments?externalId=pedido_123
Authorization: Bearer sk_test_...

Resposta 200 com o pagamento se existe; 404 not_found se não. Veja Consultar pagamento para detalhes.

Exemplo de retentativa segura

async function createPaymentWithRetry(order: Order) {
  const externalId = `pedido_${order.id}`;

  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      const res = await fetch("https://payments-api.allyasolutions.com/v1/payments", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.ALLYA_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          amount: order.amount,
          currency: "BRL",
          method: "pix",
          externalId,
          customer: order.customer,
        }),
      });

      if (res.ok || res.status === 201) {
        return res.json();
      }

      // Erro do cliente (4xx que não seja 5xx/timeout): não retentar.
      if (res.status >= 400 && res.status < 500) {
        throw new Error(`allya error: ${await res.text()}`);
      }

      // 5xx ou timeout cairá aqui: segura para tentar de novo.
    } catch (err) {
      if (attempt === 3) throw err;
      await new Promise((r) => setTimeout(r, 500 * attempt));
    }
  }
}

Mesmo que a primeira tentativa tenha chegado no servidor e criado o pagamento (mas a resposta nunca voltou), a segunda tentativa devolve o pagamento existente. Sem duplicidade.

Veja também

On this page