Skip to content

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

  1. Registration: You register a webhook endpoint via the API, specifying the event type you want to listen for.
  2. Event Occurrence: An event occurs in XS2Event (e.g., a booking order is completed) or is manually triggered.
  3. Queueing: An action is queued in our system for delivery.
  4. Worker Processing: A background worker picks up the action and sends the HTTP POST request to your registered endpoint.
  5. 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 TypeResource TypeDescription
bookingorder_completedbookingorderTriggered when a booking order is successfully completed. (For buyers)
bookingorder_cancelledbookingorderTriggered when a booking order is cancelled. (For buyers)
sale_createdsaleTriggered when a new sale resource is created. (For suppliers on our marketplace)
sale_cancelledsaleTriggered 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 secret field 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

HeaderDescription
Content-TypeWill always be application/json.
User-AgentIdentifies the XS2Event webhook worker.
X-XS2Event-Webhook-IdThe unique ID of the webhook that triggered the request.
X-XS2Event-EventThe type of event (e.g., bookingorder_completed).
X-XS2Event-SignatureThe 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

  1. The signature format is sha256=<hex_digest>.
  2. The digest is calculated using HMAC-SHA256: HMAC-SHA256(webhook_secret, raw_request_body_bytes).
  3. Use a constant-time comparison function to prevent timing attacks.
  4. 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 Accepted immediately. 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-Id or 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_active status of your webhook via the GET endpoint.

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 Unauthorized immediately if the signature is invalid.

10. Final checklist

  1. [ ] Register the webhook with a valid secret (8–128 characters).
  2. [ ] Store the secret securely in your application's environment.
  3. [ ] Implement receiver logic that uses the raw request body for HMAC-SHA256 verification.
  4. [ ] Return a 2xx response (e.g., 200 OK) only after successful validation.
  5. [ ] Trigger a test event using the /v1/webhooks/trigger endpoint.
  6. [ ] Confirm the event type in your receiver logs.
  7. [ ] Confirm that the received signature is valid according to your code.
  8. [ ] Ensure your processing logic is idempotent.