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-TeamApp
  • X-Stayblox-Signature
  • X-Stayblox-Timestamp

Headers on platform → app event webhooks:

  • X-Stayblox-Event
  • X-Stayblox-Signature
  • X-Stayblox-Timestamp

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