Skip to content

Signing & security

Every signed exchange between Stayblox and your app (outbound session calls and event webhooks) uses the same HMAC scheme. Verify what Stayblox sends you, and sign what you send back the same way.

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-Team
  • 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-App

Verify an inbound request

Reconstruct the signature from the timestamp header and the raw request body (do not re-serialize JSON), then compare in constant time. Reject stale requests to bound replays.

PHP

php
function verifyStaybloxSignature(string $rawBody): void
{
    $timestamp = $_SERVER['HTTP_X_STAYBLOX_TIMESTAMP'] ?? '';
    $signature = $_SERVER['HTTP_X_STAYBLOX_SIGNATURE'] ?? '';
    $secret    = getenv('STAYBLOX_WEBHOOK_SECRET');

    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

    if (! hash_equals($expected, $signature)) {
        http_response_code(401);
        exit;
    }

    // Reject requests older than 5 minutes.
    if (abs(time() - (int) $timestamp) > 300) {
        http_response_code(401);
        exit;
    }
}

Node.js

js
import crypto from 'node:crypto'

export function verifyStaybloxSignature(rawBody, headers, secret) {
  const timestamp = headers['x-stayblox-timestamp'] ?? ''
  const signature = headers['x-stayblox-signature'] ?? ''

  const expected =
    'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')

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

  if (!ok) throw new Error('Invalid signature')
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error('Stale request')
}

Notes

  • Always verify against the raw body bytes, before any JSON parsing.
  • Use a constant-time comparison (hash_equals / crypto.timingSafeEqual).
  • The webhook secret is per install; key your verification by the X-Stayblox-App header so one server can serve many hosts.
  • Your bearer token (for calling our GraphQL API) is separate from the webhook secret; never send the token to anyone but Stayblox.

© Stayblox — Developer Platform