Integração
Webhooks e verificação de assinatura
O Eclipta Pay avisa o seu sistema quando algo muda: cobrança confirmada, pedido quitado, estorno, disputa, transferência. Cada entrega é assinada com o segredo do seu endpoint.
Como funciona
- O evento é gravado na mesma transação da mudança e enviado em segundos para cada endpoint ativo inscrito no tipo.
- Pelo menos uma vez, sem garantia de ordem. O mesmo evento pode chegar mais de uma vez (retentativas e reenvios). Deduplique por
eventIde, para estado, compareaggregateVersionpor agregado e tipo de evento (tipos diferentes do mesmoaggregateIdpodem ter sequências próprias): descarte versões menores que a já aplicada. - Responda 2xx em até 10 s e processe depois (fila no seu lado). Qualquer outro status, timeout ou erro de rede conta como falha. Redirecionamentos não são seguidos.
- Leia no máximo 64 KiB de resposta; respostas maiores são interrompidas e contam como falha. Responda algo curto, como
{"received":true}. dataé uma projeção mínima: use o evento como gatilho e decida pelo recurso lido na API (pedido, cobrança, estorno, plano). Eventos chegam para todos os endpoints da conta inscritos no tipo: ignore recursos que não são do seu sistema. As integrações de referência mostram um receptor completo, com inbox.
{
"eventId": "evt_01JDY9C1B2C3D4E5F6G7H8J9KH",
"type": "order.payment_completed",
"apiVersion": "2026-11-13",
"occurredAt": "2026-11-13T13:00:00.000Z",
"tenantId": "org_01JDY7Q2M3N4P5R6S7T8V9W0XA",
"accountId": "acc_01JDY7Q2M3N4P5R6S7T8V9W0XB",
"environment": "sandbox",
"aggregateId": "ord_01JDY8B1B2C3D4E5F6G7H8J9KB",
"aggregateVersion": 1,
"data": {
"orderId": "ord_01JDY8B1B2C3D4E5F6G7H8J9KB",
"confirmedCents": "14970",
"currency": "BRL"
}
}Cadastrar o endpoint
Cada conta pode ter até 10 endpoints. Inscreva cada um nos tipos de que precisa ou em ["*"] para todos (inclusive tipos novos). O segredo whsec_… aparece só na resposta da criação: guarde-o num cofre.
curl -X POST "$ECLIPTA_PAY_API/v1/webhook-endpoints" \
-H "Authorization: Bearer $ECLIPTA_PAY_KEY" \
-H "X-Account-Id: $ECLIPTA_PAY_ACCOUNT" \
-H "Idempotency-Key: webhook-producao-v1" \
-H "Content-Type: application/json" \
-d '{"url":"https://loja.exemplo.com.br/webhooks/eclipta","events":["order.payment_completed","payment.refund_updated"]}'Headers de cada entrega
| Header | Conteúdo |
|---|---|
Eclipta-Pay-Event-Id | Identificador do evento. O mesmo em todas as tentativas e reenvios: deduplique por ele. |
Eclipta-Pay-Event-Type | Tipo do evento (também no corpo). |
Eclipta-Pay-Timestamp | Segundos desde 1970 (UTC) do momento da tentativa. Muda a cada tentativa. |
Eclipta-Pay-Signature | Uma ou mais assinaturas v1=<hex>, separadas por vírgula (duas durante a rotação do segredo). |
Eclipta-Pay-Key-Id | Versão de cada segredo que assinou, na mesma ordem das assinaturas. |
Eclipta-Pay-Delivery-Id | Identificador da entrega (evento × endpoint). |
Eclipta-Pay-Attempt | Número da tentativa desta entrega. |
User-Agent | EcliptaPay-Webhooks/1.0 |
Content-Type | application/json |
Verificar a assinatura
A assinatura é HMAC-SHA256 com o segredo do endpoint sobre <Eclipta-Pay-Timestamp>.<corpo cru>, em hexadecimal, com o prefixo v1=. Use o corpo cru, exatamente como chegou, antes de qualquer parse de JSON. Compare em tempo constante e recuse timestamps a mais de 5 minutos do seu relógio.
import { verifyWebhook, WebhookVerificationError } from "@eclipta-pay/sdk";
export async function POST(request: Request) {
const raw = await request.text(); // corpo cru
try {
const evento = verifyWebhook(raw, request.headers, process.env.ECLIPTA_PAY_WEBHOOK_SECRET!);
if (await jaProcessado(evento.eventId)) return new Response(null, { status: 200 });
await enfileirar(evento); // processe fora da requisição
return Response.json({ received: true });
} catch (e) {
if (e instanceof WebhookVerificationError) return new Response(null, { status: 400 });
throw e;
}
}import { createHmac, timingSafeEqual } from "node:crypto";
export function assinaturaValida(raw: Buffer, headers: Record<string, string>, segredos: string[]): boolean {
const ts = headers["eclipta-pay-timestamp"];
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const recebidas = (headers["eclipta-pay-signature"] ?? "")
.split(",")
.map((s) => s.trim())
.filter((s) => s.startsWith("v1="))
.map((s) => Buffer.from(s.slice(3), "hex"));
let ok = false;
for (const segredo of segredos) {
const esperada = createHmac("sha256", segredo).update(`${ts}.`).update(raw).digest();
for (const r of recebidas) if (r.length === esperada.length && timingSafeEqual(r, esperada)) ok = true;
}
return ok;
}Retentativas
Uma entrega com falha é repetida até 9 vezes, com intervalos crescentes e variação aleatória de ±20% (para não sincronizar retentativas). Depois da última, a entrega fica FAILED e pode ser reenviada.
| Tentativa | Intervalo antes | Acumulado aproximado |
|---|---|---|
| 1 | imediata | imediata |
| 2 | 30 s | 30 s |
| 3 | 2 min | 2.5 min |
| 4 | 10 min | 12.5 min |
| 5 | 1 h | 1.2083333333333333 h |
| 6 | 6 h | 7.208333333333333 h |
| 7 | 24 h | 31.208333333333332 h |
| 8 | 48 h | 79.20833333333333 h |
| 9 | 72 h | 151.20833333333334 h |
Desativação automática: um endpoint que falha sem nenhum sucesso por 3 dias seguidos passa para AUTO_DISABLED. As entregas pendentes ficam pausadas, os responsáveis técnicos da conta recebem um e-mail e nada novo é enviado até a reativação (PATCH /v1/webhook-endpoints/{id} com {"enabled": true}), que retoma as pendentes.
Reenvio e recuperação de perdas
POST /v1/webhook-deliveries/{id}/replayreenvia uma entrega com o mesmoeventIde assinatura nova. Cada reenvio fica registrado na trilha de auditoria.POST /v1/events/{id}/resendreenvia um evento para um endpoint (mesmo que ele não existisse quando o evento ocorreu) ou para todos os inscritos.GET /v1/eventslista os eventos dos últimos 30 dias, com filtros por tipo e agregado: use para reconciliar depois de uma indisponibilidade.GET /v1/webhook-deliveries/{id}mostra cada tentativa: status HTTP, duração, resultado e até 1 KB do início da sua resposta (dados que parecem sensíveis são mascarados).
Rotação do segredo
POST /v1/webhook-endpoints/{id}/rotate-secret gera um segredo novo (exibido só nessa resposta). Durante a sobreposição (padrão 24 h), cada entrega vai com as duas assinaturas e as duas versões em Eclipta-Pay-Key-Id. Passe os dois segredos ao verificador, troque o segredo no seu cofre e, depois da janela, remova o antigo.
Regras de destino
- HTTPS obrigatório em produção. No Sandbox,
http://é aceito para testes. - O DNS é resolvido em cada tentativa e todos os endereços são conferidos. Destinos em loopback, redes privadas, link-local, CGNAT, multicast, metadata de nuvem e seus equivalentes IPv6 (inclusive IPv4 mapeado) são bloqueados sem conexão.
- A conexão vai para o endereço validado, sem nova consulta de DNS, e o certificado é conferido pelo nome do host da URL.
- Usuário e senha na URL, fragmento (#) e nomes internos (
localhost,*.internal,*.local) são recusados no cadastro.
Catálogo de eventos (2026-11-13)
O envelope é o mesmo para todos os tipos. data traz uma projeção sem dados pessoais nem de cartão; para o estado completo, consulte o recurso pela API. Receptores devem aceitar tipos novos sem falhar.
| Tipo | Agregado | Quando | Origem |
|---|---|---|---|
payment.confirmed | payment | Pagamento confirmado pelo provedor; o valor ainda está a liberar (não é saldo disponível). | Base |
payment.settlement_reported | payment | O provedor informou a liquidação. O caixa só entra pela importação do extrato (recebido, conciliando). | Base |
payment.refund_updated | payment | Mudança no estorno da cobrança (solicitado, parcial, total ou revertido). | Base |
payment.dispute_updated | payment | Mudança em disputa (chargeback) da cobrança. | Base |
transfer.updated | transfer | Mudança de situação de uma transferência (saque). | Base |
account.status_updated | account | Mudança na situação cadastral da conta no provedor. | Base |
organization.activated | account | Conta aprovada em todas as etapas do onboarding: pode emitir cobranças (uma vez por conta). | Extensão |
subscription.updated | subscription | Mudança de situação de uma assinatura. | Base |
reconciliation.case_opened | order | Caso de conciliação aberto (por exemplo, sobrepagamento ou lançamento sem correspondência). | Base |
order.payment_completed | order | Pedido quitado: o total confirmado cobre o pedido sem parte incerta. Libere o produto uma única vez por pedido. Pedido aberto pelo link de uma oferta traz também `offerId` e `productId`. | Base |
payment_group.updated | payment_group | Mudança no pagamento com dois cartões (autorização, captura ou compensação). | Base |
payment_plan.installment_updated | payment_plan | Mudança numa parcela de Pix em parcelas ou de parcelamento mensal no cartão. | Base |
payment_plan.updated | payment_plan | Mudança de situação do plano de parcelas. | Base |
payment.created | payment | Cobrança registrada no provedor (A charge.created). | Extensão |
payment.failed | payment | Cobrança recusada (cartão recusado, reprovada na análise de risco ou rejeitada pelo provedor) (A charge.failed). | Extensão |
payment.overdue | payment | Cobrança venceu sem pagamento (A charge.overdue). | Extensão |
payment.cancelled | payment | Cobrança cancelada no provedor (A charge.canceled). | Extensão |
payment.restored | payment | Cobrança cancelada foi reaberta no provedor (A charge.restored). | Extensão |
payment.updated | payment | Valor, vencimento ou descrição da cobrança alterados no provedor; um novo QR Code/linha digitável é gerado. | Extensão |
checkout.session_updated | checkout_session | Mudança na sessão de checkout (concluída ou expirada localmente) (A checkout.session.*). | Extensão |
webhook_endpoint.test | webhook_endpoint | Evento de teste enviado só para o endpoint escolhido, a pedido do integrador. Não representa mudança financeira. | Plataforma |