Generated from renderApiRequestSigningGuide()
API request signing
Generated from the request-signing module. Regenerate this page from renderApiRequestSigningGuide(). Do not type the header names or the canonical string by hand.
High-risk writes accept a signed request. The signature covers the method, the path including the query string, a unix timestamp, a one-time nonce, and the SHA-256 of the raw body. Lists still use cursor, limit, filter, and sort. Errors still use { success: false, error } with code, message, remediation, and requestId. Rate-limit responses still return limit, remaining, and reset headers.
Enforcement runs when PYROSYNC_REQUEST_SIGNING_SECRET is set. The check does not change who is authenticated. Provider paths whose segments include webhook or webhooks keep their own signatures.
Which requests are signed
A request under /api/ is high-risk when it is not a provider webhook and any of these is true:
- The method is
DELETE. - The method is
POST,PUT, orPATCHand a path segment is one ofapi-keys,credentials,model-credentials,secrets,billing,checkout,subscriptions,invoices,payments,sessions,operator,infrastructure, or ends in-credentialsor-secrets.
GET, HEAD, and OPTIONS are not signed by this mechanism.
Headers
x-pyrosync-timestamp: unix seconds.x-pyrosync-nonce: 16 to 128 characters fromA-Z,a-z,0-9,_, and-. A nonce is accepted once inside the window.x-pyrosync-signature:v1=plus the lowercase hex HMAC-SHA256 of the canonical string.
The timestamp must be within 300 seconds of the server clock.
Canonical string
Five lines, separated by newline:
METHOD
/path?query
timestamp
nonce
sha256-hex of the raw body
The method is uppercase. An empty body hashes as the empty string.
Result
- A fresh, matching signature is accepted.
- A missing header is
REQUEST_SIGNATURE_MISSING(401). - A bad nonce, a bad signature shape, or a mismatched HMAC is
REQUEST_SIGNATURE_INVALID(401). A failed attempt does not consume the nonce. - A timestamp outside the window is
REQUEST_SIGNATURE_STALE(401). - A second use of the same nonce inside the window is
REQUEST_SIGNATURE_REPLAY(409).
One process keeps nonces in memory. More than one server needs a shared store with the same claim rule.
Reference implementations
TypeScript:
import { signPublicApiRequest } from '@pyrosync/shared';
const signed = signPublicApiRequest({
secret: process.env.PYROSYNC_REQUEST_SIGNING_SECRET ?? '',
method: 'POST',
path: '/api/v1/api-keys',
body,
timestamp: Math.floor(Date.now() / 1000),
nonce,
});
Python:
import hashlib, hmac
canonical = '\n'.join([
method.upper(),
path,
str(timestamp),
nonce,
hashlib.sha256(body.encode()).hexdigest(),
])
signature = "v1=" + hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
curl:
curl -sS -X POST "$HOST/api/v1/api-keys" \
-H "authorization: Bearer $API_KEY" \
-H "x-pyrosync-timestamp: $TS" \
-H "x-pyrosync-nonce: $NONCE" \
-H "x-pyrosync-signature: $SIG" \
-H "content-type: application/json" \
--data "$BODY"