Testing
Everything on Flint is testable with a flint_test_... key: orders, checkout sessions, payments, refunds, subscriptions, invoices, payment links, coupons, and webhooks. Card charges run against Stripe test mode, so no money ever moves and no real card is ever needed.
If you are signed in, every request on this page is runnable against your own sandbox: your test key is filled in automatically, and IDs captured from each response carry into the requests that follow. You can go from "create an order" to "refund the payment you just simulated" without leaving this page.
Test and live traffic share one host, https://api.withflintpay.com; the key prefix decides the mode, and each test key is bound to an isolated sandbox. How sandboxes work, how keys bind to them, and how to reset or create more is covered in Sandboxes & Test Mode.
What You Need#
A sandbox-bound test API key (flint_test_...). Create one on the dashboard's API keys page, or use the API setup flow to provision the merchant and mint its first sandbox key programmatically. Then confirm the key works and that your sandbox can take card payments:
curl "https://api.withflintpay.com/v1/capabilities?capability=accept_card_payments" \
-H "Authorization: Bearer YOUR_API_KEY"
A 200 with "status": "ready" means every scenario below will work.
Test Cards#
Use these card numbers anywhere Flint collects a card. All of them accept any future expiry date, any 3-digit CVC, and any ZIP code (for example 44444).
| Card number | Scenario | What happens |
|---|---|---|
4242 4242 4242 4242 | Success | Payment succeeds immediately; the order becomes paid. |
4000 0025 0000 3155 | Requires authentication | A 3D Secure challenge opens; complete it and the payment succeeds. |
4000 0000 0000 3220 | 3D Secure challenge | Same challenge flow; complete it to succeed, fail it to decline. |
4000 0000 0000 9995 | Decline: insufficient funds | Checkout shows a payment-failed message; the order stays open. |
4000 0000 0000 0002 | Decline: generic | Same, simulating a generic issuer decline. |
4000 0000 0000 0069 | Decline: expired card | Same, simulating an expired card. |
4000 0000 0000 0127 | Decline: incorrect CVC | Same, simulating a bad security code. |
4000 0000 0000 0119 | Decline: processing error | Same, simulating a processor-side failure. |
4000 0000 0000 0341 | Saves, then fails | The card attaches to a customer, but every charge fails. Use it to rehearse renewal failures. |
Declines never end a checkout session: the buyer sees the failure inline and can retry with another card, and the order keeps its balance until something succeeds.
These cards work in Flint-hosted checkout, on payment links and invoices, and in embedded Stripe Elements. Flint processes card payments on Stripe, so Stripe's standard test cards work in test mode; you don't need a Stripe account. Test-mode pages show a banner so nobody mistakes a rehearsal for real money.
Simulate a Payment Outcome#
The fastest way to see any card scenario end to end: create an order, open its checkout page, pay with the card for your scenario, and verify the result from your backend.
Create an order#
Amounts are integers in the currency's minor unit, so 2500 is $25.00. See Accept Your First Payment for the full explanation of this flow.
curl -X POST https://api.withflintpay.com/v1/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: order-testing-001" \
-d '{
"line_items": [{
"name": "Test tote bag",
"quantity": 1,
"unit_price_money": {"amount": 2500, "currency": "USD"}
}]
}'
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"pricing_amounts": {
"subtotal_money": {"amount": 2500, "currency": "USD"},
"total_money": {"amount": 2500, "currency": "USD"}
},
"line_items": [...]
}
}
Create a checkout session#
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: checkout-testing-001" \
-d '{
"order_id": "ord_1kmn0aExample",
"redirects": {
"success_redirect_url": "https://example.com/thanks",
"cancel_redirect_url": "https://example.com/checkout"
}
}'
{
"data": {
"checkout_session_id": "cs_1kmn0aExample",
"status": "open",
"order_id": "ord_1kmn0aExample",
"url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=..."
}
}
Pay with the card for your scenario#
If your sandbox can't take card payments yet, the hosted page shows "no payment methods available" instead of a card form. Check GET /v1/capabilities?capability=accept_card_payments; once it reports ready, reload the page.
Open data.url in your browser. The page shows the test-mode banner, so nothing you do here moves money. Fill in any buyer details, pick a card from the table above, and pay.
With 4242 4242 4242 4242 the payment succeeds and the page redirects to your success_redirect_url with csId and orderId appended as query parameters.
Verify the outcome#
Confirm the result from your backend rather than trusting the browser redirect:
curl https://api.withflintpay.com/v1/orders/ord_1kmn0aExample \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "closed",
"payment_status": "paid",
"settlement_amounts": {
"paid_money": {"amount": 2500, "currency": "USD"},
"refunded_money": {"amount": 0, "currency": "USD"},
"outstanding_money": {"amount": 0, "currency": "USD"}
}
}
}
payment_status: "paid" with outstanding_money.amount at zero means the payment landed. If you registered a webhook endpoint (next sections), an order.paid event was delivered too.
Simulate a Decline or 3D Secure#
Run steps 1 and 2 again to get a fresh order and checkout page, then pay with a decline card such as 4000 0000 0000 9995. Checkout surfaces the failure and lets the buyer retry in place; the verify call in step 4 shows the order still open with paid_money at 0. That pair of behaviors, a graceful failure in the UI and an unpaid order in the API, is exactly what your integration should expect from real declines.
With 4000 0025 0000 3155, checkout opens a 3D Secure challenge instead. Complete it and the payment succeeds like the happy path; fail it and the payment declines like the decline path. Test both branches: authentication is common on real cards, and abandoning the challenge is something real buyers do.
Test Risk Rules and Reviews#
Card numbers simulate authorization outcomes, not a guaranteed Flint risk score. Test your own deterministic controls by creating a sandbox rule against attributes you control, such as payment_flow, email, amount, or a sandbox list. Use POST /v1/risk-rules/validate first, create the rule with an idempotency key, then make a new payment attempt. An attempt freezes its rule and list snapshot, so editing a rule after confirmation starts does not rewrite that attempt.
These Stripe test cards exercise the risk lifecycle itself:
| Card number | Risk scenario |
|---|---|
4000 0000 0000 9235 | Elevated risk. Flint's default policy opens a review in eligible flows. |
4100 0000 0000 0019 | Payment blocked. |
4000 0000 0000 5423 | Payment succeeds, then receives an early fraud warning. |
These cards simulate risk outcomes Flint reports to you; deterministic Flint rule tests remain the source of truth for your own block, review, allow, and 3DS policies.
Use a block rule to verify that checkout shows a generic decline and lets the buyer retry with a different payment method on the same session. Buyer copy must not mention risk, fraud, a rule, or a list. The payment intent returns to requires_payment_method, and last_payment_error.code is payment_blocked.
Use a review rule with a manual-capture payment to test the capture gate. Confirm the payment, observe review.opened, and verify capture returns PAYMENT_REVIEW_OPEN. Approving the review must not capture automatically. Submit capture separately and confirm payment_intent.succeeded. For the decline path, verify the hold is canceled and a review.closed event arrives.
Risk scoring may be unavailable in a sandbox. Check GET /v1/risk-rules/attributes before testing a risk_score predicate. A missing score is null, not zero. risk_level and pre-authorization attributes remain valid test inputs where the registry marks them available.
Test ACH Debit#
ACH uses routing number 110000000 in test mode. Pick the account number for the lifecycle branch you need:
| Account number | Expected outcome |
|---|---|
000123456789 | Succeeds. |
000222222227 | Fails for insufficient funds. |
000111111113 | Fails because the account is closed. |
000111111116 | Fails because no account exists. |
000333333335 | Fails because the debit is not authorized. |
000555555559 | Succeeds, then creates a return dispute. |
000000000009 | Remains processing. |
The release supports instant bank verification only. Do not use microdeposit account numbers or verification codes. Test that processing leaves the order unpaid, exposes no retry or cancellation action, and fulfills only after payment_intent.succeeded or order.paid. The full collection and return flow is in ACH Debit Payments.
Test Refunds#
Refund the payment you just simulated. A partial amount_money refunds part of the order; omit it to refund everything. 500 is $5.00.
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-testing-001" \
-d '{
"order_id": "ord_1kmn0aExample",
"amount_money": {"amount": 500, "currency": "USD"},
"reason": "requested_by_customer"
}'
{
"data": {
"refund_id": "ref_1kmn0aExample",
"order_id": "ord_1kmn0aExample",
"status": "pending",
"amount_money": {"amount": 500, "currency": "USD"},
"refund_method": "original_payment",
"reason": "requested_by_customer"
}
}
Refunds start pending and complete asynchronously, in test mode just like live. Treat refund.created and refund.updated webhook events (or a re-fetch of the refund) as the source of truth, not the create response. Refunding an order with nothing captured fails with a validation error, which is worth testing too; see Error Handling for the envelope shape and the Refunds API reference for every field.
Test Webhook Deliveries#
Sandbox webhooks are real deliveries: real signatures, real retries, sent to any public HTTPS URL. To receive them on your machine, expose your local server through an HTTPS tunnel and register the tunnel URL. To just see the delivery machinery work, you can register a throwaway URL and inspect the failed attempts it produces.
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: webhook-testing-001" \
-d '{
"url": "https://example.com/webhooks/flint",
"enabled_events": [
"order.paid",
"refund.created",
"subscription.payment_failed"
]
}'
{
"data": {
"webhook_endpoint_id": "whep_1kmn0aExample",
"url": "https://example.com/webhooks/flint",
"secret": "whsec_2q9Zb64mVh3rTk8wLpDx41Ns7GaeYcJf0Ui5",
"enabled_events": [
"order.paid",
"refund.created",
"subscription.payment_failed"
],
"enabled": true
}
}
The signing secret is returned only in this response. Store it now; you'll need it to verify signatures, and Flint never shows it again.
Now fire a synthetic event at the endpoint. A test event carries a sample payload for the event type you name, signed with your real secret, so it exercises your handler without you having to stage the underlying payment:
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints/whep_1kmn0aExample/test-events \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"event_type": "order.paid"}'
{
"data": {
"webhook_event_id": "whev_1kmn0aExample",
"webhook_delivery_id": "wdel_1kmn0aExample",
"webhook_delivery_attempt_id": "watt_1kmn0aExample",
"attempt_number": 1,
"attempt_status": "failed",
"status_code": 405,
"delivery_trigger": "test_event",
"error_summary": "The webhook endpoint rejected the delivery.",
"recommended_action": "Inspect the endpoint response body and signature verification logic, then resend after fixing the handler.",
"duration_milliseconds": 63
}
}
The response is the delivery attempt itself, with the HTTP status your endpoint returned and a diagnosis when it failed. (The example above shows what a rejected delivery looks like; with a tunnel to a working handler you get attempt_status: "delivered".) Use the response's webhook_delivery_id to inspect that concrete delivery:
curl https://api.withflintpay.com/v1/webhook-deliveries/wdel_1kmn0aExample/attempts \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"webhook_delivery_attempt_id": "watt_1kmn0aExample",
"attempt_number": 1,
"delivery_trigger": "test_event",
"status": "failed",
"status_code": 405,
"error_summary": "The webhook endpoint rejected the delivery.",
"duration_milliseconds": 63
}
]
}
Failed deliveries retry automatically with backoff. You can also replay one concrete delivery with POST /v1/webhook-deliveries/{webhook_delivery_id}/resend, and watch real events land with GET /v1/webhook-events after you run the payment walkthrough above. Signature verification, deduplication with webhook-id, and secret rotation are covered in Webhooks; payload shapes are in the Webhooks API reference.
Test Subscriptions and Renewals#
Billing runs on real schedules in sandbox; there is no way to fast-forward the clock. Create test plans with a daily interval so renewal events arrive within a day, and cancel test subscriptions when you're done so they stop generating billing noise.
curl -X POST https://api.withflintpay.com/v1/subscription-plans \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: plan-testing-001" \
-d '{
"name": "Test Daily Plan",
"billing_interval": "daily",
"billing_interval_count": 1,
"currency": "USD",
"line_items": [{
"name": "Test subscription item",
"quantity": 1,
"unit_price_money": {"amount": 500, "currency": "USD"}
}]
}'
{
"data": {
"plan_id": "plan_1kmn0aExample",
"name": "Test Daily Plan",
"status": "active",
"billing_interval": "daily",
"billing_interval_count": 1,
"currency": "USD",
"line_items": [...]
}
}
Subscribing a customer needs a saved payment method, which comes from a card-setup flow rather than a raw API call: follow Subscription Billing for the API-driven flow, or a subscription signup link for a hosted signup page you can complete in the browser with 4242 4242 4242 4242.
Two renewal scenarios are worth rehearsing:
- Successful renewal: subscribe with
4242 4242 4242 4242, then watch forsubscription.payment_succeededthe next day. - Failed renewal: subscribe with
4000 0000 0000 0341, the card that saves but never charges. The first cycle fails, and you'll seesubscription.payment_failedandsubscription.past_dueevents, which is exactly the dunning path your integration needs to handle.
Clean up when you're finished:
curl -X POST https://api.withflintpay.com/v1/subscriptions/sub_1kmn0aExample/cancel \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"cancel_immediately": true}'
{
"data": {
"subscription_id": "sub_1kmn0aExample",
"status": "canceled",
"canceled_at": "2026-07-03T02:45:00Z"
}
}
Test-Mode Errors#
A few errors are specific to test mode and worth recognizing on sight:
| Error code | When you see it | The fix |
|---|---|---|
SANDBOX_SELECTION_REQUIRED | 401 on any request: your test key isn't bound to a sandbox. | Use a dashboard-issued key or mint one from a sandbox; see the fix. |
RESOURCE_NOT_FOUND | Fetching a resource that exists in the other mode. Test and live data never mix. | Check which mode created the resource and use the matching key. |
API_KEY_SANDBOX_UNAVAILABLE | 401: the sandbox your key is bound to was archived or is inactive. | Mint a new key in an active sandbox. |
API_KEY_MODE_MISMATCH | 401: the key's mode doesn't match how it was issued. | Create a fresh key for the mode you need from the dashboard. |
The full error envelope and every code are in Error Handling.
Before You Go Live#
Work through this list in your sandbox before swapping in a live key:
- Every card scenario in the table above behaves sensibly in your UI: success, authentication, and each decline.
- Fulfillment is driven by a webhook or a backend fetch, never by the browser redirect alone.
- Your webhook handler verifies signatures and dedupes on the
webhook-idheader (Webhooks). - Retries of the same business action reuse the same
Idempotency-Key, so a network blip can't double-charge or double-create (Idempotency). - You log
request_idfrom API responses, so failures can be correlated with Flint support. - If you bill subscriptions, you've watched at least one renewal succeed and one fail (the
0341card) and handled both.
Going live is a key swap, not a code change: create a flint_live_... key in the dashboard, replace the test key, keep the same base URL, and register a production webhook endpoint. See Going Live.
Next Steps#
- Test card numbers: the full cross-processor reference, including every Stripe scenario card that works on Flint.
- CLI: run these scenarios from your terminal, and forward events to a local handler with
flint listen. - Sandboxes & Test Mode: where test data lives, resetting, and multi-sandbox setups for CI and QA.
- Webhooks: signature verification and production-grade handlers.
- Accept Your First Payment: the happy-path walkthrough these scenarios build on.
- Subscription Billing: the full recurring billing flow.
- Error Handling: the error envelope and every error code.
- Going Live: moving from simulated payments to real ones.
