Sandboxes & test mode
Flint has one API and one hostname, https://api.withflintpay.com, for test and live traffic. The key you send decides the mode. A flint_test_... key runs in test mode against a sandbox, and a flint_live_... key runs in live mode against real money. There is no test hostname and no per-request mode flag.
Getting SANDBOX_SELECTION_REQUIRED? Your test key is not bound to a sandbox. See the sandbox-bound key requirement.
Test mode vs live mode#
| Test mode | Live mode | |
|---|---|---|
| Key prefix | flint_test_... | flint_live_... |
| Money | Nothing moves. Card charges are simulated. | Real charges |
| Cards | Test cards only | Real cards |
| Data | A sandbox, isolated from live | Your live merchant data |
| Response header | Flint-Mode: test and Flint-Sandbox-Id: test_... | Flint-Mode: live |
Test and live data never mix. An order created with a test key does not exist for a live key, and the reverse. Test-mode IDs left in production configuration return 404 in live mode, so re-create products, plans, and webhook endpoints when you go live.
What a sandbox is#
A sandbox is a self-contained test copy of your merchant account. Each one has its own orders, customers, products, saved payment methods, subscriptions, invoices, webhook endpoints and signing secrets, idempotency keys, request logs, and risk and fulfillment settings. Nothing in one sandbox is visible from another.
Every merchant gets one sandbox automatically. It is named Default Test and has is_default: true. Flint prepares it for test card payments when the account is created; for a short time after sign-up, its accept_card_payments capability can still be pending. It cannot be reset or archived, so treat it as your long-lived development sandbox and put disposable work in sandboxes you create.
Every test key is bound to exactly one sandbox when it is created. The key selects the sandbox, so there is no sandbox header to send. Every test-mode response carries a Flint-Sandbox-Id header naming the sandbox the request ran in, so you can confirm which sandbox a key points at.
Sandbox-bound test keys come from:
- The dashboard. The environment menu in the top bar lists live and each active sandbox. A test key created on the API keys page is bound to the sandbox selected at the time.
- The sandbox test-key endpoint,
POST /v1/developer/sandboxes/{sandbox_id}/test-key. See Issue a key for an existing sandbox. - API-first setup. API & agent onboarding mints the first key for the default sandbox from your backend, before anyone opens the dashboard.
If you are signed in, the test key filled into the code samples is bound to your default sandbox. It can create orders and take test payments, but it cannot create, reset, or archive sandboxes.
When to create another sandbox#
Create a sandbox for each use that must not share data. The usual split is one for local development, one for CI, and one for partner or QA testing. Each has its own customers, catalog, and webhook endpoints, and resetting one leaves the others untouched. Sandbox names are unique within your merchant account. Creating a second CI sandbox returns 409 SANDBOX_ALREADY_EXISTS until the first one is archived.
Managing sandboxes#
Creating, resetting, and archiving sandboxes requires a live key that holds developer.sandboxes.write. The developer.sandboxes.read and developer.sandboxes.write scopes exist only on live keys, so a test key cannot manage sandboxes, including its own. During API-first setup, the onboarding session token can call the same endpoints.
When a request also mints a test key, the live key needs accounts.api_keys.write as well, plus every scope it grants to the new key. A key can only delegate scopes it holds.
Create a sandbox#
Pass issue_test_key: true to get a bound test key in the same response. scopes is required when a key is minted this way. test_key_name is optional and defaults to the sandbox name followed by Test Key.
curl -X POST https://api.withflintpay.com/v1/developer/sandboxes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer flint_live_YOUR_KEY" \
-d '{
"name": "CI",
"issue_test_key": true,
"test_key_name": "ci-runner",
"scopes": ["commerce.orders.read", "commerce.orders.write", "capabilities.read"]
}'
{
"data": {
"sandbox_id": "test_01KWJS7ZW5YAC86HMHWTCV93GX",
"name": "CI",
"status": "active",
"is_default": false,
"secret_key": "flint_test_2686d60a...",
"api_key": {
"api_key_id": "key_01KWJS8006FVHT8CRWDKBWA1T9",
"name": "ci-runner",
"key_prefix": "flint_test_2686d60a",
"sandbox_id": "test_01KWJS7ZW5YAC86HMHWTCV93GX",
"scopes": ["commerce.orders.read", "commerce.orders.write", "capabilities.read"],
"status": "active"
}
},
"request_id": "7c1f0d2e-3b5a-4e8f-9a6d-2f4b8c1e5d90"
}
data.sandbox_id is what you pass to the reset, archive, and test-key endpoints. data.secret_key is the new test key.
secret_key is returned only in this response. If you lose it, mint a replacement with the test-key endpoint.
Confirm the key works by listing orders with it. A new sandbox returns an empty list, and the response headers name the sandbox:
curl -i https://api.withflintpay.com/v1/orders \
-H "Authorization: Bearer flint_test_2686d60a..."
HTTP/1.1 200 OK
Flint-Mode: test
Flint-Sandbox-Id: test_01KWJS7ZW5YAC86HMHWTCV93GX
A new sandbox starts empty, including its catalog and webhook endpoints. Before relying on it for card payments, check GET /v1/capabilities?capability=accept_card_payments with the new key and wait for "status": "ready".
Issue a key for an existing sandbox#
A sandbox can have more than one test key, for example one per developer or one per CI pipeline. Mint them from the sandbox's test-key endpoint:
curl -X POST https://api.withflintpay.com/v1/developer/sandboxes/test_01KWJS7ZW5YAC86HMHWTCV93GX/test-key \
-H "Content-Type: application/json" \
-H "Authorization: Bearer flint_live_YOUR_KEY" \
-d '{
"name": "local-dev",
"scopes": ["commerce.orders.read", "commerce.orders.write", "capabilities.read"]
}'
{
"data": {
"api_key_id": "key_01KWJS80E4G3CYMKRA955524FF",
"name": "local-dev",
"key_prefix": "flint_test_ca7f32e8",
"secret_key": "flint_test_ca7f32e8...",
"sandbox_id": "test_01KWJS7ZW5YAC86HMHWTCV93GX",
"scopes": ["commerce.orders.read", "commerce.orders.write", "capabilities.read"],
"status": "active"
},
"request_id": "b4e2a9f1-6c7d-4d3e-8f0a-1c5b9e7d2a64"
}
The same rule applies: secret_key is returned only here. A test key that holds accounts.api_keys.write can also mint keys for its own sandbox with POST /v1/api-keys, and revoking any key goes through POST /v1/api-keys/{api_key_id}/revoke. Both are described in Authentication.
List sandboxes#
GET /v1/developer/sandboxes returns every sandbox, including archived ones. Filter with status=active or status=archived, and page with page_size and page_token.
curl "https://api.withflintpay.com/v1/developer/sandboxes?status=active" \
-H "Authorization: Bearer flint_live_YOUR_KEY"
{
"data": [
{
"sandbox_id": "test_01KNB061AGD0CYYF16M5QAQE3N",
"name": "Default Test",
"status": "active",
"is_default": true,
"created_at": "2026-05-12T16:02:41Z",
"updated_at": "2026-05-12T16:02:41Z"
},
{
"sandbox_id": "test_01KWJS7ZW5YAC86HMHWTCV93GX",
"name": "CI",
"status": "active",
"is_default": false,
"created_at": "2026-08-30T09:15:07Z",
"updated_at": "2026-08-30T09:15:07Z"
}
],
"request_id": "d9a3c7e5-1f2b-4a6c-b8e0-3d5f7a9c1e22"
}
Reset a sandbox#
A reset wipes a sandbox's data and gives you a clean one with the same name and a new sandbox_id. The old sandbox is archived. The replacement keeps the same test payment account, so you do not repeat payment setup, but confirm card readiness with the capabilities check before the first payment.
curl -X POST https://api.withflintpay.com/v1/developer/sandboxes/test_01KWJS7ZW5YAC86HMHWTCV93GX/reset \
-H "Authorization: Bearer flint_live_YOUR_KEY"
{
"data": {
"sandbox_id": "test_01KX0B3M9Q4R7T2V5W8Y1Z6A3C",
"name": "CI",
"status": "active",
"is_default": false,
"created_at": "2026-09-04T14:20:33Z",
"updated_at": "2026-09-04T14:20:33Z"
},
"request_id": "e6b1f4a8-2d9c-4c7e-a3f5-8b0d2e6c4a17"
}
Three things stop working after a reset, and each needs one follow-up:
- Keys bound to the old sandbox return
401 API_KEY_SANDBOX_UNAVAILABLE. Mint new keys against the newsandbox_idwith the test-key endpoint. - Webhook endpoints are deleted with the rest of the data. Register your endpoint again with a new key and store the new signing secret. See Webhooks.
- IDs from before the reset no longer exist. Re-create the products, plans, and customers your tests depend on.
Reset a CI sandbox before each run, or a QA sandbox whose test orders you no longer need. The default sandbox cannot be reset. The call returns 409 DEFAULT_SANDBOX_CANNOT_BE_RESET, so keep disposable work in a sandbox you created.
Archive a sandbox#
Archiving retires a sandbox you no longer need. Its status becomes archived, keys bound to it return 401 API_KEY_SANDBOX_UNAVAILABLE, and its name is freed for reuse. The archived sandbox stays listed in GET /v1/developer/sandboxes with "status": "archived", but nothing in it can be read again.
curl -X DELETE https://api.withflintpay.com/v1/developer/sandboxes/test_01KWJS7ZW5YAC86HMHWTCV93GX \
-H "Authorization: Bearer flint_live_YOUR_KEY"
{
"data": {
"sandbox_id": "test_01KWJS7ZW5YAC86HMHWTCV93GX",
"name": "CI (Archived WTCV93GX)",
"status": "archived",
"is_default": false,
"created_at": "2026-08-30T09:15:07Z",
"updated_at": "2026-09-04T15:02:18Z"
},
"request_id": "f2c8d5b3-7e1a-4f9c-b6d4-0a3e5c7f9b21"
}
There is no unarchive. To keep working under the same name, create a new sandbox. The default sandbox cannot be archived and returns 409 DEFAULT_SANDBOX_CANNOT_BE_ARCHIVED.
The sandbox-bound key requirement#
Every test key must be bound to a sandbox. A test key with no sandbox binding cannot authenticate any request, and Flint returns 401 instead of guessing which sandbox you meant:
{
"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_steps": "Create or use a sandbox-bound test key for the sandbox this request should target.",
"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"]
}
]
}
}
}
Replace the key with one that is bound to a sandbox: create one on the dashboard API keys page, or mint one with the test-key endpoint.
Errors#
From the CLI and the SDKs#
The CLI manages sandboxes with the same live key. It reads the mode from the key, and a live API key requires --live on each command:
flint sandboxes list --live
flint sandboxes create --name qa-scenarios --live
flint sandboxes test-key test_01KWJS7ZW5YAC86HMHWTCV93GX --input key.json --live
flint sandboxes reset test_01KWJS7ZW5YAC86HMHWTCV93GX --live --confirm
In the SDKs, the same routes are flint.developer.listSandboxes() and flint.developer.resetSandbox(sandboxId).
Next steps#
- Testing: test cards, declines, 3D Secure, refunds, and renewals in your sandbox.
- Sandboxes API reference: every endpoint, field, and response shape.
- Authentication: how keys, scopes, and the
Authorizationheader work. - Going live: swap the test key for a live one and re-create your configuration in live mode.
