Account setup & onboarding
Walk through sign-up, company creation, and KYB so your seller account reaches operational mode live and can accept real payments.
Before you can accept live payments you must complete three sequential phases: sign-up (a short REST wizard that ends with a Demo account), optionally additional company entities (a GraphQL chain), and KYB (identity verification via our KYB provider). At the end of KYB approval your account's operationalMode flips from Demo to Live.
Note
This guide mixes REST calls and GraphQL mutations. REST calls hit
https://api.fynex.aiand use no auth header — they rely on an HttpOnly session cookie. GraphQL calls hithttps://api.fynex.ai/dashboard/graphqland also use the cookie. See GraphQL Authentication for a standalone reference.
Overview
POST /onboarding/complete creates the organization, legal entity, organization profile and a
Demo seller account, and provisions the sandbox.
Phase 1 — Sign-up (draft → verified → complete)
Sign-up is a short, resumable wizard over REST. Four calls, all on
https://api.fynex.ai, all cookie-authenticated after the first:
| # | Call | What it does |
|---|---|---|
| 1 | POST /api/v1/onboarding/start |
Creates a draft user from email + password, sets the HttpOnly dashboard_session cookie, and e-mails a verification code. No auth header. |
| 2 | PATCH /api/v1/onboarding/step |
Saves one wizard step: { "step": "<name>", "data": { … } }. Steps must be submitted in order; a step may be re-submitted to edit it. |
| 3 | POST /api/v1/verify-email |
Consumes the e-mailed code: { "code": "123456" }. POST /api/v1/verify-email/resend sends a fresh one. |
| 4 | POST /api/v1/onboarding/complete |
Turns the draft into a real account: organization, legal entity, organization profile and a Demo seller account, pre-provisioned as a sandbox. Refused with 403 emailNotVerified until step 3 is done. |
Note
startalready sets the session cookie — there is no separate sign-in step during sign-up.POST /api/v1/login/dashboardis for returning sessions.
Step names and what each collects
The step sequence depends on legalEntityType, chosen in the first step:
The last step of each track (business_details, individual_details) is where
complete is called from; KYB is not part of sign-up.
Every data object is a partial of one form; send only the fields the step
collects. The fields are camelCase and match the wizard: legalEntityType
(Company or Individual), firstName, lastName, phone, role,
businessName, industry, businessModel, country (ISO 3166-1 alpha-2),
businessWebsite, registrationNumber, registeredAddress,
estimatedMonthlyVolume, averageTransactionSize, employeeCount,
paymentFlowsDescription.
country is screened against the supported jurisdictions when it is submitted;
an unsupported country answers 422 countryNotSupported.
Required per step (a missing or out-of-range value answers 400 naming the field):
| Step | Required |
|---|---|
personal_details |
firstName, lastName |
company_details |
businessName, country |
business_details |
businessModel (marketplace, mor_marketplace, creator_platform, saas, service_aggregator, investment_platform, crowdfunding, professional_services, home_services, other), estimatedMonthlyVolume (no_revenue_yet, under_10k, 10k_50k, 50k_250k, 250k_1m, over_1m — USD per month), averageTransactionSize (not_known_yet, under_50, 50_200, 200_1k, over_1k — USD), employeeCount (just_me, 1_10, 11_50, 51_200, 201_1000, over_1000). The first value of each is for a business that has not started trading |
individual_details |
country |
curl
# 1 — start the draft; -c saves the session cookie, -b sends it on later calls
curl -sS -c cookies.txt -X POST https://api.fynex.ai/api/v1/onboarding/start \
-H "Content-Type: application/json" \
-d '{"email": "alice@example.com", "password": "s3cur3-P@ssw0rd"}'
# 2 — the wizard steps, in order
curl -sS -b cookies.txt -X PATCH https://api.fynex.ai/api/v1/onboarding/step \
-H "Content-Type: application/json" \
-d '{"step": "legal_entity_type", "data": {"legalEntityType": "Company"}}'
curl -sS -b cookies.txt -X PATCH https://api.fynex.ai/api/v1/onboarding/step \
-H "Content-Type: application/json" \
-d '{"step": "personal_details", "data": {"firstName": "Alice", "lastName": "Smith", "phone": "+447700900000", "role": "owner"}}'
curl -sS -b cookies.txt -X PATCH https://api.fynex.ai/api/v1/onboarding/step \
-H "Content-Type: application/json" \
-d '{"step": "company_details", "data": {"businessName": "Acme Ltd", "country": "GB", "businessWebsite": "https://acme.example", "registeredAddress": "1 High St, London", "registrationNumber": "12345678"}}'
curl -sS -b cookies.txt -X PATCH https://api.fynex.ai/api/v1/onboarding/step \
-H "Content-Type: application/json" \
-d '{"step": "business_details", "data": {"businessModel": "saas", "estimatedMonthlyVolume": "10k_50k", "averageTransactionSize": "50_200", "employeeCount": "1_10"}}'
# 3 — the code from the verification e-mail
curl -sS -b cookies.txt -X POST https://api.fynex.ai/api/v1/verify-email \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
# 4 — complete: organization, legal entity, profile and a Demo seller account
curl -sS -b cookies.txt -X POST https://api.fynex.ai/api/v1/onboarding/complete
# Response: { "userId": "42", "organizationUuid": "…", "legalEntityIds": [77],
# "organizationProfileIds": [12], "permissionTypes": [...] }
What complete leaves you with is a working Demo account: the sandbox
auto-provisioning that runs on completion adds wallets, a demo card terminal, a
demo counterparty payee, a pre-set split rule and three test customers, and
issues the account's first API key pair (retrieve the secret from
Integration → API keys in the dashboard — see Getting your API token).
Nothing here moves real money; Phase 3 is what takes the account live.
Phase 2 — Company creation (GraphQL chain)
Important
Most integrations don't need this phase.
POST /api/v1/onboarding/complete(Phase 1) already creates the organization, legal entity, and organization profile for you in a single transaction — its response returnsorganizationUuid,legalEntityIdsandorganizationProfileIds. The mutations below are only for advanced cases where you manage additional legal entities or profiles under one account. To get an API token, you don't run these — see Getting your API token below.
Company creation runs as sequential GraphQL mutations on /dashboard/graphql. Each step depends on the identifier returned by the previous one. The organization is addressed by its UUID (organizationUuid from complete); the legal entity by its numeric legalEntityId.
createOrganizationProfile takes both organizationUuid and legalEntityId.
Caution
Partial rollback: if
createOrganizationProfilefails after the first two mutations have succeeded, you are left with an orphanedOrganizationandLegalEntity. There is no automatic rollback. Contact your Fynex representative to clean up and retry.
Helper: send a GraphQL mutation
All three calls below share the same shape. Use this wrapper or your preferred GraphQL client.
curl (reusable)
gql() {
curl -sS -b cookies.txt \
-X POST https://api.fynex.ai/dashboard/graphql \
-H "Content-Type: application/json" \
-d "$1"
}
JavaScript (reusable)
async function gql(query, variables = {}) {
const res = await fetch('https://api.fynex.ai/dashboard/graphql', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const body = await res.json();
if (body.errors?.length) throw new Error(body.errors[0].message);
return body.data;
}
Python (reusable)
def gql(session, query, variables=None):
resp = session.post(
'https://api.fynex.ai/dashboard/graphql',
json={'query': query, 'variables': variables or {}},
)
resp.raise_for_status()
body = resp.json()
if 'errors' in body:
raise RuntimeError(body['errors'][0]['message'])
return body['data']
Step 1 — Create an organization
Caution
createOrganizationis not exposed on/dashboard/graphql. The mutation exists on the Fynex staff surface only; a dashboard session calling it receivesCannot query field "createOrganization" on type "Mutation". Your organization is created byPOST /api/v1/onboarding/complete(Phase 1), which returns itsorganizationUuid. To add an additional organization under the same account, ask your Fynex representative for it and continue from Step 2 with theorganizationUuidthey give you.
For reference, the operation as it exists on the staff surface — it takes a single
displayName and returns the organizationUuid the next two mutations need:
# Not available on /dashboard/graphql — staff surface only. Shown for reference.
mutation CreateOrganization($input: CreateOrganizationInput!) {
createOrganization(input: $input) {
organizationUuid
displayName
}
}
Input: CreateOrganizationInput
| Field | Type | Required | Description |
|---|---|---|---|
displayName |
String | Yes | Your company trading name as it appears in Fynex. |
The samples below address the organization two ways. Prefer the …ByUuid mutations,
which take the organizationUuid that complete returns:
mutation CreateLegalEntityByUuid($input: CreateLegalEntityByUuidInput!) {
createLegalEntityByUuid(input: $input) {
id
name
status
}
}
mutation CreateOrganizationProfileByUuid($input: CreateOrganizationProfileByUuidInput!) {
createOrganizationProfileByUuid(input: $input) {
id
statementDescriptor
}
}
The legacy createLegalEntity and createOrganizationProfile mutations were removed.
Use the UUID-only …ByUuid mutations above and pass the organizationUuid returned by
onboarding (or supplied by your Fynex representative).
Step 2 — Create a legal entity
createLegalEntity registers the incorporated company behind the organization. Requires the organizationUuid from step 1.
mutation CreateLegalEntity($input: CreateLegalEntityByUuidInput!) {
createLegalEntityByUuid(input: $input) {
id
name
legalName
status
}
}
Input: CreateLegalEntityByUuidInput
| Field | Type | Required | Description |
|---|---|---|---|
organizationUuid |
ID | Yes | UUID returned by createOrganization. |
name |
String | Yes | Short display name for the legal entity. |
type |
LegalEntityType | Yes | Corporate form, e.g. limited_company, sole_trader. |
legalName |
String | Yes | Full registered legal name. |
registrationCountry |
CountryCode | Yes | ISO 3166-1 alpha-2, e.g. GB. |
registrationNumber |
String | Yes | Companies House / state registration number. |
incorporationDate |
Time | No | RFC 3339 date, e.g. "2019-03-15T00:00:00Z". |
globalTaxId |
String | No | VAT / EIN / tax identification number. |
curl
gql "{
\"query\": \"mutation (\$input: CreateLegalEntityByUuidInput!) { createLegalEntityByUuid(input: \$input) { id name status } }\",
\"variables\": {
\"input\": {
\"organizationUuid\": \"$ORG_UUID\",
\"name\": \"Acme Ltd\",
\"type\": \"limited_company\",
\"legalName\": \"Acme Limited\",
\"registrationCountry\": \"GB\",
\"registrationNumber\": \"12345678\",
\"incorporationDate\": \"2019-03-15T00:00:00Z\"
}
}
}"
# Response: { "data": { "createLegalEntity": { "id": 3, "name": "Acme Ltd", "status": "pending" } } }
LEGAL_ENTITY_ID=3
JavaScript
const { createLegalEntityByUuid } = await gql(
`mutation ($input: CreateLegalEntityByUuidInput!) {
createLegalEntityByUuid(input: $input) { id name status }
}`,
{
input: {
organizationUuid,
name: 'Acme Ltd',
type: 'limited_company',
legalName: 'Acme Limited',
registrationCountry: 'GB',
registrationNumber: '12345678',
incorporationDate: '2019-03-15T00:00:00Z',
},
}
);
const legalEntityId = createLegalEntityByUuid.id; // e.g. 3
Python
data = gql(session, '''
mutation($input: CreateLegalEntityByUuidInput!) {
createLegalEntityByUuid(input: $input) { id name status }
}
''', {
'input': {
'organizationUuid': organization_uuid,
'name': 'Acme Ltd',
'type': 'limited_company',
'legalName': 'Acme Limited',
'registrationCountry': 'GB',
'registrationNumber': '12345678',
'incorporationDate': '2019-03-15T00:00:00Z',
}
})
legal_entity_id = data['createLegalEntityByUuid']['id'] # e.g. 3
Step 3 — Create an organization profile
createOrganizationProfile links the legal entity to the organization and sets operational parameters. This completes the company-creation chain.
mutation CreateOrganizationProfile($input: CreateOrganizationProfileByUuidInput!) {
createOrganizationProfileByUuid(input: $input) {
id
countryCode
statementDescriptor
timezone
}
}
Input: CreateOrganizationProfileByUuidInput
| Field | Type | Required | Description |
|---|---|---|---|
organizationUuid |
ID | Yes | From step 1. |
legalEntityId |
Int | Yes | From step 2. |
countryCode |
CountryCode | Yes | Country of primary operation, e.g. GB. |
statementDescriptor |
String | Yes | Text that appears on the customer's bank statement (max ~22 chars). |
timezone |
String | Yes | IANA timezone, e.g. Europe/London. The dashboard defaults to the browser's Intl.DateTimeFormat().resolvedOptions().timeZone. |
industry |
String | No | Business industry / MCC category. |
platformRoleId |
Int | No | Assigned by Fynex — leave unset unless instructed. |
curl
gql "{
\"query\": \"mutation (\$input: CreateOrganizationProfileByUuidInput!) { createOrganizationProfileByUuid(input: \$input) { id statementDescriptor } }\",
\"variables\": {
\"input\": {
\"organizationUuid\": \"$ORG_UUID\",
\"legalEntityId\": $LEGAL_ENTITY_ID,
\"countryCode\": \"GB\",
\"statementDescriptor\": \"ACME LTD\",
\"timezone\": \"Europe/London\"
}
}
}"
JavaScript
const { createOrganizationProfileByUuid } = await gql(
`mutation ($input: CreateOrganizationProfileByUuidInput!) {
createOrganizationProfileByUuid(input: $input) { id statementDescriptor }
}`,
{
input: {
organizationUuid,
legalEntityId,
countryCode: 'GB',
statementDescriptor: 'ACME LTD',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
}
);
Python
import datetime
data = gql(session, '''
mutation($input: CreateOrganizationProfileByUuidInput!) {
createOrganizationProfileByUuid(input: $input) { id statementDescriptor }
}
''', {
'input': {
'organizationUuid': organization_uuid,
'legalEntityId': legal_entity_id,
'countryCode': 'GB',
'statementDescriptor': 'ACME LTD',
'timezone': 'Europe/London',
}
})
profile_id = data['createOrganizationProfile']['id']
Getting your API token
Your API bearer token belongs to a seller account. Create one with the createSellerAccount
mutation — it mints and returns the token immediately. You need a legalEntityId and an
organizationProfileId (both came back in your login response), plus a name and currency.
curl -sS -b cookies.txt -X POST https://api.fynex.ai/dashboard/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation Create($input: SellerAccountInput!) { createSellerAccount(input: $input) { id name status operationalMode authorizationToken } }",
"variables": { "input": { "legalEntityId": 3, "organizationProfileId": 5, "name": "Acme Ltd", "currency": "GBP" } }
}'
# data.createSellerAccount.authorizationToken is your /payments-api/v1 Bearer token
The dashboard equivalent is the Integration page (reveal / copy, or Generate Token). To
rotate, call regenerateSellerAccountToken(id:) or use the dashboard's Regenerate button.
Important
A new seller account is created in
Demomode and activated automatically, so the token works immediately for sandbox payments. KYB approval is what flips the account toLivefor real-money processing (with a Fynex-assigned live terminal). If a sandbox call returns403 seller account is not active, the token is still valid — the account just isn't active yet. See Authentication.
Phase 3 — KYB (Know Your Business)
Fynex uses a third-party KYB provider. Your integration obtains a short-lived KYB access token from Fynex, embeds the provider's Web SDK in your UI, and lets the customer fill the verification form. Fynex staff then review the submission and approve the account.
All three KYB endpoints require the dashboard_session cookie.
| Endpoint | Purpose |
|---|---|
POST /api/v1/kyb/initiate |
Start a KYB application. Returns { token, status }. |
GET /api/v1/kyb/token |
Refresh an existing KYB access token (tokens expire). |
GET /api/v1/kyb/status |
Poll the current KYB state. |
- Initiate KYB and obtain the KYB access token
curl -sS -b cookies.txt \
-X POST https://api.fynex.ai/api/v1/kyb/initiate \
-H "Content-Type: application/json"
# Response: { "token": "_act-...", "status": "pending" }
- Embed the KYB provider's Web SDK in your UI
Pass the token to the provider's Web SDK to render the verification form. When the token expires (these tokens are short-lived), refresh it:
// When the SDK calls your token refresh handler:
const refreshRes = await fetch('https://api.fynex.ai/api/v1/kyb/token', {
credentials: 'include',
});
const { token: newToken } = await refreshRes.json();
return newToken; // return to the SDK
A minimal SDK integration:
<div id="sumsub-websdk-container"></div>
<script src="https://static.sumsub.com/idensic/static/sns-websdk-builder.js"></script>
<script>
const snsWebSdkInstance = snsWebSdk
.init(KYB_TOKEN, () =>
fetch('/api/kyb/token', { credentials: 'include' })
.then(r => r.json())
.then(d => d.token)
)
.withConf({ lang: 'en' })
.withOptions({ addViewportTag: false, adaptIframeHeight: true })
.on('idCheck.onApplicantSubmitted', () => console.log('Submitted'))
.build();
snsWebSdkInstance.launch('#sumsub-websdk-container');
</script>
- Poll KYB status
After the customer submits the form, Fynex staff review the application. Poll /api/v1/kyb/status until the status is terminal.
const statusRes = await fetch('https://api.fynex.ai/api/v1/kyb/status', {
credentials: 'include',
});
const { status } = await statusRes.json();
// status values: "init" | "pending" | "approved" | "rejected"
When approved, your SellerAccount.operationalMode flips from Demo to Live.
Caution
KYB token expiry. KYB access tokens are short-lived. If you get a
401orinvalid tokenerror from the SDK, callGET /api/v1/kyb/tokento refresh. Pass the refresh function directly to the SDK's token-provider callback so it can renew tokens automatically without user interaction.
Common pitfalls
| Pitfall | Fix |
|---|---|
complete answers 403 emailNotVerified |
Consume the e-mailed code with POST /api/v1/verify-email first; POST /api/v1/verify-email/resend sends a fresh one. |
step answers 409 invalidDraftState |
Steps must be submitted in order; you skipped one, or the draft is already completed. |
createOrganizationProfile fails mid-chain |
No automatic rollback. Contact your Fynex representative to clean up orphaned records before retrying. |
| KYB token expired | Call GET /api/v1/kyb/token and pass the refreshed token to the SDK. |
Payments still in Demo mode after approval |
Check kybStatus via GraphQL; if approved, allow a few minutes for the operationalMode flip. If it persists, contact support. |
timezone missing from createOrganizationProfile |
Required field. Use Intl.DateTimeFormat().resolvedOptions().timeZone in a browser or an IANA string server-side. |
See also
- GraphQL Authentication — Full reference for session cookies and the /dashboard/graphql endpoint.
- Going Live — Checklist and final steps once KYB is approved and operationalMode is live.
- Hosted Checkout — Start accepting payments immediately after your account is active.