Guides
Webhooks
Webhooks allow you to receive HTTP callbacks when events occur in your Liveday account. Instead of polling the API, you configure a URL and Liveday will send a POST request to it whenever a subscribed event fires.
All webhook events, payload schemas, and the delivery-inspection endpoints are also documented in the interactive API Reference under the Webhooks tag and the spec's webhooks section.
Getting started
- Create an endpoint — an HTTPS URL on your server that accepts
POSTrequests with a JSON body and responds with a2xxstatus code. - Register it — in the Integrations section of your Liveday dashboard, create a webhook integration, provide your URL, and select the events to subscribe to.
- Acknowledge quickly — return
2xxwithin 30 seconds. If processing takes longer, acknowledge first and process asynchronously. - Deduplicate — store the
X-Webhook-Delivery-IDheader value and skip deliveries you have already processed.
Events
| Event | Description | Payload |
|---|---|---|
OrderCompleted | Fired when an order is successfully completed for your organization | The complete order object — the same Order model returned by GET /analytics_search/api/transactions (see the API Reference) |
OrderUpdated | Fired when an order that was already completed for your organization changes, for example when one of its tickets is scanned (cart.items[].Ticket.scanned_at). Subscribe to it separately from OrderCompleted. Refunds are separate orders and are not sent as OrderUpdated. | The complete order object in its new state — the same Order model as OrderCompleted |
Delivery
When an event fires, Liveday sends an HTTP POST request to your configured URL with the event payload as JSON.
Headers
Every webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Delivery-ID | Unique ID for this delivery — stable across retries of the same delivery |
X-Webhook-Event-Type | The event type (OrderCompleted or OrderUpdated) |
X-Webhook-Attempt | Attempt number (starts at 1, up to 5) |
Example payload (OrderCompleted, abridged)
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "anna@example.com",
"phone_number": "+46701234567",
"total_price": 59800,
"payment_method": "card",
"payment_method_brand": "visa",
"is_refunded": false,
"order_number": 10042,
"order_source": "webshop",
"created_at": "2026-03-01T14:30:00Z",
"cart": {
"organization": "your-org-id",
"event": "evt_summer2026",
"currency": "SEK",
"total_price": 59800,
"original_price": 69800,
"items": [
{
"product_type": "Ticket",
"name": "General Admission",
"quantity": 2,
"price": 29900,
"vat": 6.0,
"event": "evt_summer2026",
"event_name": "Summer Festival 2026",
"category": "ticket"
}
],
"campaigns": [
{
"uuid": "camp_earlybird",
"name": "Early Bird",
"code": "EARLY2026",
"discount_amount": 10000
}
]
}
}
The payload is the full Order object and contains more fields than shown here — the complete, always up-to-date schema is in the Webhooks section of the API Reference.
Responding
Your endpoint should return a 2xx status code to acknowledge receipt. Any other status code — or a timeout or connection error — is treated as a failed attempt and triggers a retry.
Respond quickly
Your endpoint must respond within 30 seconds (connection timeout: 5 seconds). If processing takes longer, acknowledge the webhook immediately and process it asynchronously.
Retries and delivery guarantees
| Aspect | Behavior |
|---|---|
| Success | Any 2xx response within 30 seconds |
| Failure | Non-2xx response, timeout (30 s total / 5 s connect), or connection error |
| Attempts | Up to 5 attempts in total (1 initial + up to 4 retries) |
| Retry timing | Failed deliveries are re-queued immediately — a retry typically arrives within seconds of the failed attempt. A nominal exponential backoff schedule (5 s doubling per attempt, capped at 5 minutes, ±15% jitter) is recorded on the delivery record's next_retry_at field, but is not enforced as a wait — do not rely on spacing between attempts |
| After the 5th failure | The delivery is marked PermanentlyFailed and never retried |
| Ordering | No ordering guarantee — deliveries are independent messages on a standard queue and can arrive out of order |
| Duplicates | At-least-once delivery — in rare cases the same delivery can arrive more than once; deduplicate on X-Webhook-Delivery-ID |
Delivery statuses
| Status | Description |
|---|---|
Pending | Queued for delivery |
InProgress | Currently being delivered |
Success | Endpoint returned 2xx |
Failed | Attempt failed, will retry |
PermanentlyFailed | All attempts exhausted |
Verifying authenticity
Every delivery is signed. Your signing secret (whsec_…) is generated when the webhook integration is created and is shown on the integration in the Liveday dashboard (and returned by the integrations API).
Each delivery carries two extra headers:
| Header | Value |
|---|---|
X-Webhook-Timestamp | Unix timestamp (seconds) when this attempt was signed |
X-Webhook-Signature | v1=<hex> — HMAC-SHA256 of {timestamp}.{raw_body} keyed with your signing secret |
To verify:
- Reject the request if
|now − X-Webhook-Timestamp|exceeds 300 seconds (replay protection). - Compute
expected = "v1=" + hex(hmac_sha256(secret, timestamp + "." + raw_body))over the exact raw request body bytes — verify before parsing, never re-serialize the JSON. - Compare with a constant-time comparison.
import hmac, hashlib, time
def verify_webhook(secret: str, headers: dict, raw_body: bytes, tolerance: int = 300) -> bool:
ts, sig = headers.get("X-Webhook-Timestamp", ""), headers.get("X-Webhook-Signature", "")
if not ts or not sig.startswith("v1="):
return False
if abs(time.time() - int(ts)) > tolerance:
return False # stale — possible replay
expected = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig) # constant-time
Retried deliveries are re-signed with a fresh timestamp. Integrations created before signing shipped receive their secret the next time the integration is updated; unsigned deliveries (no signature headers) can still be authenticated by looking up the X-Webhook-Delivery-ID via GET /webhooks/deliveries/{delivery_id}.
Monitoring deliveries
You can monitor webhook delivery status from the Integrations section of your Liveday dashboard, or via the API:
| Endpoint | Description |
|---|---|
GET /webhooks/deliveries | List all webhook deliveries for your organization |
GET /webhooks/deliveries/{delivery_id} | Fetch a single delivery (status, attempt count, retry schedule) |
GET /webhooks/deliveries/{delivery_id}/attempts | List each attempt: HTTP status code, response body (truncated to 5000 characters), duration, and error message |
These endpoints currently authenticate with a Liveday dashboard user JWT (not sk_ API keys).
Payload
The OrderCompleted and OrderUpdated payloads are the full Order object as returned by the transactions API — see the Webhooks section of the API Reference for the complete, always up-to-date schema.
Updates can arrive out of order. The order's version increases with every change, so ignore an OrderUpdated delivery whose version is lower than one you have already processed.
Best practices
- Verify the event type — check the
X-Webhook-Event-Typeheader before processing - Use the delivery ID for idempotency — the
X-Webhook-Delivery-IDheader uniquely identifies each delivery and is stable across its retries; use it to avoid processing duplicates - Return 2xx quickly — acknowledge receipt immediately and process asynchronously if needed
- Don't rely on ordering or retry spacing — retries can arrive within seconds and deliveries can arrive out of order
- Verify authenticity — deliveries are unsigned; confirm the delivery via the read API before acting on sensitive data