Skip to content

Embedded payments

Merchant fifty-one is the same four calls.

For vertical SaaS and ISV teams whose customers need to take payments. Onboard each business as its own merchant through an API state machine, mint them a scoped key, collect cards in your own UI under their brand, and read every merchant's events off one endpoint. Stripe processes every card. Each merchant is paid out to their own bank, so nothing routes through you.

curl
curl -X POST https://api.withflintpay.com/v1/onboarding/start \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_PLATFORM_KEY" \
  -H "Idempotency-Key: onboard-northside-001" \
  -d '{
    "email": "owner@northsidecoffee.example",
    "first_name": "Ada",
    "last_name": "Okonkwo"
  }'

An email and a name. No processor application, no spreadsheet, no waiting on a human.

one merchant, minutes later
Readiness answers the two questions your product actually has: can this merchant charge, and can they be paid? The two are tracked separately, because they clear separately.

one state machine per merchant · Stripe processes the cards · you never hold funds

01The problem

The feature is payments. The build is a control plane.

Adding payments looks like one feature. It is one feature, for one merchant.

For merchant one you walk a processor application through email and paste credentials into an environment variable. By merchant fifty you are running an onboarding wizard with document re-collection, a vault holding fifty sets of credentials, a router that works out whose event just arrived, a poller answering whether a given merchant can charge yet, and a reconciliation job across fifty payout schedules. None of that is the product your customers buy, and all of it pages you.

Each of those subsystems already exists here as an endpoint.

What you would build, and what replaces it

The onboarding wizard, including re-collecting a document Stripe rejected.

POST /v1/onboarding/advance

The verification UI, hosted inside your product rather than on someone else's domain.

POST /v1/merchant-account-sessions

The poller that answers whether this merchant can charge yet.

GET /v1/merchants/{merchant_id}/readiness

The credential vault, one least-privilege key per merchant.

POST /v1/onboarding/api-key

The webhook router that works out which merchant an event belongs to.

POST /v1/webhook-endpoints

The reconciliation job, with each fee and net amount tied to an order.

GET /v1/balance-transactions

Reviewed 2026-07-24 against the published API. Every row above can be built by a capable team on a raw processor. On day one they are calls, not projects.

02The model

Each business is a first-class merchant.

Tenancy is built in rather than bolted on: every business you onboard owns a merchant record of its own, and the keys, readiness, events, and payouts it needs all attach to that record.

merchant

POST /v1/onboarding/start

Created from an email address and a name. Underwritten individually, keyed individually, paid out individually.

  • onboarding

    GET /v1/onboarding/state

    The state machine. It reports what compliance still needs and what your UI should render next.

  • merchant_account_session

    POST /v1/merchant-account-sessions

    Credentials for the embedded verification component you mount in your own pages.

  • api_key

    POST /v1/onboarding/api-key

    That merchant's first credential, bound to them and scoped by you.

  • readiness

    GET /v1/merchants/{merchant_id}/readiness

    Whether they can charge and whether they can be paid, as two separate answers.

  • webhook_endpoint

    POST /v1/webhook-endpoints

    One URL of yours, receiving events for every merchant that installed your app.

  • payout_destination

    GET /v1/payout-settings/destinations

    Their bank, not yours. Settled money leaves Flint to this record.

The useful part: they can start integrating before verification finishes.

Most platforms serialize this. Application, then review, then credentials, then the developer starts work, and every stalled document blocks the whole chain. Here the state machine reports the two things separately, so a merchant whose document is still outstanding can already hold a sandbox key and have your integration finished by the time Stripe clears them.

GET /v1/onboarding/state
{
  "data": {
    "merchant_id": "mer_1kmn0aExample",
    "merchant_created": true,
    "email_verified": true,
    "can_issue_api_key": true,
    "requested_capabilities": [
      "accept_card_payments",
      "receive_payouts"
    ],
    "requirements": {
      "currently_due": ["business_verification_document"],
      "current_deadline_at": "2026-08-07T00:00:00Z"
    },
    "next_step": {
      "machine_completable": false,
      "launch": {
        "method": "POST",
        "endpoint": "/v1/merchant-account-sessions",
        "component": "account_onboarding",
        "recommended_policy": {
          "collection_strategy": "incremental",
          "future_requirements": "omit"
        }
      }
    }
  }
}

