Skip to content

Webhooks

Stayblox pushes events to your server as they happen. Deliveries are thin: the signed JSON envelope tells you what changed, and you fetch the current state through the GraphQL API. That keeps payloads stable across API versions and means a missed webhook never leaves you with stale data — the fetch always returns the latest truth.

Subscribing

Subscriptions are declared in your app's manifest:

json
{
  "scopes": ["read_bookings", "read_rates"],
  "webhooks": ["booking.confirmed", "booking.cancelled", "rates.updated"]
}
  • webhooks — the topic strings you want delivered.
  • scopes — the access scopes your app holds. A topic is only delivered if the app holds its required scope; subscribing without the scope silently delivers nothing.

Deliveries are POSTs to the install's webhook URL, signed with the install's webhook secret (the same one shown under Connection details, see Getting started). Both are per install, so one server can receive events for every host that installs your app — key your handling by the X-Stayblox-TeamApp header.

Topics

Topics an app may subscribe to (from app/Enums/WebhookTopic.php). A topic is only delivered if the app's manifest also holds its required scope:

TopicResourceRequired scopeNotes
booking.createdbookingread_bookings
booking.confirmedbookingread_bookings
booking.cancelledbookingread_bookings
booking.checked_inbookingread_bookings
booking.checked_outbookingread_bookings
booking.no_showbookingread_bookings
booking_request.createdbooking_requestread_bookings
contact.createdcontactread_contacts
contact.updatedcontactread_contacts
conversation.createdconversationread_conversations
message.createdmessageread_conversationsResource carries "direction": "inbound" or "outbound".
rates.updatedunit_type_rates digestread_ratesDebounced; resource is a date span, not an id.
availability.changedunit_type_availability digestread_ratesDebounced; resource is a date span, not an id.
payment.succeededpaymentread_payments
payment.failedpaymentread_payments
payment.refundedpaymentread_payments
invoice.createdinvoiceread_invoices
invoice.voidedinvoiceread_invoices
review.createdreviewread_reviews

The envelope

Every delivery carries the same envelope shape:

json
{
  "event_id": "01J9XYZ...",
  "topic": "booking.confirmed",
  "occurred_at": "2026-06-11T14:02:11Z",
  "api_version": "2026-01",
  "team_id": 42,
  "resource": { "type": "booking", "id": 1234 }
}
FieldDescription
event_idULID, unique per event. Use it as your idempotency key — the same event can be delivered more than once (retries, replays).
topicThe topic string, also sent as X-Stayblox-Topic.
occurred_atWhen the event happened, UTC ISO 8601.
api_versionThe current Developer API version at emit time.
team_idThe installing team (host) the event belongs to.
resourceWhat changed — a type plus an id for entity topics. Fetch the current state via the API; the envelope never carries entity fields.

message.created adds a direction to the resource — { "type": "message", "id": 88, "direction": "inbound" } — so you can ignore your own outbound traffic without a fetch.

Digest topics

rates.updated and availability.changed would fire constantly during bulk calendar edits, so they are debounced: changes buffer until a team + topic has been quiet for about 60 seconds, then one digest event is emitted. Its resource is a span instead of an id:

json
{ "type": "unit_type_rates", "unit_type_ids": [3, 7], "from": "2026-07-01", "to": "2026-07-31" }

The from/to span is the union across the listed unit types — re-fetch each one via unitTypeRates for the span to get exact values.

Verifying deliveries

Webhooks use the platform-wide HMAC scheme — same as session calls, verified against the raw request body. The full walkthrough (and a PHP example) lives in Signing & security.

Algorithm: HMAC-SHA256, keyed with the install's webhook secret.

Signed string: {timestamp}.{body} — the request timestamp, a literal dot, then the raw JSON body.

Signature header value: sha256=<hex digest>

Headers on platform → app session calls:

  • X-Stayblox-TeamApp
  • X-Stayblox-Signature
  • X-Stayblox-Timestamp

Headers on platform → app event webhooks:

  • X-Stayblox-Signature
  • X-Stayblox-Timestamp
  • X-Stayblox-Event-Id
  • X-Stayblox-Topic
  • X-Stayblox-TeamApp

A minimal Node.js receiver — verify, acknowledge fast, process after responding:

js
import crypto from 'node:crypto'
import express from 'express'

const app = express()

app.post('/webhooks/stayblox', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.header('X-Stayblox-Timestamp') ?? ''
  const signature = req.header('X-Stayblox-Signature') ?? ''
  const secret = process.env.STAYBLOX_WEBHOOK_SECRET

  // The signature covers the exact raw body bytes — never re-serialize JSON.
  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${req.body}`).digest('hex')

  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))

  if (!ok) return res.status(401).end()

  // X-Stayblox-Timestamp is unix seconds; reject stale deliveries to bound replays.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end()

  res.status(200).end() // acknowledge before doing any work

  const event = JSON.parse(req.body)
  // Queue for processing, deduplicated by event.event_id.
})

Delivery & retries

  • Respond 2xx quickly. Requests time out after 15 seconds; do your work after acknowledging, not before.
  • Failed deliveries retry up to 3 attempts with 30s / 2m / 8m backoff.
  • Use event_id for idempotency — duplicates are possible after retries and replays.
  • If every delivery to an install keeps failing for about 3 days, webhooks for that install are auto-disabled and both the host and you (the app owner) are notified. The host re-enables them from the app's settings page once the endpoint is fixed; events missed while disabled can be replayed by support.

Next

© Stayblox — Developer Platform