How to integrate Canonical
Learn how the Canonical PSS driver lets you connect a payment service provider to B2CORE by implementing a standard deposit API contract, including B2CORE-side configuration, webhooks, polling, and the deposit status lifecycle.
The Canonical driver lets you connect a payment service provider (PSP) of your choice to B2CORE through PSS, even when B2CORE does not yet offer a dedicated driver for that provider.
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.
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 below and follow the behavioral requirements on this page.
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
These fields are configured on the B2CORE side (Back Office) and used to authenticate with the PSP API. See the 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
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)
Three configuration-level string fields sent with every authenticated request to the PSP.
- For
POSTendpoints, they are included in the JSON request body. - For
GETendpoints, 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)
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)
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
Type: Select
Default: 4h
Maximum duration after deposit creation during which B2CORE polls for the deposit status. After this deadline:
StatusSyncInProgress→ the deposit is moved tounexpected.StatusSyncUnexpected→ the deposit is moved tounexpected.
The unexpected status requires manual admin investigation via the LifecycleService.
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 tofailed. The client has not lost any money.no— even on error, the deposit is moved toin_progresswith 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
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
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
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
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) → pollerHow it works:
- When a webhook arrives, B2CORE stores a flag in
driver_transitkeyed byexternalID. - The poller checks
driver_transitbefore it makes an API call:- If a webhook flag exists for the
externalID, B2CORE immediately callsGET /api/v1/deposits/{externalID}. - If no flag exists and less than 5 minutes have passed, B2CORE waits (see Wait for webhook before polling).
- If no flag exists and more than 5 minutes have passed, B2CORE proceeds with standard polling.
- If a webhook flag exists for the
- When the deposit reaches a terminal status (success or failed), the
driver_transitentry is deleted.
Webhook payload
The webhook payload is minimal (see the webhook callback under POST /api/v1/deposits in the OpenAPI specification):
{
"externalID": "550e8400-e29b-41d4-a716-446655440000",
"status": "success"
}The payload contains the following fields:
externalID— matches the UUID from thePOST /api/v1/depositsrequest.status— one of"success","failed", or"unprocessable".
The webhook should be sent only when the deposit transitions to a terminal status.
Deposit start flow
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.
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): without a valid redirect URL, the end user cannot interact with the PSP payment page, so no money can be lost.
Redirect flow
On a successful start, B2CORE receives a redirect URL and sends the end user to the PSP payment page:
sequenceDiagram
participant B as B2CORE
participant P as PSP
participant U as End user
B->>P: POST /api/v1/deposits
P-->>B: 200 OK<br/>action.type: "redirect"<br/>action.redirect.url: "..."
B->>U: Redirect end user to PSP payment page
U->>P: Open PSP payment page
U->>P: Complete payment
P-->>U: Redirect to returnURL
U->>B: Land on B2CORE "in progress" page- The
returnURLtakes 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).
- Currently,
"redirect"is the only supported action type.
Polling and status sync
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
Default deadline: 4 hours after deposit creation (configurable, see 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
When Wait for webhook before polling is yes (current default, see Wait for webhook before polling):
flowchart TD
A["t=0min: deposit created, redirect URL returned<br/>Poller scheduled but WAITS for webhook"] --> B{Webhook received<br/>before t=5min?}
B -- yes --> C["Immediately poll<br/>GET /api/v1/deposits/{externalID}"]
B -- "no (t=5min elapsed)" --> D["Begin standard polling<br/>(1-minute intervals initially)"]
C --> E[Continue with standard<br/>polling backoff]
D --> E
E --> F[Continue until terminal status<br/>or deadline]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
This section describes B2CORE deposit statuses. For the mapping of PSP response statuses to B2CORE statuses, see Start result mapping and Polling result mapping.
Full status diagram
flowchart TD
created[created]
post["POST /api/v1/deposits"]
failed1[failed]
inProgress1[in_progress]
unexpected1[unexpected]
poll["GET /api/v1/deposits/{externalID}<br/>(polling)"]
retryIP["(in_progress)<br/>(retry)"]
retryUX["(unexpected)<br/>(retry)"]
success[success]
failed2[failed]
unprocessable["(unprocessable)<br/>(stop polling)"]
inProgress2[in_progress]
created --> post
post --> failed1
post --> inProgress1
inProgress1 --> poll
poll --> retryIP
poll --> retryUX
poll --> success
poll --> failed2
poll --> unprocessable
retryIP --> inProgress2
retryUX --> inProgress2
unprocessable --> unexpected1
unexpected1 -->|Manual recovery| inProgress2Recovery 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
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); automatic polling has stopped. Entered on a deadline timeout while in_progress, or when the PSP reports unprocessable. |
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 with reason "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
Distributed tracing
All HTTP requests from B2CORE to the PSP include standard tracing headers per the W3C 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
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
currencyCodeto 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
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.
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.
Last updated on