Skip to navigation

Embedded Onboarding Integration Guide

Create a sub-merchant, mint an embed session from your backend, embed Coinflow’s onboarding in an iframe, follow progress and handle information requests.

This guide walks through a complete embedded onboarding integration, in the order you’ll build it. Complete Partner Setup first, and read the security rules and rules for handling future changes before you start.

The examples use sandbox URLs. In production, use the production equivalents:

SandboxProduction
APIhttps://api-sandbox.coinflow.cash/apihttps://api.coinflow.cash/api
Onboarding pageshttps://sandbox-merchant.coinflow.cashhttps://merchant.coinflow.cash

Values such as acme-store, owner@acme.example.com and https://app.example.com are examples only. Replace them with your own. The curl examples read your API key from the COINFLOW_ADMIN_API_KEY or COINFLOW_API_KEY environment variable.

1

Create and pre-fill a sub-merchant

Call Create Submerchant from your backend when a business signs up for payments on your platform.

Request
curl --request POST \
--url https://api-sandbox.coinflow.cash/api/submerchant \
--header "Authorization: $COINFLOW_ADMIN_API_KEY" \
--header 'content-type: application/json' \
--data '{
"merchantId": "acme-store",
"email": "owner@acme.example.com",
"dba": "Acme Store",
"industry": "retailEcommerce",
"businessEmail": "support@acme.example.com",
"billingEmailSameAsBusinessEmail": true,
"websiteUrls": ["https://acme.example.com"],
"privacyPolicyUrl": "https://acme.example.com/privacy",
"termsOfServiceUrl": "https://acme.example.com/terms",
"payinMethods": "card,applePay"
}'
Response
{
"merchantId": "acme-store",
"email": "owner@acme.example.com",
"dba": "Acme Store",
"industry": "retailEcommerce",
"businessEmail": "support@acme.example.com",
"billingEmail": "support@acme.example.com",
"websiteUrl": "https://acme.example.com",
"privacyPolicyUrl": "https://acme.example.com/privacy",
"termsOfServiceUrl": "https://acme.example.com/terms",
"payinMethods": "card,applePay",
"products": "checkout"
}
  • merchantId is your identifier for the sub-merchant: 3 to 50 letters, numbers and hyphens, unique across Coinflow. It’s stored in lowercase, so on every later call for this sub-merchant, send the merchantId returned in the response as the x-coinflow-submerchant-id header. Later lookups match the exact case.
  • email becomes the sub-merchant’s account owner. Use a real inbox the business can open: Coinflow emails the account owner, for example a one-time code during settlement setup.
  • Every other field is optional. Anything you send is shown to the sub-merchant pre-filled, and they can change it. Some fields don’t apply to every sub-merchant; those are accepted and kept but not shown.
  • Don’t send the x-coinflow-submerchant-id header on this call. It fails with a 400.

Pre-fill fields

FieldFormat
dbaDoing-business-as name, or the legal name if there is none
industryOne of the values listed on Create Submerchant
businessEmailEmail address
billingEmailEmail address. Leave out when billingEmailSameAsBusinessEmail is true
billingEmailSameAsBusinessEmailtrue to use businessEmail as the billing email. Requires businessEmail
businessPhoneNumberAt least 4 digits
businessPhoneCountryCodeFor example +1
websiteUrls1 to 5 live website URLs
developmentUrlsUp to 5 staging or development URLs
privacyPolicyUrl, termsOfServiceUrl, returnPolicyUrlURLs
payinMethodsComma-separated pay-in method keys, for example card,applePay,ach
payoutMethodsComma-separated payout method keys, for example standard,card
endUserJurisdictionsComma-separated regions the business’s customers are in, for example US,EU,UK

Unknown fields are rejected with a 400. The response returns the URLs as separate fields (websiteUrl, websiteUrl2, …) and adds products, derived from the pay-in and payout methods you sent.

Updating pre-filled details

Call Update Submerchant with only the fields you want to change:

Request
curl --request PATCH \
--url https://api-sandbox.coinflow.cash/api/submerchant/acme-store \
--header "Authorization: $COINFLOW_ADMIN_API_KEY" \
--header 'content-type: application/json' \
--data '{"businessPhoneNumber": "4155550142", "businessPhoneCountryCode": "+1"}'
  • Fields you leave out keep their current values. websiteUrls and developmentUrls replace the whole list.
  • Once the sub-merchant submits its application, updates fail with a 409.

Sub-merchant creation is limited to 30 per hour per API key. Past that, requests fail with a 429 and "error": "CREATE_SUBMERCHANT_RATE_LIMITED".

2

Mint an embed session from your backend

When a signed-in user opens onboarding in your product, your backend mints an embed session for their sub-merchant with Create Embed Session and returns the token to your page.

