Skip to main content

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:

FieldTypeDescription
idstring (format: key_xxx)Unique identifier for the key
labelstringHuman-readable name (e.g. "Production summary service")
prefixstringKey prefix for identification — ipk_ for ItPay keys (cf. Stripe rk_)
keystringThe full key value. Shown only once at creation. Omni present on all other responses.
permissionsobjectMap of endpoint groups to permission levels (see Permission Model)
constraintsobjectUsage restrictions (see below)
expires_atstring (ISO 8601)Optional expiration timestamp. Key automatically expires after this point.
last_used_atstring (ISO 8601)Timestamp of the most recent API request using this key
created_atstring (ISO 8601)Timestamp of key creation
updated_atstring (ISO 8601)Timestamp of last modification

Constraints Object

FieldTypeDescription
allowed_ipsarray[string] (CIDR)IPv4 CIDR ranges allowed to use this key. Empty array = no IP restriction.
allowed_methodsarray[string]HTTP methods this key may use (e.g. ["GET", "POST"]). Empty = all methods.
max_daily_requestsnumberMaximum 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.

MethodEndpointDescription
POST/v1/keysCreate a new API key

Request Fields

FieldTypeRequiredDescription
labelstringyesHuman-readable label for the key
permissionsobjectyesPermission levels per endpoint group
constraintsobjectnoUsage restrictions (IPs, methods, rate limits)
constraints.allowed_ipsarray[string] (CIDR)noIPv4 CIDR allowlist
constraints.allowed_methodsarray[string]noAllowed HTTP methods
constraints.max_daily_requestsnumbernoDaily request cap (0 = unlimited)
expires_atstring (ISO 8601)noOptional 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 key field value is returned only at creation time. It will not be present in subsequent GET /v1/keys/:id responses. Store it immediately.


3. List Keys

List all API keys for the authenticated account. Supports pagination.

MethodEndpointDescription
GET/v1/keysList all API keys

Query Parameters

ParameterTypeDefaultDescription
limitnumber10Number of keys to return (max 100)
starting_afterstringCursor for pagination — return results after this key ID
ending_beforestringCursor 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 key field 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.

MethodEndpointDescription
PATCH/v1/keys/{id}Update an API key

Request Fields

All fields are optional. Only provided fields are updated.

FieldTypeDescription
labelstringNew human-readable label
permissionsobjectUpdated permission map (replaces entire map)
constraintsobjectUpdated constraints (replaces entire constraints object)
constraints.allowed_ipsarray[string] (CIDR)Replaces IP allowlist
constraints.allowed_methodsarray[string]Replaces method allowlist
constraints.max_daily_requestsnumberReplaces daily request cap
expires_atstring (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.

MethodEndpointDescription
POST/v1/keys/{id}/rotateRotate an API key

Request Fields

FieldTypeRequiredDescription
expire_old_afternumbernoSeconds 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

  1. New key is created — A brand new key object with a new id and key value. All permissions, constraints, and label are copied from the original.
  2. Old key cooldown — If expire_old_after is set, the original key remains valid for that duration. During cooldown, both the old and new keys are accepted.
  3. Audit trail — The rotated_from field on the new key links back to the original. The old key records a rotated_to field on its object.
  4. 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.

MethodEndpointDescription
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

GroupEndpointsDescription
payments/v1/payment-intents, /v1/payments/one-timeCreate, read, and manage payment intents
subscriptions/v1/subscriptionsCreate, read, and manage subscriptions
refunds/v1/refundsProcess and query refunds
webhooks/v1/webhook-endpoints, webhook secret managementConfigure and manage webhook endpoints
deliveriesDelivery status endpointsQuery delivery confirmations and status
installs/v1/installsCreate, read, and manage service installs
analyticsAnalytics and reporting endpointsRead aggregated usage and transaction data

Permission Levels

LevelDescription
noneNo access. Requests to endpoints in this group are rejected with a 403 error.
readRead-only access. Only GET requests (and equivalent list/query operations) are permitted.
writeFull 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

CodeHTTP StatusDescription
permission_denied403Key lacks sufficient permissions for the endpoint group
expired403Key has passed its expires_at timestamp
ip_restricted403Request origin IP is not in the key's allowed_ips
method_restricted403HTTP method is not in the key's allowed_methods
rate_limit_exceeded403Key has exceeded its max_daily_requests quota
key_not_found404No key matches the provided bearer token
key_deleted401Key has been deleted

Audit Logging

Every API request is logged with the associated key ID, enabling per-key usage tracking:

Audit FieldDescription
key_idThe key used for authentication
key_prefixKey prefix for identification
endpointThe API endpoint requested
methodHTTP method
ip_addressRequest origin IP
status_codeResponse HTTP status code
timestampRequest timestamp
request_idUnique 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: /32 exact IP
  • VPC subnet: /24 or /20
  • CI/CD runners: add runner IP ranges explicitly

Regular Rotation Schedule

EnvironmentRecommended RotationDelayed Expiry
ProductionEvery 90 days7-day overlap
StagingEvery 180 days1-day overlap
DevelopmentOn-demandNo 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_at timestamp to identify stale keys before expiry

Emergency Revocation

If a key is suspected compromised:

  1. Immediate: DELETE /v1/keys/{id} — instantly invalidates the key
  2. Investigate: Check audit logs for the compromised key's activity
  3. Rotate sibling keys: Review and rotate any other keys with similar permissions
  4. Notify: Update affected services with new credentials

Error Codes

CodeHTTP StatusDescription
key_not_found404No API key found with the given ID
key_deleted401Key has been deleted and cannot authenticate
permission_denied403Key lacks the required permission level
expired403Key has passed its expires_at timestamp
ip_restricted403Request IP is not in allowed_ips
method_restricted403HTTP method is not in allowed_methods
rate_limit_exceeded403max_daily_requests quota exceeded
invalid_rotation400Rotation 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