Guide

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

  1. Settings → Integrations → Webhooks → New configuration.
  2. URL: a publicly-reachable HTTPS URL on your service. HTTP isn't accepted in production.
  3. 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.
  4. Events: tick the event types you want delivered. Listening to fewer is better — server load and noise both scale with the count.
  5. 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.
  6. 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 id may arrive twice (retry after a transient flap, or processor-side replay). Dedupe on id.
  • No ordering guarantee. Two events emitted in close succession may arrive in either order. Order by timestamp if 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.