Three layers: a public API key, a bearer secret (compared inside the database in constant time), and an optional HMAC request signature. Per-tenant IP allowlist on top.
Every mutating request to /v1/* requires these three headers. GET /v1/health is the only unauthenticated route.
| Header | Purpose |
|---|---|
| X-API-Key | Public tenant identifier. pk_live_* in production or pk_test_* in sandbox. Also enforced at the AWS API Gateway layer (usage plan + quota). |
| Authorization | Bearer sk_*. Compared against a bcrypt hash stored in the database via pgcrypto.crypt() — the plaintext never leaves the DB server, comparison is constant-time. |
| Idempotency-Key | Opaque key (≤ 80 chars) you choose per request. A retry with the same key returns the prior response without re-executing. Required on every POST, PATCH, and DELETE. |
If your tenant has requireSignature: true (configurable on live tenants), every request must also carry:
| Header | Purpose |
|---|---|
| X-Signature | sha256=<hex> — HMAC-SHA256 of METHOD\nPATH\nTIMESTAMP\nBODY using your bearer secret as the key. |
| X-Timestamp | Unix epoch seconds. Drift > 300s is rejected (replay window). |
Even if your bearer leaks, requests can’t be replayed without the signing secret, and old captures can’t be replayed past the 5-minute drift window.
import hmac, hashlib, time, json, requests def sign_and_post(url, path, body, api_key, bearer): ts = str(int(time.time())) body_bytes = json.dumps(body, separators=(",", ":")).encode() canonical = f"POST\n{path}\n{ts}\n".encode() + body_bytes sig = hmac.new(bearer.encode(), canonical, hashlib.sha256).hexdigest() return requests.post(url, data=body_bytes, headers={ "X-API-Key": api_key, "Authorization": f"Bearer {bearer}", "X-Timestamp": ts, "X-Signature": f"sha256={sig}", "Idempotency-Key": "order-7421", "Content-Type": "application/json", })
const crypto = require("crypto"); function signAndPost(url, path, body, apiKey, bearer) { const ts = Math.floor(Date.now() / 1000).toString(); const bodyJson = JSON.stringify(body); const canonical = `POST\n${path}\n${ts}\n${bodyJson}`; const sig = crypto.createHmac("sha256", bearer).update(canonical).digest("hex"); return fetch(url, { method: "POST", headers: { "X-API-Key": apiKey, "Authorization": `Bearer ${bearer}`, "X-Timestamp": ts, "X-Signature": `sha256=${sig}`, "Idempotency-Key": "order-7421", "Content-Type": "application/json", }, body: bodyJson, }); }
Live tenants can configure a CIDR allowlist. Requests outside the allowlist are rejected at the auth layer with a 403 forbidden. The check honors X-Forwarded-For from API Gateway, so configure your allowlist with your real egress IP, not the gateway’s.
| HTTP | error.code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or unknown X-API-Key, missing bearer, invalid bearer. |
| 401 | invalid_signature | HMAC mismatch, missing signature/timestamp, or timestamp drift > 300s. |
| 403 | forbidden | Client status is not Processor Active, or source IP outside the allowlist. |
| 400 | bad_request | Missing Idempotency-Key on a mutating request. |
| 403 | (API Gateway) | The key isn’t registered in the usage plan, or you’ve exceeded the daily quota. |