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.

Bash
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.

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

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

Bash
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.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:

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={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/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.

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 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.

Create merchant account session#

POST/v1/merchant-account-sessionsIdempotentRequires scope: merchants.account_sessions.write

Creates an embedded browser handoff for one or more allowlisted account components.

Request body
collection_strategyenum
upfrontincremental
componentsarray of enumrequired
account_onboardingaccount_managementpayoutsbalancestax_documentsnotification_banner
future_requirementsenum
omitinclude
sandbox_idstring
targeted_requirement_idsarray of string
Response · 201
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINTERNAL_ERRORINVALID_API_KEYINVALID_REQUESTMERCHANT_ACCOUNT_SESSION_ACCOUNT_SETUP_REPAIR_REQUIREDMERCHANT_ACCOUNT_SESSION_ACCOUNT_SETUP_REQUIREDMERCHANT_ACCOUNT_SESSION_ACCOUNT_SETUP_UNAVAILABLEMERCHANT_ACCOUNT_SESSION_INELIGIBLEMERCHANT_ACCOUNT_SESSION_INVALID_COMPONENTMERCHANT_ACCOUNT_SESSION_INVALID_LAUNCH_TOKENMERCHANT_ACCOUNT_SESSION_INVALID_POLICY_COMBINATIONMERCHANT_ACCOUNT_SESSION_LAUNCH_TOKEN_SCOPE_MISMATCHMERCHANT_ACCOUNT_SESSION_POLICY_NOT_SUPPORTED_BY_COMPONENTMERCHANT_ACCOUNT_SESSION_PREPARATION_FAILEDMERCHANT_ACCOUNT_SESSION_TARGETED_REMEDIATION_UNAVAILABLERATE_LIMIT_EXCEEDEDSERVICE_UNAVAILABLE
Bash
curl -X POST https://api.withflintpay.com/v1/merchant-account-sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "components": [
      "account_onboarding"
    ],
    "collection_strategy": "upfront",
    "future_requirements": "include"
  }'
JSON
{
  "data": {
    "components": [
      "account_onboarding"
    ],
    "effective_policy": {
      "collection_strategy": "upfront",
      "future_requirements": "include",
      "targeting": "all"
    },
    "external_action": {
      "kind": "embedded",
      "provider_session_expires_at": "2026-03-18T20:00:00Z",
      "launch_token": "aslaunch_v1.example",
      "launch_token_expires_at": "2026-03-18T20:30:00Z",
      "stripe": {
        "client_secret": "acct_session_client_secret_123",
        "publishable_key": "pk_test_123",
        "components": [
          {
            "component": "account_onboarding",
            "component_props": {
              "collectionOptions": {
                "fields": "eventually_due",
                "futureRequirements": "include"
              }
            }
          }
        ]
      }
    },
    "requirements": {
      "currently_due": [
        "merchant_category_code",
        "business_website"
      ],
      "eventually_due": [
        "representative_first_name"
      ],
      "current_deadline_at": "2026-04-01T00:00:00Z"
    }
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Refresh merchant account session#

POST/v1/merchant-account-sessions/refreshIdempotentRequires 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.

Request body
launch_tokenstringrequired
Response · 201
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINTERNAL_ERRORINVALID_API_KEYINVALID_REQUESTMERCHANT_ACCOUNT_SESSION_ACCOUNT_SETUP_REPAIR_REQUIREDMERCHANT_ACCOUNT_SESSION_ACCOUNT_SETUP_REQUIREDMERCHANT_ACCOUNT_SESSION_ACCOUNT_SETUP_UNAVAILABLEMERCHANT_ACCOUNT_SESSION_INELIGIBLEMERCHANT_ACCOUNT_SESSION_INVALID_COMPONENTMERCHANT_ACCOUNT_SESSION_INVALID_LAUNCH_TOKENMERCHANT_ACCOUNT_SESSION_INVALID_POLICY_COMBINATIONMERCHANT_ACCOUNT_SESSION_LAUNCH_TOKEN_SCOPE_MISMATCHMERCHANT_ACCOUNT_SESSION_POLICY_NOT_SUPPORTED_BY_COMPONENTMERCHANT_ACCOUNT_SESSION_PREPARATION_FAILEDMERCHANT_ACCOUNT_SESSION_TARGETED_REMEDIATION_UNAVAILABLERATE_LIMIT_EXCEEDEDSERVICE_UNAVAILABLE
Bash
curl -X POST https://api.withflintpay.com/v1/merchant-account-sessions/refresh \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "launch_token": "aslaunch_v1.example"
  }'
JSON
{
  "data": {
    "components": [
      "account_onboarding"
    ],
    "effective_policy": {
      "collection_strategy": "upfront",
      "future_requirements": "include",
      "targeting": "all"
    },
    "external_action": {
      "kind": "embedded",
      "provider_session_expires_at": "2026-03-18T20:00:00Z",
      "launch_token": "aslaunch_v1.example",
      "launch_token_expires_at": "2026-03-18T20:30:00Z",
      "stripe": {
        "client_secret": "acct_session_client_secret_123",
        "publishable_key": "pk_test_123",
        "components": [
          {
            "component": "account_onboarding",
            "component_props": {
              "collectionOptions": {
                "fields": "eventually_due",
                "futureRequirements": "include"
              }
            }
          }
        ]
      }
    },
    "requirements": {
      "currently_due": [
        "merchant_category_code",
        "business_website"
      ],
      "eventually_due": [
        "representative_first_name"
      ],
      "current_deadline_at": "2026-04-01T00:00:00Z"
    }
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}
Rate this doc