Webhooks

Webhooks notify your application when objects change in Rollo. You register an HTTPS endpoint and subscribe to specific event types. Only subscribed events are delivered to that endpoint.

Subscribe in the dashboard

Open Dashboard → Webhooks, enter your endpoint URL, and select the event types your integration handles. You can edit subscriptions later, disable an endpoint temporarily, or roll the signing secret if it is compromised. Each endpoint shows its signing secret with a copy control anytime you need it.

Prefer the narrowest set of events that meets your needs. Subscribing to every event increases noise and processing cost without improving reliability.

Envelope shape

Every delivery is a JSON object with id, type, created (Unix seconds), and a flat data object. There is no nested data.object.

Example request

POST https://api.yourapp.com/webhooks/rollo
Content-Type: application/json
Rollo-Signature: t=1723000000,v1=abc123...
User-Agent: Rollo-Webhooks/1.0

Example response

{
  "id": "evt_lx9k2m_a3f8c1d2",
  "type": "payment.succeeded",
  "created": 1723000000,
  "data": {
    "id": "pay_01HXYZ...",
    "amount": 4900,
    "currency": "usd",
    "status": "succeeded",
    "fee": 339,
    "net_amount": 4561,
    "customer_email": "ada@example.com",
    "checkout_session": "cs_01HXYZ...",
    "payment_method_type": "card",
    "merchant_id": "acct_...",
    "livemode": true
  }
}

How to handle it

  • Verify Rollo-Signature before parsing business logic.
  • Persist id and skip duplicates (idempotent handlers).
  • Return HTTP 2xx within a few seconds. Do heavy work asynchronously.
  • Read fields from data directly (e.g. event.data.id).

Verify signatures

// Node / Express example
import crypto from 'crypto'

function verifyRolloSignature(rawBody, header, secret, toleranceSec = 300) {
  // header: "t=1723000000,v1=<hex>"
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('='))
  )
  const t = parts.t
  const v1 = parts.v1
  if (!t || !v1) return false
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')

  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}

app.post('/webhooks/rollo', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8')
  if (!verifyRolloSignature(raw, req.get('Rollo-Signature'), process.env.ROLLO_WH_SECRET)) {
    return res.status(400).send('invalid signature')
  }
  const event = JSON.parse(raw)
  // enqueue for async processing; respond 200 immediately
  res.status(200).json({ received: true })
})

Delivery & retries

Rollo POSTs to your URL with Content-Type: application/json. Non-2xx responses and network errors are retried up to 5 times with backoff (about 30s → 2m → 10m → 30m). After that the delivery is marked failed in Dashboard → Webhooks.

Event types

  • checkout_session.completed — Checkout finished successfully
  • checkout_session.expired — Session expired unpaid
  • payment.succeeded — Charge succeeded
  • payment.failed — Charge failed
  • payment.refunded — Refund issued
  • customer.created — Customer record created
  • subscription.created — Subscription created
  • subscription.updated — Subscription changed
  • subscription.trial_will_end — Trial ending soon (~3 days)
  • subscription.canceled — Subscription canceled
  • subscription.paused — Billing collection paused
  • subscription.resumed — Billing collection resumed
  • invoice.paid — Subscription invoice paid
  • invoice.payment_failed — Invoice payment failed
  • invoice.payment_action_required — Customer action needed on invoice
  • dispute.created — Dispute opened
  • dispute.updated — Dispute status changed
  • payout.paid — Payout deposited
  • payout.failed — Payout failed
  • payout.canceled — Payout canceled

Subscription lifecycle payloads

Use these events to provision and revoke access. Amounts are always in the smallest currency unit.

subscription.created (after recurring checkout)

Example request

# Delivered automatically when checkout completes with a subscription

Example response

{
  "id": "evt_...",
  "type": "subscription.created",
  "created": 1723000100,
  "data": {
    "id": "sub_01HXYZ...",
    "payment": "pay_01HXYZ...",
    "amount": 2900,
    "currency": "usd",
    "interval": "month",
    "status": "trialing",
    "merchant_id": "acct_...",
    "livemode": true
  }
}

How to handle it

Create the user's entitlement using data.id as your subscription key. If status is trialing, grant trial access; if active, grant paid access. Optionally call GET /v1/subscriptions/:id for full fields.

invoice.paid (renewal or trial conversion)

Example request

# Fired when a renewal charge succeeds

Example response

{
  "id": "evt_...",
  "type": "invoice.paid",
  "created": 1725680000,
  "data": {
    "id": "pay_01HREN...",
    "subscription": "sub_01HXYZ...",
    "amount": 2900,
    "currency": "usd",
    "merchant_id": "acct_...",
    "livemode": true
  }
}

How to handle it

Extend the billing period in your app. Pair with subscription.updated (status active) when a trial converts.

invoice.payment_failed

Example request

# Fired when a renewal charge fails

Example response

{
  "id": "evt_...",
  "type": "invoice.payment_failed",
  "created": 1725680000,
  "data": {
    "id": "sub_01HXYZ...",
    "subscription": "sub_01HXYZ...",
    "message": "Card declined",
    "merchant_id": "acct_...",
    "livemode": true
  }
}

How to handle it

Mark the account past-due, email the customer to update payment method, and keep access according to your grace policy until subscription.canceled or recovery succeeds.

subscription.canceled

Example request

# Fired when cancel completes (immediate or at period end)

Example response

{
  "id": "evt_...",
  "type": "subscription.canceled",
  "created": 1726000000,
  "data": {
    "id": "sub_01HXYZ...",
    "status": "canceled",
    "cancel_at_period_end": false,
    "payment_collection_paused": false,
    "merchant_id": "acct_...",
    "livemode": true
  }
}

How to handle it

Revoke paid access for data.id. Do not wait for another API poll—this event is the source of truth.

checkout_session.completed

Example request

# Fired for one-time and subscription checkouts

Example response

{
  "id": "evt_...",
  "type": "checkout_session.completed",
  "created": 1723000000,
  "data": {
    "id": "cs_01HXYZ...",
    "payment": "pay_01HXYZ...",
    "amount": 4900,
    "currency": "usd",
    "merchant_id": "acct_...",
    "livemode": true
  }
}

How to handle it

Safe signal that checkout finished. For digital delivery, prefer payment.succeeded (money captured) and, for plans, subscription.created.

Recommended starter subscription set: subscription.created, subscription.updated, subscription.canceled, subscription.trial_will_end, invoice.paid, invoice.payment_failed, payment.succeeded.