# Guide to ROLLER Webhooks: Features and Best Practices

ROLLER webhooks tell your systems what is happening at your venue as it happens: a booking is made, changed or cancelled, a guest record is created, a ticket is scanned at the gate, a waiver is signed, a payment link is paid, or a bulk export finishes. If you run a trampoline park, water park, museum or other attraction on ROLLER, webhooks are how bookings and redemptions reach your CRM, marketing tools and reporting without polling the API.

This guide covers how ROLLER webhooks are registered and authenticated, the message envelope and event types, ROLLER's strict response rules and retry schedule, and the practices that keep a webhook from being disabled.

## What are ROLLER webhooks?

ROLLER webhooks are managed entirely through the REST API. You register a webhook with an HTTPS URL, an `apiKey` of your choosing, and the webhook types and events you want, such as booking `Created` and `Cancelled`. ROLLER then sends an HTTP `POST` to the URL each time one of those events happens.

Every message uses the same envelope. `type` says which webhook it is (`Booking`, `Customer`, `Redemption` and so on) and `eventType` says what happened (`Created`, `Updated` or `Cancelled`). The record itself, such as the booking, is in `data`.

ROLLER doesn't sign webhooks. The `authentication.apiKey` you set when you create the webhook comes back as plain text in the `x-roller-apikey` header of every delivery. Comparing that header with the value you chose is how your receiver tells a ROLLER request from a forged one.

## ROLLER webhook features

| Feature | Details |
| --- | --- |
| Registration | REST API only: `POST /webhooks`, plus view, update and delete endpoints |
| Activation | `enabled` defaults to `false`. Set it to `true` |
| Endpoint | HTTPS |
| Method | `POST` |
| Envelope | `id`, `sendDate`, `type`, `eventType`, `eventDate`, `data` |
| Authentication | `x-roller-apikey: <authentication.apiKey>`. No signature |
| Response window | 10 seconds maximum, under 1 second ideally |
| Success | `200 OK` |
| Retries | Seven attempts over about four days for failures and `500`, `503` and `504` responses |
| Other status codes | Trigger a review and possible deactivation |
| Ordering | Not guaranteed |
| Typical latency | 2 to 5 seconds after the event |
| Source IPs | Published per region (US, APAC, EMEA), optional allowlist |
| API usage | Managing webhooks counts toward your API package limit; receiving them does not |

## Common events

| `type` | `eventType` | Sent when |
| --- | --- | --- |
| `Booking` | `Created` | A reserved booking is created (draft bookings are not included) |
| `Booking` | `Updated` | A booking changes: items, contacts, payments, refunds, discounts, ticket details and more |
| `Booking` | `Cancelled` | A booking is cancelled |
| `Customer` | `Created` | A guest is created |
| `Customer` | `Updated` | A guest is updated |
| `Redemption` | `Created` | A ticket is redeemed |
| `BulkExport` | `Created` | An export has finished and its files are ready to download |
| `SignedWaiver` | `Created` | An adult waiver, with any associated minors, is signed |
| `PaymentLink` | `Created` | A payment link is created |
| `PaymentLink` | `Updated` | A payment is recorded against a payment link |
| `PaymentLink` | `Cancelled` | A payment link is cancelled |

ROLLER never sends a combined name such as `Booking.Created`. Route on the pair of `type` and `eventType`. The guest webhook's `type` is `Customer`, not `Guest`. The envelope's `eventType` enum also lists `Deleted`, though no webhook type documents sending it, so log it if it ever turns up.

Several events have rules worth knowing before you build on them. ROLLER POS batches redemptions every 15 seconds, so one `Redemption` message can carry several tickets, and redeeming stock and add-on products sends no webhook at all. Creating a booking does not also fire `Booking` `Updated`, and neither do redeeming tickets or editing the guest record attached to a booking.

The envelope looks like this:

```json
{
  "id": "67d2ada6-10cc-4177-909d-430f3b2593d4",
  "sendDate": "2024-06-14T05:30:48.1597733Z",
  "type": "Booking",
  "eventType": "Created",
  "eventDate": "2024-06-14T05:30:48.1132441Z",
  "data": {
    "...": "the booking payload"
  }
}

```

The `id`, `sendDate` and `eventDate` values come from ROLLER's schema. ROLLER publishes the models for `data` (such as the booking payload) but not a complete delivered example. Optional fields in `data`, such as tickets, payments or membership details, appear only if you turn on the matching `include` flag when you register the webhook.

> **Inspect ROLLER webhooks as they arrive.** Register a webhook against a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to see the exact envelope and `data` your venue sends, with no account or setup required.

## Setting up ROLLER webhooks

Generate a long random value for the API key first, for example with `openssl rand -hex 32`. ROLLER's own example value is a readable label (`CRM Trigger Webhook 002`), which anyone could guess.

Register the webhook with the REST API, using an access token for your venue:

```bash
curl --request POST \
  --url https://api.roller.app/webhooks \
  --header "Authorization: Bearer $ROLLER_ACCESS_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{
    "url": "https://example.com/webhooks/roller",
    "enabled": true,
    "authentication": { "apiKey": "YOUR_LONG_RANDOM_VALUE" },
    "webhooks": {
      "booking": { "events": ["Created", "Updated", "Cancelled"] },
      "redemption": { "events": ["Created"] }
    }
  }'

```

The access token comes from ROLLER's OAuth client credentials flow. Use `https://api.play.roller.app` instead to test against a ROLLER Playground venue. `enabled` must be `true` or the webhook stays inactive, and at least one webhook type needs an `events` list. Each type also takes optional `include` flags for extra fields and `filter` objects, for example to limit booking webhooks to certain products, sales channels or the application that triggered the change.

