Merchant account sessions

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 componentUse it for
account_onboardingInitial identity, business, document, agreement, and payout-destination collection. Also use it for targeted remediation.
account_managementOngoing business-profile and compliance maintenance.
payoutsAdding, removing, and updating payout destinations, plus payout management.
balancesViewing balances and payout timing.
tax_documentsViewing provider-issued account and tax documents. The Connect.js element name is documents.
notification_bannerShowing 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
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.

JSON
{
  "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
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:

Shell
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:

HTML
<div id="notifications"></div>
<div id="onboarding"></div>
<script type="module" src="/account-onboarding.js"></script>
JavaScript
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:

TSX
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/state after onExit. 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 only pending_verification remains.
  • 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.com and https://js.stripe.com in frame-src and script-src, and https://*.stripe.com in img-src. If you set Cross-Origin-Opener-Policy, use unsafe-none; same-origin breaks 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, without sandbox_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:

FieldTest value
SMS code000-000
Date of birth1901-01-01
SSN or personal ID000000000 (or 0000 when only the last four digits are requested)
Business tax ID000000000
Phone0000000000
Successful website validationhttps://accessible.stripe.com
US payout routing number110000000
US payout account number000123456789

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:

JSON
{
  "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.

Create merchant account session#

POST/v1/merchant-account-sessionsIdempotent

Requires scope merchants.account_sessions.write

Creates an embedded browser handoff for one or more allowlisted account components. An onboarding session is limited to the merchant's default sandbox unless sandbox_id in the JSON body names another sandbox of the same merchant. Live onboarding requires a live API key.

Request body

collection_strategyenum
  • upfront
  • incremental
componentsarray of enumRequired
  • account_onboarding
  • account_management
  • payouts
  • balances
  • tax_documents
  • notification_banner
future_requirementsenum
  • omit
  • include
sandbox_idstring
targeted_requirement_idsarray of string

Response · 201

dataobjectRequired
metaobject
request_idstring
curl -X POST https://api.withflintpay.com/v1/merchant-account-sessions \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "collection_strategy": "upfront",
    "components": [
      "account_onboarding"
    ],
    "future_requirements": "include"
  }'

Refresh merchant account session#

POST/v1/merchant-account-sessions/refreshIdempotent

Requires scope merchants.account_sessions.write

Creates a fresh provider session from a signed launch token after rechecking the authenticated principal, merchant environment, account controller, and component grant. An onboarding session can refresh only sandbox launch tokens created by the same user with an onboarding session. The sandbox is pinned by the launch token, using the merchant's default sandbox unless sandbox_id named another sandbox of the same merchant at creation. Live onboarding requires a live API key, and refresh requires the same API key that created the launch token.

Request body

launch_tokenstringRequired

Response · 201

Same response as Create merchant account session.

curl -X POST https://api.withflintpay.com/v1/merchant-account-sessions/refresh \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "launch_token": "aslaunch_v1.example"
  }'

Was this helpful?