Skip to content
In this section

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.ai and use no auth header — they rely on an HttpOnly session cookie. GraphQL calls hit https://api.fynex.ai/dashboard/graphql and also use the cookie. See GraphQL Authentication for a standalone reference.


Overview

Onboarding phasesThree sequential phases; Phase 2 is optional.
cookie setlegalEntityIdKYB tokenapprovedStart sign-upPOST /onboarding/startPhase 1 · RESTWizard steps ×NPATCH /onboarding/stepVerify emailPOST /verify-emailComplete sign-upPOST /onboarding/completeDemo sellercreateOrganizationStaff onlyPhase 2 · GraphQLCreate legal entitycreateLegalEntityByUuidCreate org profilecreateOrganizationProfileByUuidAdditional entitiesRegisteredInitiate KYBPOST /api/v1/kyb/initiatePhase 3 · REST+KYBKYB Web SDKCustomer fills the formFynex staff reviewReview and approveoperationalMode → Live

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

start already sets the session cookie — there is no separate sign-in step during sign-up. POST /api/v1/login/dashboard is for returning sessions.

Step names and what each collects

The step sequence depends on legalEntityType, chosen in the first step:

CompanyIndividuallegal_entity_typeCompany or Individualpersonal_detailscompany_detailsbusiness_detailsindividual_details

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 returns organizationUuid, legalEntityIds and organizationProfileIds. 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.

organizationUuidlegalEntityIdcreateOrganizationStaff surface only (Step 1)createLegalEntityTakes organizationUuidCreate org profilecreateOrganizationProfileTakes both IDs

createOrganizationProfile takes both organizationUuid and legalEntityId.

Caution

Partial rollback: if createOrganizationProfile fails after the first two mutations have succeeded, you are left with an orphaned Organization and LegalEntity. 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

createOrganization is not exposed on /dashboard/graphql. The mutation exists on the Fynex staff surface only; a dashboard session calling it receives Cannot query field "createOrganization" on type "Mutation". Your organization is created by POST /api/v1/onboarding/complete (Phase 1), which returns its organizationUuid. To add an additional organization under the same account, ask your Fynex representative for it and continue from Step 2 with the organizationUuid they 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).


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 Demo mode and activated automatically, so the token works immediately for sandbox payments. KYB approval is what flips the account to Live for real-money processing (with a Fynex-assigned live terminal). If a sandbox call returns 403 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.
  1. 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" }
  1. 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>
  1. 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 401 or invalid token error from the SDK, call GET /api/v1/kyb/token to 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.