Webhooks
A ribbo envia um POST assinado ao seu endpoint a cada evento. Cadastre a URL e escolha os
eventos no painel (Configurações → Webhooks).
Envelope
{
"id": "evt_01J…",
"type": "invoice.paid",
"created_at": "2026-08-01T12:00:00Z",
"tenant_id": "tn_01J…",
"data": { "...": "payload específico do tipo" }
}
Os 19 eventos
| Evento | Quando |
|---|---|
subscription.activated | assinatura ativada (1ª cobrança aprovada no checkout, ou reativação) |
subscription.updated | re-sync do estado (ex.: reagendamento, troca de forma de pagamento, cupom removido, entitlements do plano editados, cancelamento agendado ou desfeito) |
subscription.plan_changed | troca de plano efetivada |
subscription.plan_change_scheduled | troca de plano agendada |
subscription.plan_change_canceled | troca de plano agendada cancelada |
subscription.migration_link_issued | troca agendada para uma oferta de outro gateway venceu e virou link de pagamento |
subscription.trial_started | trial iniciado |
subscription.past_due | assinatura inadimplente (em retentativa) |
subscription.suspended | assinatura suspensa (retentativas esgotadas) |
subscription.canceled | assinatura cancelada |
invoice.paid | fatura paga (1ª cobrança ou renovação) |
invoice.payment_failed | falha no pagamento da fatura |
order.paid | pedido avulso pago |
order.refunded | pedido avulso estornado |
payment.refunded | pagamento estornado, total ou parcial (de assinatura ou de pedido) |
referral.created / referral.qualified / referral.rewarded / referral.reverted | ciclo de indicação |
Os eventos subscription.* e invoice.* trazem o data de uma assinatura; os order.*, o
de pedido; payment.refunded traz um ou outro, conforme o que foi estornado. Veja os campos
exatos em /docs/reference.
Campos do data de assinatura
subscription_id, status, customer (id, email, external_id), plan_id,
plan_name, interval, current_period_end, cancel_at_period_end,
collection_method (card_auto/pix_manual), payment_method (cartão mascarado ou
null), next_charge_amount_cents, currency, pending_offer (troca agendada ou
null), entitlements, e ainda:
last_payment(emorder.*,payment): o pagamento que liquidou (método,gateway_charge_id,gateway_provider,paid_at, valor em centavos). Noinvoice.paidé o desta fatura;nullem trial/sem cobrança.tracking: UTMs e click-ids (gclid/fbclid/fbc) do link do checkout, e emparamsos demais parâmetros do link (ex.:sck,src), sem dados pessoais;inherited: truequando vem herdado da assinatura (renovação/upgrade).nullsem contexto.
O data de pedido traz order_id, status, customer, plan_id, plan_name,
quantity, amount_cents, unit_amount_cents, currency, paid_at,
target_subscription_id, entitlements e grant_days (crus, nunca multiplicados por
quantity), payment e tracking.
O customer traz só id/email/external_id: doc, telefone e nome não cruzam
(minimização de PII).
🔑 A regra de ouro
O campo data.entitlements vem cheio mesmo em suspended/past_due/canceled. Decida o
acesso do cliente por data.status, NUNCA pela presença de entitlements. Um receptor que só
"aplica os entitlements no perfil" re-libera acesso a um inadimplente.
Validar a assinatura (obrigatório)
Cada POST traz os headers:
X-Billing-Event-Id: evt_…: chave de dedupe (a entrega é at-least-once; trate idempotente).X-Billing-Signature: ts=<epoch>,v1=<hmac_hex>, ondev1 = HMAC-SHA256(secret, "<ts>.<corpo_cru>").
Valide em tempo constante, sobre o corpo cru (não o re-serializado), com o secret do
endpoint (do painel).
Node.js:
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.ts}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Python:
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['ts']}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Confirmação e retries
Responda 2xx para confirmar. Sem 2xx (ou timeout de 10s), a ribbo re-tenta: 1min → 5min → 30min → 2h → 6h → 24h (6 retries, 7 tentativas no total). Depois marca como esgotado e alarma. Um endpoint com URL malformada é fatal (sem retry).
Reconciliação
Perdeu um evento? Releia o estado atual com GET /v1/subscriptions/{id} (qualquer status): é a
mesma forma do payload, sempre atual.