Authentication
Every request to https://api.withflintpay.com carries a secret API key in a header. The key decides everything else: a flint_test_... key runs in test mode against one sandbox, and a flint_live_... key runs against your live account with real money. There is no separate test hostname and no mode flag on the request. Every Flint key is secret and belongs on your server, never in browser or mobile code.
Send your key#
Put the key in the Authorization header as a bearer token. This request reads your card-payments capability and confirms the key authenticates:
curl "https://api.withflintpay.com/v1/capabilities?capability=accept_card_payments" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{ "capability": "accept_card_payments", "status": "ready" }
],
"request_id": "0b8f2b26-9c1e-4f6a-8d35-7e4a2c9b1d60",
"meta": { "api_version": "2026-02-01" }
}
If you're signed in to the docs, your sandbox test key replaces YOUR_API_KEY in every sample it has the scopes to run.
Every response, success or failure, returns a request_id in the body and the same value in the X-Request-Id header. Quote it when you ask Flint Help. A failure returns an error object in place of data.
Flint accepts the key in either of two headers:
The bearer scheme. Use it unless your client reserves Authorization.
Authorization: Bearer YOUR_API_KEY
A dedicated header for clients and gateways that reserve Authorization for something else.
X-API-Key: YOUR_API_KEY
Send one or the other. If both are present, Flint reads X-API-Key and ignores the Authorization header. HTTP Basic auth and a key in the query string are rejected: Basic returns 401 INVALID_AUTHORIZATION_HEADER, and a key in the query string is not read at all, so the request fails with 401 API_KEY_REQUIRED.
Test and live keys#
Flint reads the key prefix on every request to pick the mode.
flint_test_
Test keys
Run against one sandbox with its own orders, customers, and payments. Card charges are simulated and no money moves. Each test key is bound to exactly one sandbox when it is created, so the key you send selects the sandbox. Keys from the dashboard are sandbox-bound automatically.
flint_live_
Live keys
Run against your live merchant data and charge real cards. Live keys are never bound to a sandbox. Create one when you are ready to take real payments, and grant it only the scopes that integration needs.
Test and live data never mix. An order created with a test key is invisible to a live key, and the reverse. A test key that is not bound to a sandbox fails with 401 SANDBOX_SELECTION_REQUIRED; a test key bound to an archived sandbox fails with 401 API_KEY_SANDBOX_UNAVAILABLE. Both fixes are the same: create a key in an active sandbox. For test cards, creating extra sandboxes, and resetting test data, see Sandboxes & test mode.
Get a key#
- The dashboard. Create test and live keys at app.withflintpay.com/developers/api-keys.
- The API setup flow. Create the merchant account and its first key from a script or an agent. See API & agent onboarding.
- A sandbox. With a live key,
POST /v1/developer/sandboxes/{sandbox_id}/test-keymints a test key bound to that sandbox. See Sandboxes & test mode. - The API keys endpoints. Create more keys with the key you already have. See Manage keys over the API.
The full secret is returned once, in the response that creates the key, and Flint cannot show it again. If you lose a key, create a replacement and revoke the old one.
The key object#
A key has a non-secret id for managing it and a secret value for authenticating with it.
api_key_idstringThe record id, such as key_01KWJ93G11C7MF8REX91MDS0CD. Use it to retrieve, update, or revoke the key. Safe to log.
key_prefixstringThe first characters of the secret, such as flint_test_1a2b3c4d. Safe to show in a UI or a log so you can tell keys apart.
secret_keystringThe full secret. Present only in the create response. This is the value you send in the header.
scopesstring[]The permissions the key holds. See Scopes.
statusstringactive or revoked. A revoked key cannot authenticate.
expires_atstringWhen the key stops authenticating, as an RFC 3339 timestamp. Absent when the key never expires.
| Field | Description |
|---|---|
merchant_id | The merchant the key belongs to. |
sandbox_id | The sandbox a test key is bound to. Absent on live keys. |
name | The label you gave the key. |
key_type | Always external for keys used against the public API. |
last_used_at | When the key last authenticated a request. |
created_at | When the key was created. |
updated_at | When the key was last changed. |
Scopes#
Each key holds a set of scopes such as commerce.orders.read, payments.payment_intents.write, or webhooks.write. Each endpoint checks for the scopes it needs, and a key that lacks them gets 403 INSUFFICIENT_SCOPE. Grant each key the fewest scopes its job needs, so a leaked key can do less.
A write scope satisfies its own read scope. A key holding commerce.orders.write can read and write orders, so there is no need to grant commerce.orders.read alongside it. A write scope does not satisfy a different resource, and an unqualified scope does not satisfy a qualified one such as commerce.refunds.tax_overrides.write.
When a request fails on scope, the error names the requirement and what is missing:
{
"error": {
"type": "authorization_error",
"code": "INSUFFICIENT_SCOPE",
"message": "The API key must include all required scopes: commerce.orders.write.",
"param": "scope",
"scope_requirement": {
"mode": "all",
"scopes": ["commerce.orders.write"]
},
"missing_scopes": ["commerce.orders.write"],
"request_id": "4a1f0c9e-2b7d-4f60-8c25-9e0b3d6f2a81"
}
}
scope_requirement.mode is all when the endpoint needs every listed scope and any when one of them is enough. missing_scopes lists the ones your key does not satisfy.
Manage keys over the API#
Key management needs accounts.api_keys.read to list and retrieve, and accounts.api_keys.write to create, update, and revoke.
/v1/api-keysRequired API key scopes: accounts.api_keys.read or accounts.api_keys.writeList your keys.
/v1/api-keysRequired API key scope: accounts.api_keys.writeCreate a key. Returns secret_key once.
/v1/api-keys/{api_key_id}Required API key scopes: accounts.api_keys.read or accounts.api_keys.writeRetrieve one key.
/v1/api-keys/{api_key_id}Required API key scope: accounts.api_keys.writeChange a key's name, scopes, or expires_at. Send "expires_at": null to remove an expiry.
/v1/api-keys/{api_key_id}/revokeRequired API key scope: accounts.api_keys.writeRevoke a key. Returns the key with status: "revoked".
A key can only see and manage keys in its own mode. A test key lists and edits keys bound to its sandbox, and a live key lists and edits live keys. A key outside that set returns 404 API_KEY_NOT_FOUND.
curl "https://api.withflintpay.com/v1/api-keys?page_size=1" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"api_key_id": "key_01KWJ93G11C7MF8REX91MDS0CD",
"name": "Production server",
"key_prefix": "flint_live_1a2b3c4d",
"scopes": ["commerce.orders.write", "payments.payment_intents.write"],
"key_type": "external",
"status": "active",
"last_used_at": "2026-07-02T18:04:11Z",
"created_at": "2026-06-01T09:12:00Z",
"updated_at": "2026-06-01T09:12:00Z"
}
],
"next_page_token": "eyJjIjoia2V5XzAxS1cifQ",
"request_id": "1b9d7c2a-4e8f-4a31-b7f0-5c9a2e8d4b36"
}
Three rules apply when you create a key:
- A test key must pass its own
sandbox_id. The new key is bound to that sandbox. Omitting it returns400 SANDBOX_ID_REQUIRED, and any other sandbox returns400 INVALID_SANDBOX_ID. - A live key must not pass
sandbox_id. Sending one returns400 SANDBOX_ID_NOT_ALLOWED. To mint test keys from a live key, use the sandbox test-key endpoint instead. - A key can only grant scopes it holds. Requesting more returns
403 INSUFFICIENT_SCOPEwith the extra scopes inmissing_scopes, so no key can escalate its own permissions.
expires_at is optional. Set it for a contractor, a migration script, or a load test, and the key stops authenticating at that time.
curl -X POST https://api.withflintpay.com/v1/api-keys \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"name": "CI runner",
"scopes": ["commerce.orders.read"],
"sandbox_id": "test_01KNB061AGD0CYYF16M5QAQE3N",
"expires_at": "2026-12-31T00:00:00Z"
}'
{
"data": {
"api_key_id": "key_01KWK4Q8Z1MB7F0T2S9R3V6HAX",
"name": "CI runner",
"key_prefix": "flint_test_9f8e7d6c",
"secret_key": "flint_test_9f8e7d6c...",
"sandbox_id": "test_01KNB061AGD0CYYF16M5QAQE3N",
"scopes": ["commerce.orders.read"],
"key_type": "external",
"status": "active",
"expires_at": "2026-12-31T00:00:00Z"
},
"request_id": "7c3e9f12-5a8b-4d61-9e27-1b4f8c6a2d95"
}
Rotate a key#
There is no rotate endpoint. Rotation is a create and a revoke, with the switchover in between so traffic never stops:
Authentication and authorization errors#
A 401 means Flint could not accept the credential. It has type: "authentication_error" and a WWW-Authenticate: Bearer realm="Flint API" header. A 403 means the key is valid but not allowed to do this. It has type: "authorization_error" and no WWW-Authenticate header. Neither is retryable as sent: fix the key or the request first.
{
"error": {
"type": "authentication_error",
"code": "API_KEY_NOT_FOUND",
"message": "No API key matches the provided credential.",
"request_id": "6f2c9a71-b0d3-4e58-9c16-7a4e0b2d8f53"
}
}
SANDBOX_SELECTION_REQUIRED is the most common first-run failure. Its remediation block says what to do next:
{
"error": {
"type": "authentication_error",
"code": "SANDBOX_SELECTION_REQUIRED",
"message": "Test mode API keys must be sandbox-bound before they can authenticate public API requests.",
"request_id": "f81d4fae-7dec-4b1d-a765-00a0c91e6bf6",
"remediation": {
"retryable": false,
"next_actions": [
{
"action_type": "issue_developer_sandbox_test_key",
"reason_code": "SANDBOX_BOUND_TEST_KEY_REQUIRED",
"reason_message": "Create or use a sandbox-bound test key for the sandbox this request should target.",
"required_fields": ["sandbox_id"]
}
]
}
}
}
A request rejected before Flint identifies the key, because the key is missing, malformed, unknown, revoked, or expired, leaves no entry in your request log, so debug it from the error body. Once the key is identified, the request is logged even when it fails, including sandbox and scope failures. See Debugging for tracing a request by id and Error handling for the full error object and retry rules.
Keep keys on the server#
Important: Never put a key in client code
There is no publishable key. Every flint_ key acts as your merchant with every scope it holds. Treat a key that has shipped in browser JavaScript, a mobile binary, or a public repo as leaked, and revoke it.
- Load keys from environment variables or a secrets manager, not from source.
- Give each key the fewest scopes it needs and an
expires_atwhen the job has an end date. - If a key leaks, revoke it from the API or the dashboard first, then deploy a replacement.
- For local development,
flint auth importfrom the CLI stores the key in your OS keychain instead of a dotfile.
Key security covers source-control hygiene, webhook secrets, and the playbook for a leaked key.
Next steps#
- Accept your first payment: take a $25 test payment with the key you just verified.
- Sandboxes & test mode: create sandboxes for local, CI, and QA, and reset their data.
- API key scope catalog: every scope and what it grants.
- Going live: swap the test key for a live key when you ship.
- Key security: rotation, secret hygiene, and incident response.
