Going live

Your integration works in a sandbox. Taking it to production is five steps, in order: finish account verification, create a live key, confirm the account is ready to charge, re-create your configuration in live mode, then cut over and verify with one real payment.

Your code does not change. Flint has one hostname and one API for both modes: same base URL, same request shapes, same webhook signature scheme. What changes is the key you send, the account checks behind real money, and the small set of configuration that exists per mode.

What changes in live mode#

Test modeLive mode
Keyflint_test_..., bound to a sandboxflint_live_..., never sandbox-bound
DataIsolated test dataYour real merchant data
CardsTest cards onlyReal cards only; test cards decline
MoneySimulated; nothing movesReal charges, refunds, and payouts
WebhooksSandbox events, to endpoints registered with a test keyLive events, to endpoints registered with a live key
Response headerFlint-Mode: testFlint-Mode: live

Test and live data never mix. Nothing you created with a test key exists in live mode: not your orders and customers, and not your webhook endpoints, products, plans, promotions, or settings either. Going live is not a data migration. You re-create the handful of resources your integration references, with a live key, and point your production configuration at them.

Warning:

The most common go-live bug is a test-mode ID left in production configuration. A prod_... or plan_... ID created in a sandbox does not exist in live mode and returns 404. Sweep your config and environment variables for IDs you created while testing, and re-create each one in live mode in Step 4.

Step 1: finish account verification#

Charging real cards requires a verified account. Start on the dashboard's Verify your business page. It asks only for what Stripe needs to turn on payments: your business details, the owner's identity details, and a bank account for payouts. Stripe can ask for more details later, and the same page shows what is due.

Live card payments start as soon as Stripe turns on card payments for your account. Payouts start once Stripe confirms your bank account, which can happen later. Until then, live payments collect in your Flint balance.

If your product embeds onboarding over the API, create a live key first (Step 2). You do not need to verify the business before creating one, and the key can take live card payments once Stripe turns on card payments. Live onboarding over the API needs that live key. The onboarding session token from signup and sandbox keys reach only sandbox onboarding, never the live account. With the live key, read GET /v1/onboarding/state without sandbox_id and follow next_step. Steps the API can complete go through POST /v1/onboarding/advance; steps that need a person, such as identity documents, hand off to an embedded browser session from POST /v1/merchant-account-sessions that you mount in your own product. Omit sandbox_id from these requests too. After the browser exits, read state again and continue. The same loop handles verification requests that arrive after launch. See API & agent onboarding.

You do not have to wait for review to finish before continuing. The readiness check in Step 3 tells you when payments and payouts turn on.

Step 2: create your live key#

Create a live key on the dashboard's API keys page. Live keys start with flint_live_ and are never bound to a sandbox. The prefix alone is what switches your requests into live mode.

Two habits to start with:

  • Grant the fewest scopes the integration needs. A checkout backend does not need settings.write. Scopes are listed in the key scope catalog, and a .write scope already includes its matching .read.
  • Capture the secret immediately and store it in a secrets manager. The full secret_key is shown once, at creation, and never again.

Once one live key exists, you can create additional live keys over the API with POST /v1/api-keys (a live caller omits sandbox_id), and rotate by creating a replacement and revoking the old key. See Manage keys over the API.

Note:

Every authenticated response includes a Flint-Mode header, test or live. Assert on it in your deploy smoke test. A test key that reaches production config produces no error, just simulated payments and a Flint-Mode: test header.

Step 3: confirm your account is ready to charge#

Before routing real buyers to checkout, ask Flint whether the account can take payments and receive payouts.

cURL
curl https://api.withflintpay.com/v1/merchant \
  -H "Authorization: Bearer YOUR_LIVE_KEY"
JSON
{
  "data": {
    "merchant_id": "mer_1kmn0aExample",
    "business_name": "Canvas & Co.",
    "payments": {
      "status": "ready",
      "status_reason": null,
      "next_actions": []
    },
    "payouts": {
      "status": "ready",
      "status_reason": null,
      "next_actions": []
    },
    "requirements": {
      "currently_due": [],
      "past_due": [],
      "eventually_due": [],
      "pending_verification": [],
      "disabled_reason": null
    },
    "observed_at": "2026-07-02T18:04:11Z"
  }
}

payments must be ready before you launch. payouts can follow later. They answer different questions:

  • payments gates charging. ready means the account can take live card payments. Anything else means a live buyer cannot pay. While the account is pending or blocked, creating a payment fails with MERCHANT_ONBOARDING_REQUIRED.
  • payouts gates money reaching your bank. It can still be pending after payments is ready, while Stripe confirms your bank account. Charges succeed in the meantime, and the funds collect in your balance until payouts are ready. If payouts are blocked, the requirements arrays name what you need to provide.

