Skip to main content

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" }
}
}'
FieldRequiredDescription
typeYesAlways webhook.
nameYesA label for the integration.
config.urlYesYour endpoint. Must be https:// in production.
config.signingSecretYesThe HMAC key. Generate a long random string and keep a copy for your receiver.
config.headersNoExtra headers sent with every delivery, for example your own auth header.
isActiveNoDefault 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"
}
}
FieldDescription
eventAlways booking.confirmed today.
timestampWhen Sessiq built the payload, ISO 8601 in UTC.
booking.idThe booking ID. Use it to deduplicate.
booking.startTimeStart of the booking, ISO 8601 in UTC.
booking.endTimeCurrently always an empty string. Look up the booking or product duration if you need it.
booking.partySizeNumber of people.
booking.customerNameThe 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:

  1. Read the raw body bytes before any JSON parsing. Re-serialising parsed JSON changes the bytes and breaks the signature.
  2. Compute HMAC-SHA256 of those bytes with your signing secret, as hex.
  3. Compare it with the header value after sha256=, using a constant-time comparison.
  4. Reject the request with 401 if 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 2xx response 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.

ProviderEndpointWhere to configure it
Stripehttps://api.sessiq.com/api/v1/payments/stripe/webhookA webhook endpoint in your Stripe Dashboard
Paytrail/webhooks/paytrail/callback, /webhooks/paytrail/notifyNothing 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.