Verification is outstanding and the key is already available. Branch on can_issue_api_key rather than on the workflow status, and render next_step.launch instead of guessing what to show.

curl
curl -X POST https://api.withflintpay.com/v1/onboarding/api-key \
  -H "Authorization: Bearer YOUR_PLATFORM_KEY" \
  -d '{
    "name": "Northside Coffee checkout backend",
    "scopes": [
      "commerce.orders.write",
      "payments.payment_intents.write",
      "checkouts.checkout_sessions.write"
    ]
  }'

You choose the scopes. A checkout backend gets checkout scopes and nothing else.

the key, one call later
You asked for three scopes and the key carries six: a write scope grants its matching read. The secret is on that response only, and a key can never grant a scope its creator lacks.
The status vocabulary is prose, not an enumThe workflow status reports needs_input, needs_external_action, waiting_for_review, ready_for_api_key, or complete. Those names are documented, but the field is a plain string in the spec, so treat the booleans and the requirements arrays as the contract and the status as a label for your logs.

03Verification

Compliance collection, inside your product.

The one step that genuinely needs the merchant is also the one platforms usually give away to a redirect. It stays in your pages.

Components you can mount

  • account_onboarding
  • account_management
  • payouts
  • balances
  • tax_documents
  • notification_banner

That is the whole set. One policy-aware component per session, so a session either onboards or manages, never both.

Readiness, the complete set of answers

  • ready
  • pending
  • blocked
  • not_available
  • not_requested

Payments and payouts each report one of these, with a reason when it is not ready. Subscribe to merchant.readiness.updated and gate your own features on the event rather than polling.

GET /v1/merchants/{merchant_id}/readiness
{
  "data": {
    "merchant_id": "mer_1kmn0aExample",
    "business_name": "Northside Coffee",
    "payments": { "status": "ready" },
    "payouts": {
      "status": "pending",
      "status_reason": "pending_verification"
    },
    "observed_at": "2026-07-24T14:04:11Z"
  }
}

They can take money and cannot yet be paid. That is a normal state for a new merchant, and worth designing a screen for.

curl
curl -X POST https://api.withflintpay.com/v1/merchant-account-sessions \
  -H "Authorization: Bearer MERCHANT_KEY" \
  -d '{
    "components": ["account_onboarding"],
    "collection_strategy": "incremental",
    "future_requirements": "omit"
  }'

Ask for the component the state machine told you to launch. Incremental collection asks only for what is currently due; upfront collects the whole set at once.

201 Created
{
  "data": {
    "components": ["account_onboarding"],
    "effective_policy": {
      "targeting": "all_due",
      "collection_strategy": "incremental",
      "future_requirements": "omit"
    },
    "external_action": {
      "kind": "embedded",
      "launch_token": "LAUNCH_TOKEN",
      "launch_token_expires_at": "2026-07-24T14:34:00Z",
      "stripe": {
        "client_secret": "CLIENT_SECRET",
        "publishable_key": "pk_test_1kmn0aExample",
        "components": [{ "component": "account_onboarding" }]
      }
    },
    "requirements": {
      "currently_due": ["business_verification_document"]
    }
  }
}

Mount Stripe's embedded onboarding component with the client secret and publishable key. The launch token rotates, and the refresh route hands you the next one before it expires.

Not fully headless, and worth knowing earlyProvider compliance, risk, and payout email routes through Flint's authenticated gateway, so a merchant's primary owner keeps a Flint login for those actions. Everything your product drives is API-driven; the legal correspondence is not something a platform can absorb.

04Two shapes

You onboard them, or they install you.

Which one you need depends on whether your product is where the merchant starts, or something they connect to an account they already have.

Shape one: your product owns signup

You create the merchant, render the state machine, mint the key, and gate launch on readiness. Your product is the only surface the merchant sees.

Direct onboarding

