Webhooks
This page covers two directions:
- Outbound webhooks: Sessiq calls your URL when a booking is confirmed.
- Inbound callbacks: payment providers call Sessiq. You configure these in the provider's dashboard.
Outbound webhooks
An outbound webhook sends a signed JSON POST to your endpoint when a booking is confirmed. Use it to push new bookings into a rostering, CRM or kitchen system without polling.
Webhooks are set up through the API. There is no screen for them in the admin console yet.
Set up a webhook
You need a staff token with the admin:write permission.
1. Create an integration. It holds your endpoint and the secret Sessiq signs with.
curl -X POST https://api.sessiq.com/api/v1/admin/integrations \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "webhook",
"name": "Rostering",
"config": {
"url": "https://hooks.example.com/sessiq",
"signingSecret": "<a-long-random-string>",
"headers": { "X-Source": "sessiq" }
}
}'
| Field | Required | Description |
|---|---|---|
type | Yes | Always webhook. |
name | Yes | A label for the integration. |
config.url | Yes | Your endpoint. Must be https:// in production. |
config.signingSecret | Yes | The HMAC key. Generate a long random string and keep a copy for your receiver. |
config.headers | No | Extra headers sent with every delivery, for example your own auth header. |
isActive | No | Default true. An inactive integration receives nothing. |
The response includes the integration's id. Manage integrations with GET /api/v1/admin/integrations, and with GET, PATCH and DELETE on /api/v1/admin/integrations/{id}.
The signing secret is write-only: responses never include config.signingSecret, only signingSecretSet: true. On PATCH, leave signingSecret out of config (or send it empty) to keep the stored secret, or send a new value to rotate it.
2. Create an automation rule that sends booking confirmations to the integration:
curl -X POST https://api.sessiq.com/api/v1/admin/messaging/automation-rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Push confirmed bookings to rostering",
"triggerEvent": "booking_confirmed",
"triggerOffset": 0,
"templateKey": "webhook",
"destinationType": "webhook",
"integrationId": "<integration-id>"
}'
templateKey is required by the rules API but not used for webhooks. You can narrow a rule with siteIds, productIds or productTags, as with email rules. See Notifications.
Webhook rules fire as soon as the booking is confirmed. triggerOffset has no effect on them.
The request
POST https://hooks.example.com/sessiq
Content-Type: application/json
X-Sessiq-Event: booking_confirmed
X-Sessiq-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
X-Source: sessiq
{
"event": "booking.confirmed",
"timestamp": "2026-06-01T09:30:12.345Z",
"booking": {
"id": "booking-id",
"siteId": "your-site-id",
"siteName": "Downtown",
"productId": "product-id",
"productName": "Escape room",
"startTime": "2026-06-12T15:00:00.000Z",
"endTime": "",
"partySize": 4,
"customerName": "Aino Virtanen"
}
}
| Field | Description |
|---|---|
event | Always booking.confirmed today. |
timestamp | When Sessiq built the payload, ISO 8601 in UTC. |
booking.id | The booking ID. Use it to deduplicate. |
booking.startTime | Start of the booking, ISO 8601 in UTC. |
booking.endTime | Currently always an empty string. Look up the booking or product duration if you need it. |
booking.partySize | Number of people. |
booking.customerName | The booker's name. No email or phone number is sent. |
Verify the signature
X-Sessiq-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the integration's signingSecret. There is no timestamp in the signature.
To verify:
- Read the raw body bytes before any JSON parsing. Re-serialising parsed JSON changes the bytes and breaks the signature.
- Compute HMAC-SHA256 of those bytes with your signing secret, as hex.
- Compare it with the header value after
sha256=, using a constant-time comparison. - Reject the request with
401if they differ.
Node.js with Express:
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.SESSIQ_WEBHOOK_SECRET;
app.post(
'/sessiq',
express.raw({ type: 'application/json' }), // keep the raw bytes
(req, res) => {
const header = req.get('X-Sessiq-Signature') ?? '';
const received = Buffer.from(header.replace(/^sha256=/, ''), 'hex');
const expected = crypto
.createHmac('sha256', SECRET)
.update(req.body)
.digest();
if (
received.length !== expected.length ||
!crypto.timingSafeEqual(received, expected)
) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body.toString('utf8'));
// Queue the work, then answer quickly.
res.sendStatus(204);
},
);
Because the signature has no timestamp, protect against replays yourself: store booking.id and ignore a booking you have already processed.
Delivery and retries
- Deliveries come from a background worker, usually a few seconds after the confirmation.
- Any
2xxresponse counts as delivered. Anything else, or no answer within 10 seconds, counts as a failure. - A failed delivery is retried twice: about 30 seconds after the first attempt, then about 5 minutes after the second. After three failed attempts, Sessiq stops trying.
- Delivery is at least once. You may get the same booking more than once, so deduplicate on
booking.id. - Deliveries are not guaranteed to arrive in order.
Answer fast and do the real work in the background. A slow endpoint turns into timeouts and retries.
Inbound callbacks
Sessiq receives payment events from the payment providers. These endpoints are called by the providers, not by you. You only need them when you set up a payment provider.
| Provider | Endpoint | Where to configure it |
|---|---|---|
| Stripe | https://api.sessiq.com/api/v1/payments/stripe/webhook | A webhook endpoint in your Stripe Dashboard |
| Paytrail | /webhooks/paytrail/callback, /webhooks/paytrail/notify | Nothing to configure: Sessiq sends the callback URLs with each payment |
Sessiq verifies each provider's signature and rejects requests that fail the check. A provider event that arrives twice is processed only once. For how payments move through a booking, see Payments.