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.completedCheckout finished successfully
  • checkout_session.expiredSession expired unpaid
  • payment.succeededCharge succeeded
  • payment.failedCharge failed
  • payment.refundedRefund issued
  • customer.createdCustomer record created
  • subscription.createdSubscription created
  • subscription.updatedSubscription changed
  • subscription.trial_will_endTrial ending soon (~3 days)
  • subscription.canceledSubscription canceled
  • subscription.pausedBilling collection paused
  • subscription.resumedBilling collection resumed
  • invoice.paidSubscription invoice paid
  • invoice.payment_failedInvoice payment failed
  • invoice.payment_action_requiredCustomer action needed on invoice
  • dispute.createdDispute opened
  • dispute.updatedDispute status changed
  • payout.paidPayout deposited
  • payout.failedPayout failed
  • payout.canceledPayout 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.