Core concepts
Request signing (HMAC)
The signed string, clock skew, replay protection, and a worked example.
Opt-in per key (requires_signature). Recommended for every push partner: a
key alone proves you hold a secret; a signature also proves the body arrived
unaltered and that this exact request has not been sent before. A replayed pull
request costs a duplicate query; a replayed push costs a duplicate reservation.
Signed string#
Four fields, newline-joined, order fixed:
php-template
<timestamp>\n<METHOD>\n<path-with-query>\n<sha256-hex-of-raw-body>
timestamp— Unix seconds, the same value you send inX-Staylah-Timestamp.METHOD— uppercase (GET,POST,PUT).path-with-query— leading slash, no scheme or host (we sit behind a load balancer; signing a host you see and we don't makes every request fail irreproducibly). Include the query string when there is one.- body hash —
sha256hex of the raw body; empty string hashes to the sha256 of"".
Header value is the algorithm name, then =, then the HMAC hex:
css
X-Staylah-Signature: sha256=<hex_hmac_sha256(signed_string, signing_secret)>
The sha256= prefix is required so the algorithm can change later without
guessing what an old client meant.
Rules#
- Clock skew tolerance: 300 seconds by default. Outside it the request is
rejected as stale.
GET /pingreturns our server clock so you can diagnose skew directly instead of guessing. - One use per signature, remembered for the tolerance window. Retrying a failed request means generating a new timestamp and signature — resending the identical signed request is treated as a replay.
- A key flagged as requiring a signature but issued without a signing secret fails closed. It never degrades to "no signature needed".
Example (PHP)#
php
$timestamp = (string) time();
$body = json_encode($payload, JSON_UNESCAPED_SLASHES);
$signed = implode("\n", [
$timestamp,
'PUT',
'/api/connectivity/v1/ari',
hash('sha256', $body),
]);
$signature = 'sha256=' . hash_hmac('sha256', $signed, $signingSecret);
