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.
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"],
"collection_strategy": "upfront",
"future_requirements": "include",
"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 one provider client secret, one publishable key, a canonical component grant, and a signed Flint launch token. The provider session and launch token have separate expiry times.
{
"data": {
"components": ["account_onboarding", "notification_banner"],
"effective_policy": {
"collection_strategy": "upfront",
"future_requirements": "include",
"targeting": "all"
},
"external_action": {
"kind": "embedded",
"provider_session_expires_at": "2026-07-13T18:00:00Z",
"launch_token": "aslaunch_v1.example",
"launch_token_expires_at": "2026-07-13T18:05:00Z",
"stripe": {
"client_secret": "acct_session_secret_example",
"publishable_key": "pk_test_example",
"components": [
{
"component": "account_onboarding",
"component_props": {
"collectionOptions": {
"fields": "eventually_due",
"futureRequirements": "include"
}
}
},
{
"component": "notification_banner",
"component_props": {}
}
]
}
},
"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.external_action.launch_token;
let initialSecret = initial.data.external_action.stripe.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.external_action.launch_token;
return response.data.external_action.stripe.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.external_action.stripe;
const connect = loadConnectAndInitialize({
publishableKey: launch.publishable_key,
fetchClientSecret: refreshClientSecret,
});
const props = Object.fromEntries(
launch.components.map((grant) => [grant.component, grant.component_props]),
);
const notifications = connect.create("notification-banner");
document.querySelector("#notifications").appendChild(notifications);
const onboarding = connect.create("account-onboarding");
onboarding.setCollectionOptions(props.account_onboarding.collectionOptions);
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 apply each returned component's props through that component's real React props:
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={onboardingGrant.component_props.collectionOptions}
onExit={() => void pollOnboardingState()}
/>
</ConnectComponentsProvider>
);
}
Do not invent or override component props. Flint fails closed when it cannot map targeted requirement IDs and restricts the provider session to the requested component grant. The browser still applies component_props, 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.
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 component_props.collectionOptions.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.
