# Submerchant onboarding

Use the marketplace onboarding flow to connect a submerchant to your Finance Computer account and send them to a hosted onboarding experience at `dashboard.xmoney.com`.

Your marketplace creates a short-lived, signed redirect URL on its backend. The submerchant follows the URL, creates or connects a Finance Computer account, and provides their business and compliance information directly to Finance Computer. Your marketplace does not collect or send KYB documents through this flow.

## How the flow works

1. Your backend assigns a stable internal `merchantId` to the submerchant.
2. Your backend creates and signs a Finance Computer Connect URL.
3. Your application redirects the submerchant's browser to the signed URL.
4. Finance Computer validates the signature and starts a secure browser session.
5. The submerchant creates an account or signs in to an existing account.
6. The submerchant completes the hosted business onboarding flow.
7. Your backend polls the onboarding status using the same `merchantId`. Finance Computer does not send marketplace onboarding-status webhooks.


Hosted onboarding
The onboarding form and all KYB data collection are hosted by Finance Computer. Do not include business data, personal data, or documents in the redirect URL.

## Before you start

Contact your Finance Computer representative to receive the credentials for your marketplace:

| Value | Purpose |
|  --- | --- |
| `slug` | Public identifier used in your Connect URL, for example `acme-marketplace`. |
| `signingSecret` | Shared secret used by your backend to sign redirect URLs. |
| `keyId` | Username for onboarding-status polling. |
| `pollingSecret` | Password for onboarding-status polling. |


Keep `signingSecret` and `pollingSecret` in a secret manager and use them only from your backend. Never expose them in browser code, mobile applications, client-side logs, or public repositories.

Confirm the production and test dashboard base URLs with your Finance Computer representative before integrating.

## Create a signed onboarding URL

The production redirect URL has this format:

```text
https://dashboard.xmoney.com/connect/{slug}?merchantId={MERCHANT_ID}&timestamp={ISO8601_UTC}&nonce={NONCE}&signature={SIGNATURE}
```

### URL parameters

| Parameter | Required | Rules |
|  --- | --- | --- |
| `slug` | Yes | Your Finance Computer-provisioned marketplace identifier. Lowercase letters, numbers, and hyphens only; 2–64 characters. |
| `merchantId` | Yes | Your stable internal identifier for the submerchant; maximum 191 characters. |
| `timestamp` | Yes | Current UTC time in ISO 8601 format. Use `YYYY-MM-DDTHH:mm:ssZ`. The URL is accepted for 15 minutes. |
| `nonce` | Yes | A cryptographically random, one-time value. Use 8–128 letters, numbers, underscores, or hyphens. |
| `signature` | Yes | A base64url-encoded HMAC-SHA256 signature of the complete URL without the `signature` parameter. |


Use a new timestamp and nonce every time you create a redirect, including when retrying a failed or expired redirect. Keep your server clock synchronized with UTC.

### Sign the exact URL

First, construct the complete URL without `signature`. This unsigned URL is the payload:

```text
https://dashboard.xmoney.com/connect/acme-marketplace?merchantId=merchant-123&timestamp=2026-09-03T10:15:00Z&nonce=I6d9jK2L0mN4pQ8R
```

Then compute:

```text
signature = base64url(
  HMAC-SHA256(
    key = signingSecret,
    message = unsignedUrl
  )
)
```

Append the result as the final `signature` query parameter.

The signed value must match exactly
Query parameter order and encoding are part of the signature. Sign the exact URL string that you redirect the browser to, before appending `signature`. Do not parse, reorder, decode, or re-encode the URL after signing it.

Use the recommended query parameter order: `merchantId`, `timestamp`, then `nonce`.

## Signing examples

These examples use only standard libraries.

```js Node.js
const crypto = require('crypto');

function createSubmerchantOnboardingUrl({
  dashboardBaseUrl = 'https://dashboard.xmoney.com',
  slug,
  signingSecret,
  merchantId,
}) {
  const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
  const nonce = crypto.randomBytes(16).toString('base64url');

  const payloadUrl = new URL(`${dashboardBaseUrl}/connect/${slug}`);
  payloadUrl.searchParams.set('merchantId', merchantId);
  payloadUrl.searchParams.set('timestamp', timestamp);
  payloadUrl.searchParams.set('nonce', nonce);

  const payload = payloadUrl.toString();
  const signature = crypto
    .createHmac('sha256', signingSecret)
    .update(payload, 'utf8')
    .digest('base64url');

  return `${payload}&signature=${signature}`;
}
```

