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
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:
429withRetry-Afterheader - 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 --> [*]