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 entirely over the API with the API setup flow. Then confirm the key works and that your sandbox can take card payments:

Bash
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 numberScenarioWhat happens
4242 4242 4242 4242SuccessPayment succeeds immediately; the order becomes paid.
4000 0025 0000 3155Requires authenticationA 3D Secure challenge opens; complete it and the payment succeeds.
4000 0000 0000 32203D Secure challengeSame challenge flow; complete it to succeed, fail it to decline.
4000 0000 0000 9995Decline: insufficient fundsCheckout shows a payment-failed message; the order stays open.
4000 0000 0000 0002Decline: genericSame, simulating a generic issuer decline.
4000 0000 0000 0069Decline: expired cardSame, simulating an expired card.
4000 0000 0000 0127Decline: incorrect CVCSame, simulating a bad security code.
4000 0000 0000 0119Decline: processing errorSame, simulating a processor-side failure.
4000 0000 0000 0341Saves, then failsThe 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.

1

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.

Bash
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"}
    }]
  }'
JSON
{
  "data": {
    "order_id": "ord_1kmn0aExample",
    "status": "open",
    "pricing_amounts": {
      "subtotal_money": {"amount": 2500, "currency": "USD"},
      "total_money": {"amount": 2500, "currency": "USD"}
    },
    "line_items": [...]
  }
}
2

Create a checkout session#

Bash
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/cart"
    }
  }'
JSON
{
  "data": {
    "checkout_session_id": "cs_1kmn0aExample",
    "status": "open",
    "order_id": "ord_1kmn0aExample",
    "url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=..."
  }
}
3

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.

4

Verify the outcome#

Confirm the result from your backend rather than trusting the browser redirect:

Bash
curl https://api.withflintpay.com/v1/orders/ord_1kmn0aExample \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "order_id": "ord_1kmn0aExample",
    "status": "paid",
    "settlement_amounts": {
      "paid_money": {"amount": 2500, "currency": "USD"},
      "refunded_money": {"amount": 0, "currency": "USD"},
      "outstanding_money": {"amount": 0, "currency": "USD"}
    }
  }
}

paid with nothing outstanding means the payment landed. If you registered a webhook endpoint (next sections), an order.payment_succeeded 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 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.

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

Bash
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.payment_succeeded",
      "refund.created",
      "subscription.payment_failed"
    ]
  }'
JSON
{
  "data": {
    "webhook_endpoint_id": "whep_1kmn0aExample",
    "url": "https://example.com/webhooks/flint",
    "secret": "whsec_2q9Zb64mVh3rTk8wLpDx41Ns7GaeYcJf0Ui5",
    "enabled_events": [
      "order.payment_succeeded",
      "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:

Bash
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.payment_succeeded"}'
JSON
{
  "data": {
    "webhook_event_id": "whev_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: "succeeded".) The same diagnostics stay queryable per event:

Bash
curl https://api.withflintpay.com/v1/webhook-events/whev_1kmn0aExample/delivery-attempts \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "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 any event on demand with POST /v1/webhook-events/{webhook_event_id}/resend, and watch real events land with GET /v1/webhook-events after you run the payment walkthrough above. Signature verification, deduplication with X-Flint-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.

Bash
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"}
    }]
  }'
JSON
{
  "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 for subscription.payment_succeeded the next day.
  • Failed renewal: subscribe with 4000 0000 0000 0341, the card that saves but never charges. The first cycle fails, and you'll see subscription.payment_failed and subscription.past_due events, which is exactly the dunning path your integration needs to handle.

Clean up when you're finished:

Bash
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}'
JSON
{
  "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 codeWhen you see itThe fix
SANDBOX_SELECTION_REQUIRED401 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_FOUNDFetching 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_UNAVAILABLE401: the sandbox your key is bound to was archived or is inactive.Mint a new key in an active sandbox.
API_KEY_MODE_MISMATCH401: 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 X-Flint-Webhook-ID (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_id from 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 0341 card) 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#

Rate this doc