Payments API
The payment knows what it paid for.
Flint starts at the order, not the charge. You send line items; the API computes the totals, holds the balance, and every payment, refund, and webhook after that speaks in order terms. Stripe processes the cards underneath.
curl -X POST https://api.withflintpay.com/v1/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: order-tote-001" \
-d '{
"line_items": [
{ "name": "Canvas tote bag", "quantity": 1,
"unit_price_money": { "amount": 2500, "currency": "USD" } },
{ "name": "Enamel pin", "quantity": 2,
"unit_price_money": { "amount": 800, "currency": "USD" } }
]
}'Three fields per line item. No total is sent.
one host · one /v1 · test mode is a key, not a flag
01Start here
Four steps to a real payment.
No frontend code. Create the order, hand the buyer a hosted page, then confirm from your own backend.
# 1. the order: line items in, totals computed
curl -X POST https://api.withflintpay.com/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{ "line_items": [ ... ] }'
# 2. a hosted page over that order
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"order_id": "ord_1kmn0aExample",
"redirects": {
"success_redirect_url": "https://example.com/thanks",
"cancel_redirect_url": "https://example.com/cart"
}
}'
# 3. send the buyer to data.url, exactly as returned
# 4. confirm it from your own backend
curl https://api.withflintpay.com/v1/orders/ord_1kmn0aExample \
-H "Authorization: Bearer YOUR_API_KEY"{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"payment_status": "unpaid",
"pricing_amounts": {
"subtotal_money": { "amount": 4100, "currency": "USD" },
"tax_money": { "amount": 328, "currency": "USD" },
"total_money": { "amount": 4428, "currency": "USD" }
},
"settlement_amounts": {
"paid_money": { "amount": 0, "currency": "USD" },
"outstanding_money": { "amount": 4428, "currency": "USD" }
}
}
}You sent no total. Subtotal, tax, and total came back computed, and the balance is what payments settle against.
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "closed",
"payment_status": "paid",
"settlement_amounts": {
"paid_money": { "amount": 4428, "currency": "USD" },
"outstanding_money": { "amount": 0, "currency": "USD" }
}
}
}Nothing outstanding. If the buyer abandoned checkout the order stays open and unpaid instead.
02The model
One record, and everything hangs off it.
A charge-first API gives you a payment and leaves the sale to your schema. Here the sale is the primitive, and the payment is one of the things attached to it.
orderPOST /v1/orders
Line items, discounts, tax, and the running balance. Totals recompute server-side on every change.
payment_intentPOST /v1/orders/{order_id}/payment-intents
A leg of money against the balance. An order can have several, which is how split tender works.
checkout_sessionPOST /v1/checkout-sessions
A hosted page that is a view over the order, not a copy of it.
refundPOST /v1/refunds
Targets line items by id and quantity. Flint computes the money, including each line's share of tax.
disputeGET /v1/disputes
Read-only through the API today, and always linked back to the order it came from.
payoutGET /v1/payouts
Where the settled money goes. Balance transactions tie each payout back to the payments inside it.
fulfillmentPOST /v1/orders/{order_id}/fulfillments
What you owe the buyer, tracked on the same record as what they paid.
What that buys you: refunds stop being arithmetic.
The buyer returns one pin out of the order. You name the line and the quantity. Flint works out what that line actually settled for, including its share of tax, and writes the allocation back onto the order so support and reporting read the same story.
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-ord-003" \
-d '{
"order_id": "ord_1kmn0aExample",
"reason": "defective_product",
"line_items": [
{ "order_line_item_id": "li_1kmn0aExample", "quantity": 1 }
]
}'{
"data": {
"refund_id": "ref_1kmn0aExample",
"status": "succeeded",
"amount_money": { "amount": 864, "currency": "USD" },
"line_item_allocations": [
{
"order_line_item_id": "li_1kmn0aExample",
"quantity": 1,
"amount_money": { "amount": 800, "currency": "USD" },
"tax_money": { "amount": 64, "currency": "USD" }
}
]
}
}$8.00 for the pin plus $0.64 of its tax. You did not compute either number.
03Collection
Three ways to take the money.
Same order, same refunds, same webhooks. The only thing that changes is who renders the form.
{
"data": {
"checkout_session_id": "cs_1kmn0aExample",
"status": "open",
"order_id": "ord_1kmn0aExample",
"url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=..."
}
}Send the buyer to data.url exactly as returned. The fragment is what authenticates them, so do not rebuild the URL.
{
"data": {
"payment_intent": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "requires_confirmation",
"amount_money": { "amount": 4428, "currency": "USD" }
},
"payment_collection": {
"stripe": {
"account_id": "acct_1kmn0aExample",
"publishable_key": "pk_test_1kmn0aExample",
"elements": {
"mode": "payment",
"amount_money": { "amount": 4428, "currency": "USD" },
"payment_method_types": ["card"],
"digital_wallets": ["apple_pay", "google_pay"],
"payment_method_creation": "manual"
}
}
}
}
}Creating the payment hands back the connected account and publishable key, so you can mount Stripe Elements without a second round trip.
curl -X POST \
https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/pay \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pay-ord-1kmn0a-1" \
-d '{
"payment_intents": [{
"payment_intent_id": "pi_1kmn0aExample",
"token": "pm_1kmn0aExample"
}],
"expected_outstanding_money": { "amount": 4428, "currency": "USD" },
"completion_behavior": "complete_order"
}'For flows where your server holds the credential. Paying through the order keeps everything on the same record, and expected_outstanding_money fails the call if the order changed after the buyer approved it.
Payment methods, complete list
- ach_debit
- affirm
- apple_pay
- card
- google_pay
Availability comes from the same payment-option product catalog as pricing. There is no Klarna, Afterpay, SEPA, Link, or Cash App today. ACH is USD, one-time, on-session, and uses instant bank verification.
04Failure
A decline is an answer, not an error.
Most of integrating a payments API is handling the times it does not work. Here is what a no looks like.
{
"data": {
"order": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"payment_status": "unpaid"
},
"payment_attempt": {
"payment_attempt_id": "opat_1kmn0aExample",
"status": "failed",
"is_resumable": false,
"payment_intents": [{
"payment_intent_id": "pi_1kmn0aExample",
"status": "requires_payment_method",
"last_payment_error": {
"code": "insufficient_funds",
"message": "The card was declined for insufficient funds."
}
}]
}
}
}The reason travels with the response. You do not fetch the payment afterwards to find out why it failed.
last_payment_error.code, closed set
- card_declined
- insufficient_funds
- expired_card
- incorrect_cvc
- authentication_required
- processing_error
- payment_method_unavailable
- payment_failed
Provider-specific strings are mapped into this set at the API boundary, so a raw processor code never reaches your handler and a default branch is a safety net rather than the common case. Branch on the code, never on the message.
05Afterwards
Where the money shows up.
The payment is the middle of the story. What follows is the part you live with.
{
"webhook_event_id": "whev_1kmn0aExample",
"event_type": "order.paid",
"payload_version": 1,
"mode": "test",
"merchant_id": "mer_1kmn0aExample",
"created_at": "2026-07-03T14:00:00Z",
"data": {
"order_id": "ord_1kmn0aExample",
"status": "closed",
"payment_status": "paid",
"total_money": { "amount": 4428, "currency": "USD" },
"paid_money": { "amount": 4428, "currency": "USD" },
"outstanding_money": { "amount": 0, "currency": "USD" },
"order_payment_intent_ids": ["pi_1kmn0aExample"]
}
}Every delivery carries a stable webhook_event_id. Retries and manual resends reuse it, so it is your deduplication key.
Events you can subscribe to
- payment_intent.succeeded
- payment_intent.requires_action
- payment_intent.payment_failed
- order.paid
- order.partially_paid
- order.refunded
- refund.created
- dispute.created
- payout.paid
Receive them on localhost
No tunnel and no public URL. The CLI mints a signing secret for the session, so you are testing the same verification path you will run in production.
06Surface
The rest of the API, briefly.
Every one of these attaches to the order you already created.
Orders
Line items, discounts, tips, and tax on one record, with totals computed server-side and a balance that runs down as money lands.
- POST /v1/orders
- pricing_amounts.total_money
- settlement_amounts.outstanding_money
Payments
Automatic or manual capture. Several payments can settle one order, which is how split tender and partial payment work.
- POST /v1/orders/{order_id}/pay
- POST /v1/payment-intents
- capture_method
Refunds
By amount, by line item, or by charge. Flint guards against over-refunding using settled payments rather than list price.
- POST /v1/refunds
- line_items[].order_line_item_id
- line_item_allocations
Attempts
Every try at paying an order is inspectable, including the failed ones, with the reason and whether it can be resumed.
- GET /v1/orders/{order_id}/payment-attempts
- is_resumable
- last_payment_error
Disputes
Read-only through the API today. Each dispute links back to the payment and the order it came from.
- GET /v1/disputes
- evidence_due_at
- evidence_response_allowed
Money out
Balances, payouts, and the ledger behind them. One processing fee per payment, so net is captured minus fee.
- GET /v1/balances
- POST /v1/payouts
- processing_fee_money
Conventions that hold everywhere
- Money
- { "amount": 2500, "currency": "USD" }
- Pagination
- page_size, page_token, next_page_token
- Retries
- Idempotency-Key on any write, 24h replay
- Tracing
- request_id on every response and every error
- Environments
- one host, one /v1, the key picks the mode
Where this differs from a charge-first API
The amount is derived from the order, not submitted by your code.
pricing_amountsRefunds target items and quantities, so the tax split is not yours to compute.
line_item_allocationsWebhooks report the sale becoming paid, not just a charge succeeding.
order.paidOne order can hold several payments, and the balance tracks them.
settlement_amountsA decline is a 402 with a normalized code, never a 5xx.
last_payment_errorReviewed 2026-07-24 against the published API.
07Limits
What this does not do.
Flint is not a payment processor. Stripe processes every card, and Flint provisions and operates the processing account. You cannot attach a Stripe account you already have, and Flint inherits Stripe underwriting: if Stripe declined your business, Flint cannot approve it.
There is no card-present support. Terminals, chip readers, and in-person capture are not part of the API today. If your volume is over a counter, this is the wrong tool for that half of it.
Disputes are read-only over the API. You can list them, read them, and receive their events, but evidence submission happens outside these endpoints.
Two clients, not ten. There is a Node SDK and a CLI. No Python, Go, Ruby, PHP, or Java SDK exists. Everything else talks to plain REST, which is documented and stable, but it is REST.
Payouts are standard speed. Instant payouts are not offered.
08Start
Ten minutes, no card required.
Node
CLI
Test keys are prefixed flint_test_ and bound to a sandbox with its own data, so nothing you do there touches live money. Live keys are prefixed flint_live_. Same host, same paths; the key decides. Pay with 4242 4242 4242 4242, any future expiry, any CVC.
Then read these, in order
FAQ
Questions worth asking first.
Is Flint a payment processor?
No. Stripe processes every card. Card data goes directly to Stripe, with the same PCI scope, fraud detection, and dispute handling as a direct Stripe integration. Flint is the commerce layer above it: orders, server-computed totals, item-level refund math, and order-level webhooks.
Can I connect my existing Stripe account?
No. Flint provisions and operates its own Stripe-based processing accounts, and there is no way to attach a Stripe account you already have. Flint also inherits Stripe underwriting, so if Stripe declined your business, Flint cannot approve it.
What does order-first actually change?
The amount is derived rather than submitted. You send line items and the API computes subtotal, tax, and total server-side; payments settle against that balance, refunds target specific line items by id and quantity, and webhooks report order state rather than charge state. A charge-first integration leaves all four of those to your database.
Does Stripe have an Orders API?
Not anymore. Stripe deprecated its original Orders API in October 2019 and removed the 2022 replacement beta before it reached general availability; its documentation now starts new integrations at payments surfaces like Checkout Sessions. Flint provides that missing layer as its core product: orders with server-computed totals, item-level refunds, and order-level webhooks, with Stripe processing every card underneath.
Which payment methods are supported?
ACH debit, Affirm, Apple Pay, Card, Google Pay are available. There is no Klarna, Afterpay, SEPA, Link, or Cash App. ACH is USD, one-time, on-session, and uses instant bank verification. Affirm appears when the merchant enables it and the connected account and transaction are eligible.
Do I have to use the hosted checkout?
No. The same order can be paid by mounting Stripe Elements in your own UI, by confirming from your server with a confirmation token, or through a payment link or invoice. Every surface settles into the same order, so refunds, webhooks, and reporting behave identically no matter how the money was collected.
How do I know a payment really succeeded?
Not from the browser redirect, which a buyer can skip by closing the tab and anyone can open directly. Treat the webhook or a backend read of the order as the signal to fulfill. Payments are asynchronous in the cases that matter: ACH can sit in processing after the buyer is done, and 3D Secure hands control to the issuer mid-flow.