API Key Management
The API Key Management capability provides a restricted key system inspired by Stripe's Restricted API Key (RAK) pattern. Keys are scoped with granular per-resource permissions, IP allowlisting, scheduled expiration, and rotation with delayed expiry. This enables the principle of least privilege — one key per service, per use case, with the minimum permissions required.
Interaction Traces & Swimlane Diagrams
1. Key Creation Flow
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │
│ (Admin) │ │ (Operator) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Create live │ │
│ payment key │ │
│─────────────────>│ │
│ │ POST /v1/keys │
│ │──────────────────>│
│ │ │── Validate
│ │ │ permissions,
│ │ │ constraints
│ │ │
│ │ 201 + full key │
│ │ (key shown once) │
│ │<──────────────────│
│ Return key │ │
│ (store safely) │ │
│<─────────────────│ │
│ │ │
│ Create test │ │
│ read-only key │ │
│─────────────────>│ │
│ │ POST /v1/keys │
│ │ (test perms: │
│ │ read only) │
│ │──────────────────>│
│ │ │
│ │ 201 + test key │
│ │<──────────────────│
│ Test key stored │ │
│<─────────────────│ │
Creation Request (live key with IP whitelist):
POST /v1/keys
Content-Type: application/json
Authorization: Bearer ***
{
"label": "prod-summary-bot",
"permissions": {
"payments": "write",
"subscriptions": "read",
"webhooks": "write",
"analytics": "none"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24"],
"allowed_methods": ["GET", "POST"],
"max_daily_requests": 10000
},
"expires_at": "2027-01-01T00:00:00Z"
}
Creation Response (key value shown only once):
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"label": "prod-summary-bot",
"prefix": "ipk_",
"key": "ipk_test_5a8b3c2d1e0f9g7h6i5j4k3l2m1n0o9p8q7r6s5t4u3v2w1x0y9z",
"permissions": {
"payments": "write",
"subscriptions": "read",
"webhooks": "write",
"analytics": "none"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24"],
"allowed_methods": ["GET", "POST"],
"max_daily_requests": 10000
},
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": null,
"created_at": "2026-05-27T08:00:00Z",
"updated_at": "2026-05-27T08:00:00Z"
}
Key Listing (keys are listed without the key field):
GET /v1/keys?limit=2
Authorization: Bearer ***
HTTP/1.1 200 OK
Content-Type: application/json
{
"object": "list",
"data": [
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"label": "prod-summary-bot",
"prefix": "ipk_",
"permissions": { "payments": "write", "subscriptions": "read", "webhooks": "write", "analytics": "none" },
"constraints": { "allowed_ips": ["203.0.113.0/24"], "allowed_methods": ["GET", "POST"], "max_daily_requests": 10000 },
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": "2026-05-27T14:30:00Z",
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-27T14:30:00Z"
},
{
"id": "key_01J7Z0C3D4E5F6G7H8I9J0K1L",
"label": "staging-readonly",
"prefix": "ipk_",
"permissions": { "payments": "read", "subscriptions": "read", "webhooks": "read", "analytics": "read" },
"constraints": { "allowed_ips": [], "allowed_methods": ["GET"], "max_daily_requests": 0 },
"expires_at": null,
"last_used_at": "2026-05-26T09:12:00Z",
"created_at": "2026-05-15T10:00:00Z",
"updated_at": "2026-05-20T16:30:00Z"
}
],
"has_more": false
}
2. HMAC Request Signing Flow
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Service │ │ Agent │ │ ItPay │
│ (Client) │ │ (Operator) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ 1. Compose API │ │
│ request body │ │
│ │ │
│ 2. Get stored │ │
│ key + secret │ │
│ │ │
│ 3. Create HMAC │ │
│ SHA-256 of: │ │
│ HTTP method + │ │
│ path + body │ │
│ + timestamp │ │
│ │ │
│ 4. Add headers: │ │
│ Authorization: │ │
│ Bearer <key> │ │
│ X-Signature: │ │
│ t=<timestamp>,│ │
│ v1=<hmac> │ │
│─────────────────>│ │
│ │ Forward signed │
│ │ request │
│ │──────────────────>│
│ │ │
│ │ │── Verify:
│ │ │ 1. Key exists
│ │ │ 2. Not deleted
│ │ │ 3. Not expired
│ │ │ 4. IP allowed
│ │ │ 5. Method ok
│ │ │ 6. Permission ok
│ │ │ 7. HMAC match
│ │ │ (constant-time)
│ │ │
│ │ 200 OK │
│ │<──────────────────│
│ 200 OK │ │
│<─────────────────│ │
HMAC Signing Example (Python):
import hmac
import hashlib
import time
def sign_request(method: str, path: str, body: str, secret: bytes) -> dict:
timestamp = int(time.time())
payload = f"{method}{path}{body}{timestamp}"
signature = hmac.new(secret, payload.encode(), hashlib.sha256).hexdigest()
return {
"t": str(timestamp),
"v1": signature
}
# Usage
sig = sign_request("POST", "/v1/payment-intents", '{"amount":5000}', secret_bytes)
headers = {
"Authorization": "Bearer ipk_test_...",
"X-Signature": f"t={sig['t']},v1={sig['v1']}"
}
3. Key Rotation Flow
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │
│ (Admin) │ │ (Operator) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Initiate key │ │
│ rotation │ │
│─────────────────>│ │
│ │ POST /v1/keys/ │
│ │ {id}/rotate │
│ │ {"expire_old_ │
│ │ after":604800} │
│ │──────────────────>│
│ │ │
│ │ │── Create new key
│ │ │ (same perms)
│ │ │── Old key stays
│ │ │ valid 7 days
│ │ │── rotated_from
│ │ │ linkage
│ │ │
│ │ 201 + new key │
│ │ + old_key_expires│
│ │<──────────────────│
│ │ │
│ New key value + │ │
│ 7-day cooldown │ │
│<─────────────────│ │
│ │ │
│ Deploy new key │ │
│ to services │ │
│ ═══════════════════════════ │
│ ║ 7-Day Overlap Window ║ │
│ ║ Both keys accepted ║ │
│ ═══════════════════════════ │
│ │ │
│ 7 days later │ │
│─────────────────>│ │
│ │ Verify rotation │
│ │ completed │
│ │──────────────────>│
│ │ │── Old key
│ │ │ auto-expired
│ │ │
│ │ Revoke old key │
│ │ (cleanup) │
│ │──────────────────>│
│ │ │
│ │ 200 deleted=true │
│ │<──────────────────│
Rotation Request:
POST /v1/keys/key_01J7Z0A1B2C3D4E5F6G7H8I9J/rotate
Content-Type: application/json
Authorization: Bearer ***
{
"expire_old_after": 604800
}
Rotation Response:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "key_01J7Z0D4E5F6G7H8I9J0K1L2M",
"label": "prod-summary-bot (rotated 2026-05-27)",
"prefix": "ipk_",
"key": "ipk_test_9o8p7q6r5s4t3u2v1w0x9y8z7a6b5c4d3e2f1g0h9i8j7k6l5m4n3o2p1q",
"permissions": { "payments": "write", "subscriptions": "read", "webhooks": "write", "analytics": "none" },
"constraints": { "allowed_ips": ["203.0.113.0/24"], "allowed_methods": ["GET", "POST"], "max_daily_requests": 10000 },
"expires_at": null,
"last_used_at": null,
"created_at": "2026-05-27T15:05:00Z",
"updated_at": "2026-05-27T15:05:00Z",
"rotated_from": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"old_key_expires_at": "2026-06-03T15:05:00Z"
}
4. Key Revocation Flow (Emergency)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Human │ │ Agent │ │ ItPay │
│ (Admin) │ │ (Operator) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Key compromised │ │
│ detected │ │
│─────────────────>│ │
│ │ DELETE /v1/keys/ │
│ │ {id} │
│ │──────────────────>│
│ │ │
│ │ │── Mark deleted
│ │ │ deleted=true
│ │ │── Immediate
│ │ │ invalidation
│ │ │── All in-flight
│ │ │ requests fail
│ │ │── Write audit
│ │ │ log entry
│ │ │
│ │ 200 deleted:true │
│ │ + deleted_at │
│ │<──────────────────│
│ │ │
│ Investigate │ │
│ scope │ │
│─────────────────>│ │
│ │ GET /audit? │
│ │ key_id=... │
│ │──────────────────>│
│ │ │
│ │ [audit log] │
│ │<──────────────────│
│ Audit findings │ │
│<─────────────────│ │
│ │ │
│ Rotate sibling │ │
│ keys │ │
Revocation Request & Response:
DELETE /v1/keys/key_01J7Z0A1B2C3D4E5F6G7H8I9J
Authorization: Bearer ***
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"deleted": true,
"label": "prod-summary-bot (rotated 2026-05-27)",
"deleted_at": "2026-05-27T16:00:00Z"
}
Attempted use of revoked key:
GET /v1/payments
Authorization: Bearer ipk_test_revoked_***
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"type": "authentication_error",
"code": "key_deleted",
"message": "Key ipk_test_*** has been deleted and cannot authenticate requests.",
"request_id": "req_01J7Z0J1K2L3M4N5O6P7Q8R9S"
}
}
5. Error Scenarios
Invalid IP (IP Restriction)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Attacker │ │ Agent │ │ ItPay │
│ │ │ (Operator) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Stolen key │ │
│ from IP │ │
│ 192.0.2.5 │ │
│─────────────────>│ │
│ │ GET /v1/ │
│ │ payment-intents │
│ │──────────────────>│
│ │ │
│ │ │── Extract key
│ │ │── IP check:
│ │ │ allowed_ips=
│ │ │ ["203.0.113.0/24"]
│ │ │── Client IP:
│ │ │ 192.0.2.5
│ │ │── ✗ NOT allowed
│ │ │── Audit: ip_restricted
│ │ │
│ │ 403 ip_restricted│
│ │<──────────────────│
│ 403 + audit │ │
│<─────────────────│ │
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": {
"type": "authorization_error",
"code": "ip_restricted",
"message": "Request from 192.0.2.5 is not allowed. Key permits: 203.0.113.0/24.",
"key_id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"key_prefix": "ipk_",
"request_id": "req_01J7Z0E5F6G7H8I9J0K1L2M3N"
}
}
Expired Key
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Client │ │ Agent │ │ ItPay │
│ │ │ (Operator) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Old key used │ │
│─────────────────>│ │
│ │ POST /v1/payments│
│ │──────────────────>│
│ │ │── Check expiry:
│ │ │ expires_at =
│ │ │ 2026-05-01T00:00:00Z
│ │ │── Now: 2026-05-27
│ │ │── ✗ EXPIRED
│ │ │
│ │ 403 expired │
│ │<──────────────────│
│ 403 │ │
│<─────────────────│ │
GET /v1/payments
Authorization: Bearer ipk_test_expired***
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": {
"type": "authorization_error",
"code": "expired",
"message": "Key ipk_test_*** expired at 2026-05-01T00:00:00Z.",
"key_id": "key_01J7Z0F1G2H3I4J5K6L7M8N9O",
"key_prefix": "ipk_",
"request_id": "req_01J7Z0G1H2I3J4K5L6M7N8O9P"
}
}
Permission Denied
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Client │ │ Agent │ │ ItPay │
│ │ │ (Operator) │ │ Protocol │
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ Read-only key │ │
│ tries write │ │
│─────────────────>│ │
│ │ POST /v1/ │
│ │ payment-intents │
│ │──────────────────>│
│ │ │── Key has
│ │ │ payments:read
│ │ │── POST needs
│ │ │ payments:write
│ │ │── ✗ DENIED
│ │ │
│ │ 403 permission │
│ │ _denied │
│ │<──────────────────│
│ 403 + details │ │
│<─────────────────│ │
POST /v1/payment-intents
Authorization: Bearer ipk_test_readonly_***
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": {
"type": "authorization_error",
"code": "permission_denied",
"message": "Key ipk_test_*** is not authorized to write to payments.",
"key_id": "key_01J7Z0H1I2J3K4L5M6N7O8P9Q",
"key_prefix": "ipk_",
"resource": "payments",
"required_level": "write",
"actual_level": "read",
"request_id": "req_01J7Z0I1J2K3L4M5N6O7P8Q9R"
}
1. Key Object
Every API key is represented by the following object:
| Field | Type | Description |
|---|---|---|
id | string (format: key_xxx) | Unique identifier for the key |
label | string | Human-readable name (e.g. "Production summary service") |
prefix | string | Key prefix for identification — ipk_ for ItPay keys (cf. Stripe rk_) |
key | string | The full key value. Shown only once at creation. Omni present on all other responses. |
permissions | object | Map of endpoint groups to permission levels (see Permission Model) |
constraints | object | Usage restrictions (see below) |
expires_at | string (ISO 8601) | Optional expiration timestamp. Key automatically expires after this point. |
last_used_at | string (ISO 8601) | Timestamp of the most recent API request using this key |
created_at | string (ISO 8601) | Timestamp of key creation |
updated_at | string (ISO 8601) | Timestamp of last modification |
Constraints Object
| Field | Type | Description |
|---|---|---|
allowed_ips | array[string] (CIDR) | IPv4 CIDR ranges allowed to use this key. Empty array = no IP restriction. |
allowed_methods | array[string] | HTTP methods this key may use (e.g. ["GET", "POST"]). Empty = all methods. |
max_daily_requests | number | Maximum API requests permitted per rolling 24-hour window. 0 = unlimited. |
Full Key Object Example
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"label": "prod-summary-bot",
"prefix": "ipk_",
"key": "ipk_test_5a8b3c2d1e0f9g7h6i5j4k3l2m1n0o9p8q7r6s5t4u3v2w1x0y9z",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "read",
"webhooks": "none",
"deliveries": "read",
"installs": "none",
"analytics": "read"
},
"constraints": {
"allowed_ips": [
"203.0.113.0/24",
"198.51.100.10/32"
],
"allowed_methods": ["GET", "POST", "PATCH"],
"max_daily_requests": 10000
},
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": "2026-05-27T14:30:00Z",
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-27T14:30:00Z"
}
2. Create Key
Create a new restricted API key with granular permissions and constraints.
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/keys | Create a new API key |
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
label | string | yes | Human-readable label for the key |
permissions | object | yes | Permission levels per endpoint group |
constraints | object | no | Usage restrictions (IPs, methods, rate limits) |
constraints.allowed_ips | array[string] (CIDR) | no | IPv4 CIDR allowlist |
constraints.allowed_methods | array[string] | no | Allowed HTTP methods |
constraints.max_daily_requests | number | no | Daily request cap (0 = unlimited) |
expires_at | string (ISO 8601) | no | Optional key expiration timestamp |
Request Example
POST /v1/keys
Content-Type: application/json
Authorization: Bearer sk_live_***
{
"label": "prod-summary-bot",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "read",
"webhooks": "none",
"deliveries": "read",
"installs": "none",
"analytics": "read"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24"],
"allowed_methods": ["GET", "POST"],
"max_daily_requests": 10000
},
"expires_at": "2027-01-01T00:00:00Z"
}
Response Example
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"label": "prod-summary-bot",
"prefix": "ipk_",
"key": "ipk_test_5a8b3c2d1e0f9g7h6i5j4k3l2m1n0o9p8q7r6s5t4u3v2w1x0y9z",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "read",
"webhooks": "none",
"deliveries": "read",
"installs": "none",
"analytics": "read"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24"],
"allowed_methods": ["GET", "POST"],
"max_daily_requests": 10000
},
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": null,
"created_at": "2026-05-27T08:00:00Z",
"updated_at": "2026-05-27T08:00:00Z"
}
Important: The
keyfield value is returned only at creation time. It will not be present in subsequentGET /v1/keys/:idresponses. Store it immediately.
3. List Keys
List all API keys for the authenticated account. Supports pagination.
| Method | Endpoint | Description |
|---|---|---|
GET | /v1/keys | List all API keys |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 10 | Number of keys to return (max 100) |
starting_after | string | — | Cursor for pagination — return results after this key ID |
ending_before | string | — | Cursor for pagination — return results before this key ID |
Response Example
HTTP/1.1 200 OK
Content-Type: application/json
{
"object": "list",
"data": [
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"label": "prod-summary-bot",
"prefix": "ipk_",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "read",
"webhooks": "none",
"deliveries": "read",
"installs": "none",
"analytics": "read"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24"],
"allowed_methods": ["GET", "POST"],
"max_daily_requests": 10000
},
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": "2026-05-27T14:30:00Z",
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-27T14:30:00Z"
},
{
"id": "key_01J7Z0C3D4E5F6G7H8I9J0K1L",
"label": "staging-readonly",
"prefix": "ipk_",
"permissions": {
"payments": "read",
"subscriptions": "read",
"refunds": "read",
"webhooks": "read",
"deliveries": "read",
"installs": "read",
"analytics": "read"
},
"constraints": {
"allowed_ips": [],
"allowed_methods": ["GET"],
"max_daily_requests": 0
},
"expires_at": null,
"last_used_at": "2026-05-26T09:12:00Z",
"created_at": "2026-05-15T10:00:00Z",
"updated_at": "2026-05-20T16:30:00Z"
}
],
"has_more": false
}
Note: The
keyfield is never present in list responses. It is only included in the creation response.
4. Update Key
Update the label, permissions, constraints, or expiration of an existing key.
| Method | Endpoint | Description |
|---|---|---|
PATCH | /v1/keys/{id} | Update an API key |
Request Fields
All fields are optional. Only provided fields are updated.
| Field | Type | Description |
|---|---|---|
label | string | New human-readable label |
permissions | object | Updated permission map (replaces entire map) |
constraints | object | Updated constraints (replaces entire constraints object) |
constraints.allowed_ips | array[string] (CIDR) | Replaces IP allowlist |
constraints.allowed_methods | array[string] | Replaces method allowlist |
constraints.max_daily_requests | number | Replaces daily request cap |
expires_at | string (ISO 8601) | Update expiration. Pass null to remove expiration. |
Request Example
PATCH /v1/keys/key_01J7Z0A1B2C3D4E5F6G7H8I9J
Content-Type: application/json
Authorization: Bearer sk_live_***
{
"label": "prod-summary-bot-v2",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "write",
"webhooks": "read",
"deliveries": "read",
"installs": "none",
"analytics": "read"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24", "198.51.100.0/24"],
"allowed_methods": ["GET", "POST", "PATCH"],
"max_daily_requests": 25000
}
}
Response Example
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"label": "prod-summary-bot-v2",
"prefix": "ipk_",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "write",
"webhooks": "read",
"deliveries": "read",
"installs": "none",
"analytics": "read"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24", "198.51.100.0/24"],
"allowed_methods": ["GET", "POST", "PATCH"],
"max_daily_requests": 25000
},
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": "2026-05-27T14:30:00Z",
"created_at": "2026-05-20T08:00:00Z",
"updated_at": "2026-05-27T15:00:00Z"
}
5. Rotate Key
Rotate a key to generate a new credential while optionally keeping the old key active for a cooldown period. This enables zero-downtime credential rotation.
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/keys/{id}/rotate | Rotate an API key |
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
expire_old_after | number | no | Seconds to keep the old key valid after rotation (max 2592000 = 30 days). If omitted, the old key is immediately revoked. |
Request Example (Immediate Rotation)
POST /v1/keys/key_01J7Z0A1B2C3D4E5F6G7H8I9J/rotate
Content-Type: application/json
Authorization: Bearer sk_live_***
{
"expire_old_after": 604800
}
Response Example
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "key_01J7Z0D4E5F6G7H8I9J0K1L2M",
"label": "prod-summary-bot-v2 (rotated 2026-05-27)",
"prefix": "ipk_",
"key": "ipk_test_9o8p7q6r5s4t3u2v1w0x9y8z7a6b5c4d3e2f1g0h9i8j7k6l5m4n3o2p1q",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "write",
"webhooks": "read",
"deliveries": "read",
"installs": "none",
"analytics": "read"
},
"constraints": {
"allowed_ips": ["203.0.113.0/24", "198.51.100.0/24"],
"allowed_methods": ["GET", "POST", "PATCH"],
"max_daily_requests": 25000
},
"expires_at": null,
"last_used_at": null,
"created_at": "2026-05-27T15:05:00Z",
"updated_at": "2026-05-27T15:05:00Z",
"rotated_from": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"old_key_expires_at": "2026-06-03T15:05:00Z"
}
Rotation Behavior
- New key is created — A brand new key object with a new
idandkeyvalue. All permissions, constraints, and label are copied from the original. - Old key cooldown — If
expire_old_afteris set, the original key remains valid for that duration. During cooldown, both the old and new keys are accepted. - Audit trail — The
rotated_fromfield on the new key links back to the original. The old key records arotated_tofield on its object. - Recommended workflow:
POST /v1/keys/{id}/rotate { "expire_old_after": 604800 }│├── Deploy new key to services│└── Wait 7 days → old key auto-expires
6. Delete / Expire Key
Delete (expire) a key immediately. Once deleted, the key can no longer authenticate any request. A deleted key object is retained for audit purposes but marked with deleted: true.
| Method | Endpoint | Description |
|---|---|---|
DELETE | /v1/keys/{id} | Delete (expire) an API key |
Response Example
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"deleted": true,
"label": "prod-summary-bot-v2 (rotated 2026-05-27)",
"deleted_at": "2026-05-27T16:00:00Z"
}
Error Response (Key Not Found)
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"type": "invalid_request_error",
"code": "key_not_found",
"message": "No API key found with id: key_01J7Z0NONEXISTENT"
}
}
7. Permission Model
Permissions are defined as a map of endpoint groups to access levels. Each group corresponds to a set of ItPay API endpoints.
Endpoint Groups
| Group | Endpoints | Description |
|---|---|---|
payments | /v1/payment-intents, /v1/payments/one-time | Create, read, and manage payment intents |
subscriptions | /v1/subscriptions | Create, read, and manage subscriptions |
refunds | /v1/refunds | Process and query refunds |
webhooks | /v1/webhook-endpoints, webhook secret management | Configure and manage webhook endpoints |
deliveries | Delivery status endpoints | Query delivery confirmations and status |
installs | /v1/installs | Create, read, and manage service installs |
analytics | Analytics and reporting endpoints | Read aggregated usage and transaction data |
Permission Levels
| Level | Description |
|---|---|
none | No access. Requests to endpoints in this group are rejected with a 403 error. |
read | Read-only access. Only GET requests (and equivalent list/query operations) are permitted. |
write | Full access. All HTTP methods are permitted, including POST, PATCH, and DELETE. |
Default Permissions
If a group is omitted from the permissions object, it defaults to none.
Example: Read-Only Analytics Key
{
"label": "analytics-reader",
"permissions": {
"payments": "none",
"subscriptions": "none",
"refunds": "none",
"webhooks": "none",
"deliveries": "read",
"installs": "read",
"analytics": "read"
}
}
Example: Payment Processing Key
{
"label": "payment-processor",
"permissions": {
"payments": "write",
"subscriptions": "write",
"refunds": "write",
"webhooks": "write",
"deliveries": "read",
"installs": "none",
"analytics": "read"
}
}
8. Authorization Behavior
Every incoming API request is gated through the following authorization pipeline:
Request arrives
│
▼
1. Extract bearer token → match to key
│
▼
2. Key exists and !deleted? ─No──→ 401 Unauthorized
│Yes
▼
3. Key expired? ─Yes─→ 403 expired
│No
▼
4. IP allowed? (if allowed_ips set) ─No──→ 403 ip_restricted
│Yes
▼
5. Method allowed? (if set) ─No──→ 403 method_restricted
│Yes
▼
6. Daily quota available? ─No──→ 403 rate_limit_exceeded
│Yes
▼
7. Group permission = read/write? ─No──→ 403 permission_denied
│Yes
▼
8. For read level: method is GET? ─No──→ 403 insufficient_permissions
│Yes
▼
Request forwarded to handler
403 Error Response Format
All authorization failures return a consistent JSON error body:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": {
"type": "authorization_error",
"code": "permission_denied",
"message": "Key ipk_test_*** is not authorized to write to payments.",
"key_id": "key_01J7Z0A1B2C3D4E5F6G7H8I9J",
"key_prefix": "ipk_",
"resource": "payments",
"required_level": "write",
"actual_level": "read",
"request_id": "req_01J7Z0E5F6G7H8I9J0K1L2M3N"
}
}
Authorization Error Codes
| Code | HTTP Status | Description |
|---|---|---|
permission_denied | 403 | Key lacks sufficient permissions for the endpoint group |
expired | 403 | Key has passed its expires_at timestamp |
ip_restricted | 403 | Request origin IP is not in the key's allowed_ips |
method_restricted | 403 | HTTP method is not in the key's allowed_methods |
rate_limit_exceeded | 403 | Key has exceeded its max_daily_requests quota |
key_not_found | 404 | No key matches the provided bearer token |
key_deleted | 401 | Key has been deleted |
Audit Logging
Every API request is logged with the associated key ID, enabling per-key usage tracking:
| Audit Field | Description |
|---|---|
key_id | The key used for authentication |
key_prefix | Key prefix for identification |
endpoint | The API endpoint requested |
method | HTTP method |
ip_address | Request origin IP |
status_code | Response HTTP status code |
timestamp | Request timestamp |
request_id | Unique request identifier |
This audit data is accessible via the Analytics endpoints and the dashboard.
9. Best Practices
One Key Per Service
Create a separate restricted key for each service, agent, or deployment. This limits blast radius — a compromised key only affects the single service it was created for.
┌─ Summary Bot ──────┐ ┌─ Transcription ────┐ ┌─ Analytics ──────┐
│ ipk_summ_*** │ │ ipk_trans_*** │ │ ipk_analytics_***│
│ payments: write │ │ payments: write │ │ analytics: read │
│ webhooks: write │ │ webhooks: none │ │ other: none │
│ refunds: read │ │ subscriptions: write│ └─────────────────┘
└────────────────────┘ └────────────────────┘
IP Restrictions for Production
Always set allowed_ips for production keys. Use the narrowest CIDR range possible:
- Single deploy:
/32exact IP - VPC subnet:
/24or/20 - CI/CD runners: add runner IP ranges explicitly
Regular Rotation Schedule
| Environment | Recommended Rotation | Delayed Expiry |
|---|---|---|
| Production | Every 90 days | 7-day overlap |
| Staging | Every 180 days | 1-day overlap |
| Development | On-demand | No overlap |
Monitor last_used_at
Regularly review keys with no recent last_used_at activity:
- Keys unused for > 90 days should be rotated or deleted
- Sudden changes in usage patterns may indicate key compromise
- Use the
last_used_attimestamp to identify stale keys before expiry
Emergency Revocation
If a key is suspected compromised:
- Immediate:
DELETE /v1/keys/{id}— instantly invalidates the key - Investigate: Check audit logs for the compromised key's activity
- Rotate sibling keys: Review and rotate any other keys with similar permissions
- Notify: Update affected services with new credentials
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
key_not_found | 404 | No API key found with the given ID |
key_deleted | 401 | Key has been deleted and cannot authenticate |
permission_denied | 403 | Key lacks the required permission level |
expired | 403 | Key has passed its expires_at timestamp |
ip_restricted | 403 | Request IP is not in allowed_ips |
method_restricted | 403 | HTTP method is not in allowed_methods |
rate_limit_exceeded | 403 | max_daily_requests quota exceeded |
invalid_rotation | 400 | Rotation request is malformed or the key is already pending rotation |
Next Steps
- Security Overview — Platform-wide security model and threat analysis
- Install — Create an install and generate an installation-scoped key
- Authentication — How bearer tokens work across the ItPay API