Each reports a status of ready, pending, blocked, not_available, or not_requested, with a status_reason when it is not ready. A blocked account looks like this:

JSON
{
  "data": {
    "payments": {
      "status": "blocked",
      "status_reason": "requirements_past_due",
      "next_actions": []
    },
    "requirements": {
      "currently_due": [],
      "past_due": ["business_website"],
      "eventually_due": [],
      "pending_verification": [],
      "disabled_reason": "requirements_past_due"
    }
  }
}

The requirements arrays name what is outstanding. Anything in currently_due or past_due needs action from you, in the dashboard or through the onboarding flow from Step 1. Entries in pending_verification are submitted and under review, so re-check later. Items in eventually_due are not blocking yet; handle them before they move up.

For a per-capability breakdown, including individual payout capabilities, use GET /v1/capabilities. Subscribe to capability.updated to hear about changes after launch instead of polling.

If you accept ACH debit, the ACH capability is checked separately in each mode, and Flint support records the settlement and mandate-email evidence per environment. Follow the go-live checklist in ACH debit payments.

Step 4: re-create your configuration in live mode#

Everything your integration references by ID must exist in live mode. The one almost every integration needs is a webhook endpoint.

Register your production webhook endpoint#

Webhook endpoints are per mode: the endpoint you registered while testing receives sandbox events only. Register your production URL with your live key, and you get a fresh signing secret in the response.

cURL
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_LIVE_KEY" \
  -H "Idempotency-Key: webhook-prod-001" \
  -d '{
    "url": "https://example.com/webhooks/flint",
    "enabled_events": [
      "order.paid",
      "payment_intent.requires_action",
      "payment_intent.processing",
      "payment_intent.succeeded",
      "payment_intent.payment_failed",
      "payment_intent.canceled",
      "refund.created",
      "refund.updated",
      "refund.failed",
      "dispute.created",
      "capability.updated"
    ],
    "description": "Production endpoint"
  }'
JSON
{
  "data": {
    "webhook_endpoint_id": "whep_1kmn0aExample",
    "url": "https://example.com/webhooks/flint",
    "secret": "whsec_...",
    "enabled_events": [
      "order.paid",
      "payment_intent.requires_action",
      "payment_intent.processing",
      "payment_intent.succeeded",
      "payment_intent.payment_failed",
      "payment_intent.canceled",
      "refund.created",
      "refund.updated",
      "refund.failed",
      "dispute.created",
      "capability.updated"
    ],
    "enabled": true
  }
}

Store data.secret. It is your production FLINT_WEBHOOK_SECRET and is only returned here. Do not reuse the sandbox endpoint's secret: each endpoint signs with its own. Omitting enabled_events subscribes the endpoint to every event type. The list above covers fulfillment, payment failures, refunds, disputes, and account status changes; the webhook events catalog has the rest.

Then prove the endpoint works end to end, signature verification included, by sending a synthetic event:

cURL
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints/whep_1kmn0aExample/test-events \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_LIVE_KEY" \
  -d '{"event_type": "payment_intent.succeeded"}'
JSON
{
  "data": {
    "webhook_event_id": "whev_1kmn0aExample",
    "webhook_delivery_id": "wdel_1kmn0aExample",
    "webhook_delivery_attempt_id": "watt_1kmn0aExample",
    "delivery_status": "delivered",
    "attempt_status": "delivered",
    "attempt_number": 1,
    "delivery_trigger": "test_event",
    "status_code": 200,
    "duration_milliseconds": 312
  }
}

"attempt_status": "delivered" with your endpoint's 200 means your production handler received, verified, and acknowledged a signed Flint event before any real money was involved. If it reports failed instead, the response carries the status code your endpoint returned, an error_summary, and a recommended_action. Fix the handler and send another test event.

Re-create referenced resources#

Beyond webhooks, re-create anything your code or config references by ID, with your live key this time. That usually means the products, variants, and bundles you sell by prod_... ID, any subscription plans and promotions, and any checkout or receipt settings you changed from the defaults while testing.

Processing pricing is not something you re-create. Confirm your live rates under Processing rates on Flint billing before you charge anyone.

Resources you create per request (orders, checkout sessions, payment links, invoices) need no migration. Your backend creates them at runtime.

Step 5: cut over and verify with real money#

The deploy itself is a configuration change:

