RMO

Authentication

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.

The three required headers

Every mutating request to /v1/* requires these three headers. GET /v1/health is the only unauthenticated route.

HeaderPurpose
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.

Optional: HMAC request signing

If your tenant has requireSignature: true (configurable on live tenants), every request must also carry:

HeaderPurpose
X-Signaturesha256=<hex> — HMAC-SHA256 of METHOD\nPATH\nTIMESTAMP\nBODY using your bearer secret as the key.
X-TimestampUnix 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.

Sign in Python

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",
    })

Sign in Node.js

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,
  });
}

Optional: IP allowlist

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.

Failure modes

HTTPerror.codeMeaning
401unauthorizedMissing or unknown X-API-Key, missing bearer, invalid bearer.
401invalid_signatureHMAC mismatch, missing signature/timestamp, or timestamp drift > 300s.
403forbiddenClient status is not Processor Active, or source IP outside the allowlist.
400bad_requestMissing 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.

Where to next

Authorize a card → Webhook signatures → Error reference →
Start typing to search across all pages