Merchant API keys
List, create, and revoke a merchant's API keys from the Tokeflow Dashboard, and discover which scopes each key type can carry.
These endpoints power the API keys screen of the Tokeflow Dashboard for a merchant: list the keys that exist, mint a new one, look up which scopes it may carry, and revoke a key that should no longer work. The keys they manage are the same sk_… (secret) and pk_… (public) credentials your integration presents to the public API — the Dashboard is simply the human-friendly way to manage them.
Secret keys grant server-side access and belong in a secrets manager; public keys are client-safe and restricted to what the Bridge SDK needs in the browser. See Authentication for the full model and Environments & API keys for how keys encode environment and entity in their prefix.
These endpoints power the Tokeflow Dashboard UI. They are session-authenticated (JWT bearer) — the token comes from signing in to the Dashboard, not from an sk_/pk_ API key. Every request must also carry the x-merchant-id header identifying the merchant whose keys are being managed; the signed-in user must be a member with access to that merchant. To manage keys programmatically from your backend instead, use the public API keys endpoints.
How the key screen flows
The scopes endpoint feeds the key-creation form: it returns only the scopes valid for this merchant and the chosen key type, so the user can never select a scope the key could not carry. After creation, the full key is never shown again — the list identifies each key by its key_prefix alone.
The API key object
Returned on creation; the list endpoint returns the same object without the creation-only api_key secret.
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the API key, prefixed ak_. |
entity_type | string | Type of entity the key belongs to — organization or merchant. Keys created on this surface belong to a merchant. |
entity_id | string | ID of the owning entity (mrc_… for merchant keys). |
name | string | Human-readable name for the key. |
key_prefix | string | Non-secret key prefix used for identification (e.g. sk_live_mer_7d21). The only part of the key shown after creation. |
scopes | array | Scopes/permissions granted to the key. |
allowed_ips | array | null | IP addresses and CIDR ranges allowed to use the key. null when the key is unrestricted. |
is_active | boolean | Whether the key is active. Requests with an inactive key fail authentication. |
expires_at | string | null | Expiration date (ISO 8601 UTC), or null if the key never expires. |
last_used_at | string | null | Last time the key was used, or null if it has never been used. |
created_at | string | Creation timestamp (ISO 8601 UTC). |
api_key | string | The actual API key. Only returned on creation. |
key_hash | string | Hash of the key material, used server-side for verification. Not a credential — it cannot be used to authenticate. |
last_used_at is the Dashboard's best signal for whether a key is really in use. Before revoking a key that looks idle, check that its last_used_at has not moved recently — a stale name does not guarantee a stale key.
Endpoints
GET/api/v1/merchant/api-keys
Auth: Dashboard session (JWT bearer). Merchant membership required.
Returns a paginated list of the merchant's API keys — the merchant is taken from the x-merchant-id header.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
page | number | No | Page number, 1-indexed (default 1, min 1). |
limit | number | No | Items per page (default 20, min 1, max 100). |
Example request
curl -G https://api.tokeflow.com/api/v1/merchant/api-keys \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "x-merchant-id: mrc_8a3f12d9" \
--data-urlencode "page=1" \
--data-urlencode "limit=20"Example response — 200 OK
{
"data": [
{
"id": "ak_9f2e6c81",
"entity_type": "merchant",
"entity_id": "mrc_8a3f12d9",
"name": "Checkout service (production)",
"key_prefix": "sk_live_mer_7d21",
"scopes": ["transactions:read", "transactions:write"],
"allowed_ips": ["203.0.113.10", "198.51.100.0/24"],
"is_active": true,
"expires_at": null,
"last_used_at": "2026-08-11T18:22:04.000Z",
"created_at": "2026-03-02T09:15:00.000Z"
},
{
"id": "ak_5b0d3a47",
"entity_type": "merchant",
"entity_id": "mrc_8a3f12d9",
"name": "Storefront widget",
"key_prefix": "pk_live_mer_0c1d",
"scopes": ["tokens:read"],
"allowed_ips": null,
"is_active": false,
"expires_at": "2026-12-31T23:59:59.000Z",
"last_used_at": "2026-07-30T10:41:52.000Z",
"created_at": "2026-01-20T16:00:00.000Z"
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 2,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
}data holds the keys for the current page and meta the pagination metadata — advance page while has_next is true. The full key is never included here; each key is identified by its non-secret key_prefix.
POST/api/v1/merchant/api-keys
Auth: Dashboard session (JWT bearer). Merchant membership required.
Creates an API key for the merchant identified by x-merchant-id. The response is the only place the full key (api_key) is ever returned.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name for the key (max 255 characters). Name it after the workload, e.g. Checkout service (production). |
scopes | array of strings | Yes | Scopes/permissions for the key — at least one. Must be valid for this merchant and key type; load the assignable values with GET /merchant/api-keys/scopes. |
allowed_ips | array of strings | No | IP addresses allowed to use the key — up to 50 entries, each up to 45 characters. Omit the field entirely for no restriction — an empty array is rejected, since it reads as a restriction but enforces none. See the note below for accepted formats. |
key_type | string | No | Type of key to create — secret (server-side, sk_…) or public (client-safe, pk_…). |
expires_at | string | No | Expiration date for the key (ISO 8601). Omit for a key that never expires. |
entity_type | string | No | Type of entity the key belongs to — organization or merchant. On this surface, omit it or send merchant. |
entity_id | string | No | ID of the entity the key belongs to (e.g. mrc_8a3f12d9). Omit to use the merchant from x-merchant-id. |
allowed_ips formats. IPv4 addresses must be dotted-quad (203.0.113.10); CIDR entries must be in network form (192.168.1.0/24, not 192.168.1.5/24); IPv6 addresses and ranges are accepted (2001:db8::/32). A wildcard entry — *, 0.0.0.0/0, or ::/0 — allows every IP and cannot be combined with specific addresses.
The full key is returned exactly once. The api_key field appears only in this creation response — Tokeflow stores a hash, not the key itself, and cannot show it again. Copy it to a secrets manager immediately; every later response identifies the key by its non-secret key_prefix only.
Example request
curl -X POST https://api.tokeflow.com/api/v1/merchant/api-keys \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "x-merchant-id: mrc_8a3f12d9" \
-H "Content-Type: application/json" \
-d '{
"name": "Checkout service (production)",
"scopes": ["transactions:read", "transactions:write"],
"allowed_ips": ["203.0.113.10", "198.51.100.0/24"],
"key_type": "secret"
}'Example response — 201 Created
{
"id": "ak_9f2e6c81",
"entity_type": "merchant",
"entity_id": "mrc_8a3f12d9",
"name": "Checkout service (production)",
"key_prefix": "sk_live_mer_7d21",
"scopes": ["transactions:read", "transactions:write"],
"allowed_ips": ["203.0.113.10", "198.51.100.0/24"],
"is_active": true,
"expires_at": null,
"last_used_at": null,
"created_at": "2026-08-12T14:03:00.000Z",
"api_key": "sk_live_mer_7d21c39ab8e4f560",
"key_hash": "6dcd4ce23d88e2ee9568ba546c007c63d9131c1b"
}Handle this response like the credential it contains: surface api_key to the user exactly once and keep the body out of logs, error trackers, and analytics. From here on, identify the key by id and recognize it by key_prefix.
GET/api/v1/merchant/api-keys/scopes
Auth: Dashboard session (JWT bearer). Merchant membership required.
Returns the scopes that can be assigned to this merchant's API keys. Org-only scopes are excluded. Filter by key_type to get the scopes valid for public or secret keys — this is what the Dashboard's key-creation form uses to populate its scope picker before calling POST /merchant/api-keys.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
key_type | string | Yes | Key type to list scopes for — secret or public. Public keys are restricted to client-safe scopes. |
Example request
curl -G https://api.tokeflow.com/api/v1/merchant/api-keys/scopes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "x-merchant-id: mrc_8a3f12d9" \
--data-urlencode "key_type=secret"Example response — 200 OK (truncated)
{
"scopes": [
{
"scope": "transactions:read",
"description": "List and view transactions"
},
{
"scope": "transactions:write",
"description": "Create, capture, and refund transactions"
},
{
"scope": "customers:read",
"description": "List and view customers"
}
],
"wildcards": [
"transactions:*",
"customers:*"
]
}Each item in scopes pairs the scope string with a human-readable description. wildcards lists the wildcard scope strings valid for this entity and key type — a wildcard grants every scope it covers, so reserve them for trusted, tightly-controlled services and prefer explicit scope lists everywhere else.
DELETE/api/v1/merchant/api-keys/:id
Auth: Dashboard session (JWT bearer). Merchant membership required.
Revokes an API key permanently. Revocation takes effect immediately: any request presenting the revoked key fails authentication from that moment on. There is no undo — if the workload still needs access, create a replacement key first.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | ID of the API key to revoke (e.g. ak_9f2e6c81). |
Example request
curl -X DELETE https://api.tokeflow.com/api/v1/merchant/api-keys/ak_9f2e6c81 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "x-merchant-id: mrc_8a3f12d9"Example response — 204 No Content
The response body is empty.
If a secret key leaks, revoke it now — before creating the replacement. A brief outage of one workload is a better outcome than an attacker holding a live server-side credential. For planned rotation (no exposure), do the reverse: create the new key, deploy it, confirm it works, then revoke the old one.
Errors
The most common cases for these endpoints:
| HTTP | Typical cause |
|---|---|
400 | Invalid input — missing name or scopes, an unknown scope, an empty or malformed allowed_ips list, or a missing key_type on the scopes endpoint. |
401 | Missing, invalid, or expired Dashboard session token. |
403 | Signed in, but not a member with access to the merchant in x-merchant-id. |
404 | API key not found, or it does not belong to this merchant. |
See Errors for the general error format used across the API.
Related
- API keys (public API) — manage keys programmatically from your backend, including pause/reactivate.
- Authentication — the Bearer scheme, scopes, and key anatomy.
- Environments & API keys — key lifecycle, storage, and rotation practices.
- Errors — the standard error envelope.