Docs

Buscar na documentação

Guias, endpoints e eventos

Ir ao painel

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

EventoQuando
subscription.activatedassinatura ativada (1ª cobrança aprovada no checkout, ou reativação)
subscription.updatedre-sync do estado (ex.: reagendamento, troca de forma de pagamento, cupom removido, entitlements do plano editados, cancelamento agendado ou desfeito)
subscription.plan_changedtroca de plano efetivada
subscription.plan_change_scheduledtroca de plano agendada
subscription.plan_change_canceledtroca de plano agendada cancelada
subscription.migration_link_issuedtroca agendada para uma oferta de outro gateway venceu e virou link de pagamento
subscription.trial_startedtrial iniciado
subscription.past_dueassinatura inadimplente (em retentativa)
subscription.suspendedassinatura suspensa (retentativas esgotadas)
subscription.canceledassinatura cancelada
invoice.paidfatura paga (1ª cobrança ou renovação)
invoice.payment_failedfalha no pagamento da fatura
order.paidpedido avulso pago
order.refundedpedido avulso estornado
payment.refundedpagamento estornado, total ou parcial (de assinatura ou de pedido)
referral.created / referral.qualified / referral.rewarded / referral.revertedciclo 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 (em order.*, payment): o pagamento que liquidou (método, gateway_charge_id, gateway_provider, paid_at, valor em centavos). No invoice.paid é o desta fatura; null em trial/sem cobrança.
  • tracking: UTMs e click-ids (gclid/fbclid/fbc) do link do checkout, e em params os demais parâmetros do link (ex.: sck, src), sem dados pessoais; inherited: true quando vem herdado da assinatura (renovação/upgrade). null sem 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>, onde v1 = 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.