```php PHP
<?php

function base64url_encode(string $data): string {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function createSubmerchantOnboardingUrl(
    string $slug,
    string $signingSecret,
    string $merchantId,
    string $dashboardBaseUrl = 'https://dashboard.xmoney.com'
): string {
    $timestamp = gmdate('Y-m-d\TH:i:s\Z');
    $nonce = base64url_encode(random_bytes(16));

    $payload = sprintf(
        '%s/connect/%s?merchantId=%s&timestamp=%s&nonce=%s',
        rtrim($dashboardBaseUrl, '/'),
        rawurlencode($slug),
        rawurlencode($merchantId),
        rawurlencode($timestamp),
        rawurlencode($nonce)
    );

    $signature = base64url_encode(
        hash_hmac('sha256', $payload, $signingSecret, true)
    );

    return $payload . '&signature=' . rawurlencode($signature);
}
```

```python Python
import base64
import hashlib
import hmac
import secrets
from datetime import datetime, timezone
from urllib.parse import urlencode


def base64url_encode(raw: bytes) -> str:
    return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")


def create_submerchant_onboarding_url(
    slug: str,
    signing_secret: str,
    merchant_id: str,
    dashboard_base_url: str = "https://dashboard.xmoney.com",
) -> str:
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    nonce = base64url_encode(secrets.token_bytes(16))
    query = urlencode(
        {
            "merchantId": merchant_id,
            "timestamp": timestamp,
            "nonce": nonce,
        }
    )
    payload = f"{dashboard_base_url.rstrip('/')}/connect/{slug}?{query}"
    digest = hmac.new(
        signing_secret.encode("utf-8"),
        payload.encode("utf-8"),
        hashlib.sha256,
    ).digest()
    signature = base64url_encode(digest)

    return f"{payload}&signature={signature}"
```

Call the relevant function on your backend and redirect the browser to the returned URL.

## Redirect the submerchant

Return the signed URL from your backend and navigate the submerchant's browser to it using a full-page redirect.

Do not generate the signature in frontend code. A frontend application can request a newly signed URL from your backend immediately before redirecting:

```js
const response = await fetch('/api/xmoney/onboarding-url', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ merchantId: 'merchant-123' }),
});

const { redirectUrl } = await response.json();
window.location.assign(redirectUrl);
```

A valid redirect starts a Finance Computer browser session for up to 24 hours. If the link is not validated within 15 minutes, or if its nonce was already used, generate a new signed URL.

## What the submerchant completes

After Finance Computer validates the redirect, the submerchant can create a new Finance Computer account or connect an existing account. New accounts must verify their email address.

The hosted onboarding flow then collects:

- legal business and contact information;
- business activity, ownership, and legal-representative information;
- website and processing information;
- required company and identity documents;
- payout bank-account information; and
- final declarations and submission for review.


Referred submerchants must apply for FIAT processing. After submission, Finance Computer reviews the business and may request additional information before activation.

## Poll onboarding status

Polling instead of webhooks
Finance Computer does not send webhook notifications when a referred submerchant's onboarding status changes. Poll this endpoint from your backend to retrieve the latest status.

Use the marketplace's internal `merchantId` to retrieve the current high-level onboarding status:

```http
GET {partnersApiBaseUrl}/business-referrals/{merchantId}/onboarding-status
Authorization: Basic base64(keyId:pollingSecret)
Accept: application/json
```

For example:

```bash
curl --user "$XMONEY_KEY_ID:$XMONEY_POLLING_SECRET" \
  --header "Accept: application/json" \
  "$XMONEY_PARTNERS_API_URL/business-referrals/merchant-123/onboarding-status"
```

A successful response has this shape:

```json
{
  "merchantId": "merchant-123",
  "xmoneyMerchantId": 7291,
  "status": "kyb_review_in_progress",
  "kybDecisionStatus": "pending",
  "contractStatus": null
}
```

