The **Canonical** driver lets you connect a payment service provider (PSP) of your choice to B2CORE through [PSS](/b2core/integrations/payment-systems#payment-system-service-pss), even when B2CORE does not yet offer a dedicated driver for that provider.

## Why use the Canonical driver [#why-use-the-canonical-driver]

Most payment systems in B2CORE rely on a dedicated driver built specifically for one provider. The Canonical driver takes a different approach: it defines a single, standard API contract — the Canonical Deposit API — that any provider can implement. B2CORE then handles authentication, the deposit start flow, polling, webhooks, and the status lifecycle in a uniform way, regardless of which provider sits behind the contract.

The Canonical driver is useful when you want to:

* Connect a preferred or in-house PSP that has no dedicated B2CORE driver, without waiting for custom development.
* Reduce time to market by having your provider implement one documented, stable contract instead of a bespoke integration.
* Keep full control of the provider side, while B2CORE manages the deposit workflow on its side.

<Callout type="info">
  The Canonical driver currently supports deposit flows. To offer deposits through your provider, the provider must implement the Canonical Deposit API described in the [OpenAPI specification](#openapi-specification) below and follow the behavioral requirements on this page.
</Callout>

## OpenAPI specification [#openapi-specification]

The Canonical Deposit API is defined in the following OpenAPI specification, which covers authentication, request and response schemas, endpoints, and status codes. Download it to review the full contract that your provider must implement:

The rest of this page describes the behavioral requirements, B2CORE-side configuration, and design decisions that the specification cannot express. Read both together for a complete picture of the integration.

## B2CORE driver credentials [#b2core-driver-credentials]

These fields are configured **on the B2CORE side** (Back Office) and used to authenticate with the PSP API. See the [OpenAPI specification](#openapi-specification) for full JWT token generation and verification details.

| Field        | Description                                                                                                                          |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| API base URL | HTTPS base URL of the PSP API.                                                                                                       |
| App ID       | Unique merchant identifier. Used as the `sub` claim in the JWT.                                                                      |
| App secret   | Secret key for HMAC-SHA256 signing. Base64 URL-encoded, 32 bytes (43 characters). Never sent in requests — used only to sign tokens. |

## B2CORE driver configuration fields [#b2core-driver-configuration-fields]

These fields are configured **on the B2CORE side** (Back Office) and control driver behavior. They are not part of the PSP-facing API.

### Global parameters (`globalParam1`, `globalParam2`, `globalParam3`) [#global-parameters-globalparam1-globalparam2-globalparam3]

Three configuration-level string fields sent with **every authenticated request** to the PSP.

* For `POST` endpoints, they are included in the JSON request body.
* For `GET` endpoints, they are included as query parameters.

These represent PSP-specific values such as merchant ID, channel, or project ID. The exact semantics depend on the PSP implementation. The B2CORE admin fills them in during configuration setup.

### Required fields (read-only) [#required-fields-read-only]

**Type:** Multi-select

Selects which user info fields are displayed in the payment form as **read-only** (non-editable).

Selected fields **must** already be configured and saved in the user's B2CORE profile. If any selected field is missing from the profile, the payment form generation fails with a missing fields error — the user cannot proceed until the data is filled in their B2CORE profile.

The selected fields, along with the fields from Required fields (editable), determine which user data is meaningfully populated in the `startDepositUserInfo` object sent to the PSP in the `POST /api/v1/deposits` request. Fields not selected in either list are hidden in the form and may be sent as empty values.

### Required fields (editable) [#required-fields-editable]

**Type:** Multi-select

Selects which user info fields are displayed in the payment form as **editable**. The user can fill in or modify these fields directly in the payment form. Unlike Required fields (read-only), there is no requirement for these fields to be pre-configured in the user's B2CORE profile.

**Priority rule:** If a field is selected in both Required fields (read-only) and Required fields (editable), it is displayed as **read-only**. The read-only setting always takes priority.

**Email behavior:** Email is always displayed in the payment form and always sent in the `startDepositUserInfo`, regardless of whether it is selected in either list. The configuration only controls how it is displayed:

| Email selected in           | Behavior                                                   |
| --------------------------- | ---------------------------------------------------------- |
| Neither list                | Displayed as an editable field                             |
| Required fields (editable)  | Displayed as an editable field                             |
| Required fields (read-only) | Displayed as a read-only field (value from B2CORE profile) |
| Both lists                  | Displayed as a read-only field (read-only takes priority)  |

### Default sync deadline [#default-sync-deadline]

**Type:** Select\
&#x2A;*Default:** `4h`

Maximum duration after deposit creation during which B2CORE polls for the deposit status. After this deadline:

* `StatusSyncInProgress` → the deposit is moved to `unexpected`.
* `StatusSyncUnexpected` → the deposit is moved to `unexpected`.

The `unexpected` status requires manual admin investigation via the LifecycleService.

### Safe to fail at start [#safe-to-fail-at-start]

**Type:** Boolean (currently hardcoded to `yes`, no choice)

Determines whether it is safe to mark a deposit as `failed` (terminal) when the start request encounters an unexpected error.

* `yes` — if the PSP returns an error during deposit start, and B2CORE is confident the deposit was not created on the PSP side (for example, B2CORE never received a redirect URL), the deposit can safely be moved to `failed`. The client has not lost any money.
* `no` — even on error, the deposit is moved to `in_progress` with an unverified external ID and polled, because the PSP might have created the deposit despite the error.

**Currently, always `yes`.** The typical case: without a redirect URL, the client cannot complete the PSP payment page, so the deposit cannot succeed.

### Wait for webhook before polling [#wait-for-webhook-before-polling]

**Type:** Boolean (currently hardcoded to `yes`, no choice)

Controls whether B2CORE waits for a webhook notification before it starts polling `GET /api/v1/deposits/{externalID}`.

| Value | Behavior                                                                                                                                                                   |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `yes` | After deposit start, B2CORE waits **up to 5 minutes** for a webhook before it begins to poll. If no webhook arrives within 5 minutes, B2CORE proceeds to standard polling. |
| `no`  | B2CORE begins polling immediately according to the standard backoff schedule.                                                                                              |

**Rationale:** Many PSPs send a webhook notification quickly when the deposit status changes. Waiting for the webhook before polling reduces the number of unnecessary API calls, which helps stay within rate limits. The 5-minute timeout ensures progress even if the webhook is delayed or lost.

## Test configuration flow [#test-configuration-flow]

When an admin clicks **Test Configuration** in B2CORE:

```
1. B2CORE generates a one-time JWT signed with appSecret
2. B2CORE → POST /api/v1/configuration/test (with Bearer JWT + globalParams)
3. If response status = "available" → test result: "available"
4. If response status = "failed" → test result: "failed" (with error from PSP)
5. If unexpected error (5xx, timeout, and similar) → test result: "unexpected"
```

The test configuration method is the only method where any credential issue is expected to return not `401 Unauthorized`, but `200 OK` with a response body.

The `code` and `description` are shown to the B2CORE administrator, so return clean, non-sensitive data.

## Webhook system design [#webhook-system-design]

### Two webhook channels [#two-webhook-channels]

B2CORE supports **two webhook channels** per deposit:

| Channel                | Registration                                                 | Description                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Automatic**          | Via `notificationURL` in the `POST /api/v1/deposits` request | Always active. B2CORE generates the URL and passes it to the PSP.                                                                                |
| **Admin-configurable** | Set by the admin in the B2CORE Back Office                   | Optional. The admin can configure a separate webhook URL that the PSP sends notifications to (for example, registered in the PSP's admin panel). |

Both channels feed into the same B2CORE webhook handler → `driver_transit` store → poller optimization pipeline.

### Webhook as polling optimization [#webhook-as-polling-optimization]

The webhook is **not** the source of truth. It is an **optimization** that reduces unnecessary polling requests.

```
PSP → B2CORE webhook handler → driver_transit (key-value store) → poller
```

How it works:

1. When a webhook arrives, B2CORE stores a flag in `driver_transit` keyed by `externalID`.
2. The poller checks `driver_transit` before it makes an API call:
   * If a webhook flag exists for the `externalID`, B2CORE immediately calls `GET /api/v1/deposits/{externalID}`.
   * If no flag exists and less than 5 minutes have passed, B2CORE waits (see [Wait for webhook before polling](#wait-for-webhook-before-polling)).
   * If no flag exists and more than 5 minutes have passed, B2CORE proceeds with standard polling.
3. When the deposit reaches a terminal status (success or failed), the `driver_transit` entry is deleted.

### Webhook payload [#webhook-payload]

The webhook payload is minimal (see the webhook callback under `POST /api/v1/deposits` in the [OpenAPI specification](#openapi-specification)):

```json
{
  "externalID": "550e8400-e29b-41d4-a716-446655440000",
  "status": "success"
}
```

The payload contains the following fields:

* `externalID` — matches the UUID from the `POST /api/v1/deposits` request.
* `status` — one of `"success"`, `"failed"`, or `"unprocessable"`.

The webhook should be sent only when the deposit transitions to a **terminal status**.

## Deposit start flow [#deposit-start-flow]

### Start request [#start-request]

B2CORE initiates a deposit by calling `POST /api/v1/deposits`.

The request includes a `returnURL` field — a B2CORE frontend page URL to redirect the user back to after PSP page interaction. This is **not a webhook** — it is a browser redirect only.

For all other details, see the [OpenAPI specification](#openapi-specification).

### Start result mapping [#start-result-mapping]

The PSP response maps to a B2CORE action as follows:

| PSP response                                                      | B2CORE action         |
| ----------------------------------------------------------------- | --------------------- |
| PSP returns a redirect URL                                        | Move to `in_progress` |
| 2xx with a specification violation (for example, no redirect URL) | Move to `failed`      |
| HTTP 4xx / 5xx / timeout / network error                          | Move to `failed`      |

All failure scenarios result in `failed` (not `unexpected`) because Safe to fail at start is `yes` (see [Safe to fail at start](#safe-to-fail-at-start)): without a valid redirect URL, the end user cannot interact with the PSP payment page, so no money can be lost.

### Redirect flow [#redirect-flow]

On a successful start, B2CORE receives a redirect URL and sends the end user to the PSP payment page:

<Mermaid chart="&#x22;sequenceDiagram\n    participant B as B2CORE\n    participant P as PSP\n    participant U as End user\n\n    B->>P: POST /api/v1/deposits\n    P-->>B: 200 OK<br/>action.type: \&#x22;redirect\&#x22;<br/>action.redirect.url: \&#x22;...\&#x22;\n    B->>U: Redirect end user to PSP payment page\n    U->>P: Open PSP payment page\n    U->>P: Complete payment\n    P-->>U: Redirect to returnURL\n    U->>B: Land on B2CORE \&#x22;in progress\&#x22; page&#x22;" />

* The `returnURL` takes the user back to a B2CORE frontend page that indicates the deposit is being processed.
* After start, B2CORE begins the polling and webhook flow (see [Polling and status sync](#polling-and-status-sync)).
* Currently, `"redirect"` is the only supported action type.

## Polling and status sync [#polling-and-status-sync]

### Polling backoff schedule [#polling-backoff-schedule]

B2CORE uses **progressive backoff** to poll `GET /api/v1/deposits/{externalID}`:

| Time since deposit start | Poll interval    |
| ------------------------ | ---------------- |
| 0–15 minutes             | Every 1 minute   |
| 15–60 minutes            | Every 3 minutes  |
| 1–3 hours                | Every 5 minutes  |
| 3–5 hours                | Every 10 minutes |
| 5+ hours                 | Every 15 minutes |

### Deadline handling [#deadline-handling]

Default deadline: **4 hours** after deposit creation (configurable, see [Default sync deadline](#default-sync-deadline)).

When the deadline is exceeded, intermediate statuses are resolved as follows:

| Last poll result                             | B2CORE action        |
| -------------------------------------------- | -------------------- |
| `inProgress`                                 | Move to `unexpected` |
| Network error, 5xx, parse error, and similar | Move to `unexpected` |

The `unexpected` status stops automatic polling and requires manual admin action via B2CORE's LifecycleService.

### Webhook wait logic [#webhook-wait-logic]

When Wait for webhook before polling is `yes` (current default, see [Wait for webhook before polling](#wait-for-webhook-before-polling)):

<Mermaid chart="&#x22;flowchart TD\n    A[\&#x22;t=0min: deposit created, redirect URL returned<br/>Poller scheduled but WAITS for webhook\&#x22;] --> B{Webhook received<br/>before t=5min?}\n    B -- yes --> C[\&#x22;Immediately poll<br/>GET /api/v1/deposits/{externalID}\&#x22;]\n    B -- \&#x22;no (t=5min elapsed)\&#x22; --> D[\&#x22;Begin standard polling<br/>(1-minute intervals initially)\&#x22;]\n    C --> E[Continue with standard<br/>polling backoff]\n    D --> E\n    E --> F[Continue until terminal status<br/>or deadline]&#x22;" />

### Polling result mapping [#polling-result-mapping]

Each poll result from `GET /api/v1/deposits/{externalID}` maps to a B2CORE internal action:

| PSP response status                          | B2CORE action                                                                                                                                             |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"inProgress"` + deadline not exceeded       | Continue polling (retry)                                                                                                                                  |
| `"inProgress"` + deadline exceeded           | Move to `unexpected`                                                                                                                                      |
| `"success"`                                  | Move to `success`, stop polling. Save `finalAmount` and `finalCurrencyCode`                                                                               |
| `"failed"`                                   | Move to `failed`, stop polling. Save `reason`                                                                                                             |
| `"unprocessable"`                            | Move to `unexpected`, stop polling. Save `reason`. Requires admin investigation                                                                           |
| HTTP 5xx / timeout / parse error             | Treat as an `unexpected` poll attempt. Continue polling if the deadline is not exceeded                                                                   |
| HTTP 404 with `X-Safe-To-Fail-After-Seconds` | Continue polling. After the indicated time plus a safety margin (5 minutes), if still 404, move to `failed` with reason "deposit redirect URL is expired" |
| HTTP 404 without the header                  | Continue polling. Wait for the deposit to appear on the PSP side or the sync deadline to be exceeded                                                      |

## Deposit status lifecycle [#deposit-status-lifecycle]

This section describes B2CORE deposit statuses. For the mapping of PSP response statuses to B2CORE statuses, see [Start result mapping](#start-result-mapping) and [Polling result mapping](#polling-result-mapping).

### Full status diagram [#full-status-diagram]

<Mermaid chart="&#x22;flowchart TD\n    created[created]\n    post[\&#x22;POST /api/v1/deposits\&#x22;]\n    failed1[failed]\n    inProgress1[in_progress]\n    unexpected1[unexpected]\n    poll[\&#x22;GET /api/v1/deposits/{externalID}<br/>(polling)\&#x22;]\n    retryIP[\&#x22;(in_progress)<br/>(retry)\&#x22;]\n    retryUX[\&#x22;(unexpected)<br/>(retry)\&#x22;]\n    success[success]\n    failed2[failed]\n    unprocessable[\&#x22;(unprocessable)<br/>(stop polling)\&#x22;]\n    inProgress2[in_progress]\n\n    created --> post\n    post --> failed1\n    post --> inProgress1\n\n    inProgress1 --> poll\n    poll --> retryIP\n    poll --> retryUX\n    poll --> success\n    poll --> failed2\n    poll --> unprocessable\n\n    retryIP --> inProgress2\n    retryUX --> inProgress2\n    unprocessable --> unexpected1\n    unexpected1 -->|Manual recovery| inProgress2&#x22;" />

### Recovery from the `unexpected` status [#recovery-from-the-unexpected-status]

An admin can perform these manual transitions via the LifecycleService:

| Transition                   | When to use                                                                      |
| ---------------------------- | -------------------------------------------------------------------------------- |
| `unexpected` → `in_progress` | Retry polling (for example, after a PSP outage is resolved)                      |
| `unexpected` → `success`     | Only if the last poll attempt was `unprocessable` and the admin confirms success |
| `unexpected` → `failed`      | The admin confirms the deposit failed                                            |

### Deposit status definitions [#deposit-status-definitions]

The following table defines each B2CORE deposit status:

| Status        | Business meaning                                                                                                                                                                                                                                                                                            |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `in_progress` | The deposit is being processed. Covers all intermediate states: pending bank transfer, 3DS verification in progress, awaiting manual review on the PSP side, the client interacting with the PSP payment page, and similar. This is a non-terminal status — B2CORE continues polling.                       |
| `success`     | The broker has received the client's money. The PSP has confirmed the funds were credited. The `finalAmount` and `finalCurrencyCode` fields reflect the actual amount and currency received, which may differ from the initial request due to fees or conversion.                                           |
| `failed`      | The deposit did not go through. The client did **not** lose any money, and the broker did **not** receive any funds. Examples: card declined, bank transfer rejected, user canceled on the PSP page, or redirect link expired.                                                                              |
| `unexpected`  | The deposit could not be resolved automatically and requires manual admin action via B2CORE (see [Recovery from the `unexpected` status](#recovery-from-the-unexpected-status)); automatic polling has stopped. Entered on a deadline timeout while `in_progress`, or when the PSP reports `unprocessable`. |

### Deposit creation timing [#deposit-creation-timing]

PSPs follow one of two patterns for deposit creation:

| Pattern                | Behavior                                                                                   | Polling impact                                                                                 |
| ---------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| **Immediate creation** | The PSP creates the deposit record on `POST /api/v1/deposits`.                             | `GET /api/v1/deposits/{externalID}` returns a result immediately after start.                  |
| **Deferred creation**  | The PSP creates the deposit record only after the end user completes the PSP payment page. | `GET /api/v1/deposits/{externalID}` returns `404 Not Found` until the user completes the page. |

For deferred creation, the PSP **should** return the `X-Safe-To-Fail-After-Seconds` header with the 404 response. This header tells B2CORE how long the redirect link is valid. After `redirect_time + header_value + safety_margin`, if the deposit is still 404, B2CORE marks it as `failed&#x60; with reason &#x2A;*"deposit redirect URL is expired"**.

If the header is absent, B2CORE continues polling until the deposit appears or the sync deadline (default 4 hours) is exceeded, at which point the deposit moves to `unexpected`.

## Behavioral requirements [#behavioral-requirements]

### Distributed tracing [#distributed-tracing]

All HTTP requests from B2CORE to the PSP include standard tracing headers per the [W3C Trace Context](https://www.w3.org/TR/trace-context/) specification:

* `traceparent` — contains the trace ID, parent span ID, and trace flags.
* `tracestate` — vendor-specific trace data.

PSP implementations should propagate these headers to their downstream services for end-to-end observability.

### PSP payment page currency lock [#psp-payment-page-currency-lock]

When the start deposit response includes a redirect to a PSP payment page:

* The PSP page **must not** allow the end user to change the `currencyCode` to any analog, equivalent, or alternative currency.
* The currency shown on the PSP page must match exactly what was sent in the start deposit request.
* If the PSP page allows currency selection, the currency must be pre-selected and locked.

### IP whitelisting recommendation [#ip-whitelisting-recommendation]

While not required by the API specification, PSP implementations are **strongly recommended** to configure IP whitelisting on their side. This provides an additional layer of security beyond JWT authentication, limiting API access to known B2CORE IP addresses.

<Callout type="info">
  To make the Canonical driver available in your environment, please reach out to your account manager. Kindly specify how exactly and for what purposes you plan to use the Canonical driver so we can arrange access accordingly.
</Callout>
