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-Signaturebefore parsing business logic. - Persist
idand skip duplicates (idempotent handlers). - Return HTTP 2xx within a few seconds. Do heavy work asynchronously.
- Read fields from
datadirectly (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 successfullycheckout_session.expired— Session expired unpaidpayment.succeeded— Charge succeededpayment.failed— Charge failedpayment.refunded— Refund issuedcustomer.created— Customer record createdsubscription.created— Subscription createdsubscription.updated— Subscription changedsubscription.trial_will_end— Trial ending soon (~3 days)subscription.canceled— Subscription canceledsubscription.paused— Billing collection pausedsubscription.resumed— Billing collection resumedinvoice.paid— Subscription invoice paidinvoice.payment_failed— Invoice payment failedinvoice.payment_action_required— Customer action needed on invoicedispute.created— Dispute openeddispute.updated— Dispute status changedpayout.paid— Payout depositedpayout.failed— Payout failedpayout.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.
subscription.created, subscription.updated, subscription.canceled, subscription.trial_will_end, invoice.paid, invoice.payment_failed, payment.succeeded.