Four calls from an email address to a merchant that can charge, with the verification step mounted in your own pages.

  • POST /v1/onboarding/start
  • GET /v1/onboarding/state
  • requirements.currently_due

Embedded verification

Session-scoped credentials for the compliance component, with a rotating launch token and a refresh route.

  • POST /v1/merchant-account-sessions
  • external_action.launch_token
  • collection_strategy

Scoped keys

One key per merchant, least privilege by default, mintable from the onboarding session before verification completes.

  • POST /v1/onboarding/api-key
  • commerce.orders.write
  • GET /v1/api-keys

Shape two: they install your app

For merchants already running on Flint. Register a partner app, send them through hosted consent, and exchange the code for a token scoped to that merchant. The preview route lets you render your own explanation of what you are asking for before they see the consent screen.

curl
curl -X POST https://api.withflintpay.com/v1/oauth/token \
  -u "FPC_CLIENT_ID:FPS_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=FPAC_AUTHORIZATION_CODE" \
  -d "redirect_uri=https://yourapp.example/flint/oauth/callback"

Standard authorization-code exchange, with the app credential in the Authorization header.

200 OK
{
  "access_token": "ACCESS_TOKEN",
  "refresh_token": "REFRESH_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "merchant_id": "mer_1kmn0aExample",
  "partner_app_id": "papp_1kmn0aExample",
  "partner_app_install_id": "pinst_1kmn0aExample",
  "environment_grant_id": "egrt_1kmn0aExample",
  "mode": "test",
  "scope": "commerce.orders.read payments.payment_intents.read"
}

The token is stamped with the merchant, the install, the environment grant, and the mode it belongs to. A test-mode grant cannot act on live data.

Install lifecycle events

  • partner_app.install.created
  • partner_app.install.permissions_updated
  • partner_app.install.environment_grant.created
  • partner_app.install.environment_grant.revoked
  • partner_app.install.revoked

Grants are revocable one at a time, so losing live access does not cost you the sandbox integration. Partner app guide

Neither shape is a plugin for an account you already have. Flint provisions and operates its own Stripe-based processing per merchant, and there is no way to attach an existing Stripe account. Each merchant is underwritten by Stripe individually, which means some applications are declined and your onboarding flow should treat that as a state rather than a bug.

05Collection

Their brand, your UI, our rails.

The buyer is your merchant's customer. Nothing on the page has to say Flint, and the one thing that does is a footer credit.

POST /v1/orders/{order_id}/payment-intents
{
  "data": {
    "payment_intent": {
      "payment_intent_id": "pi_1kmn0aExample",
      "status": "requires_confirmation",
      "amount_money": { "amount": 1053, "currency": "USD" }
    },
    "payment_collection": {
      "stripe": {
        "account_id": "acct_1kmn0aExample",
        "publishable_key": "pk_test_1kmn0aExample",
        "elements": {
          "mode": "payment",
          "amount_money": { "amount": 1053, "currency": "USD" },
          "payment_method_types": ["card"],
          "digital_wallets": ["apple_pay", "google_pay"],
          "payment_method_creation": "manual"
        }
      }
    }
  }
}

The response carries that merchant's connected account and publishable key, so Stripe Elements mounts under the right account with no lookup in a credential vault.

northsidecoffee.example/checkout
Rendered from the same component library the product ships. The pay button is wearing theme.primary_color, and the kit only applies a merchant's color when it clears a contrast check, falling back to ember when it would not.

Embedded collection

Stripe Elements in your own pages, confirmed from your server. The client is not given confirmation authority, by design.

  • POST /v1/orders/{order_id}/payment-intents
  • payment_collection.stripe.elements
  • expected_outstanding_money

Per-merchant sandboxes

Each merchant gets isolated test data, resettable between runs, with a request log you can read when an integration misbehaves.

  • POST /v1/developer/sandboxes
  • POST /v1/developer/sandboxes/{sandbox_id}/reset
  • GET /v1/developer/request-logs

Money out

Payouts land in the merchant's own destination on their own schedule. Reconciliation is a read, not a nightly job.

  • GET /v1/balances
  • GET /v1/payout-settings/destinations
  • net_money

