Webhooks
Webhooks are how Trybe pushes events to your systems in real time — new bookings, customer updates, completed payments, membership state changes. Register one or more endpoints, pick the events you care about, and Trybe POSTs a signed JSON payload to your URL every time one of those events fires.
Set up a webhook endpoint
- Settings → Integrations → Webhooks → New configuration.
- URL: a publicly-reachable HTTPS URL on your service. HTTP isn't accepted in production.
- Secret: a string of 12+ characters, used to sign every payload. Treat it like a password; rotate via the same UI when needed. You must set a secret — without one, every delivery is sent unsigned and your handler has no way to verify the request came from Trybe. The signature header is only present when a secret is configured.
- Events: tick the event types you want delivered. Listening to fewer is better — server load and noise both scale with the count.
- Notification emails (optional): addresses to alert when a webhook is failing repeatedly. Highly recommended on every config — webhooks can be silently auto-disabled (see Reliability) and you want to hear about it before partners do.
- Save, flip Enabled to on. Deliveries start immediately.
Per-site, not global
A WebhookConfig belongs to a single site. To receive deliveries
across a multi-tenant setup, create one config per site. Exception:
customer-scoped events (customer.*, customer_address.*,
customer_labels.*, customer_note.*,
marketing_preference_opt_in.*) are emitted at the brand level
— customers are first-class on a brand. Registering a webhook on one
site within a brand is enough to receive every customer event for
the whole brand.
Brands and sites aren't always 1:1. Confirm the brand layout with your Onboarding Manager before assuming a single config will receive everything you expect.
Payload envelope
Every webhook Trybe sends follows the same envelope:
{
"id": "66c36d4eef7657ae2b096128",
"event": "customer.updated",
"timestamp": "2026-05-19T16:05:34+00:00",
"webhook_config_id": "66ba3d8f13e43589da030ad8",
"site_id": "00000000-0000-0000-0000-111111111111",
"data": { /* shape varies per event — see below */ }
}
| Field | Notes |
|---|---|
id |
Stable per event. Retries reuse the same id — use it for idempotent processing. |
event |
<resource>.<action> (e.g. basket.submitted). |
timestamp |
When the event was generated, RFC 3339. |
webhook_config_id |
Which of your configs produced this delivery. |
site_id |
The Trybe site the affected object belongs to. |
data |
Per-event payload — see the table below for shape. |
Event types
Trybe emits 24 event types across 10 groups. For each, data matches
the shape of an existing API response — click the operation link to
see the full schema.
Basket
| Event | When | data shape |
|---|---|---|
basket.submitted |
An order is submitted, from any source | Order |
basket.settled |
An order is checked out | Order |
basket.cancelled |
A whole order is cancelled | Order |
basket.no_showed |
An order is marked as no-show | Order |
Basket items
| Event | When | data shape |
|---|---|---|
basket_item.added |
An item is added to a submitted order | OrderItem |
basket_item.updated |
An item in a submitted order is updated | OrderItem |
Customer
| Event | When | data shape |
|---|---|---|
customer.created |
A customer is created | Customer |
customer.updated |
A customer is updated | Customer |
customer.deleted |
A customer is deleted (returns just the deleted ID) | Customer |
Customer addresses
| Event | When | data shape |
|---|---|---|
customer_address.created |
An address is added to a customer | CustomerAddress |
customer_address.updated |
A customer address is updated | CustomerAddress |
customer_address.deleted |
A customer address is deleted (returns just the deleted ID) | CustomerAddress |
Customer labels
data includes a customer_id plus an array of labels, each as
returned from getCustomerLabels.
| Event | When |
|---|---|
customer_labels.added |
Label(s) added to a customer |
customer_labels.removed |
Label(s) removed from a customer |
Customer notes
| Event | When | data shape |
|---|---|---|
customer_note.created |
A note is added to a customer | CustomerNote |
customer_note.updated |
A customer note is updated | CustomerNote |
customer_note.deleted |
A customer note is deleted (returns just the deleted ID) | CustomerNote |
Marketing preferences
| Event | When | data shape |
|---|---|---|
marketing_preference_opt_in.updated |
A marketing-preference opt-in is updated | MarketingPreferenceOptIn |
Memberships
| Event | When | data shape |
|---|---|---|
membership.confirmed |
A membership is confirmed | Membership |
membership.charged |
A membership recurring charge runs | Membership |
membership.cancelled |
A membership is cancelled | Membership |
membership.expired |
A membership expires | Membership |
Payments
| Event | When | data shape |
|---|---|---|
payment.complete |
A payment settles | Payment |
Refunds
| Event | When | data shape |
|---|---|---|
refund.complete |
A refund completes | Refund |
Verifying the signature
Every delivery carries a signature header — a SHA-256 HMAC of the
raw request body, keyed by the secret you set on the webhook config.
Always verify it. A request that doesn't validate is either
malicious or stale; reject with a 400.
The hash is computed against the raw body bytes — not against a JSON re-serialisation. If you parse the body, hash, then compare, you'll get the wrong signature whenever Trybe's JSON encoder and yours format whitespace differently.
The most common failure mode: receiving the body, calling
JSON.parse (or your language's equivalent), then JSON.stringify
to re-serialise, then HMAC-ing the result. Languages disagree on
forward-slash escaping inside string values ("https:\/\/…" vs
"https://…"), Unicode escapes, and key ordering — your computed
signature will silently never match. Capture the raw body once,
verify the signature against it, then parse a copy. Express's
express.raw({ type: 'application/json' }) and equivalent middleware
in other frameworks exists precisely for this.
Node.js / Express
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRET = process.env.TRYBE_WEBHOOK_SECRET
// Capture the raw body for signature verification.
app.use(
'/trybe-webhooks',
express.raw({ type: 'application/json' }),
)
app.post('/trybe-webhooks', (req, res) => {
const expected = crypto
.createHmac('sha256', SECRET)
.update(req.body) // raw Buffer, not parsed JSON
.digest('hex')
const actual = req.header('signature')
if (!actual || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(actual))) {
return res.status(400).end()
}
const event = JSON.parse(req.body.toString('utf8'))
// …handle the event…
res.status(202).end()
})
PHP
$secret = getenv('TRYBE_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $secret);
$actual = $_SERVER['HTTP_SIGNATURE'] ?? '';
if (!hash_equals($expected, $actual)) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// …handle the event…
http_response_code(202);
Python
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["TRYBE_WEBHOOK_SECRET"].encode()
@app.post("/trybe-webhooks")
def webhook():
expected = hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
actual = request.headers.get("signature", "")
if not hmac.compare_digest(expected, actual):
abort(400)
event = request.get_json()
# …handle the event…
return "", 202
Test it locally
You can validate your verification code against a fixed payload + a
known secret (trybe2025) before pointing a real config at it:
curl --request POST \
--url https://your-domain.test/test-webhooks \
--header 'content-type: application/json' \
--header 'signature: fbae492a93a96bf4f70d49f8df24890d442fe633489c921febabf2e654f2a7f5' \
--data '{"id":"67b3183b6089b7bbfc031cf3","event":"basket.cancelled","timestamp":"2025-02-17T11:06:35+00:00","webhook_config_id":"665d9555b14332b5d40684c2","site_id":"00000000-0000-0000-0000-111111111111","data":{"id":"67b3169c94d924a821068de2","order_ref":"TRYCW"}}'
That signature is the SHA-256 HMAC of the body with secret trybe2025.
Reliability
Retries
Failed deliveries (network error, timeout, or any non-2xx response) are retried up to 3 times with a short backoff. After the third failure the event is marked failed and the delivery audit log records it; no further automatic retry.
Inspect the audit log via listWebhookCallAttempts —
useful when triaging "did we miss this event?" questions.
Auto-disable
If a webhook config is failing repeatedly, Trybe will disable it automatically to stop pummelling a broken endpoint. The trigger: 30 failed deliveries in the most recent 100 attempts within a rolling 7-day window. Once disabled, no further events are attempted until the config is re-enabled in the admin UI.
The notification_emails on the config receive an alert when this
happens. Always set at least one notification address — silently-
disabled webhooks are the worst class of integration bug because
the symptom is "no events", which can take days to notice.
Outbound IPs
Trybe sends webhook deliveries from a fixed list of IPv4 addresses and an IPv6 prefix. If your endpoint is behind an allowlist, ask your Onboarding Manager for the current list — it changes rarely but does shift during region migrations, so reconfirm before any critical rollout.
Delivery guarantees
- At-least-once delivery. The same event
idmay arrive twice (retry after a transient flap, or processor-side replay). Dedupe onid. - No ordering guarantee. Two events emitted in close succession
may arrive in either order. Order by
timestampif you need a deterministic sequence within your handler. - No replay API today. Once an event is exhausted on retries, it's not redeliverable via the API — request a manual replay via your Onboarding Manager if the data loss is material.
Handler best practices
Verify every signature. Already covered above; reject anything that doesn't match.
Return 2xx fast. Acknowledge with 202 before doing any expensive work. Push the event onto a queue and process asynchronously. A slow handler will trip retries and double-deliver.
Be idempotent. Trybe guarantees at-least-once delivery, not
exactly-once. The same event id may arrive twice — once from the
first attempt and once from a retry, or once on a flap. Log
processed IDs and short-circuit duplicates.
Subscribe narrowly. Tick only the events you actually consume. Listening to everything inflates your handler's load, your logs, and the chance you trip retries on events you don't care about.
Exempt the route from CSRF. Frameworks like Laravel or Rails
default to CSRF protection on POST. The webhook route needs an
explicit exemption — every Trybe delivery would otherwise be rejected.
Don't trust the body until you've verified. Parse and process after signature check, never before.