Shell
# Production configuration. The base URL does not change.
FLINT_API_KEY=flint_live_...          # Step 2
FLINT_WEBHOOK_SECRET=whsec_...        # Step 4, the live endpoint's secret

Plus any live resource IDs from Step 4. Then verify with one real transaction before you announce anything:

  1. Run your real flow with a real card for $1.00. Test cards decline in live mode.
  2. Confirm the order settled. GET /v1/orders/{order_id} shows "status": "closed", "payment_status": "paid", and settlement_amounts.outstanding_money at zero. The response carries Flint-Mode: live.
  3. Confirm the webhook arrived. Your handler received order.paid for that order and your fulfillment logic ran.
  4. Check what you netted. GET /v1/payment-intents/{payment_intent_id} shows processing_fee_money, Flint's all-in fee for that payment, and merchant_net_money, the captured amount minus that fee. See Processing fees.
  5. Refund yourself and confirm the refund events arrive too:
cURL
curl -X POST https://api.withflintpay.com/v1/refunds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_LIVE_KEY" \
  -H "Idempotency-Key: golive-refund-001" \
  -d '{
    "order_id": "ord_1kmn0aExample",
    "amount_money": {"amount": 100, "currency": "USD"},
    "reason": "requested_by_customer"
  }'

One paid-and-refunded dollar exercises your charge path, webhook path, and refund path against production, with real money, once, while you are watching.

The go-live checklist#

Run this list before the first real buyer does it for you.

Credentials#

  • Live keys live on your server only: never in browser or mobile code, never in source control. Load them from a secrets manager or environment variables.
  • Each key carries only the scopes its service needs.
  • Test-mode IDs are gone from production config (Step 4).
  • Keys you no longer use are revoked.

Correctness#

  • Fulfillment is driven by webhooks or a backend fetch of the order, never by the browser redirect alone. A buyer can close the tab before redirecting, or open your success URL directly.
  • Every write your retry logic touches sends an Idempotency-Key, and retries reuse the same key for the same business action. See Idempotency.
  • Amounts are confirmed server-side from the order's settlement state, not from anything the client sends you.
  • Declines and 3D Secure challenges are handled. Real cards fail more often than test cards, and in more ways. You tested those paths with the decline test cards already.

Webhooks#

  • Signatures are verified against the raw request body, and deliveries are deduplicated by the webhook-id header (or its legacy equivalent, X-Flint-Webhook-ID). See Webhooks.
  • Your handler returns 2xx quickly, after durably queuing the work, not after finishing it.
  • The production secret in your config is the live endpoint's secret from Step 4.
  • Someone is watching for delivery failures: GET /v1/webhook-events?delivery_status=failed.

Operations#

  • You log the request_id (also in the X-Request-Id header) from every API response, so failures are traceable and support can find your request.
  • Your workers back off on 429 and respect Retry-After. Size them against the rate limits, which count per key and per merchant.
  • You subscribed to dispute.created and capability.updated, so disputes and account status changes reach you instead of surprising you.

If something fails on launch day#

SymptomLikely causeFix
Payments "succeed" but no money movesA test key is still in production config; responses say Flint-Mode: testDeploy the flint_live_ key from Step 2
A card that worked in testing is declinedTest cards decline in live modeUse a real card to verify
Webhook signature verification failsConfig still holds the sandbox endpoint's secretUse the live endpoint's whsec_... from Step 4
404 on a product, plan, or promotionA test-mode ID referenced in live modeRe-create the resource with your live key (Step 4)
Creating a payment fails with MERCHANT_ONBOARDING_REQUIREDThe payments readiness status is pending or blockedCheck readiness (Step 3) and clear the listed requirements
Live payments succeed but no payout arrivesThe payouts readiness status is still pending while Stripe confirms your bank account, or blockedCheck readiness (Step 3). Funds stay in your balance until payouts are ready

Every failing response includes a request_id. Quote it if you ask Flint Help, and see Debugging for tracing a request end to end.

Next steps#

  • Webhooks: signature verification, retries, and delivery monitoring in depth.
  • Idempotency: retry-safe writes and which endpoints accept the header.
  • Error handling: the error envelope and a production retry strategy.
  • Rate limits: the budgets your workers must respect.
  • Key security: rotation, secret hygiene, and incident response.
  • Authentication: how keys, modes, and auth errors work.
  • CLI: production access is explicit, either a live context you approve at flint login or --live on each command with a live API key, and destructive commands prompt before acting. Use it for anything you do to production by hand.

Was this helpful?