The endpoint returns only referrals associated with the business represented by your credentials. A `merchantId` belonging to another marketplace is not accessible.

### Response fields

| Field | Type | Description |
|  --- | --- | --- |
| `merchantId` | string | Your parent-scoped merchant identifier from the signed redirect. |
| `xmoneyMerchantId` | number or null | The Finance Computer business identifier. It remains `null` until the submerchant submits a business application. |
| `status` | string | The current overall onboarding lifecycle status. |
| `kybDecisionStatus` | string or null | The current KYB decision: `pending`, `success`, `failed`, or `null` when no decision is available. |
| `contractStatus` | string or null | The contract delivery status: `sent`, `completed`, or `null`. A declined contract is represented by `status: contract_declined`. |


### Status values

| Status | Meaning |
|  --- | --- |
| `redirect_received` | The signed redirect was validated, but the submerchant has not connected an account. |
| `application_started` | An account is connected, but the FIAT business application has not been submitted. |
| `ubo_kyc_pending` | At least one ultimate beneficial owner still needs to complete KYC. |
| `kyb_review_in_progress` | Finance Computer is reviewing the submitted FIAT business application. |
| `kyb_approved` | KYB is approved and the application has reached the contract stage. |
| `kyb_rejected` | The FIAT business application was rejected. |
| `contract_sent` | The contract was sent for signature. |
| `contract_declined` | The contract was declined. |
| `contract_signed` | The contract was signed and account activation is pending. |
| `account_active` | The FIAT onboarding is complete and the account is active. |


Before the submerchant first opens and validates a signed redirect URL, Finance Computer has no record of the `merchantId`. Polling that ID returns `404 Not Found`.

Treat the other common responses as follows:

| HTTP status | Meaning |
|  --- | --- |
| `200 OK` | Status returned successfully. |
| `401 Unauthorized` | The polling credentials are missing or invalid. |
| `404 Not Found` | The `merchantId` is unknown to this marketplace. |


Continue polling while the onboarding process is progressing and stop after `account_active`. Coordinate any follow-up for `kyb_rejected` or `contract_declined` with Finance Computer. Use exponential backoff and avoid polling more often than the interval agreed with Finance Computer.

### Authentication and IP restrictions

The polling endpoint uses HTTP Basic authentication:

- Set the username to your `keyId`.
- Set the password to your `pollingSecret`.
- Send credentials only over HTTPS.


Finance Computer validates that the credential is active and verifies the polling secret securely. If an IP allowlist is configured for your credential, requests must also originate from an allowed IP address. Invalid credentials and disallowed IP addresses return `401 Unauthorized`.

## Security checklist

- Create signed URLs only on your backend.
- Store `signingSecret` and `pollingSecret` in a secret manager.
- Generate a cryptographically random nonce for every redirect.
- Never reuse a nonce, even for the same `merchantId`.
- Generate the timestamp immediately before redirecting.
- Keep the unsigned URL byte-for-byte identical after signing.
- Use HTTPS in production.
- Do not log secrets or complete signed URLs.
- Do not place personal, business, or KYB data in query parameters.
- Scope polling results by the `merchantId` stored in your own system.


## Troubleshooting signed redirects

For security, invalid, expired, and reused redirects display a generic invalid-link response. Check the following before contacting Finance Computer:

| Problem | What to check |
|  --- | --- |
| Invalid signature | Confirm that both systems use the same `signingSecret`, HMAC-SHA256, UTF-8 input, and unpadded base64url output. |
| Invalid signature | Confirm that you signed the complete URL without `signature` and did not alter its parameter order or encoding afterward. |
| Expired link | Generate the timestamp at redirect time and confirm that your server clock is synchronized. |
| Reused link | Generate a fresh nonce and a new signature for every redirect attempt. |
| Unknown marketplace | Confirm that the URL path uses the exact lowercase `slug` provisioned by Finance Computer. |
| Missing merchant | Include a non-empty `merchantId` that is no longer than 191 characters. |


When requesting support, share the assigned `slug`, environment, your `merchantId`, and the approximate UTC time of the failure. Do not send your signing or polling secrets.