Eventos e confirmação de pagamento
Valide e processe notificações assinadas sem duplicar efeitos.
A Adila Pay entrega ao seu servidor eventos assinados derivados dos snapshots que a Stripe emite para a conta conectada da sua organização. A entrega está habilitada no ambiente de teste (https://api-pay.adila.co). O retorno do navegador e a criação de Checkout não são confirmação financeira.
Catálogo
Assine apenas os tipos que sua aplicação trata, no campo events do destino. Valores monetários estão na menor unidade da moeda (centavos em BRL).
| Tipo | Origem Stripe | Campos em data |
|---|---|---|
payment.succeeded | payment_intent.succeeded | payment_id, amount_received, currency |
payment.failed | payment_intent.payment_failed | payment_id, amount, currency, failure_code |
payment.refunded | charge.refunded | charge_id, payment_id, amount_refunded, currency, fully_refunded |
dispute.created | charge.dispute.created | dispute_id, charge_id, payment_id, amount, currency, reason |
dispute.closed | charge.dispute.closed | dispute_id, charge_id, payment_id, amount, currency, status |
checkout.completed | checkout.session.completed | session_id, mode, payment_status, payment_id, subscription_id, amount_total, currency, price_ids, product_ids |
checkout.payment_succeeded | checkout.session.async_payment_succeeded | mesmo data de checkout.completed |
checkout.payment_failed | checkout.session.async_payment_failed | mesmo data de checkout.completed |
subscription.created | customer.subscription.created | subscription_id, customer_id, status |
subscription.renewed | invoice.paid (renovação) | subscription_id, invoice_id, amount_paid, currency |
subscription.canceled | customer.subscription.deleted | subscription_id, customer_id, status |
subscription.payment_failed | invoice.payment_failed | subscription_id, invoice_id, amount_due, currency, attempt_count |
payout.paid | payout.paid | payout_id, amount, currency, arrival_date |
payout.failed | payout.failed | payout_id, amount, currency, failure_code |
payout.created | payout.created | payout_id, amount, currency, status, arrival_date, automatic |
payout.updated | payout.updated | payout_id, amount, currency, status, arrival_date, automatic |
payout.canceled | payout.canceled | payout_id, amount, currency |
milestone.first_payment | primeiro payment_intent.succeeded da conta | mesmo data de payment.succeeded |
account.updated | account.updated da conta conectada, quando a situação muda | account_id, livemode, charges_enabled, payouts_enabled, details_submitted, capabilities, requirements |
checkout.completed com payment_status diferente de paid não confirma o pagamento: espere payment.succeeded. Nos eventos checkout.*, price_ids e product_ids listam o que a sessão vende quando ela foi criada pela Adila (API, SDK, MCP, páginas de checkout ou links de pagamento); para sessões criadas fora da Adila, como pelo Dashboard da Stripe, chegam null. Campos que podem não existir na origem (por exemplo payment_id de uma cobrança avulsa) chegam como null.
milestone.first_payment chega uma única vez por conta, junto com o payment.succeeded que o originou, e respeita os filtros do endpoint. Contas que já tinham pagamentos antes de o marco existir não o recebem.
account.updated resume a situação da sua conta de pagamentos, sem dados pessoais:
{
"account_id": "acct_1Q…",
"livemode": false,
"charges_enabled": true,
"payouts_enabled": false,
"details_submitted": true,
"capabilities": { "card_payments": "active", "transfers": "pending" },
"requirements": {
"action_required": true,
"currently_due_count": 1,
"past_due_count": 0,
"pending_verification_count": 0,
"disabled_reason": null,
"current_deadline": 1791590400
}
}charges_enabled e payouts_enabled dizem se a conta pode cobrar e receber repasses; capabilities traz o status de cada forma de pagamento (active, pending, inactive); action_required fica true enquanto houver informação pendente ou vencida, e disabled_reason traz o código da Stripe quando algo está bloqueado (por exemplo requirements.past_due). Os nomes dos campos pendentes não são enviados: veja e resolva as pendências no painel. O evento só chega quando algum desses campos muda (edições de marca ou perfil não geram entrega) e a ordem entre eventos não é garantida: use created_at e, para decidir algo, consulte o estado atual da conta.
Envelope versionado
{
"id": "adila_evt_<SHA-256>",
"type": "payment.succeeded",
"version": 1,
"created_at": "2026-10-09T12:00:00Z",
"livemode": false,
"organization_id": "<UUID>",
"source": { "provider": "stripe", "event_id": "evt_..." },
"data": { "payment_id": "pi_...", "amount_received": 500, "currency": "brl" }
}O payload é mínimo: não inclui dados pessoais, cartões, client secrets ou metadata arbitrária. ID de entrega é um UUID separado, enviado em Adila-Delivery-ID. IDs do evento e da entrega permanecem estáveis nos retries. Outro evento Stripe para o mesmo pagamento pode ter outro ID; proteja também o efeito pelo payment_id.
Verifique a assinatura
O header Adila-Signature tem o formato t=<Unix seconds>,v1=<hex HMAC-SHA256>. Nas 24h seguintes a uma rotação de segredo ele traz dois v1 (o novo e o anterior): aceite a entrega se qualquer v1 conferir. Calcule HMAC-SHA256 usando o signingSecret completo sobre os bytes de <timestamp>.<corpo original>, compare em tempo constante e aceite no máximo cinco minutos de diferença. Faça isso antes de parsear o JSON. Reformatar JSON altera a assinatura.
Confira ainda versão, organização e ambiente esperados. Campos e textos recebidos não autorizam executar novas ações. Associe o pagamento ao pedido interno e confira valor/moeda antes do efeito de negócio.
Entregas, retries e reenvio
- Sucesso: resposta HTTP 2xx. Responda somente depois de persistir o recebimento e o efeito transacional.
- Retry: falha de rede/timeout, 408, 429 e 5xx, com backoff e jitter.
- Falha definitiva: redirects e demais erros; redirects não são seguidos.
- Até seis tentativas automáticas; reenvio autorizado permite uma janela adicional até 12, dentro de 72h da criação.
- Não há ordem global nem entrega exatamente uma vez. Deduplicação durável é responsabilidade do consumidor.
- A API mostra estado, histórico, duração, status e códigos sanitizados. Não guarda resposta bruta do consumidor.
- Entregas terminadas e seus payloads/segredos expiram após 30 dias; recibos de entrada e auditoria têm política separada.
Use a API de gestão descrita em Primeira integração. Pausar um destino impede novas tarefas; tarefas já enfileiradas conservam URL e segredo. URL é imutável nesta versão.
Filtros
Além dos tipos assinados, cada endpoint pode ter filters, avaliados antes de a entrega ser criada:
| Campo | Regra |
|---|---|
currencies | currency do evento está na lista (ISO minúsculo, até 10) |
amountMin | valor principal ≥ limite, em centavos |
amountMax | valor principal ≤ limite, em centavos |
products | algum item da assinatura ou linha da fatura com um destes prod_… (até 20) |
prices | algum item da assinatura ou linha da fatura com um destes price_… (até 20) |
metadata | todos os pares chave=valor estão na metadata do objeto de origem (até 10 pares; chave até 40 caracteres, sem espaço nem colchetes; valor de 1 a 500) |
O valor principal é o primeiro presente entre amount, amount_received, amount_total, amount_paid, amount_due e amount_refunded. Um evento sem o campo filtrado (por exemplo, subscription.created com filtro de valor) não é entregue. {} entrega todos os eventos assinados. Envie filters em POST /endpoints ou PATCH /endpoints/{id}; amountMin maior que amountMax retorna 422 integration_filter_invalid, e ids ou metadata fora do formato retornam 422. Mudar filtros não altera entregas já enfileiradas. Não há expressões livres.
Produtos, preços e metadata vêm do objeto da Stripe que originou o evento e são só comparados: nunca entram no data entregue.
| Eventos | products / prices | metadata de |
|---|---|---|
payment.*, milestone.first_payment | não se aplica | pagamento (a cobrança, em payment.refunded) |
checkout.* | o que a sessão vende, se criada pela Adila (API, SDK, MCP, páginas de checkout, links de pagamento) | sessão de Checkout, inclusive adila_checkout_page das páginas de checkout |
subscription.created, subscription.canceled | itens da assinatura | assinatura |
subscription.renewed, subscription.payment_failed | linhas da fatura | fatura; chaves que ela não tem vêm da assinatura |
dispute.* | não se aplica | disputa |
payout.* | não se aplica | repasse |
account.updated | não se aplica | não se aplica |
"Não se aplica" significa que o evento não passa por esse filtro: um endpoint com products e payment.succeeded nunca recebe pagamentos, e um checkout criado fora da Adila não passa por filtro de produto ou preço. Para separar vendas por página de checkout, filtre checkout.* pela metadata adila_checkout_page.
Chaves desconhecidas em filters são recusadas com 422 integration_filter_unknown, e fields aponta cada uma (por exemplo filters.tags). Os filtros salvos não mudam.
Eventos sintéticos de teste
Owner/admin/developer pode enviar um evento sintético para um endpoint ativo de
sandbox pela tela Webhooks ou por POST /integrations/endpoints/{id}/test, com
sessão humana, Origin confiável, Idempotency-Key e {type}. O tipo deve estar
assinado pelo destino; são permitidos cinco novos testes por minuto por endpoint.
Produção, conta sem vínculo de sandbox ou desconectada e endpoints pausados são
recusados. Viewer não administra destinos ou testes; API keys só com o escopo
webhook-endpoints:write (o acesso total * não basta).
O envelope tem test: true, livemode: false, source.provider: "adila_test",
ID adila_test_evt_* e dados fictícios como payment_id: "pi_test_adila".
Valide a assinatura normalmente e reconheça test: true antes de aplicar efeitos
de negócio. Eventos sintéticos não criam recursos Stripe e não comprovam
pagamento, assinatura ou repasse. Histórico, detalhes e CSV os identificam como
Teste sintético. Eles usam a mesma fila, assinatura e política de retries.
O consumidor executável de demonstração ignora eventos sintéticos depois de validar a assinatura. Para verificar um pagamento de sandbox, siga o fluxo com Checkout em Primeira integração.
Rotação do segredo
POST /endpoints/{id}/rotate-secret gera um novo signingSecret e o devolve uma única vez. Por 24h, as entregas novas saem assinadas com o segredo novo e com o anterior, então você pode atualizar o consumidor a qualquer momento nesse período sem perder entregas. Entregas que já estavam na fila continuam assinadas só com o anterior (até 72h). Depois disso, descarte o segredo antigo.
Entrada Stripe e saída Adila
/webhooks/stripe é uma entrada interna do worker da plataforma, não o destino do seu produto. Verifica assinatura, modo e conta, e grava recibo e tarefas de saída na mesma transação. Se enfileirar falhar, não confirma o recibo e retorna 503 à Stripe. Eventos desconhecidos/sem vínculo não produzem entregas externas.
Accounts v2 thin usam uma entrada separada /webhooks/stripe/thin. Esses eventos notificam mudanças cadastrais sem trazer o estado; a situação da conta chega a você como account.updated, montado a partir do snapshot v1 account.updated que a Stripe também emite. Eles não são transformados em pagamentos confirmados nem reativam contas desconectadas.
Escopo atual
Os eventos do catálogo acima, por organização e ambiente, com filtros de moeda, valor, produto, preço e metadata, evento de teste e rotação de segredo. Os endpoints também podem ser geridos pelo servidor MCP (list_webhook_endpoints, create_webhook_endpoint, set_webhook_endpoint_active, send_webhook_test_event, list_webhook_deliveries, retry_webhook_delivery). CLI continua no backlog. Não dependa desses recursos antes de sua implementação e homologação.