Documentation

Partner keys

Mint, scope, rotate and revoke the keys your integration authenticates with.

Partner API keys

Your partner secret key (sk_live_… / sk_test_…) authenticates every call on the provisioning API. This page covers managing those keys yourself: minting scoped keys, rotating with zero downtime, restricting by IP, and setting expiry.

All endpoints below require a key with the keys:write scope, and are bounded by the key you call with — you can never create or preserve a key more powerful than the one making the request.


Least-privilege minting

Issue additional keys scoped to exactly what a given system needs — e.g. a read-only key for a dashboard, or an invoices-only key for a till fleet.

POST /api/v1/partner/keys
Authorization: Bearer sk_live_…

{
  "name": "reporting (read-only)",
  "scopes": ["tenants:read", "invoices:read"],
  "environment": "production",
  "expiresInDays": 90,
  "allowedIps": ["41.72.0.0/16", "203.0.113.10"]
}

Response (201) — the plaintext:

{
  "key": "sk_live_…",
  "apiKey": {
    "id": 44, "type": "partner", "environment": "production",
    "prefix": "sk_live_ab12", "lastFour": "9f2c",
    "scopes": ["tenants:read", "invoices:read"],
    "allowedIps": ["41.72.0.0/16", "203.0.113.10"],
    "expiresAt": "2026-10-17T09:00:00+00:00", "revoked": false
  },
  "message": "Store this key now. If you lose it, GET /partner/keys/{id}/secret reads it back."
}

Guardrails (each returns 403 with an error.code):

Rule error.code
Requested scopes ⊄ your scopes scope_escalation
Sandbox key minting a production key environment_escalation
New key would outlive your key expiry_escalation
Your key is IP-restricted but the new key isn't ip_escalation

scopes values must come from: tenants:read, tenants:write, devices:read, devices:write, keys:write, invoices:read, webhooks:read, webhooks:write.


Reading a key back

A secret you have lost does not need to be replaced. GET /partner/keys/{id}/secret returns the key's plaintext, unchanged — whatever is already using it keeps working.

GET /api/v1/partner/keys/44/secret
Authorization: Bearer sk_live_…
{
  "key": "sk_live_…",
  "apiKey": { "id": 44, "prefix": "sk_live_ab12", "lastFour": "9f2c", "revoked": false }
}

Prefer this to rotating. A rotation your fleet has not finished picking up is an outage waiting on the grace window to close; reading the key back changes nothing.

Holding a key's plaintext is holding the key, so this is bounded by exactly the rules that bound minting a copy of it — the calling key must hold every scope the target holds, be able to use its environment, and, if the caller is itself IP-restricted or expiring, the target must sit inside its allowlist and lifetime. A key can read itself and anything narrower, never anything broader.

Rule Status error.code
Target holds a scope you don't, or is production while you are sandbox 403 insufficient_privilege
You are IP-restricted and the target is broader 403 ip_escalation
The target outlives your key 403 expiry_escalation
The key is revoked, or predates this endpoint 404 secret_unavailable

Every read is recorded against the key that asked. Revoking a key destroys the stored copy, so a revoked key answers secret_unavailable rather than handing back a string that authenticates nothing.


Rotation (zero-downtime)

Rotate a key that may be aging or compromised. We mint a fresh replacement and put the old key into a grace window (default 7 days) before it expires — deploy the new secret, then let the old one lapse.

POST /api/v1/partner/keys/{id}/rotate
Authorization: Bearer sk_live_…

Response (201):

{
  "key": "sk_live_…",
  "apiKey": { "id": 45, "scopes": ["…"], "expiresAt": null },
  "rotatedKeyExpiresAt": "2026-07-26T09:00:00+00:00",
  "message": "Rotated. The old key keeps working until it expires — deploy this new key before then."
}
  • The replacement inherits the old key's scopes, environment, and IP allowlist.
  • Both keys work until rotatedKeyExpiresAt; after that only the new key does.
  • A key can be rotated once (already_rotated afterwards). You can only rotate a key whose scopes you hold.
  • Rotation hands you a new secret, so the minting guardrails apply to the key you rotate too: an IP-restricted key cannot rotate a key whose allowlist is broader than its own (ip_escalation), and an expiring key cannot rotate a key that outlives it (expiry_escalation).

IP allowlisting

Set allowedIps (exact addresses or CIDR ranges, IPv4 or IPv6; up to 20 entries) when minting. A request presenting that key from any other address is rejected 403 ip_not_allowed — before scopes or account status are even evaluated, so a stolen key used off-allowlist learns nothing.

"allowedIps": ["41.72.0.0/16", "2001:db8::/32", "203.0.113.10"]

The enforced client IP is your request's source address. If you call us through a proxy/load balancer, allowlist the address we actually see (configure TrustProxies on our side if you front us). An empty/absent allowlist means "any IP".


Expiry

Pass expiresInDays (1…3650) to make a key self-destruct. A key with an expiry cannot mint a key that outlives it, so a short-lived CI key can't bootstrap a permanent one.


List & revoke

GET    /api/v1/partner/keys        # your keys, masked (never the plaintext)
DELETE /api/v1/partner/keys/{id}   # revoke immediately

Revoking is instant — the next request with that key gets 401. You can only revoke a key whose scopes you hold. Full schemas are in openapi.yaml.


Security notes

  • Store secrets in a secrets manager, never in source. We only keep a SHA-256 hash — a lost key must be rotated/revoked, not recovered.
  • Prefer several narrow keys (one per system) over one all-powerful key; blast radius on leak is then limited to that system's scopes and IPs.
  • Set expiresInDays on CI/automation keys; rotate long-lived server keys periodically.
  • Restrict production keys with allowedIps to your known egress ranges.