Every request to the gateway carries one authentication header:

HTTP
X-API-Key: cmp_...

There’s no OAuth, no session tokens and no tenant id in the URL: the key is self-identifying. The gateway looks it up, checks that it’s active and not expired, and derives from it the tenant (your entity), the scopes, the environment (sandbox/prod), the per-minute limit and the monthly quota. The SDKs set the header for you on every call.

What gets validated, in order #

  1. Presence. No X-API-Key → 401.
  2. Valid key. Nonexistent, revoked, suspended or expired → 403.
  3. Allowed IP. If the key has an IP allowlist, the source IP must fall within its ranges (behind proxies, X-Forwarded-For is only trusted from known proxies) → otherwise 403.
  4. Scope. The operation requires a scope (for example wallet:transfer); if the key doesn’t have it → 403.
  5. Contracted module. Even if the key has the scope, your entity must have the module enabled → 403.
  6. High-value signature (optional, per key): see below → 401 if it’s missing or invalid.
  7. Limits. Per-minute limit and monthly quota → 429 (Rate limits).

Each operation’s scope appears in the API reference, and the per-module table is in Get your key.

Checking the key #

GET /v1/gateway/health/ accepts any valid key and returns the tenant and scopes: call it when your service starts so it fails fast if the key doesn’t work.

from compense_gateway import AuthError

try:
    health = client.system.health()
    assert "credits:submit" in health.scopes, "The key can't submit credit applications"
except AuthError as e:
    raise SystemExit(f"Invalid Compense key: {e.status} {e.detail} (trace {e.trace_id})")

401 vs. 403 #

CodeCauseWhat to do
401The X-API-Key header is missing, or the high-value signature is missing or doesn’t match.Make sure your HTTP client (or a proxy) isn’t stripping the header.
403Invalid, revoked, suspended or expired key; missing scope; module not contracted; IP not on the allowlist.Check detail; compare the scopes with system.health(); give the trace_id to support.

In the SDKs both map to the same exception: AuthError (Python, JS), AuthException (PHP, .NET) and compense.ErrAuth / compense.IsAuthError (Go). Don’t retry a 401/403: it won’t fix itself.

Signing high-value requests #

A key can have request signing enabled for the high-value scopes (credits:decide, credits:disburse, subsidies:decide, subsidies:disburse). Those requests must then carry, in addition to the key:

HTTP
X-Compense-Signature: t=<unix>,v1=<hex>

where <hex> is HMAC-SHA256(signing_secret, "<t>.<raw body>") in hexadecimal; the gateway tolerates up to 5 minutes of clock skew and compares in constant time.

The 1.1.0 SDKs don't sign requests

None of the five SDKs generates X-Compense-Signature. If your key has signing enabled, add the header with your own HTTP client (WithHTTPClient in Go, an HttpMessageHandler in .NET, a custom fetch in JS, a decorated PSR-18 client in PHP) or use plain HTTP. Signing is optional and disabled by default: check with Compense whether your key requires it.

Shell
BODY='{"decision":"APPROVE","approved_amount":"3500000"}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$COMPENSE_SIGNING_SECRET" -hex | sed 's/^.* //')
curl -X POST "https://sandbox.dev.compense.co/gw/v1/gateway/credits/applications/$APP_ID/decision/" \
  -H "X-API-Key: $COMPENSE_API_KEY" \
  -H "Idempotency-Key: horizonte-decision-$APP_ID" \
  -H "X-Compense-Signature: t=$T,v1=$SIG" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Best practices #

  • One key per system and per environment, with the minimum set of scopes.
  • Store keys in a secrets manager and never in code, the repository, a mobile app or a browser.
  • If your infrastructure reaches the Internet through fixed IPs, request an IP allowlist for your key.
  • Log the trace_id of every error: it’s the first thing support will ask you for.