06The feed

One endpoint, every installed merchant.

The subsystem platforms most regret building is the one that works out whose event just arrived.

curl
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_PLATFORM_KEY" \
  -d '{
    "url": "https://yourapp.example/flint/merchant-events",
    "event_sources": ["installed_merchants"],
    "partner_app_id": "papp_1kmn0aExample",
    "mode": "both",
    "enabled_events": [
      "order.paid",
      "refund.created",
      "payout.paid",
      "merchant.readiness.updated"
    ]
  }'

One endpoint of yours, named against your partner app. Forwarding is scope-guarded: an event only arrives if that install granted the matching read scope.

POST https://yourapp.example/flint/merchant-events
{
  "webhook_event_id": "whev_1kmn0aExample",
  "event_type": "order.paid",
  "payload_version": 1,
  "mode": "test",
  "merchant_id": "mer_1kmn0aExample",
  "created_at": "2026-07-24T14:19:02Z",
  "data": {
    "order_id": "ord_1kmn0aExample",
    "payment_status": "paid",
    "total_money":       { "amount": 1053, "currency": "USD" },
    "outstanding_money": { "amount": 0,    "currency": "USD" }
  }
}

Every delivery carries merchant_id, so routing is a lookup rather than a puzzle, and webhook_event_id is stable across retries and manual resends, which makes it your deduplication key.

When your handler was the broken one

Every delivery for one event
GET /v1/webhook-events/{webhook_event_id}/deliveries
Push it again after a fix
POST /v1/webhook-deliveries/{webhook_delivery_id}/resend
Read the feed, installed merchants only
GET /v1/webhook-events?event_source=installed_merchants
Tail it while you build
GET /v1/webhook-events/stream

Deliveries are at-least-once and unordered, so build handlers that tolerate both. Nothing on that list needs a support ticket.

The router you never buildFifty merchants, one URL, and an envelope that already says which merchant it belongs to.
deliveries, across merchants
Delivery state is inspectable per event. Automatic retries run up to nine attempts across roughly three days, and a failed delivery tells you the response code it got rather than making you guess.

The fan-in endpoint

Registered against your partner app, filtered to the events you asked for, delivered per mode so test and live traffic stay apart.

  • POST /v1/webhook-endpoints
  • event_sources
  • GET /v1/webhook-events/stream

Two limits worth knowing before you design around it

Event matching is exact. There are no wildcard subscriptions, so a new event type needs a config change.

enabled_events

One app picks app-lifecycle events or installed-merchant forwarding, not both on the same endpoint today.

event_sources

Partner lifecycle deliveries also use a thinner envelope than resource events: no payload_version, mode, or merchant_id at the top level, with partner_app_id instead and the merchant inside data.

07The money

It is never yours to hold.

That design decides which regulatory conversations your company never has to have.

Stripe settles each merchant's payments; Flint pays out to that merchant's own destination.

GET /v1/payout-settings/destinations

Merchants choose their own cadence, including turning automatic payouts off.

interval: manual, daily, weekly, monthly

Every movement carries its fee and its net, tied back to the order that caused it.

fee_money, net_money, order_id

Balances are read per merchant, so a platform-wide view is your query, not our object.

GET /v1/balances

There is no platform balance in this design, because there is no platform account for money to sit in. That is the same reason the API has no fee-split field.

GET /v1/balance-transactions
{
  "data": [{
    "type": "payment",
    "status": "available",
    "merchant_id": "mer_1kmn0aExample",
    "order_id": "ord_1kmn0aExample",
    "amount_money": { "amount": 1053, "currency": "USD" },
    "fee_money":    { "amount": 70,   "currency": "USD" },
    "net_money":    { "amount": 983,  "currency": "USD" },
    "available_at": "2026-07-26T00:00:00Z"
  }]
}

One payment, one fee, one net, linked to an order id. Reconciling a merchant's month is a filtered read rather than a job you maintain.

What your platform sees

Fleet state is assembled from per-merchant reads, and the shape below is what most platforms end up putting in front of their own support team.

