Skip to content

Authentication

All API endpoints (except inbound provider webhooks) require an X-API-Key header.

API Keys

Keys are generated with a prefix indicating the environment:

Prefix Environment
sk_test_ Sandbox / testing
sk_live_ Production

Keys are stored as HMAC-SHA256 hashes with a server-side pepper. A database leak does not expose raw keys.

Request Headers

X-API-Key: sk_live_your_key_here
Idempotency-Key: order-abc-789    # required on POST /v1/payments

Scopes

Each API key is issued with specific scopes that control access:

Scope Grants
payments:read GET /v1/payments/*
payments:write POST /v1/payments/*
refunds:read GET /v1/refunds/*
refunds:write POST /v1/refunds
webhooks:read GET /v1/webhooks/logs
merchant:read GET /v1/merchant/*
merchant:write PUT /v1/merchant/*
audit:read GET /v1/audit/logs

Scope enforcement

A request to an endpoint requiring a scope the key doesn't have returns 403 with code AUTH_INSUFFICIENT_SCOPE.

Rate Limiting

Failed authentication attempts are rate-limited per IP:

  • Threshold: 10 failures per 5 minutes
  • Response: 429 with Retry-After header
  • Tracking: Redis-backed counter per IP
flowchart LR
    Request["API Request"] --> Check{"Rate limit<br/>exceeded?"}
    Check -->|No| Auth{"Valid<br/>API key?"}
    Check -->|Yes| Block["429 Too Many Requests"]
    Auth -->|Yes| Scope{"Has required<br/>scope?"}
    Auth -->|No| Fail["401 + increment counter"]
    Scope -->|Yes| OK["✓ Process request"]
    Scope -->|No| Deny["403 Insufficient Scope"]

Error Responses

Code HTTP When
AUTH_INVALID_KEY 401 Invalid, revoked, or expired key
AUTH_RATE_LIMITED 429 Too many failed attempts
AUTH_INSUFFICIENT_SCOPE 403 Key missing required scope
{
  "success": false,
  "errors": [{
    "code": "AUTH_INVALID_KEY",
    "message": "Invalid or revoked API key"
  }]
}

Creating API Keys

Admin (CLI)

make createsuperadmin    # Full-scope admin key
make createapikey        # Scoped key for existing merchant
make revokeapikey        # Deactivate by prefix

Key Lifecycle

stateDiagram-v2
    [*] --> Active: Key created
    Active --> Active: Used (last_used_at updated)
    Active --> Expired: expires_at reached
    Active --> Revoked: Admin revokes
    Expired --> [*]
    Revoked --> [*]