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
curl "https://api.withflintpay.com/v1/capabilities?capability=accept_card_payments" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "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.

HTTP
Authorization: Bearer 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 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_idstring

The record id, such as key_01KWJ93G11C7MF8REX91MDS0CD. Use it to retrieve, update, or revoke the key. Safe to log.

key_prefixstring

The 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_keystring

The 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.

statusstring

active or revoked. A revoked key cannot authenticate.

expires_atstring

When the key stops authenticating, as an RFC 3339 timestamp. Absent when the key never expires.

FieldDescription
merchant_idThe merchant the key belongs to.
sandbox_idThe sandbox a test key is bound to. Absent on live keys.
nameThe label you gave the key.
key_typeAlways external for keys used against the public API.
last_used_atWhen the key last authenticated a request.
created_atWhen the key was created.
updated_atWhen 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:

JSON
{
  "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.

API key scope catalogEvery scope, the operations it grants, and which scope each payment route checks.

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.

GET/v1/api-keysRequired API key scopes: accounts.api_keys.read or accounts.api_keys.write

List your keys.

POST/v1/api-keysRequired API key scope: accounts.api_keys.write

Create a key. Returns secret_key once.

GET/v1/api-keys/{api_key_id}Required API key scopes: accounts.api_keys.read or accounts.api_keys.write

Retrieve one key.

PATCH/v1/api-keys/{api_key_id}Required API key scope: accounts.api_keys.write

Change a key's name, scopes, or expires_at. Send "expires_at": null to remove an expiry.

POST/v1/api-keys/{api_key_id}/revokeRequired API key scope: accounts.api_keys.write

Revoke 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
curl "https://api.withflintpay.com/v1/api-keys?page_size=1" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "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 returns 400 SANDBOX_ID_REQUIRED, and any other sandbox returns 400 INVALID_SANDBOX_ID.
  • A live key must not pass sandbox_id. Sending one returns 400 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_SCOPE with the extra scopes in missing_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
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"
  }'
Response
{
  "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:

  1. Create a replacement#

    Create a new key with the same scopes as the one you are replacing.

  2. Deploy it#

    Put the new secret_key in your servers' configuration and confirm requests are using it. last_used_at on the old key stops advancing once nothing sends it.

  3. Revoke the old key#

    Call POST /v1/api-keys/{api_key_id}/revoke on the old key. From that moment it returns 401 API_KEY_REVOKED.

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.

JSON
{
  "error": {
    "type": "authentication_error",
    "code": "API_KEY_NOT_FOUND",
    "message": "No API key matches the provided credential.",
    "request_id": "6f2c9a71-b0d3-4e58-9c16-7a4e0b2d8f53"
  }
}
  • HTTP 401
    No key was sent. Add an Authorization: Bearer or X-API-Key header.
  • HTTP 401
    The Authorization header is not Bearer followed by a flint_ key. Basic auth and other schemes land here.
  • HTTP 401
    The value does not start with flint_test_ or flint_live_. Check that you sent the whole key.
  • HTTP 401
    The prefix is valid but no key matches. Check for truncation or stray whitespace, or create a new key.
  • HTTP 401
    The key was revoked. Create a new one.
  • HTTP 401
    The key is past its expires_at. Extend the date or create a new key.
  • HTTP 401
    A test key that is not bound to a sandbox. Create the key from the dashboard or a sandbox.
  • HTTP 401
    The key's sandbox was archived or is inactive. Create a key in an active sandbox.
  • HTTP 401
    A live key that is bound to a sandbox. Create an unbound live key.
  • HTTP 401
    The key holds a scope that is not allowed for its mode, such as a legacy grant. Create a new key with current scopes.
  • HTTP 403
    The key lacks a required scope. missing_scopes names it. See Scopes.
  • HTTP 403
    A test key called a sandbox management endpoint. Use a live key from the dashboard.
  • HTTP 403
    The endpoint accepts only external API keys. Every key created in the dashboard or with POST /v1/api-keys is external.

SANDBOX_SELECTION_REQUIRED is the most common first-run failure. Its remediation block says what to do next:

JSON
{
  "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_at when 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 import from 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#

Was this helpful?