yourapp.example/admin/merchants
Your screen, your data model, built from readiness reads and the event feed. Rendered here with the Flint kit to show the shape; the fields are the real response fields.

08Limits

What this does not do.

You cannot take a cut of a payment. There is no application-fee or revenue-share field, so no slice of a transaction can be routed to your platform through the API. Platforms here bill their merchants for the software, and each merchant pays Flint's published rates directly out of their own settlement. If per-transaction economics or submerchant payout terms are the model you need, that is an Enterprise conversation rather than an API feature, and it is better had before you build.

Merchants cannot bring their own Stripe account. Flint provisions and operates the processing account for each one, and inherits Stripe underwriting with it. If Stripe declines a business or a vertical, Flint cannot approve it either, which is worth checking against your customer list before you commit a roadmap to this.

Verification is embedded only. There is no hosted onboarding URL to redirect a merchant to, which is deliberate, but it does mean the integration is a component you mount rather than a link you send. Provider compliance email also stays with Flint, so the merchant's owner keeps a Flint login.

The opening capability set is two items. A new merchant requests card acceptance and payouts. Anything beyond that pair, ACH included, is enabled per environment after review rather than at onboarding time.

Nothing here works over a counter. Terminals, chip readers, and in-person capture are absent from the API, so a platform whose merchants take payments face to face needs a second vendor for that half of the volume.

The clients are Node and a command line. Both are real and both are maintained. Every other language talks to documented, stable REST, and REST is what it is.

09Start

Onboard a test merchant this afternoon.

Node

npm install @flintpay/node

CLI

brew install flintpay/tap/flint

Receive the fan-in feed on localhost

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

A merchant you onboard with a flint_test_ key lands in a sandbox with its own data, so nothing you do to them costs money or goes to underwriting. Swap the key for a flint_live_ one and the same code onboards a real business. Pay their first test order with 4242 4242 4242 4242, any future expiry, any CVC.

FAQ

What platform teams ask first.

Do I ever hold my merchants' funds?

No, and the design gives you no way to. Stripe settles each merchant's payments and Flint pays out to that merchant's own bank destination. Money never routes through a platform account, so you are not choosing a funds-flow model or inheriting the compliance posture that comes with one. Reconciliation is read-only: balances, balance transactions with fee and net amounts tied to an order, and payouts, all per merchant.

Can I take a cut of each transaction?

Not through the API. Flint has no application-fee or revenue-share surface, so there is no field that routes a slice of a payment to your platform. Platforms on Flint charge their merchants for the software instead. If per-transaction economics are the business model, say so early: that conversation happens on an Enterprise agreement, not in the API.

Does every business on my platform need its own Flint merchant?

Yes, and that is the point. Each business is underwritten individually on Stripe rails and owns its own record, its own keys, and its own payouts. The whole flow is API-driven: onboarding start creates the merchant, the state machine collects what compliance needs, and the api-key call mints that merchant's first credential.

How do merchants verify their business without leaving my app?

Mint a merchant account session and mount the returned embedded component inside your own pages. Flint returns a client secret, a publishable key, and a rotating launch token; you render Stripe's embedded onboarding component with them. There is no hosted Flint onboarding URL to redirect to, by design. Note that provider compliance and risk email still routes through Flint, so the merchant's primary owner keeps a Flint login for those actions.

What happens when a merchant fails underwriting?

Some will, and your flow should expect it rather than treat it as an error path. Stripe underwrites each merchant individually. A stalled or declined merchant reports readiness as blocked with a reason, and the requirements arrays name anything still collectable. If Stripe cannot serve a business, Flint cannot approve it.

Can my merchants connect a Stripe account they already have?

No. Flint provisions and operates the processing account for each merchant, and there is no way to attach an existing one. Stripe processes every card underneath, and Flint inherits Stripe underwriting per merchant.

Can I keep test and live installs separate?

Yes. An install carries environment grants, each scoped to test or live mode, and each revocable on its own. The token you exchange comes back stamped with the grant and the mode it belongs to, so a sandbox integration can never quietly act on live data.