How-to guide

Sign requests to Queuey (HMAC)

For higher-trust producers, replace the static API key with an HMAC signed request. Each request carries a signature over its method, path, timestamp, nonce, and body hash — so a captured request can't be replayed or tampered with.

1 — Required headers

request headers
X-Queuey-Key-Id:         {keyId}
X-Queuey-Timestamp:      {unixSeconds}
X-Queuey-Nonce:          {uniqueNonce}
X-Queuey-Content-SHA256: {sha256HexOfRawBody}
X-Queuey-Signature:      {signatureHex}

2 — The canonical string

Build the string to sign by joining these six lines with a newline (\n), each value normalized exactly as below:

canonical string
METHOD
PATH
QUERY
TIMESTAMP
NONCE
CONTENT_SHA256
  • METHOD — the HTTP method, uppercase (POST).
  • PATH — the request path exactly as sent, with any trailing slash trimmed (/events/ten_x/orders).
  • QUERY — the query parameters flattened, sorted by key, URL-encoded, and joined with &; an empty string when there are none. This is not the raw query string. E.g. ?b=2&a=1a=1&b=2.
  • TIMESTAMP — the X-Queuey-Timestamp value (Unix seconds).
  • NONCE — the X-Queuey-Nonce value, trimmed.
  • CONTENT_SHA256 — the lowercase hex SHA-256 of the raw request body bytes.

3 — Sign it (HMAC-SHA256)

The signature is the lowercase hex HMAC-SHA256 of the canonical string with your signing secret:

sign.ts
import crypto from "crypto";

const secret = process.env.QUEUEY_SIGNING_SECRET!;   // the HMAC signing key
const body = JSON.stringify({ eventType: "order.created", payload: { id: "10042" } });

const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomUUID();
const bodyHash = crypto.createHash("sha256").update(body).digest("hex").toLowerCase();

const canonical = [
  "POST",                                   // METHOD  (uppercase)
  "/events/ten_yourTenant/orders",          // PATH    (as sent, no trailing slash)
  "",                                       // QUERY   (normalized: flatten+sort+encode; "" if none)
  String(timestamp),                        // TIMESTAMP (Unix seconds)
  nonce,                                    // NONCE   (trimmed)
  bodyHash,                                 // CONTENT_SHA256 (lowercase hex sha256 of raw body)
].join("\n");

const signature = crypto
  .createHmac("sha256", Buffer.from(secret, "utf8"))
  .update(Buffer.from(canonical, "utf8"))
  .digest("hex")
  .toLowerCase();

// send X-Queuey-Key-Id / Timestamp / Nonce / Content-SHA256 / Signature
Replay protection
Queuey rejects a signed request whose timestamp is out of range or whose nonce has been seen before — so sign each request fresh with a new timestamp and nonce. Common rejection reasons: invalid_timestamp, timestamp_out_of_range, invalid_signature.

Related