Request
curl --request POST \
--url https://api-sandbox.coinflow.cash/api/embed/session \
--header "Authorization: $COINFLOW_ADMIN_API_KEY" \
--header 'x-coinflow-submerchant-id: acme-store' \
--header 'content-type: application/json' \
--data '{"parentOrigin": "https://app.example.com"}'
Response
{
"token": "EMBED_SESSION_TOKEN",
"expiresAt": 1767225600
}
  • parentOrigin is the origin of the page that frames the iframe. It must be covered by an approved Whitelist URL. The iframe only posts events to this origin.
  • expiresAt is in Unix seconds.

A minimal token endpoint in Node.js with Express:

Server
app.post('/coinflow/onboarding-session', requireSignedInUser, async (req, res) => {
// Look up the sub-merchant from your own session, never from the request.
const submerchantId = await getSubmerchantIdForUser(req.user.id);
const response = await fetch(
'https://api-sandbox.coinflow.cash/api/embed/session',
{
method: 'POST',
headers: {
Authorization: process.env.COINFLOW_ADMIN_API_KEY,
'x-coinflow-submerchant-id': submerchantId,
'content-type': 'application/json',
},
body: JSON.stringify({parentOrigin: 'https://app.example.com'}),
}
);
if (!response.ok) return res.sendStatus(502);
const {token} = await response.json();
res.set('Cache-Control', 'no-store').json({token});
});

Session lifetime and renewal

  • Each token is valid for 15 minutes. The iframe renews its own token about 2 minutes before it expires, so you mint once per visit.
  • A session can’t be renewed past 8 hours after it was minted. After that the iframe posts an onboarding.error event with code session_expired; mint a new token and reload the iframe.
  • A session stops working immediately if the sub-merchant is moved out of your account, either account is suspended, or the API key that minted it is disabled, deleted or downgraded below Admin.

If you call embed-session APIs directly, renew a token with Refresh Embed Session.

3

Embed the onboarding iframe

Load the onboarding page with the token in the URL fragment:

<iframe
id="coinflow-onboarding"
src="https://sandbox-merchant.coinflow.cash/embed/onboarding#token=EMBED_SESSION_TOKEN"
title="Coinflow onboarding"
allow="camera; microphone; clipboard-write"
style="width: 100%; border: 0;"
></iframe>
  • The allow attribute is required. Identity verification runs in a frame inside the iframe and uses the camera to capture ID documents and a selfie. A nested frame only gets the permissions every frame above it grants, so without camera the sub-merchant can’t verify. Include microphone as well, since verification may request it. clipboard-write powers the Copy link buttons used to share verification links with business owners.
  • Use the fragment, not a query string. The fragment is never sent to a server or leaked in Referer headers, and the iframe removes it from its address bar as soon as it loads.
  • Size it for one column. Onboarding shows one step at a time and works down to about 320px wide. Set the iframe’s height from onboarding.heightChanged events (next step) so it never shows its own scrollbar.
  • Don’t sandbox it unless the sandbox attribute includes allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox. Documents and terms open in a new tab, and without allow-popups-to-escape-sandbox that tab inherits the sandbox and can’t display them.

The iframe only renders on approved sites. On any other site, or when the token is missing or invalid, it shows a “Not allowed” message instead.

4

Follow progress

You have three ways to follow a sub-merchant’s progress. Most integrations use all three.

SourceUse it to
Embed eventsResize the iframe and react in your page while the sub-merchant is in it
Onboarding webhooksUpdate your records on your server when the application is submitted, approved or declined, or an RFI is sent
Get OnboardingRead the current status and steps at any time, for example to show a badge in your product

Listen for embed events from your page:

Browser
const COINFLOW_ORIGIN = 'https://sandbox-merchant.coinflow.cash';
const iframe = document.getElementById('coinflow-onboarding') as HTMLIFrameElement;
window.addEventListener('message', event => {
if (event.origin !== COINFLOW_ORIGIN) return;
if (event.source !== iframe.contentWindow) return;
const message = event.data;
if (message?.source !== 'coinflow' || message.version !== 1) return;
switch (message.type) {
case 'onboarding.heightChanged':
iframe.style.height = `${message.payload.height}px`;
break;
case 'onboarding.stepChanged':
iframe.scrollIntoView({block: 'start'});
break;
case 'onboarding.submitted':
showSubmittedBanner();
break;
case 'onboarding.error':
if (message.payload.code === 'session_expired') reloadWithNewSession();
break;
}
});

Read the current status from your backend:

Request
curl --request GET \
--url https://api-sandbox.coinflow.cash/api/onboarding \
--header "Authorization: $COINFLOW_API_KEY" \
--header 'x-coinflow-submerchant-id: acme-store'
Response
{
"merchantId": "acme-store",
"status": "in_progress",
"requirementsStatus": "determined",
"requirementsRevision": 2,
"openRfiCount": 0,
"steps": [
{"key": "profile", "status": "complete", "dependsOn": [], "blockedBy": [], "completionMethods": ["embed"]},
{"key": "corporate_structure", "status": "complete", "dependsOn": ["profile"], "blockedBy": [], "completionMethods": ["embed"]},
{"key": "verification", "status": "action_required", "dependsOn": ["profile", "corporate_structure"], "blockedBy": [], "completionMethods": ["embed"]},
{"key": "application", "status": "blocked", "dependsOn": ["profile", "corporate_structure", "verification"], "blockedBy": ["verification"], "completionMethods": ["embed"]}
]
}

