Skip to Content
Request signing

Request signing

Both clients → queue-service and queue-service → your handlers use the same header format. They use different secrets.

Default header name: x-bq-queue-request-signature (override with SIGNATURE_HEADER).

Format

t=<unix-seconds>,v1=<hmac-hex>
  • Algorithm: HMAC-SHA512
  • Message: t + "." + raw body string (exact bytes)
  • Encoding: hex
  • Clock skew allowed: ±300 seconds
  • Timestamps in milliseconds (> 1e12) are rejected

Create a signature (Node)

import crypto from 'node:crypto'; export function createSignature(body, secret) { const t = Math.floor(Date.now() / 1000); const v1 = crypto .createHmac('sha512', secret) .update(`${t}.`) .update(body) .digest('hex'); return `t=${t},v1=${v1}`; }

Which secret

DirectionSecretEnv / config
Your app → /api/jobs/*API secretREQUEST_SIGNING_SECRET
Worker → your endpointPer-queue secretqueues.*.signature_secret

Same header name on both paths. Do not reuse one secret for both.

Verify (Node)

import crypto from 'node:crypto'; export function verifySignature(rawBody, headerValue, secret) { const match = /^t=(\d+),v1=([a-f0-9]+)$/i.exec(headerValue ?? ''); if (!match) return false; const t = Number(match[1]); const v1 = match[2]; const now = Math.floor(Date.now() / 1000); if (Math.abs(now - t) > 300) return false; const expected = crypto .createHmac('sha512', secret) .update(`${t}.`) .update(rawBody) .digest('hex'); try { return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex')); } catch { return false; } }

Pitfalls

  • Sign the exact body string you send. Pretty-printing or proxy rewrites break HMAC.
  • Header lookup is case-insensitive on the way in.
  • Missing ${ENV_VAR} in YAML becomes "", so outbound signing can fail silently with an empty secret.
  • Clients use REQUEST_SIGNING_SECRET. Handlers use the queue’s signature_secret. Mixing them up is the usual “works in curl, fails in production” mistake.
Last updated on