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 -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 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/advanceThe verification UI, hosted inside your product rather than on someone else's domain.
POST /v1/merchant-account-sessionsThe poller that answers whether this merchant can charge yet.
GET /v1/merchants/{merchant_id}/readinessThe credential vault, one least-privilege key per merchant.
POST /v1/onboarding/api-keyThe webhook router that works out which merchant an event belongs to.
POST /v1/webhook-endpointsThe reconciliation job, with each fee and net amount tied to an order.
GET /v1/balance-transactionsReviewed 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.
merchantPOST /v1/onboarding/start
Created from an email address and a name. Underwritten individually, keyed individually, paid out individually.
onboardingGET /v1/onboarding/state
The state machine. It reports what compliance still needs and what your UI should render next.
merchant_account_sessionPOST /v1/merchant-account-sessions
Credentials for the embedded verification component you mount in your own pages.
api_keyPOST /v1/onboarding/api-key
That merchant's first credential, bound to them and scoped by you.
readinessGET /v1/merchants/{merchant_id}/readiness
Whether they can charge and whether they can be paid, as two separate answers.
webhook_endpointPOST /v1/webhook-endpoints
One URL of yours, receiving events for every merchant that installed your app.
payout_destinationGET /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.
{
"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 -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.
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.
{
"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 -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.
{
"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.
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 -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.
{
"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.
{
"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.
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 -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.
{
"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 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_eventsOne app picks app-lifecycle events or installed-merchant forwarding, not both on the same endpoint today.
event_sourcesPartner 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/destinationsMerchants choose their own cadence, including turning automatic payouts off.
interval: manual, daily, weekly, monthlyEvery movement carries its fee and its net, tied back to the order that caused it.
fee_money, net_money, order_idBalances are read per merchant, so a platform-wide view is your query, not our object.
GET /v1/balancesThere 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.
{
"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.
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
CLI
Receive the fan-in feed on localhost
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.