Cet article est destiné à la personne qui implémente la réception des webhooks PassPass. Pour une vue d'ensemble et la configuration depuis le dashboard, consultez API publique et Webhooks.
Comprendre les données
Chaque webhook contient un ou plusieurs blocs de données. Voici ce que chacun représente.
Order (commande) : une transaction effectuée par un acheteur. Contient le montant total, le statut de paiement, l'identité de l'acheteur, le détail des tickets achetés (line_items), et éventuellement les données de facturation société (billing).
Attendees (participants) : un élément par billet dans la commande. Si un acheteur prend 3 tickets, il y aura 3 entrées dans attendees. Chaque entrée contient le type de ticket, le statut du billet, et selon vos permissions : les coordonnées du participant, le prix payé, les réponses au formulaire, et le jeton d'accès au billet.
Event (événement) : les informations de base de votre événement PassPass (titre, dates, lieu, statut). Ce bloc est identique dans tous les types de webhooks.
Billing (facturation) : les données société de l'acheteur (nom, TVA, adresse). Présent uniquement si l'acheteur a coché "je commande au nom d'une société" au checkout. Sinon null.
Form answers (réponses formulaire) : les réponses aux champs personnalisés que vous avez configurés. Présentes à deux niveaux : au niveau commande (form_answers dans order) et au niveau participant (form_answers dans chaque attendee).
Access token (jeton d'accès) : l'identifiant secret derrière le QR code d'un billet, lu par l'application mobile PassPass Organizer lors du contrôle des entrées. Ce jeton peut par exemple être réattribué sur un badge pour répliquer un QR code lisible par le contrôle de tickets PassPass. Soyez vigilant lors de son utilisation : transmettre ce jeton revient à donner l'accès au billet.
Événements disponibles
Chaque type d'événement envoie des blocs de données différents :
order.created : commande créée (avant paiement) → order, event, attendees
order.paid : paiement confirmé (inclut aussi les commandes gratuites finalisées) → order, event, attendees
order.cancelled : commande annulée → order, event, attendees
order.expired : panier expiré (25 min sans paiement) → order, event, attendees (vide)
ticket.validated : billet scanné à l'entrée → scan, participant
event.published : événement publié → event
event.updated : événement modifié → event, changed_fields
Permissions (scopes)
Les données incluses dans le payload dépendent des permissions configurées sur votre endpoint. Un champ non autorisé est absent du payload, pas null.
events:read : bloc event (titre, dates, lieu, statut)
orders:read : bloc order de base (montant, acheteur, billing, line items, statut)
orders:read:financial : service_fee, payout_amount, refundable_amount, historical_service_fee
orders:read:form_answers : réponses au formulaire de commande
attendees:read : bloc participant minimal (ID, ticket, statut, dates)
attendees:read:contact : first_name, last_name, email
attendees:read:financial : ticket_price
attendees:read:form_answers : réponses au formulaire participant
attendees:read:full : access_token (inclut automatiquement contact et financial)
access_token est l'identifiant secret derrière le QR code du billet. Ne demandez attendees:read:full que si votre système en a réellement besoin.
Structure d'un payload
Chaque webhook est un POST JSON avec cette enveloppe :
{
"type": "order.paid",
"created_at": "2026-04-17T10:00:00.000Z",
"data": { }
}
Conventions :
Tous les champs sont en snake_case
Les montants sont en euros (pas en centimes).
25= 25,00€Les dates sont en ISO 8601 UTC
Les champs multilingues sont des objets
{"fr": "...", "en": "...", "nl": "..."}Exception : les blocs buyer et billing sont en camelCase
Le payload peut recevoir de nouveaux champs sans préavis. Ne rejetez pas un champ inconnu
Exemple complet : order.paid
Déclenché lorsqu'un paiement est confirmé, ou lorsqu'une commande gratuite est finalisée. C'est l'événement le plus utilisé : il confirme qu'un participant a bien complété son inscription. Cas d'usage : ajouter le participant dans votre CRM, envoyer un email de bienvenue, mettre à jour votre comptabilité, déclencher un workflow Zapier/Make/n8n.
{
"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"
}
]
}
]
}
}
Exemple : ticket.validated
Déclenché lorsqu'un billet est scanné via l'application mobile PassPass Organizer. Cas d'usage : mettre à jour un tableau de présences en temps réel, déclencher l'impression d'un badge, envoyer une notification à votre équipe à l'arrivée d'un VIP.
{
"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"
}
}
}
Exemple : event.updated
Déclenché lorsqu'un champ suivi de votre événement est modifié (titre, dates, lieu, description, statut...). Le payload contient l'état de l'événement après modification et la liste des champs modifiés. Cas d'usage : synchroniser les informations de l'événement sur votre site web, notifier votre équipe d'un changement de date ou de lieu.
{
"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"]
}
}
Vérification de la signature
Chaque requête contient un header de signature :
x-passpass-signature: t=1776412800,v1=9f2c...
La signature est un HMAC-SHA256 calculé avec votre secret (whsec_...) sur la chaîne <timestamp>.<corps brut de la requête>.
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", ""))
Rejetez les requêtes dont le timestamp a plus de 5 minutes.
Headers HTTP
Chaque requête webhook contient ces headers :
x-passpass-event : type d'événement (ex.
order.paid). Permet de router sans parser le corpsx-passpass-signature : signature HMAC pour vérifier l'authenticité
x-passpass-timestamp : horodatage Unix (secondes) utilisé dans la signature
x-passpass-idempotency-key : clé de déduplication. Stable pour un même événement, y compris lors des réessais. Dédupliquez sur ce champ côté receveur
x-passpass-delivery-id : identifiant unique de la tentative de livraison. Utile pour le debug
Réessais et désactivation
Votre endpoint doit répondre HTTP 2xx en moins de 10 secondes. Tout autre code, un timeout ou une erreur réseau compte comme un échec.
En cas d'échec, PassPass réessaie 6 fois avec un backoff exponentiel : 1, 2, 4, 8, 16, 32 minutes.
Après 20 échecs consécutifs, l'endpoint est désactivé automatiquement. Il doit être réactivé manuellement depuis votre dashboard. Un succès remet le compteur à zéro.
Accusez réception immédiatement (HTTP 200) et traitez en arrière-plan. Si le traitement est long, ne faites pas attendre PassPass.
Tester votre endpoint
Depuis votre dashboard (Paramètres → Développeurs), cliquez sur Tester sur votre endpoint. PassPass envoie un payload de démonstration à votre URL pour valider le branchement et la vérification de signature.
Les payloads de test utilisent des identifiants reconnaissables : IDs à partir de 9 999 000, email demo@passpass.be, noms "Demo ...".
L'historique des livraisons est consultable depuis votre dashboard. Chaque envoi (réussi ou échoué) y est enregistré avec le code de réponse de votre serveur.
Pour toute question technique ou besoin d'accompagnement, n'hésitez pas à contacter notre support.