Onboarding statuses

statusMeaning
not_startedThe sub-merchant has no saved details yet
in_progressDetails are saved but the application isn’t submitted. A sub-merchant you pre-filled starts here
submittedThe application is in review and nothing is waiting on the sub-merchant
action_requiredThe application is submitted, but the sub-merchant has an open information request or a step to finish
approvedThe sub-merchant is approved and live
declinedThe application was declined

Steps

steps lists only the steps that apply to this sub-merchant, in order. Each step has a status of blocked (waiting on the steps in blockedBy), action_required, in_review or complete.

keyWhat the sub-merchant does
profileConfirms its business details
corporate_structureLists its owners and controllers
verificationVerifies the business and its owners’ identities
questionsAnswers underwriting questions
termsAccepts your platform’s terms, when you have any
applicationReviews and submits the application
walletChooses where its funds settle (see Settlement step)

While requirementsStatus is pending, more steps may be added once the sub-merchant completes its profile. requirementsRevision increases each time the list of steps is recalculated.

5

Handle information requests

After the sub-merchant applies, or at any time after it’s live, Coinflow’s underwriting team may send it a request for information (RFI). Coinflow doesn’t email the sub-merchant about these, so your platform must show them.

  1. Listen for the Sub-merchant RFI Sent webhook, or call List RFIs with actionRequired=true.
  2. Mint an embed session for the sub-merchant, exactly as in step 2.
  3. Open the RFI in an iframe at /embed/rfi/{rfiId}#token=EMBED_SESSION_TOKEN.

See Information Requests for the details.

Settlement step

When settlement options are enabled for your sub-merchants (see Partner Setup), onboarding includes a wallet step where the sub-merchant chooses where its funds settle. It only offers the options enabled for that sub-merchant.

  • The step never blocks submitting the application. After submitting, the iframe opens the first step still needing action, usually this one, and the overall status stays action_required until it’s complete.
  • For a Coinflow-managed settlement account, Coinflow emails a one-time code to the account owner (the email you created the sub-merchant with) and the sub-merchant approves the account in the iframe. If they leave part way through, the step offers to finish setup next time.
  • An external settlement destination can’t be changed after it’s set.

Identity verification

  • Verification runs inside the iframe. If the sub-merchant’s device has no camera or it’s blocked, they may be offered the option to continue verification on their phone and come back.
  • Verification is decided after the sub-merchant finishes. The step shows that verification was submitted, and they can carry on with the other steps. The application can be submitted once verification is approved.
  • Verification links for other business owners can be copied from the iframe and shared with them.

If you’d rather not host an iframe, you can send the sub-merchant to the same pages as a full page hosted by Coinflow:

https://sandbox-merchant.coinflow.cash/embed/onboarding#token=EMBED_SESSION_TOKEN
https://sandbox-merchant.coinflow.cash/embed/rfi/{rfiId}#token=EMBED_SESSION_TOKEN
  • Mint the session when the user clicks through, and redirect straight to the link. It’s the same 15-minute session as the iframe, so don’t email or store the link. Reloading the page needs a fresh link.

  • Omit parentOrigin to mint a link-only session. A link-only session refuses to load inside any iframe. A session minted with parentOrigin works both ways.

  • Pass returnUrl to show a Return to your platform button after the sub-merchant submits. Its origin must be covered by an approved Whitelist URL.

    {"returnUrl": "https://app.example.com/onboarding/done"}

    With neither parentOrigin nor returnUrl, minting doesn’t need any Whitelist URL.

  • No embed events are posted when there’s no parent page. Follow progress with webhooks and Get Onboarding.

Testing in sandbox

  1. Complete Partner Setup in the sandbox dashboard, approving your local or staging origin.
  2. Create a sub-merchant in sandbox, mint a session and load the iframe from the approved origin.
  3. Point a sandbox webhook URL at your server to receive onboarding events.

Troubleshooting

parentOrigin (or returnUrl) isn’t covered by an approved Whitelist URL, or your account has none. Check the exact origin, including scheme and port, and that the request was approved. See Partner Setup.

The request wasn’t made with an Admin API key, or x-coinflow-submerchant-id names a merchant that isn’t one of your sub-merchants.

A page framing the iframe isn’t on an approved origin. Every page in the frame chain must be covered by a Whitelist URL. A link-only session (minted without parentOrigin) shows “This link can’t be opened inside another site” when framed instead.

Events are only posted to the parentOrigin the session was minted with. Check that it exactly matches your page’s origin, including scheme and port, and that your listener compares event.origin with the Coinflow onboarding origin for the environment.

The iframe is missing allow="camera; microphone; clipboard-write", or a frame above it doesn’t grant those permissions.

The sub-merchant has already submitted its application. Pre-filled details can’t be changed after that.