Appearance
Webhooks & API
Webhooks send bagevent data to your own systems the moment something happens — a registration, a payment, a check-in. The API lets your systems read an event's attendees and orders whenever they need to. Both live on the Integrations page of the organizer app, each on its own tab: Webhooks and API keys.
Plan and permission
Webhooks and API keys are on the Business plan and above; on other plans the Integrations page offers the upgrade instead. Within your organization, the person setting them up also needs the OpenAPI & webhooks permission, because an API key can read attendee data.
Webhooks
Add a webhook
- In the organizer app, open Settings → Integrations in the left menu, then the Webhooks tab.
- Choose + Add.
- Fill in:
- Name — something you will recognise later, e.g. Salesforce sync.
- Endpoint URL — the
https://address that should receive the data: your CRM's inbound URL, an automation platform's webhook trigger, or a service you run. bagevent sends a JSONPOSTand expects a2xxresponse. Plainhttp://addresses are refused. - Events — tick only the event types this endpoint needs.
- Scope — All events (every event in your organization, including ones you create later) or Selected events only.
- Save. bagevent shows the webhook's signing secret once. Copy it now and store it with your endpoint's configuration — it cannot be shown again.
Event types
| Group | Events |
|---|---|
| Attendees | attendee.registered, attendee.updated, attendee.cancelled, attendee.checked_in |
| Orders | order.created, order.paid, order.refunded, order.cancelled |
| Invoices | invoice.issued, invoice.voided |
| Speakers | speaker.submitted, speaker.accepted, speaker.declined |
| Events | event.published, event.updated |
attendee.checked_in is the one most CRMs cannot get any other way: not who registered, but who was actually in the room, and at which check-in point.
What a delivery looks like
Every delivery is a POST with a JSON body of the same shape:
json
{
"event": "attendee.checked_in",
"timestamp": "2026-09-25T08:14:03Z",
"data": { "attendee": { "…": "…" }, "event": { "…": "…" } }
}and these headers:
| Header | Contains |
|---|---|
X-BagEvent-Event | The event type, e.g. order.paid |
X-BagEvent-Delivery | A unique ID for this delivery — use it to ignore a duplicate |
X-BagEvent-Signature-V2 | t=<unix seconds>,v1=<signature> — verify this one |
X-BagEvent-Signature | Older body-only signature (sha256=<hex>), still sent on every delivery; do not build new checks on it |
Verify the signature
The signature proves a request came from bagevent and was not replayed later. For each delivery:
- Read
tandv1fromX-BagEvent-Signature-V2. - Compute
HMAC-SHA256(signing secret, t + "." + raw body)as lower-case hex. Use the raw body bytes, before any JSON parsing. - Compare it with
v1in constant time. - Reject the request if
tis more than about five minutes from your server's clock.
js
// Node.js
import crypto from 'node:crypto'
function isFromBagEvent(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}Test before real traffic
On the webhook's card, choose Test →. bagevent sends a sample payload for the first event type the webhook subscribes to, marked "test": true, and shows whether your endpoint answered with a 2xx, how long it took, and the payload it sent. Test sends do not count against the webhook's health.
Retries, the log, and pausing
- Failed deliveries are retried automatically — up to three attempts in total, 10 seconds and then 60 seconds apart. A redirect (
3xx) counts as a failure: publish the final URL. - Logs on the webhook's card lists every attempt with the request, the response, and the time taken. Open a failed delivery and choose Retry now → once your endpoint is fixed.
- After ten failures in a row the webhook is paused automatically and marked Error, so bagevent stops sending to an endpoint that is not there. Fix the endpoint, then choose Resume from the card's ··· menu. You can pause and resume a webhook yourself the same way.
API keys
The API reads data; it does not create registrations or change orders. Most integrations use webhooks to hear about changes as they happen and the API to backfill or reconcile.
Create a key
- On Settings → Integrations, open the API keys tab, then type a label (for example CRM sync) and choose Create.
- bagevent shows the key's secret once. Store it in your integration's secret storage; it cannot be shown again.
- To stop a key working, choose Revoke on its card. Anything using it stops immediately.
A key never sees more than the person who created it: if that person's role masks attendee emails or phone numbers, the API returns them masked too. Check-in barcodes are not returned.
Read attendees and orders
GET https://api.bagevent.io/api/v1/open/events/{eventId}/attendees
GET https://api.bagevent.io/api/v1/open/events/{eventId}/orders{eventId} is the event's key: the 10-character code in the organizer app's address bar when you have the event open (…/events/Hk7mPq2xYz/…). The event's numeric ID — the event.id in webhook payloads — works too. Sign the path exactly as you send it.
Sign each request
Send three headers with every request:
| Header | Value |
|---|---|
X-Api-Key | The key shown on its card |
X-Timestamp | The current time in Unix seconds |
X-Signature | HMAC-SHA256(secret, payload) as lower-case hex |
The signed payload is six lines joined by \n, with no trailing newline:
v2
<X-Timestamp>
<METHOD, upper case>
<path, without the query string>
<query string exactly as sent, or empty>
<sha256 hex of the request body — for a GET, of an empty body>The empty-body hash is e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
A signature is good for one request: resending the same one is refused, so a retry needs a fresh timestamp and signature. Requests with a timestamp more than five minutes off are refused. Each key is rate-limited, so keep polling well under 300 requests a minute and pace any catch-up after an outage.
Older keys (v1 signing)
Keys created before v2 signing was introduced still sign the old way, which covers neither the query string nor the body. They keep working, and their card shows a Legacy v1 signing chip. To move one to v2:
- Update your integration to sign the v2 payload described above.
- Prove it on a real request: send it signed the v2 way with the extra header
X-Signature-Version: v2. While the key is still on v1, that header checks that one request as v2 — a200means your integration is ready. - On the key's card, choose Switch to v2 signing and confirm. After that you can drop the header.
The switch is one-way: once a key is on v2, requests signed the v1 way are refused and the key can't be moved back.
Related
- HubSpot: a ready-made connection for HubSpot contacts and marketing events, no endpoint needed.
- Exporting data — one-off CSV exports instead of a live connection.
- Check-in — where
attendee.checked_incomes from. - API & webhooks on bagevent.io