Appearance
Webhooks
This guide explains how XS2Event webhooks work and provides practical examples for developers integrating XS2Event into Python applications.
1. Webhook overview
A webhook is an outbound HTTP POST request sent from XS2Event to a registered endpoint in your application. Webhooks allow you to receive real-time notifications when specific events occur in our system.
Delivery flow
- Registration: You register a webhook endpoint via the API, specifying the event type you want to listen for.
- Event Occurrence: An event occurs in XS2Event (e.g., a booking order is completed) or is manually triggered.
- Queueing: An action is queued in our system for delivery.
- Worker Processing: A background worker picks up the action and sends the HTTP
POSTrequest to your registered endpoint. - Retries: If the delivery fails (e.g., your server is down or returns a non-2xx status code), the system may retry delivery according to our retry policy.
Trigger only
XS2Event webhooks are triggers. They notify you that an event occurred but do not include the full resource data that changed. When you receive a webhook, you should always query our API endpoints using the provided resource_id to get the most up-to-date information.
Delivery is asynchronous. When you manually trigger a webhook, the endpoint returns 202 Accepted because the delivery is queued for processing rather than completed synchronously.
2. Supported events
The following table lists the event types currently supported by XS2Event webhooks:
| Event Type | Resource Type | Description |
|---|---|---|
bookingorder_completed | bookingorder | Triggered when a booking order is successfully completed. (For buyers) |
bookingorder_cancelled | bookingorder | Triggered when a booking order is cancelled. (For buyers) |
sale_created | sale | Triggered when a new sale resource is created. (For suppliers on our marketplace) |
sale_cancelled | sale | Triggered when a sale is cancelled. (For suppliers on our marketplace) |
3. Register a webhook
To start receiving events, you must register your endpoint using the POST /v1/webhooks endpoint.
Request fields
event_type: The type of event to subscribe to (e.g.,bookingorder_cancelled).endpoint: The full URL of your receiver endpoint (must be HTTPS in production).secret: A signing secret used to secure the webhook. This is mandatory for all webhooks.version: (Optional) Webhook configuration version, used for callback to your system (defaults to 1).is_active: (Optional) A boolean indicating whether the webhook is active (defaults to true).
Important: The Secret
- The
secretfield is mandatory. - The secret must contain between 8 and 128 characters.
- This secret is used to create the HMAC signature for all outgoing requests sent to your endpoint.
- For security reasons, the secret is never returned by the list or get endpoints. You must store it securely in your application.
Python registration example
python
import os
import requests
API_URL = os.environ.get("XS2EVENT_API_URL", "https://api.example.com")
API_TOKEN = os.environ["XS2EVENT_API_TOKEN"]
payload = {
"event_type": "bookingorder_cancelled",
"endpoint": "https://example.com/webhooks/xs2event",
"secret": os.environ["XS2EVENT_WEBHOOK_SECRET"],
}
try:
response = requests.post(
f"{API_URL}/v1/webhooks",
json=payload,
headers={
"Authorization": f"X-Api-Key {API_TOKEN}",
"Content-Type": "application/json",
},
timeout=10,
)
response.raise_for_status()
webhook = response.json()
print("Webhook registered successfully:")
print(webhook)
except requests.exceptions.RequestException as e:
print(f"Error registering webhook: {e}")Example JSON representation
Request Body:
json
{
"event_type": "bookingorder_cancelled",
"endpoint": "https://your-domain.com/webhooks/xs2event",
"secret": "YOUR_WEBHOOK_SECRET",
"version": 1
}Response Body:
json
{
"webhook_id": "wh_123456789",
"event_type": "bookingorder_cancelled",
"client_id": "cl_987654321",
"endpoint": "https://your-domain.com/webhooks/xs2event",
"is_active": true
}WARNING
Notice that the secret is not included in the response.
4. Manage webhooks
You can manage your registered webhooks using the following endpoints:
GET /v1/webhooks: List all webhooks for your client.GET /v1/webhooks/{webhook_id}: Retrieve details for a specific webhook.PATCH /v1/webhooks/{webhook_id}: Partially update a webhook. Only the fields included in the request will be changed.PUT /v1/webhooks/{webhook_id}: Replace a webhook representation. Requires a complete update payload.DELETE /v1/webhooks/{webhook_id}: Permanently delete a webhook.
Note: Just like the registration response, read responses (GET) never contain the secret.
Management examples (Python)
python
import requests
import os
API_URL = "https://api.example.com/v1/webhooks"
HEADERS = {"Authorization": f"X-Api-Key {os.environ['XS2EVENT_API_TOKEN']}"}
# List webhooks
response = requests.get(API_URL, headers=HEADERS)
webhooks = response.json()
# Patch is_active
webhook_id = "wh_123456789"
requests.patch(f"{API_URL}/{webhook_id}", json={"is_active": False}, headers=HEADERS)
# Replace with PUT
put_payload = {
"event_type": "bookingorder_completed",
"endpoint": "https://new-endpoint.com/webhook",
"secret": "NEW_SECRET_KEY"
}
requests.put(f"{API_URL}/{webhook_id}", json=put_payload, headers=HEADERS)
# Delete a webhook
requests.delete(f"{API_URL}/{webhook_id}", headers=HEADERS)5. Incoming webhook requests
When an event occurs, XS2Event sends a POST request to your endpoint.
Request headers
| Header | Description |
|---|---|
Content-Type | Will always be application/json. |
User-Agent | Identifies the XS2Event webhook worker. |
X-XS2Event-Webhook-Id | The unique ID of the webhook that triggered the request. |
X-XS2Event-Event | The type of event (e.g., bookingorder_completed). |
X-XS2Event-Signature | The HMAC-SHA256 signature used for verification. |
Example request body
WARNING
The webhook payload contains only reference information. It does not contain the actual resource data (like order details or sale prices). You must use the resource_id to fetch the data from our API.
json
{
"version": 1,
"event": "bookingorder_completed",
"resource_type": "bookingorder",
"resource_id": "5678_bkn",
"client_id": "1234_cln",
"timestamp": "2026-09-01T08:43:37.189085+00:00"
}Important
To verify the signature, you must use the exact raw request body bytes. Do not parse the JSON and then re-serialize it, as this may change the byte representation (e.g., spacing) and cause verification to fail.
6. Verify the HMAC signature
To ensure that a request actually came from XS2Event and has not been tampered with, you must verify the signature provided in the X-XS2Event-Signature header.
Verification logic
- The signature format is
sha256=<hex_digest>. - The digest is calculated using HMAC-SHA256:
HMAC-SHA256(webhook_secret, raw_request_body_bytes). - Use a constant-time comparison function to prevent timing attacks.
- Only process the event if the signature is valid.
Python Flask receiver example
python
import hashlib
import hmac
import json
import os
from flask import Flask, abort, request
app = Flask(__name__)
# Load your secret from environment variables
WEBHOOK_SECRET = os.environ["XS2EVENT_WEBHOOK_SECRET"].encode("utf-8")
@app.post("/webhooks/xs2event")
def receive_webhook():
# 1. Get the raw body bytes
raw_body = request.get_data()
# 2. Get the signature from headers
received_signature = request.headers.get("X-XS2Event-Signature", "")
# 3. Calculate the expected signature
expected_digest = hmac.new(
WEBHOOK_SECRET,
raw_body,
hashlib.sha256,
).hexdigest()
expected_signature = f"sha256={expected_digest}"
# 4. Compare signatures using constant-time comparison
if not hmac.compare_digest(received_signature, expected_signature):
abort(401, description="Invalid webhook signature")
# 5. Safe to parse and process the event
event = json.loads(raw_body)
print(
"Received event:",
request.headers.get("X-XS2Event-Event"),
event,
)
# 6. Return a 2xx response
return {"received": True}, 200
if __name__ == "__main__":
app.run(port=5000)7. Manually trigger a webhook
For development and testing, you can manually trigger a webhook delivery using the POST /v1/webhooks/trigger endpoint.
Request fields
webhook_id: Unique identifier of the webhook to trigger.
TIP
- This endpoint queues delivery to all matching active webhooks for your client.
- It returns
202 Acceptedimmediately. It does not wait for your receiver to respond.
Trigger example
python
import requests
response = requests.post(
"https://api.example.com/v1/webhooks/trigger",
json={
"webhook_id": "wh_123456789",
},
headers={"Authorization": "Bearer YOUR_API_TOKEN"},
timeout=10,
)
response.raise_for_status()
assert response.status_code == 202
print("Webhook trigger queued.")8. Delivery behavior and troubleshooting
Delivery behavior
- Asynchronous: Events are delivered in the background.
- Retries: Connection errors or non-2xx responses from your server will trigger retries.
- Deactivation: If a webhook is disabled (
is_active: false) or deleted, it will not receive any future deliveries, and pending deliveries in the queue may be dropped. - Idempotency: Your receiver should be idempotent. Because of retries, the same event may occasionally be delivered more than once. Use the
X-XS2Event-Webhook-Idor unique data in the payload to track processed events.
Troubleshooting checklist
If you are not receiving webhooks or verification is failing:
- [ ] Invalid signature: Are you using the raw request body for HMAC calculation? Is your secret exactly the same as the one registered?
- [ ] No request received: Is your endpoint publicly reachable? Check your firewall and server logs.
- [ ] Endpoint unreachable: Ensure your server is running and the URL in the registration is correct.
- [ ] HTTP 4xx or 5xx: Your server is receiving the request but returning an error. Check your application logs.
- [ ] Wrong event type: Ensure you registered for the event (e.g.,
bookingorder_completed) you are expecting. - [ ] Secret unavailable: Ensure your receiver has access to the correct environment variable.
- [ ] Webhook inactive: Check the
is_activestatus of your webhook via theGETendpoint.
Best Practice: Log the X-XS2Event-Webhook-Id, the event type, the response status you returned, and the timestamp. Never log the secret or your API authorization headers.
9. Security recommendations
- Use HTTPS: Always use HTTPS for your webhook endpoint in production to protect the payload and headers.
- Environment Variables: Store your webhook secrets and API tokens in environment variables or a dedicated secret manager.
- Source Control: Never commit secrets to your code repository.
- Verify First: Always verify the HMAC signature before doing any other processing or parsing of the request body.
- Constant-Time Comparison: Use
hmac.compare_digest(Python) to protect against timing attacks. - Replay Protection: For high-security applications, track recently processed event IDs to prevent replay attacks.
- Idempotency: Ensure that processing the same event multiple times has no unintended side effects.
- Fail Fast: Return a
401 Unauthorizedimmediately if the signature is invalid.
10. Final checklist
- [ ] Register the webhook with a valid secret (8–128 characters).
- [ ] Store the secret securely in your application's environment.
- [ ] Implement receiver logic that uses the raw request body for HMAC-SHA256 verification.
- [ ] Return a 2xx response (e.g.,
200 OK) only after successful validation. - [ ] Trigger a test event using the
/v1/webhooks/triggerendpoint. - [ ] Confirm the event type in your receiver logs.
- [ ] Confirm that the received signature is valid according to your code.
- [ ] Ensure your processing logic is idempotent.