Merchant account sessions authorize short-lived, embedded account-management components. Create and refresh them only from your backend. Your backend must authenticate and authorize the human who will use the component before it returns a client secret to the browser. Never put a Flint secret key, client secret, or launch_token in a URL or browser bundle.
Account sessions are embedded-only. Flint does not return a hosted onboarding URL.
Components#
Send a non-empty components array. The array is a set, so duplicates are rejected and the response uses canonical order. One session can authorize several components, but it can contain at most one policy-aware component: account_onboarding or account_management.
| Flint component | Use it for |
|---|---|
account_onboarding | Initial identity, business, document, agreement, and payout-destination collection. Also use it for targeted remediation. |
account_management | Ongoing business-profile and compliance maintenance. |
payouts | Adding, removing, and updating payout destinations, plus payout management. |
balances | Viewing balances and payout timing. |
tax_documents | Viewing provider-issued account and tax documents. The Connect.js element name is documents. |
notification_banner | Showing current risk, compliance, and account notifications. |
collection_strategy, future_requirements, and targeted_requirement_ids apply only to the policy-aware component. Sending policy fields without one, or requesting both policy-aware components, returns MERCHANT_ACCOUNT_SESSION_INVALID_POLICY_COMBINATION.
Until a merchant finishes onboarding for the first time, the default policy collects only what Stripe requires now: collection_strategy is incremental and future_requirements is omit. The form is shorter, and Stripe may ask for more details later. To collect everything up front, including requirements that are not due yet, send "collection_strategy": "upfront" and "future_requirements": "include". After the first finish, the merchant's onboarding_status stays completed even when Stripe later asks for more details, so use onboarding state's requirements and next_step to decide when to open a session.
curl -X POST https://api.withflintpay.com/v1/merchant-account-sessions \
-H "Authorization: Bearer $FLINT_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: onboarding-owner-123" \
-d '{
"components": ["account_onboarding", "notification_banner"],
"sandbox_id": "test_01JQEXAMPLEDEFAULT12345678"
}'
When onboarding state or readiness provides a launch recommendation, copy its component, policy, target IDs, and sandbox_id instead of deriving them from requirement names.
Response and refresh#
The response includes a canonical component grant, a signed Flint launch token, and client_session browser initialization data. The account session and launch token have separate expiry times. client_session.stripe.account_session.stripe_js_call is load_connect_and_initialize: call loadConnectAndInitialize with the publishable key and a fetchClientSecret callback. Connect initializes from the account session client secret and does not take a connected account ID.
{
"data": {
"components": ["account_onboarding", "notification_banner"],
"effective_policy": {
"collection_strategy": "incremental",
"future_requirements": "omit",
"targeting": "all"
},
"launch_token": "aslaunch_v1.example",
"launch_token_expires_at": "2026-07-13T18:05:00Z",
"client_session": {
"expires_at": "2026-07-13T18:00:00Z",
"stripe": {
"publishable_key": "pk_test_example",
"account_session": {
"stripe_js_call": "load_connect_and_initialize",
"client_secret": "acct_session_secret_example"
},
"components": [
{
"component": "account_onboarding",
"collection_options": {
"fields": "currently_due",
"future_requirements": "omit"
}
},
{
"component": "notification_banner"
}
]
}
},
"requirements": {}
}
}
Connect.js calls fetchClientSecret when its current provider session expires. Your browser calls your authenticated backend, and your backend exchanges the latest launch token with Flint:
curl -X POST https://api.withflintpay.com/v1/merchant-account-sessions/refresh \
-H "Authorization: Bearer $FLINT_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: account-session-refresh-002" \
-d '{"launch_token":"aslaunch_v1.example"}'
Every successful refresh returns a new client secret and rotates launch_token. Keep the newest token in server-side session state or page memory and serialize concurrent refreshes so each request uses the token returned by the preceding refresh. Refresh rechecks the caller's current key scope, merchant environment, account state, and pinned component grant. A launch token alone is not authorization.
Executable Connect.js recipe#
Install the packages for the integration style you use:
npm install @stripe/connect-js
# React applications also install:
npm install @stripe/react-connect-js
Your application backend needs two authenticated endpoints. The create endpoint calls Flint with your secret key after verifying the signed-in human may manage the merchant. The refresh endpoint performs the same authorization check, then calls Flint's dedicated refresh operation. Return only the Flint response body to that authorized browser. Do not accept a merchant ID from the browser as proof of access.
The following vanilla example mounts two components from one account session and one Connect instance:
<div id="notifications"></div>
<div id="onboarding"></div>
<script type="module" src="/account-onboarding.js"></script>
import { loadConnectAndInitialize } from "@stripe/connect-js";
const initial = await fetch("/account-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({
components: ["account_onboarding", "notification_banner"],
}),
}).then(requireOK);
let launchToken = initial.data.launch_token;
let initialSecret = initial.data.client_session.stripe.account_session.client_secret;
let refreshQueue = Promise.resolve();
async function refreshClientSecret() {
if (initialSecret) {
const value = initialSecret;
initialSecret = null;
return value;
}
const refresh = refreshQueue.then(async () => {
const response = await fetch("/account-session/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ launch_token: launchToken }),
}).then(requireOK);
launchToken = response.data.launch_token;
return response.data.client_session.stripe.account_session.client_secret;
});
refreshQueue = refresh.then(() => undefined, () => undefined);
return refresh;
}
async function requireOK(response) {
const body = await response.json();
if (!response.ok) throw new Error(body.error?.message ?? "Account session failed");
return body;
}
const launch = initial.data.client_session.stripe;
const connect = loadConnectAndInitialize({
publishableKey: launch.publishable_key,
fetchClientSecret: refreshClientSecret,
});
const onboardingGrant = launch.components.find(
(grant) => grant.component === "account_onboarding",
);
if (!onboardingGrant) throw new Error("Missing account_onboarding grant");
const collectionOptions = onboardingGrant.collection_options;
const notifications = connect.create("notification-banner");
document.querySelector("#notifications").appendChild(notifications);
const onboarding = connect.create("account-onboarding");
onboarding.setCollectionOptions({
fields: collectionOptions.fields,
futureRequirements: collectionOptions.future_requirements,
requirements: collectionOptions.requirements,
});
onboarding.setOnExit(() => {
void pollOnboardingState();
});
document.querySelector("#onboarding").appendChild(onboarding);
async function pollOnboardingState() {
// Read state from your backend. Exit means the user left the component,
// not that verification succeeded.
await fetch("/onboarding-state", { credentials: "same-origin" });
}
// Call this only when the application user signs out, not on page navigation.
window.addEventListener("app:logout", () => void connect.logout());
For React, pass the same Connect instance to one provider and map collection_options to the component's collectionOptions prop:
import { loadConnectAndInitialize } from "@stripe/connect-js";
import {
ConnectAccountOnboarding,
ConnectComponentsProvider,
ConnectNotificationBanner,
} from "@stripe/react-connect-js";
const connect = loadConnectAndInitialize({
publishableKey: launch.publishable_key,
fetchClientSecret: refreshClientSecret,
});
const onboardingGrant = launch.components.find(
(grant) => grant.component === "account_onboarding",
);
export function AccountSetup() {
if (!onboardingGrant) throw new Error("Missing account_onboarding grant");
return (
<ConnectComponentsProvider connectInstance={connect}>
<ConnectNotificationBanner />
<ConnectAccountOnboarding
collectionOptions={{
fields: onboardingGrant.collection_options.fields,
futureRequirements: onboardingGrant.collection_options.future_requirements,
requirements: onboardingGrant.collection_options.requirements,
}}
onExit={() => void pollOnboardingState()}
/>
</ConnectComponentsProvider>
);
}
Apply the returned collection options without changing them. Flint fails closed when it cannot map targeted requirement IDs and restricts the provider session to the requested component grant. The browser still applies collection_options, so Flint cannot prove that an integrator mounted the component with those options. Treat effective_policy as an authorization-bound instruction, not as evidence that the browser followed it.
Browser and security requirements#
- Poll
GET /v1/onboarding/stateafteronExit. A useful schedule is 2, 4, 8, and 15 seconds, then every 30 seconds for up to 15 minutes. Wait without creating another session while onlypending_verificationremains. - Handle client-secret or launch-token expiry by starting a newly authorized create flow. Do not loop refresh after a terminal authorization or expiry error.
- Allow Stripe Connect frames, images, and scripts in your Content Security Policy. At minimum, allow
https://connect-js.stripe.comandhttps://js.stripe.cominframe-srcandscript-src, andhttps://*.stripe.cominimg-src. If you setCross-Origin-Opener-Policy, useunsafe-none;same-originbreaks required authentication popups. - Do not block popups. Account Onboarding, Account Management, Balances, Payouts, and Notification Banner can require authentication in a Stripe-owned popup.
- Reuse one Connect instance for all components in the session. Call
logout()only when your application user signs out. - Partner-install tokens cannot create or refresh merchant account sessions. A secret Flint API key needs
merchants.account_sessions.write. A sandbox key or onboarding session token reaches only sandbox accounts; use a live key, withoutsandbox_id, for the live account.
Headless applications and provider emails#
A headless backend still needs an authenticated browser page for human-owned verification. Serve a short-lived, signed link into your own application, require the account owner to sign in, authorize that user for the merchant, and mount the embedded component there. The secure link identifies your pending workflow. It must not contain Flint credentials, a provider account identifier, or a provider client secret.
Provider-originated compliance, risk, payout, account, and document emails are different. Those links always enter Flint's authenticated action gateway, even when ordinary onboarding is embedded in your product. The recipient signs in to Flint, Flint resolves the provider reference server-side, replaces it with a short-lived opaque action token, and mounts the authorized component. The primary owner therefore keeps a Flint login for provider email actions.
Sandbox verification values#
In a Flint sandbox, use provider test values only. For a US account, these values exercise a successful path:
| Field | Test value |
|---|---|
| SMS code | 000-000 |
| Date of birth | 1901-01-01 |
| SSN or personal ID | 000000000 (or 0000 when only the last four digits are requested) |
| Business tax ID | 000000000 |
| Phone | 0000000000 |
| Successful website validation | https://accessible.stripe.com |
| US payout routing number | 110000000 |
| US payout account number | 000123456789 |
Complete every field shown by Account Onboarding, including the payout destination and provider agreement. Then poll onboarding state until every requested Flint capability is ready. Sandbox behavior can be asynchronous, and provider test mode does not enforce every capability exactly like live mode, so Flint's persisted readiness remains the contract you should test.
For document testing, use the provider's supplied test images in the embedded uploader. Never upload a real identity document to a sandbox.
Targeted remediation#
Use Flint requirement IDs from onboarding state or merchant readiness. Do not pass provider-shaped names:
{
"components": ["account_onboarding"],
"collection_strategy": "incremental",
"future_requirements": "omit",
"targeted_requirement_ids": ["business_website"]
}
Flint maps the selected IDs to the provider collection options and returns the mapped values only inside client_session.stripe.components[].collection_options.requirements.only. If any selected ID is unknown or cannot be mapped safely, session creation fails with a MERCHANT_ACCOUNT_SESSION_* error before a client secret is returned. Do not catch that error and retry with untargeted collection. Refresh onboarding state and present the corrected action to the user.