Deleting or disabling the API key you used to register a webhook does not delete or disable the webhook. Use the delete endpoint when you retire an integration.

To route through Hookdeck, create a source with the **ROLLER** type and use its URL as `url`. The source has one field, **API Key**. As Hookdeck's setup note says, set the webhook's `apiKey` to a long random value, then paste the same value there. Hookdeck compares it with the `x-roller-apikey` header and accepts `POST`.

## Securing ROLLER webhooks

Compare `x-roller-apikey` with your stored value, in constant time. ROLLER adds no prefix, so compare the whole value.

```javascript
const crypto = require('crypto');
const express = require('express');

const API_KEY = process.env.ROLLER_WEBHOOK_API_KEY;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/roller', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('x-roller-apikey');
  if (!API_KEY || typeof received !== 'string' || !safeEqual(received, API_KEY)) {
    // Not a ROLLER request. ROLLER itself never sees this response.
    return res.sendStatus(401);
  }

  let message;
  try {
    message = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Store message.id, queue the work keyed on `${message.type}.${message.eventType}`,
  // and answer well inside 10 seconds.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

API_KEY = os.environ.get("ROLLER_WEBHOOK_API_KEY", "")

def verify_roller_request(headers) -> bool:
    if not API_KEY:
        return False  # never accept when unconfigured
    received = headers.get("x-roller-apikey")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), API_KEY.encode("utf-8"))

```

A static API key authenticates the sender, not the body. It cannot detect a modified payload, and with no signature or timestamp a captured request can be replayed and still pass. Use a long random key, keep the HTTPS endpoint ROLLER requires, and rotate the key by updating the webhook if it might have leaked. ROLLER's published egress IPs give you a second, optional check at your firewall.

> **Make ROLLER webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `x-roller-apikey` header, answers ROLLER with a `200` in well under a second, and queues every booking and redemption durably for your service.

## ROLLER webhook limitations and pain points

### The wrong status code can switch a webhook off

**The Problem:** Your endpoint returns `401`, `404` or `422` during a deploy or because of a bug, and ROLLER flags the webhook for deactivation instead of retrying.

**Why It Happens:** ROLLER accepts `200` as success and retries only on `500`, `503` and `504`. It says any other status code "will trigger an automatic review and potential deactivation of the webhook".

**Workarounds:**

* Return `200` for every genuine ROLLER request you have stored, even if processing fails later.
* Return `503` for temporary problems on your side, so ROLLER retries.

**How Hookdeck Can Help:** Hookdeck answers ROLLER with a `200` as soon as a request is stored, so your service's status codes never reach ROLLER. Failed deliveries to your service are [retried](/docs/retries) by Hookdeck instead.

### Ten seconds, and a webhook that dies after four days

**The Problem:** A slow handler causes retries, and a sustained outage disables the webhook altogether.

**Why It Happens:** ROLLER wants a response within 10 seconds, ideally under 1. Failed deliveries are retried immediately, then after 1 minute, 1 minute, 15 minutes, 1 hour, 1 day and 3 days. The venue's account owner and primary contact are alerted at the sixth attempt, and after the seventh the webhook is disabled.

**Workarounds:**

* Store the message and respond, then process it in a background job, as ROLLER recommends.
* Make sure the venue's account owner and primary contact in ROLLER are people who will act on the alert.

**How Hookdeck Can Help:** Hookdeck's ingestion stays up when your service doesn't, so ROLLER sees successful deliveries and never reaches its deactivation threshold. [Issues](/docs/issues) alert you to failing deliveries on your side.

### Events arrive out of order

**The Problem:** A redemption arrives before the `Booking` `Created` it belongs to.

**Why It Happens:** ROLLER does not guarantee delivery order, and retries make late arrivals more likely.

**Workarounds:**

* Use `eventDate` to decide which change is newest before applying it.
* Fetch missing objects from the API when an event refers to something you haven't seen yet, as ROLLER suggests.

**How Hookdeck Can Help:** Every message is stored in Hookdeck with its arrival time, so you can see what arrived when and replay events after a fix.

### A guessable key

**The Problem:** The `x-roller-apikey` value is a readable name, such as the one in ROLLER's example, and anyone who guesses it can send fake bookings.

**Why It Happens:** `authentication.apiKey` is described as "a value used to identify this webhook", so it is easy to treat as a label.

**Workarounds:**

* Use a long random value and store it like a password.
* Add ROLLER's regional egress IPs to your allowlist as a second check.

**How Hookdeck Can Help:** Hookdeck's ROLLER source compares the header with the value you paste. Hookdeck still answers with a `200`, but a request with a missing or wrong key is recorded as failed verification and never reaches your service.

## Best practices

### Route on `type` and `eventType` together

Neither field identifies the event alone. Build a key such as `Booking.Cancelled` in your own code and acknowledge pairs you don't handle.

### Deduplicate on `id`

Each message has a unique `id`. Store it before processing and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Expect batched redemptions

A `Redemption` message can describe several tickets redeemed in the same 15-second window at POS. Process every ticket in `data`.

### Ask only for the fields you need

The `include` flags add fields such as tickets, payments and membership details to `data`. Turning on only what you use keeps payloads small.

### Acknowledge fast and process asynchronously

ROLLER's 1-second target leaves no room for work in the request path. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

ROLLER sends each webhook as a JSON `POST` with the `apiKey` you registered in the `x-roller-apikey` header and no signature. Choose a long random key, compare it in constant time, route on `type` plus `eventType`, deduplicate on `id`, and answer with `200` well inside 10 seconds, because ROLLER disables webhooks that keep failing and reviews any that return unexpected status codes.

[Hookdeck Event Gateway](https://hookdeck.com) checks the key, answers ROLLER immediately, and keeps every event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle ROLLER webhooks reliably in minutes.