Skip to content

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
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.

The same order, paid. Totals were computed server-side, never submitted.

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.

curl
# 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"
201 Created
{
  "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.

200 OK, after the buyer pays
{
  "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.

order

POST /v1/orders

Line items, discounts, tax, and the running balance. Totals recompute server-side on every change.

  • payment_intent

    POST /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_session

    POST /v1/checkout-sessions

    A hosted page that is a view over the order, not a copy of it.

  • refund

    POST /v1/refunds

    Targets line items by id and quantity. Flint computes the money, including each line's share of tax.

  • dispute

    GET /v1/disputes

    Read-only through the API today, and always linked back to the order it came from.

  • payout

    GET /v1/payouts

    Where the settled money goes. Balance transactions tie each payout back to the payments inside it.

  • fulfillment

    POST /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
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 }
    ]
  }'
201 Created
{
  "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.

app.withflintpay.com
The order after the refund. The refunded line is struck through on the same record the payment settled against.

03Collection

Three ways to take the money.

Same order, same refunds, same webhooks. The only thing that changes is who renders the form.

Hosted: POST /v1/checkout-sessions
{
  "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.

Embedded: your own UI
{
  "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.

Server-confirmed: POST /v1/orders/{order_id}/pay
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.

checkout.withflintpay.com
The hosted page, rendered from the same component library as this section. Test mode is visible because the session was created with a test key.

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.

The failed attempt
{
  "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.

One attempt, start to finish
Authentication is a typed pending action on the attempt, so you run it rather than inferring it from a status.

05Afterwards

Where the money shows up.

The payment is the middle of the story. What follows is the part you live with.

app.withflintpay.com/payments
The merchant dashboard's payments list, rendered here from the same components it ships.
POST /webhooks/flint
{
  "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

flint listen --forward-to http://localhost:8080/webhooks/flint

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_amounts

Refunds target items and quantities, so the tax split is not yours to compute.

line_item_allocations

Webhooks report the sale becoming paid, not just a charge succeeding.

order.paid

One order can hold several payments, and the balance tracks them.

settlement_amounts

A decline is a 402 with a normalized code, never a 5xx.

last_payment_error

Reviewed 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

npm install @flintpay/node

CLI

brew install flintpay/tap/flint

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.

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.