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.

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

TipoOrigem StripeCampos em data
payment.succeededpayment_intent.succeededpayment_id, amount_received, currency
payment.failedpayment_intent.payment_failedpayment_id, amount, currency, failure_code
payment.refundedcharge.refundedcharge_id, payment_id, amount_refunded, currency, fully_refunded
dispute.createdcharge.dispute.createddispute_id, charge_id, payment_id, amount, currency, reason
dispute.closedcharge.dispute.closeddispute_id, charge_id, payment_id, amount, currency, status
checkout.completedcheckout.session.completedsession_id, mode, payment_status, payment_id, subscription_id, amount_total, currency, price_ids, product_ids
checkout.payment_succeededcheckout.session.async_payment_succeededmesmo data de checkout.completed
checkout.payment_failedcheckout.session.async_payment_failedmesmo data de checkout.completed
subscription.createdcustomer.subscription.createdsubscription_id, customer_id, status
subscription.renewedinvoice.paid (renovação)subscription_id, invoice_id, amount_paid, currency
subscription.canceledcustomer.subscription.deletedsubscription_id, customer_id, status
subscription.payment_failedinvoice.payment_failedsubscription_id, invoice_id, amount_due, currency, attempt_count
payout.paidpayout.paidpayout_id, amount, currency, arrival_date
payout.failedpayout.failedpayout_id, amount, currency, failure_code
payout.createdpayout.createdpayout_id, amount, currency, status, arrival_date, automatic
payout.updatedpayout.updatedpayout_id, amount, currency, status, arrival_date, automatic
payout.canceledpayout.canceledpayout_id, amount, currency
milestone.first_paymentprimeiro payment_intent.succeeded da contamesmo data de payment.succeeded
account.updatedaccount.updated da conta conectada, quando a situação mudaaccount_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:

CampoRegra
currenciescurrency do evento está na lista (ISO minúsculo, até 10)
amountMinvalor principal ≥ limite, em centavos
amountMaxvalor principal ≤ limite, em centavos
productsalgum item da assinatura ou linha da fatura com um destes prod_… (até 20)
pricesalgum item da assinatura ou linha da fatura com um destes price_… (até 20)
metadatatodos 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.

Eventosproducts / pricesmetadata de
payment.*, milestone.first_paymentnão se aplicapagamento (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.canceleditens da assinaturaassinatura
subscription.renewed, subscription.payment_failedlinhas da faturafatura; chaves que ela não tem vêm da assinatura
dispute.*não se aplicadisputa
payout.*não se aplicarepasse
account.updatednão se aplicanã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.

Nesta página