This article is intended for the person implementing the reception of PassPass webhooks. For an overview and configuration from the dashboard, see Public API and Webhooks.
Understanding the Data
Each webhook contains one or more data blocks. Here is what each represents.
Order : a transaction made by a buyer. Contains the total amount, payment status, buyer's identity, details of purchased tickets (line_items), and possibly company billing data (billing).
Attendees : one entry per ticket in the order. If a buyer takes 3 tickets, there will be 3 entries in attendees. Each entry contains the ticket type, ticket status, and depending on your permissions: participant contact details, price paid, form responses, and the ticket access token.
Event : basic information about your PassPass event (title, dates, location, status). This block is identical in all types of webhooks.
Billing : company data of the buyer (name, VAT, address). Present only if the buyer checked "I am ordering on behalf of a company" at checkout. Otherwise null.
Form answers : responses to the custom fields you have configured. Present at two levels: at the order level (form_answers in order) and at the participant level (form_answers in each attendee).
Access token : the secret identifier behind a ticket's QR code, read by the PassPass Organizer mobile app during entry control. This token can, for example, be reassigned on a badge to replicate a QR code readable by PassPass ticket control. Be careful when using it: transmitting this token is equivalent to giving access to the ticket.
Available Events
Each type of event sends different data blocks:
order.created : order created (before payment) → order, event, attendees
order.paid : payment confirmed (also includes completed free orders) → order, event, attendees
order.cancelled : order cancelled → order, event, attendees
order.expired : cart expired (25 min without payment) → order, event, attendees (empty)
ticket.validated : ticket scanned at entry → scan, participant
event.published : event published → event
event.updated : event updated → event, changed_fields
Permissions (scopes)
The data included in the payload depends on the permissions configured on your endpoint. An unauthorized field is absent from the payload, not null.
events:read : event block (title, dates, location, status)
orders:read : basic order block (amount, buyer, billing, line items, status)
orders:read:financial : service_fee, payout_amount, refundable_amount, historical_service_fee
orders:read:form_answers : order form responses
attendees:read : minimal participant block (ID, ticket, status, dates)
attendees:read:contact : first_name, last_name, email
attendees:read:financial : ticket_price
attendees:read:form_answers : participant form responses
attendees:read:full : access_token (automatically includes contact and financial)
access_token is the secret identifier behind the ticket's QR code. Request attendees:read:full only if your system truly needs it.
Payload Structure
Each webhook is a JSON POST with this envelope:
{
"type": "order.paid",
"created_at": "2026-04-17T10:00:00.000Z",
"data": { }
}
Conventions:
All fields are in snake_case
Amounts are in euros (not in cents).
25= 25,00€Dates are in ISO 8601 UTC
Multilingual fields are objects
{"fr": "...", "en": "...", "nl": "..."}Exception: buyer and billing blocks are in camelCase
The payload can receive new fields without notice. Do not reject an unknown field
Complete Example: order.paid
Triggered when a payment is confirmed, or when a free order is finalized. It is the most used event: it confirms that a participant has completed their registration. Use case: add the participant to your CRM, send a welcome email, update your accounting, trigger a Zapier/Make/n8n workflow.
{
"type": "order.paid",
"created_at": "2026-04-17T10:00:00.000Z",
"data": {
"order": {
"id": 65817,
"event_id": 1535,
"status": "paid",
"origin": "online",
"currency": "EUR",
"amount_total": 200,
"buyer": {
"firstName": "Marie",
"lastName": "Dupont",
"email": "marie@example.com"
},
"billing": {
"identifierType": "vat",
"companyName": "Demo Corp SRL",
"vat": "BE0123456789",
"vatCountry": "BE",
"addressStreet": "Rue de la Loi 1",
"addressCity": "Bruxelles",
"addressZip": "1000",
"addressCountry": "BE"
},
"line_items": [
{
"type": "ticket",
"ticket_id": 4525,
"name": {"fr": "Entrée Standard"},
"quantity": 2,
"unit_amount": 100,
"subtotal": 200
}
],
"form_answers": null,
"paid_at": "2026-04-17T10:00:00.000Z",
"cancelled_at": null,
"created_at": "2026-04-17T10:00:00.000Z",
"updated_at": "2026-04-17T10:00:00.000Z",
"service_fee": 4.6,
"payout_amount": 195.4,
"refundable_amount": 4.6,
"historical_service_fee": 4.6
},
"event": {
"id": 1535,
"title": {"fr": "Mon événement"},
"short_description": {"fr": "Description courte"},
"address_label": {"fr": "Bruxelles"},
"locality_type": "physical",
"start_at": "2026-06-01T18:00:00.000Z",
"end_at": "2026-06-01T23:00:00.000Z",
"timezone": "Europe/Brussels",
"currency": "EUR",
"status": "published",
"visibility": "public"
},
"attendees": [
{
"id": "019fdb0d-...",
"event_id": 1535,
"order_id": 65817,
"status": "active",
"ticket": {
"id": 4525,
"type": "ticket",
"name": {"fr": "Entrée Standard"}
},
"first_name": "Marie",
"last_name": "Dupont",
"email": "marie@example.com",
"ticket_price": 100,
"access_token": "019fdb0d-...",
"created_at": "2026-04-17T10:00:00.000Z",
"updated_at": "2026-04-17T10:00:00.000Z",
"form_answers": [
{
"type": "selection",
"field_id": "abc123...",
"question_title": {"fr": "Quel est votre statut actuel ?"},
"answer": {"fr": "Indépendant"}
},
{
"type": "unique-choice",
"field_id": "def456...",
"question_title": {"fr": "Régime alimentaire"},
"answer": {"fr": "Végétarien"}
},
{
"type": "short-text",
"field_id": "ghi789...",
"question_title": {"fr": "Nom entreprise"},
"answer": "Demo Corp"
},
{
"type": "formatted-field",
"field_id": "jkl012...",
"question_title": {"fr": "Téléphone"},
"answer": "+32400000000"
},
{
"type": "formatted-field",
"field_id": "mno345...",
"question_title": {"fr": "Code postal"},
"answer": "1000"
}
]
}
]
}
}
Example: ticket.validated
Triggered when a ticket is scanned via the PassPass Organizer mobile app. Use case: update a real-time attendance board, trigger badge printing, send a notification to your team upon a VIP's arrival.
{
"type": "ticket.validated",
"created_at": "2026-04-17T10:00:00.000Z",
"data": {
"scan": {
"id": "019d-...",
"type": "scan",
"direction": "enter",
"created_at": "2026-04-17T10:00:00.000Z"
},
"participant": {
"id": "019fdb0d-...",
"event_id": 1535,
"order_id": 65817,
"status": "active",
"ticket": {
"id": 4525,
"type": "ticket",
"name": {"fr": "Entrée Standard"}
},
"first_name": "Marie",
"last_name": "Dupont",
"email": "marie@example.com",
"ticket_price": 100,
"created_at": "2026-04-17T10:00:00.000Z",
"updated_at": "2026-04-17T10:00:00.000Z"
}
}
}
Example: event.updated
Triggered when a tracked field of your event is modified (title, dates, location, description, status...). The payload contains the state of the event after modification and the list of changed fields. Use case: synchronize event information on your website, notify your team of a date or location change.
{
"type": "event.updated",
"created_at": "2026-04-17T10:00:00.000Z",
"data": {
"event": {
"id": 1535,
"title": {"fr": "Mon événement (modifié)"},
"short_description": {"fr": "Nouvelle description"},
"address_label": {"fr": "Bruxelles"},
"locality_type": "physical",
"start_at": "2026-06-01T18:00:00.000Z",
"end_at": "2026-06-01T23:00:00.000Z",
"timezone": "Europe/Brussels",
"currency": "EUR",
"status": "published",
"visibility": "public"
},
"changed_fields": ["title", "short_description"]
}
}
Signature Verification
Each request contains a signature header:
x-passpass-signature: t=1776412800,v1=9f2c...
The signature is an HMAC-SHA256 calculated with your secret (whsec_...) on the string <timestamp>.<raw request body>.
Node.js:
const crypto = require('crypto');
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('='))
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 || '', 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python:
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
ts = int(parts.get("t", 0))
if not ts or abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Reject requests with a timestamp older than 5 minutes.
HTTP Headers
Each webhook request contains these headers:
x-passpass-event : event type (e.g.
order.paid). Allows routing without parsing the bodyx-passpass-signature : HMAC signature to verify authenticity
x-passpass-timestamp : Unix timestamp (seconds) used in the signature
x-passpass-idempotency-key : deduplication key. Stable for the same event, including retries. Deduplicate on this field on the receiver side
x-passpass-delivery-id : unique delivery attempt identifier. Useful for debugging
Retries and Deactivation
Your endpoint must respond HTTP 2xx in less than 10 seconds. Any other code, a timeout, or a network error counts as a failure.
In case of failure, PassPass retries 6 times with exponential backoff: 1, 2, 4, 8, 16, 32 minutes.
After 20 consecutive failures, the endpoint is automatically deactivated. It must be manually reactivated from your dashboard. A success resets the counter to zero.
Acknowledge receipt immediately (HTTP 200) and process in the background. If processing is long, do not make PassPass wait.
Testing Your Endpoint
From your dashboard (Settings → Developers), click Test on your endpoint. PassPass sends a demo payload to your URL to validate the connection and signature verification.
Test payloads use recognizable identifiers: IDs starting from 9,999,000, email demo@passpass.be, names "Demo ...".
The delivery history is viewable from your dashboard. Each send (successful or failed) is recorded there with your server's response code.
For any technical questions or need for support, feel free to contact our support.