Skip to content

MVP Payments Client API

Version 2026-08-01 · Base URL https://api.mvp-payments.com/v1 · generated from the OpenAPI specification

The MVP Payments Client API (https://api.mvp-payments.com). One REST API, stage v1. The payment identifier is the mvpId — 18 characters [A-Za-z0-9], CSPRNG-generated.

This spec is generated-from and kept in sync with solution-design/02-client-api.md (the contract). Any change here must land in the same commit as the matching doc change.

Authentication (server-to-server)

All server-to-server endpoints except GET /banks require all of: - x-api-key — the per-client API Gateway key (usage plan: throttle, quota, kill switch) - x-mvp-client-id — the clientId - x-mvp-client-secret — the client secret for the environment; which secret you use (sandbox or live) determines isSandbox

GET /banks is credential-optional: send all three for the authenticated read, or none of them with ?environment=sandbox|live for the credential-free read — alongside the two-tier GET /payments/{mvpId}, which also answers without credentials (redacted tier).

Credentials are never accepted in request bodies. Session-scope endpoints (/session/…) are anonymous by design: the mvpId is the bearer session, protected by the redacted response tier and per-route throttling.

Environments

One base URL. Sandbox and live are separated by credentials, not hosts. Live traffic additionally requires the client's live gate to be enabled.

Statuses

CREATED → BANK_SELECTED → PROCESSING → INITIATED | FAILED — INITIATED is final success (the bank accepted the initiation); FAILED always carries failureReason (REJECTED | CANCELLED | EXPIRED | TECHNICAL). SETTLED is reserved and not written in v1.

Not every bank walks the whole ladder. Each entry from GET /banks carries statusCeiling — the highest status that bank reports on the success path. Where it is PROCESSING, the bank accepts the initiation and then reports nothing further: PROCESSING is the documented end of the success path and must not be read as success. Such a payment, if left untouched, ends FAILED with failureReason: EXPIRED at expiresAt — a platform timeout, not a bank rejection. Check statusCeiling before deciding how long to wait.

Errors

Every non-2xx response uses one envelope: { message, code, requestId } (see ErrorEnvelope). Codes are stable and append-only; validation failures add an errors[] array of AJV details. Nothing in an error ever leaks another client's data or internals.

payments

Create and read payments (server-to-server)

POST /payments

Create a payment session

AJV-strict validation (additionalProperties: false — unknown fields are 400s; localInstrument is not an input: the rail is derived from currency). The pricing snapshot, certMode and the resolved brand's branding are stamped at create; a later rebrand never alters an in-flight journey.

Parameters (3)
Name In Required Type Description
x-mvp-client-id header yes string
x-mvp-client-secret header yes string
Idempotency-Key header no string 24-hour dedupe window. Replays return the original result; a different body under the same key is a 409.
Request body — CreatePaymentRequest
Field Type Required Description
amount string · pattern ^(0|[1-9]\d{0,9})\.\d{2}$ yes Decimal string, two decimals ("12.02"). Per-rail ceiling: GBP is capped at 1000000.00 (the Faster Payments scheme value limit) — above it is 400 MVP-4013, path /amount. EUR/SEK/RON carry no platform rail ceiling. A bank's own per-payment cap is separate and independent, and is enforced at bank selection.
currency Currency yes See Currency
beneficiary object | object yes Exactly one account form, decided by currency. EUR/SEK/RON require iban and reject sortCode/accountNumber; GBP requires sortCode+accountNumber and rejects iban — including a GB IBAN. A mismatch is 400 MVP-4011 with the offending field in errors[].path. Nothing is derived between the two forms in either direction.
beneficiary.name string · ≤70 chars yes
beneficiary.iban string no Creditor IBAN (mod-97 validated). EUR/SEK/RON only.
beneficiary.sortCode string · pattern ^[0-9]{6}$ no UK sort code, exactly 6 digits. GBP only.
beneficiary.accountNumber string · pattern ^[0-9]{8}$ no UK account number, exactly 8 digits (leading zeros are significant). GBP only.
beneficiary.bic string no Optional creditor agent
beneficiary.address object no
beneficiary.address.line1 string no
beneficiary.address.city string no
beneficiary.address.postcode string no
beneficiary.address.country string · ≤2 chars no
endToEndId string · ≤35 chars no ISO 20022 EndToEndIdentification, SEPA charset; defaults to the mvpId. Per-rail ceiling: 35 for EUR/SEK/RON, 31 for GBP (the Faster Payments scheme carries 31). Over the GBP ceiling is 400 MVP-4012; over 35 on EUR/SEK/RON is 400 MVP-4000, unchanged.
remittanceInformationUnstructured string · ≤140 chars no SEPA charset, ≤140 on every rail. At most one remittance form; both omitted → defaults to the mvpId.
remittanceInformationStructured object no
remittanceInformationStructured.reference string yes ISO 11649 RF reference (mod-97 validated when RF) or domestic scheme reference. Per-rail ceiling: 35 for EUR/SEK/RON, 18 for GBP (the Faster Payments reference field). Over the GBP ceiling is 400 MVP-4012.
remittanceInformationStructured.referenceType string · default "SCOR" no
remittanceInformationStructured.referenceIssuer string no
callbackUrl string · pattern ^https:// · ≤2048 chars yes Required, https-only, at most 2048 characters. No fallback URL of any kind. Where the hosted journey sends the payer when the session finishes. The released redirect is this URL with one query parameter appended: mvpid, the payment's id from the create response. Your own query string and fragment come back byte for byte, and a mvpid you set yourself is left untouched. Treat it as a lookup key, never as the outcome — it travels through the payer's browser, which may be a different device from the one that started the payment (a desktop QR journey returns on the phone), and anyone can send a browser to your return page with any value on it. Look the id up in the payments you created and ignore ids you have no record of; having a record is not authorisation either — the id is an identifier the payer can see and share, not a secret — so show a returning browser the outcome and a reference, and order detail only to a session that already owns the order. Read the outcome from GET /v1/payments/{mvpId} with your credentials, or from the signed webhook. A response without the full-tier fields (endToEndId, callbackUrl, timestamps) means the payment is not yours OR your credentials were not accepted — the two are deliberately indistinguishable and both answer 200 — so log it and check your credentials before assuming the former. The released URL can exceed the 2048-character limit by the appended pair.
paymentPath enum: HOSTED | API_ONLY · default "HOSTED" no
debtorIban string no Optional pre-fill — avoids consumer account entry. EUR/SEK/RON only.
debtorSortCode string · pattern ^[0-9]{6}$ no Optional debtor pre-fill, GBP only; supply with debtorAccountNumber.
debtorAccountNumber string · pattern ^[0-9]{8}$ no Optional debtor pre-fill, GBP only; supply with debtorSortCode.
consumerCountry string · ≤2 chars no Optional bank-picker pre-filter
brandName string no Selects one of the client's trading brands; omitted → default brand; unknown → 400
Responses
Status Schema Description
201 CreatePaymentResponse Payment session created
400 ErrorEnvelope Validation failed (envelope + AJV details)
401 ErrorEnvelope Missing or invalid credentials
403 ErrorEnvelope Client disabled or live not enabled
409 ErrorEnvelope Idempotency conflict or invalid status transition
429 ErrorEnvelope Too many requests

GET /payments/{mvpId}

Payment status & detail (two-tier)

Two-tier response, both 200. Public tier (no/invalid credentials, or another client's payment — indistinguishable): the redacted field set. Full tier (valid credentials AND owner): adds the detail fields. The debtor account is never exposed in full on the public tier.

Parameters (1)
Name In Required Type Description
mvpId path yes string
Responses
Status Schema Description
200 PublicPayment | FullPayment Public or full tier depending on credentials + ownership
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

POST /payments/{mvpId}

POST-tolerant alias of the two-tier read

Parameters (1)
Name In Required Type Description
mvpId path yes string
Responses
Status Schema Description
200 PublicPayment | FullPayment Same as GET
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

POST /payments/{mvpId}/select-bank

Select the consumer's bank (API-only path)

Allowed from CREATED, BANK_SELECTED, or a retryable FAILED (CANCELLED/TECHNICAL, before expiry, retryCount < 3) — which archives the failed attempt and resets to BANK_SELECTED. Never allowed from PROCESSING, INITIATED, or non-retryable FAILED.

PISP-consent capture is the client's UX obligation on this path (recorded as consent.path: "API_ONLY").

Debtor account, per rail. A bank that mandates the debtor account before initiation is refused with 400 MVP-4007 (DEBTOR_IBAN_REQUIRED) on the IBAN rails — supply debtorIban and retry. On GBP / UK Faster Payments that code is never returned: there is no IBAN to ask a GBP payer for, so such a bank answers 400 MVP-4005 (BANK_NOT_ELIGIBLE) — supply debtorSortCode + debtorAccountNumber (here or as a create-time pre-fill) and retry, or offer another bank. Do not branch a GBP journey on MVP-4007.

Parameters (3)
Name In Required Type Description
mvpId path yes string
x-mvp-client-id header yes string
x-mvp-client-secret header yes string
Request body
Field Type Required Description
bankId string yes
debtorIban string no EUR/SEK/RON only
debtorSortCode string · pattern ^[0-9]{6}$ no GBP only; supply with debtorAccountNumber
debtorAccountNumber string · pattern ^[0-9]{8}$ no GBP only; supply with debtorSortCode
returnUrl string · pattern ^https:// · ≤2048 chars no https-only, at most 2048 characters. Where our session return page forwards the consumer after the bank. Overrides the create-time callbackUrl and is released exactly like it — mvpid appended, your own query and fragment preserved (see callbackUrl).
scaMode enum: HOSTED | API no EMBEDDED-authModel banks only. HOSTED (default) hands the credential leg to our hosted pages; API means the client's own UX drives the SCA endpoints and accepts the credential-handling obligation.
Responses
Status Schema Description
200 SelectBankRedirectResult | SelectBankEmbeddedHostedResult | SelectBankEmbeddedApiResult | SelectBankDeclinedResult Initiation result — shape depends on the bank's authModel/scaMode
400 ErrorEnvelope Validation failed (envelope + AJV details)
401 ErrorEnvelope Missing or invalid credentials
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)
409 ErrorEnvelope Idempotency conflict or invalid status transition
502 ErrorEnvelope Bank/connector upstream failure

banks

The eligible-bank registry

GET /banks

The eligible-bank registry (credential-optional)

Credentials are optional on this route. Send x-api-key, x-mvp-client-id and x-mvp-client-secret together for the authenticated read — the secret's environment decides sandbox or live exactly as on every other route, and an environment sent as well must agree with it — or omit all three and send environment (sandbox | live) for the credential-free read. Either credential header alone is a 401. The credential-free response carries Access-Control-Allow-Origin: * and Cache-Control: public, max-age=60 (errors: no-store). No usage plan applies to this route: calls are not metered against an API key, and every caller — authenticated or not — shares one method throttle (429 when exceeded).

Served from the published registry artefact. Returns only banks enabled for the environment whose connector status passes the gate (SANDBOX_TESTING+ for sandbox, LIVE for live). With currency, only the banks that settle that currency — a bank on the domestic non-euro rail settles its own market's currency only (a Swedish bank for SEK, a Romanian bank for RON), so the list differs per currency even where the derived rail is the same, and a UK bank for GBP. Without currency, every eligible bank for the environment across all currencies, each carrying its rails and currencies. Eligibility and requiresDebtorIban are authoritative only for a currency-specific call: re-query with currency before building a bank picker. Bank-level per-payment caps are applied on the hosted picker and at bank selection; /v1/banks lists every eligible bank.

Each entry carries statusCeiling — the highest status that bank reports on the success path in the environment. Read it before deciding how long to wait on a payment. health and testJourney describe the sandbox: health is reported for the sandbox environment only; null for live — and testJourney is null for live too.

Parameters (6)
Name In Required Type Description
environment query no enum: sandbox | live Required when no client credentials are sent — missing or invalid is 400 MVP-4000 (path ?environment). With credentials it may be omitted; when sent it must be valid and match the secret's environment, or 400 MVP-4000.
currency query no Currency The rail is derived server-side from the currency, and only banks that settle the currency are returned. Omitted — every eligible bank for the environment across all currencies. An invalid value is 400.
country query no string
q query no string Case-insensitive name search.
x-mvp-client-id header no string The clientId. Send both credential headers, with x-api-key, for the authenticated read, or omit all three and send environment; either one alone is 401.
x-mvp-client-secret header no string The client secret for the environment (it decides sandbox or live). Send both credential headers, with x-api-key, for the authenticated read, or omit all three and send environment; either one alone is 401.
Responses
Status Schema Description
200 array of Bank Eligible banks, curated rank order — a bare array
Header Cache-Control: public, max-age=60 on the credential-free read; no-store on the authenticated read.
400 ErrorEnvelope Validation failed (MVP-4000) — an invalid currency; environment missing or invalid when no credentials are sent; or, with credentials, an invalid environment or one that does not match the credentials ("environment does not match the credentials").
401 ErrorEnvelope Credentialed branch only — one credential header without the other, or invalid credentials
403 ErrorEnvelope Credentialed branch only — client disabled
429 ErrorEnvelope Too many requests

sca

EMBEDDED SCA — server twins (transit-only credential handling)

GET /payments/{mvpId}/sca

Current SCA state (masked metadata only)

Parameters (3)
Name In Required Type Description
mvpId path yes string
x-mvp-client-id header yes string
x-mvp-client-secret header yes string
Responses
Status Schema Description
200 ScaState Masked SCA state
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)
409 ErrorEnvelope Idempotency conflict or invalid status transition

POST /payments/{mvpId}/sca/credentials

Submit PSU credentials (transit-only)

Forwarded synchronously to the bank; never persisted, queued, or logged (request bodies are excluded from logging at the logger level). A wrong attempt returns 200 with attemptFailed: true and a decremented attemptsRemaining; at the cap the SCA fails and the payment finalises FAILED/REJECTED.

Parameters (3)
Name In Required Type Description
mvpId path yes string
x-mvp-client-id header yes string
x-mvp-client-secret header yes string
Request body
Field Type Required Description
psuId string · ≤128 chars yes
password string · ≤256 chars yes
Responses
Status Schema Description
200 ScaState New SCA state, or a failed-attempt marker
400 ErrorEnvelope Validation failed (envelope + AJV details)
409 ErrorEnvelope Idempotency conflict or invalid status transition

POST /payments/{mvpId}/sca/select-method

Choose an SCA method

Parameters (3)
Name In Required Type Description
mvpId path yes string
x-mvp-client-id header yes string
x-mvp-client-secret header yes string
Request body
Field Type Required Description
methodId string yes
Responses
Status Schema Description
200 ScaState Challenge issued
400 ErrorEnvelope Validation failed (envelope + AJV details)
409 ErrorEnvelope Idempotency conflict or invalid status transition

POST /payments/{mvpId}/sca/challenge

Submit the OTP/TAN (transit-only)

FINALISED flows into the normal poller/webhook finality convergence. Wrong OTPs decrement attempts and fail at the cap.

Parameters (3)
Name In Required Type Description
mvpId path yes string
x-mvp-client-id header yes string
x-mvp-client-secret header yes string
Request body
Field Type Required Description
otp string · ≤16 chars yes
Responses
Status Schema Description
200 ScaState New SCA state
400 ErrorEnvelope Validation failed (envelope + AJV details)
409 ErrorEnvelope Idempotency conflict or invalid status transition

session

Session scope — hosted page routes (anonymous, throttled)

GET /session/{mvpId}

Public-tier bootstrap for the hosted page

Parameters (1)
Name In Required Type Description
mvpId path yes string
Responses
Status Schema Description
200 PublicPayment The redacted payment view
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

GET /session/{mvpId}/banks

Registry filtered for this payment's currency, rail and environment

Parameters (3)
Name In Required Type Description
mvpId path yes string
country query no string
q query no string
Responses
Status Schema Description
200 array of SessionBank Eligible banks — a bare array
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

POST /session/{mvpId}/select-bank

Hosted-page bank selection

Records PSD2 PISP consent (text version, timestamp, server-derived IP), consumer telemetry and device→bank memory. Same status guards and retry semantics as the server flavour.

Debtor account, per rail. 400 MVP-4007 (DEBTOR_IBAN_REQUIRED) is the hosted page's "ask the payer for an IBAN" signal and is returned on the IBAN rails only. On GBP / UK Faster Payments it is never returned: a UK-rail bank that mandates the debtor account answers 400 MVP-4005 (BANK_NOT_ELIGIBLE) instead, so the page offers another bank rather than opening an IBAN field that MVP-4011 would then refuse.

Parameters (1)
Name In Required Type Description
mvpId path yes string
Request body
Field Type Required Description
bankId string yes
deviceId string · ≤64 chars · ≥8 chars no
consumerCountry string · ≤2 chars no
debtorIban string no EUR/SEK/RON only
debtorSortCode string · pattern ^[0-9]{6}$ no GBP only; supply with debtorAccountNumber
debtorAccountNumber string · pattern ^[0-9]{8}$ no GBP only; supply with debtorSortCode
consentAccepted boolean yes Must be true.
Responses
Status Schema Description
200 SessionSelectBankRedirectResult | SessionSelectBankEmbeddedResult | SessionSelectBankDeclinedResult Hand-off — REDIRECT/DECOUPLED or the embedded SCA opener
400 ErrorEnvelope Validation failed (envelope + AJV details)
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)
409 ErrorEnvelope Idempotency conflict or invalid status transition
502 ErrorEnvelope Bank/connector upstream failure

POST /session/{mvpId}/complete-auth

Complete a bank-side authorisation leg (OAuth2-code standards)

OAuth2-code standards (Revolut now, UK OB future): the bank redirects the consumer back with code+state; the return page forwards those parameters here, verbatim. The server exchanges the code and — on consent-first standards — creates the bank-side payment resource, idempotently under conditional writes. The page itself never executes the payment, and finality still converges through the normal poller/webhook path (design 04 §2, invocation path 4).

Parameters (1)
Name In Required Type Description
mvpId path yes string
Responses
Status Schema Description
200 object Authorisation completed — current status snapshot
400 ErrorEnvelope Validation failed (envelope + AJV details)
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)
409 ErrorEnvelope Idempotency conflict or invalid status transition
502 ErrorEnvelope Bank/connector upstream failure

GET /session/{mvpId}/sca

Session twin of the SCA state read

Parameters (1)
Name In Required Type Description
mvpId path yes string
Responses
Status Schema Description
200 ScaState Masked SCA state
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)
409 ErrorEnvelope Idempotency conflict or invalid status transition

POST /session/{mvpId}/sca/credentials

Session twin — submit PSU credentials (transit-only)

Parameters (1)
Name In Required Type Description
mvpId path yes string
Request body
Field Type Required Description
psuId string · ≤128 chars yes
password string · ≤256 chars yes
Responses
Status Schema Description
200 ScaState New SCA state, or a failed-attempt marker
400 ErrorEnvelope Validation failed (envelope + AJV details)
409 ErrorEnvelope Idempotency conflict or invalid status transition

POST /session/{mvpId}/sca/select-method

Session twin — choose an SCA method

Parameters (1)
Name In Required Type Description
mvpId path yes string
Request body
Field Type Required Description
methodId string yes
Responses
Status Schema Description
200 ScaState Challenge issued
400 ErrorEnvelope Validation failed (envelope + AJV details)
409 ErrorEnvelope Idempotency conflict or invalid status transition

POST /session/{mvpId}/sca/challenge

Session twin — submit the OTP/TAN (transit-only)

Parameters (1)
Name In Required Type Description
mvpId path yes string
Request body
Field Type Required Description
otp string · ≤16 chars yes
Responses
Status Schema Description
200 ScaState New SCA state
400 ErrorEnvelope Validation failed (envelope + AJV details)
409 ErrorEnvelope Idempotency conflict or invalid status transition

GET /session/{mvpId}/status

Minimal status — polled by the QR desktop tab and wait screen

Parameters (1)
Name In Required Type Description
mvpId path yes string
Responses
Status Schema Description
200 object Status only
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

POST /session/{mvpId}/release-callback

Atomic single-use merchant-callback gate

The first caller gets {redirectUrl} — the client's callbackUrl (or the select-bank returnUrl that overrode it) with mvpid={mvpId} appended, existing query and fragment preserved byte for byte, a client-set mvpid left untouched — and flips the one-shot gate; every repeat gets the calm "session finished" landing on www.mvp-payments.com, which carries the same ?mvpid=.

Parameters (1)
Name In Required Type Description
mvpId path yes string
Responses
Status Schema Description
200 object Where to send the consumer
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

GET /session/device/{deviceId}/bank

"Recommended for you" — the device's remembered bank

First-party device id (localStorage UUID). Clients validate the bank against the payment's rail before display.

Parameters (1)
Name In Required Type Description
deviceId path yes string
Responses
Status Schema Description
200 object The remembered bank
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

POST /session/{mvpId}/events

First-party funnel beacon (fire-and-forget)

Allowlisted event names only; heavily throttled; 202 always.

Parameters (1)
Name In Required Type Description
mvpId path yes string
Request body
Field Type Required Description
event enum: page_view | search_used | search_abandoned | bank_selected | consent_shown | consent_accepted | qr_scanned | outcome_shown yes
Responses
Status Schema Description
202 Accepted
400 ErrorEnvelope Validation failed (envelope + AJV details)
404 ErrorEnvelope Not found (cross-tenant reads are indistinguishable)

bank-webhooks

Inbound bank notifications — per-connector signature verification, fail-closed

POST /bank-webhooks/{connectorId}

Inbound bank notification (fail-closed)

Called by banks, not clients — no API key. The connector family named by {connectorId} verifies the notification's signature and parses it; verification is fail-closed (design 00 §9 rule 10): an unknown connector, a missing/invalid signature or an unparseable event is a 401, never a write. Verified events correlate to a payment via bankPaymentId (or consentId on consent-notifying standards) and converge through the same conditional finality writers as the poller — first writer wins, pollingSource records the winner. A verified event that matches no payment is acknowledged with 202 so the bank does not retry it forever.

Parameters (1)
Name In Required Type Description
connectorId path yes string
Responses
Status Schema Description
200 object Verified and applied to the correlated payment
202 object Verified but uncorrelated — acknowledged without a write
401 ErrorEnvelope Rejected (fail-closed) — unknown connector or failed verification

Webhooks (outbound)

Deliveries we send to your server. See the verification guide for working Node.js, PHP and Python verifiers.

POST payment.finality

Outbound client webhook — payment finality (design 02 §5)

POSTed to your configured webhook.url when a payment reaches INITIATED or FAILED. Signed with your per-client RSA-2048 key: x-mvp-signature is an RS256 JWT whose claims are this payload plus iss: "mvp-payments.com", aud: <your webhook URL>, sub: <mvpId>, iat, exp (+5 minutes) and a unique jti (replay protection — cache accepted jtis and reject repeats). The public key is published in your dashboard and rotatable.

Acknowledge with any 2xx within 8 seconds. Failures retry 5 attempts total, spaced 30/60/120/240 s; every attempt is recorded and visible in your dashboard's delivery history. Delivery is at-least-once — process idempotently on (mvpId, x-mvp-event). Verification guide (Node/PHP/Python samples): docs "Verifying MVP Payments webhooks".

Delivery headers
Header Type Description
x-mvp-signature string RS256 JWT signed with your per-client key; claims = payload + iss/aud/sub/iat/exp/jti
x-mvp-id string The payment's mvpId (equals the JWT sub)
x-mvp-attempt integer
x-mvp-event enum: payment.finality

Payload: WebhookPayload

POST payment.status.changed

Outbound client webhook — every public transition (opt-in)

Optional per-client opt-in: the same signed delivery contract as payment.finality, sent on every public status transition (CREATED, BANK_SELECTED, PROCESSING). The payload is the payment's current snapshot at delivery time — it always agrees with GET /payments/{mvpId}. Finality itself is never duplicated through this event.

Delivery headers
Header Type Description
x-mvp-event enum: payment.status.changed

Payload: WebhookPayload

Schemas

Every data shape the API sends or accepts.

Currency

EUR → SEPA_INSTANT; SEK/RON → EU_DOMESTIC_NON_EURO; GBP → UK_FASTER_PAYMENTS. The rail is derived server-side and is never an input (GET /banks reports each bank's rails, see Rail). The currency also fixes the ACCOUNT FORM: EUR/SEK/RON are addressed by IBAN, GBP by sortCode + accountNumber. The two forms are never interchangeable and are never converted.

One of: EUR · SEK · RON · GBP

PaymentStatus

One of: CREATED · BANK_SELECTED · PROCESSING · INITIATED · FAILED · SETTLED

FailureReason

One of: REJECTED · CANCELLED · EXPIRED · TECHNICAL

ErrorEnvelope

Field Type Required Description
message string yes Human-readable, no internals
code string yes Stable documented code, e.g. MVP-4102
requestId string yes
errors array of object no AJV details on 400 validation failures only
errors[].path string no
errors[].message string no

CreatePaymentRequest

Field Type Required Description
amount string · pattern ^(0|[1-9]\d{0,9})\.\d{2}$ yes Decimal string, two decimals ("12.02"). Per-rail ceiling: GBP is capped at 1000000.00 (the Faster Payments scheme value limit) — above it is 400 MVP-4013, path /amount. EUR/SEK/RON carry no platform rail ceiling. A bank's own per-payment cap is separate and independent, and is enforced at bank selection.
currency Currency yes See Currency
beneficiary object | object yes Exactly one account form, decided by currency. EUR/SEK/RON require iban and reject sortCode/accountNumber; GBP requires sortCode+accountNumber and rejects iban — including a GB IBAN. A mismatch is 400 MVP-4011 with the offending field in errors[].path. Nothing is derived between the two forms in either direction.
beneficiary.name string · ≤70 chars yes
beneficiary.iban string no Creditor IBAN (mod-97 validated). EUR/SEK/RON only.
beneficiary.sortCode string · pattern ^[0-9]{6}$ no UK sort code, exactly 6 digits. GBP only.
beneficiary.accountNumber string · pattern ^[0-9]{8}$ no UK account number, exactly 8 digits (leading zeros are significant). GBP only.
beneficiary.bic string no Optional creditor agent
beneficiary.address object no
beneficiary.address.line1 string no
beneficiary.address.city string no
beneficiary.address.postcode string no
beneficiary.address.country string · ≤2 chars no
endToEndId string · ≤35 chars no ISO 20022 EndToEndIdentification, SEPA charset; defaults to the mvpId. Per-rail ceiling: 35 for EUR/SEK/RON, 31 for GBP (the Faster Payments scheme carries 31). Over the GBP ceiling is 400 MVP-4012; over 35 on EUR/SEK/RON is 400 MVP-4000, unchanged.
remittanceInformationUnstructured string · ≤140 chars no SEPA charset, ≤140 on every rail. At most one remittance form; both omitted → defaults to the mvpId.
remittanceInformationStructured object no
remittanceInformationStructured.reference string yes ISO 11649 RF reference (mod-97 validated when RF) or domestic scheme reference. Per-rail ceiling: 35 for EUR/SEK/RON, 18 for GBP (the Faster Payments reference field). Over the GBP ceiling is 400 MVP-4012.
remittanceInformationStructured.referenceType string · default "SCOR" no
remittanceInformationStructured.referenceIssuer string no
callbackUrl string · pattern ^https:// · ≤2048 chars yes Required, https-only, at most 2048 characters. No fallback URL of any kind. Where the hosted journey sends the payer when the session finishes. The released redirect is this URL with one query parameter appended: mvpid, the payment's id from the create response. Your own query string and fragment come back byte for byte, and a mvpid you set yourself is left untouched. Treat it as a lookup key, never as the outcome — it travels through the payer's browser, which may be a different device from the one that started the payment (a desktop QR journey returns on the phone), and anyone can send a browser to your return page with any value on it. Look the id up in the payments you created and ignore ids you have no record of; having a record is not authorisation either — the id is an identifier the payer can see and share, not a secret — so show a returning browser the outcome and a reference, and order detail only to a session that already owns the order. Read the outcome from GET /v1/payments/{mvpId} with your credentials, or from the signed webhook. A response without the full-tier fields (endToEndId, callbackUrl, timestamps) means the payment is not yours OR your credentials were not accepted — the two are deliberately indistinguishable and both answer 200 — so log it and check your credentials before assuming the former. The released URL can exceed the 2048-character limit by the appended pair.
paymentPath enum: HOSTED | API_ONLY · default "HOSTED" no
debtorIban string no Optional pre-fill — avoids consumer account entry. EUR/SEK/RON only.
debtorSortCode string · pattern ^[0-9]{6}$ no Optional debtor pre-fill, GBP only; supply with debtorAccountNumber.
debtorAccountNumber string · pattern ^[0-9]{8}$ no Optional debtor pre-fill, GBP only; supply with debtorSortCode.
consumerCountry string · ≤2 chars no Optional bank-picker pre-filter
brandName string no Selects one of the client's trading brands; omitted → default brand; unknown → 400

CreatePaymentResponse

Field Type Required Description
mvpId string yes
sessionUrl string no Present for HOSTED; API_ONLY responses omit it
status const "CREATED" yes
expiresAt string (date-time) yes

PublicPayment

The redacted tier — exactly these fields, nothing more

Field Type Required Description
mvpId string yes
status PaymentStatus yes See PaymentStatus
failureReason FailureReason | null yes
retryable boolean yes True when the hosted failure screen may offer bank re-selection
amount string yes
currency Currency yes See Currency
beneficiary object yes
beneficiary.name string no
brandName string yes
branding object yes
branding.backgroundHex string no
branding.logoUrl string no
branding.accentHex string no
debtorIban object yes Presence + masked tail of the PAYER ACCOUNT, in whichever form the rail uses — an IBAN tail on EUR/SEK/RON, an account-number tail on GBP. Never the full value. The field NAME is a published contract and does not change with the rail, so render form-neutral copy: "…0003" on a GBP payment is the last digits of an account number, not of an IBAN.
debtorIban.present boolean no
debtorIban.masked string | null no
expiresAt string (date-time) yes
paymentPath enum: HOSTED | API_ONLY yes The hosted return leg branches on this — API_ONLY forwards instantly (design 03 §3)
isSandbox boolean yes Drives the hosted page's sandbox environment banner (brand book §7.7)
consumerCountry string | null yes Bank-picker pre-filter + locale country fallback (design 03 §2/§4)
decoupled object | null no DECOUPLED banks hand the payer a message instead of a URL (a payment code to type into the banking app, a BankID prompt). Present while the payer is approving; null for every other SCA shape.
licenceHolder object | null no The payment initiation service provider whose licence this payment is initiated under, snapshotted when the payment is created. The hosted payment page and the consumer terms name this entity to the payer. Null when the payment runs under the platform's own licence.

FullPayment

Field Type Required Description
mvpId string yes
status PaymentStatus yes See PaymentStatus
failureReason FailureReason | null yes
retryable boolean yes True when the hosted failure screen may offer bank re-selection
amount string yes
currency Currency yes See Currency
beneficiary object yes
beneficiary.name string no
brandName string yes
branding object yes
branding.backgroundHex string no
branding.logoUrl string no
branding.accentHex string no
debtorIban object yes Presence + masked tail of the PAYER ACCOUNT, in whichever form the rail uses — an IBAN tail on EUR/SEK/RON, an account-number tail on GBP. Never the full value. The field NAME is a published contract and does not change with the rail, so render form-neutral copy: "…0003" on a GBP payment is the last digits of an account number, not of an IBAN.
debtorIban.present boolean no
debtorIban.masked string | null no
expiresAt string (date-time) yes
paymentPath enum: HOSTED | API_ONLY yes The hosted return leg branches on this — API_ONLY forwards instantly (design 03 §3)
isSandbox boolean yes Drives the hosted page's sandbox environment banner (brand book §7.7)
consumerCountry string | null yes Bank-picker pre-filter + locale country fallback (design 03 §2/§4)
decoupled object | null no DECOUPLED banks hand the payer a message instead of a URL (a payment code to type into the banking app, a BankID prompt). Present while the payer is approving; null for every other SCA shape.
licenceHolder object | null no The payment initiation service provider whose licence this payment is initiated under, snapshotted when the payment is created. The hosted payment page and the consumer terms name this entity to the payer. Null when the payment runs under the platform's own licence.
isoStatus string | null yes
isoStatusReason string | null yes
isoStatusInferred boolean yes
bankId string | null yes
bankName string | null yes
endToEndId string yes
remittanceInformationUnstructured string | null yes
remittanceInformationStructured object | null yes
callbackUrl string yes
timestamps object yes
consumer object | null yes Payer account as the bank reported it, in the form its rail uses: iban on EUR/SEK/RON, sortCode+accountNumber on GBP. The unused fields are null; a UK payer is never written to iban.
fee object | null yes
webhookDelivery object | null yes
retryCount integer yes

WebhookPayload

Outbound client webhook body (design 02 §5) — a fixed allowlist, and also the claim set of the x-mvp-signature JWT. Never contains credentials, webhook configuration, pricing snapshots or any internal field.

Field Type Required Description
event enum: payment.finality | payment.status.changed yes
mvpId string yes
clientId string yes
status PaymentStatus yes See PaymentStatus
failureReason FailureReason | null yes
isoStatus string | null yes
isoStatusReason string | null yes
isoStatusInferred boolean yes
amount string yes
currency Currency yes See Currency
beneficiary WebhookBeneficiary yes See WebhookBeneficiary
endToEndId string yes
remittanceInformationUnstructured string | null yes
remittanceInformationStructured object | null yes
brandName string yes
bankId string | null yes
bankName string | null yes
consumer WebhookConsumer yes See WebhookConsumer
fee object | null yes Present on INITIATED; null on FAILED
isSandbox boolean yes
timestamps object yes
timestamps.createdAt string no
timestamps.finalAt string | null no

Bank

One entry of GET /banks. Every key is always present; health and testJourney are null when empty, and always null for live.

Field Type Required Description
bankId string yes
name string yes
bic string yes
countries array of string yes
logoUrl string yes
rank integer yes
authModel enum: REDIRECT | DECOUPLED | EMBEDDED yes
requiresDebtorIban boolean yes Derived from the registry item's mandatoryFields. With currency — for the rail that currency derives (always false for GBP, which is addressed by sortCode + accountNumber, never an IBAN). Without currency — true when the bank needs a debtor account on ANY IBAN rail it carries; authoritative only for a currency-specific call, so re-query with currency before building a picker.
statusCeiling StatusCeiling yes See StatusCeiling
rails array of Rail yes
currencies array of Currency yes The currencies this bank settles.
health BankHealth | null yes Health is reported for the sandbox environment only; null for live (and for a bank with no health record).
testJourney TestJourney | null yes How to walk this bank's sandbox; null for live and where none is published.
sandboxOnly boolean yes True when the bank exists only as a sandbox — a test fixture such as a mock bank or a model bank, with no production counterpart. A sandbox-only bank is never listed for live. The same bankId names a bank in both environments.

SessionBank

One entry of the hosted page's session picker (/session/{mvpId}/banks) — the 9-key picker shape, always filtered for the payment's rail, currency and amount.

Field Type Required Description
bankId string yes
name string yes
bic string yes
countries array of string yes
logoUrl string yes
rank integer yes
authModel enum: REDIRECT | DECOUPLED | EMBEDDED yes
requiresDebtorIban boolean yes Derived from the registry item's mandatoryFields for the payment's rail
statusCeiling StatusCeiling yes See StatusCeiling

Rail

A payment rail a bank carries. Derived server-side from a payment's currency (EUR → SEPA_INSTANT; SEK/RON → EU_DOMESTIC_NON_EURO; GBP → UK_FASTER_PAYMENTS) and never an input. Append-only, like Currency: new rails may be added — do not switch exhaustively.

One of: SEPA_INSTANT · EU_DOMESTIC_NON_EURO · UK_FASTER_PAYMENTS

BankHealth

Sandbox connector calls to this bank over the last 24 hours. Health is reported for the sandbox environment only; null for live.

Field Type Required Description
state enum: HEALTHY | DEGRADED | DOWN | MAINTENANCE | NO_RECENT_TRAFFIC yes NO_RECENT_TRAFFIC — no sandbox connector calls in the window (or not yet measured), so no rate is reported. MAINTENANCE — the connection is paused by operations; no numbers are reported.
successRate24h number | null · min 0 · max 1 yes Share of successful connector calls, 0–1; null for MAINTENANCE and NO_RECENT_TRAFFIC.
p95LatencyMs number | null · min 0 yes 95th-percentile connector latency in milliseconds; null for MAINTENANCE and NO_RECENT_TRAFFIC.
samples24h integer · min 0 yes Connector calls in the window at the last evaluation.
evaluatedAt string (date-time) | null yes When the platform last evaluated this bank's health; null when not yet evaluated.

TestJourney

How a tester walks this bank's sandbox. Every value is published by the bank on a public page (sourceUrl) — or, for the platform's mock banks, by us (sourceUrl null). Every key is always present.

Field Type Required Description
summary string · ≤600 chars · ≥1 chars yes
scaPage enum: NONE | BUTTON | ACCOUNT_PICKER | LOGIN | LOGIN_OTP | OTP | APP yes What the tester meets at the bank (for an EMBEDDED bank, on our page) — nothing, a button, an account picker, a login, a login plus a one-time code, a one-time code, or an app.
credentials array of TestJourneyCredential · ≤6 items yes Bank-published sandbox personas; empty where the bank publishes none.
debtorAccount object yes What our payment page asks the payer for.
debtorAccount.form enum: NONE | IBAN | CHOSEN_AT_BANK yes Nothing, a debtor IBAN, or the account is chosen at the bank.
debtorAccount.values array of object · ≤6 items yes Example debtor IBANs (form IBAN only; never a GB IBAN).
debtorAccount.values[].label string yes
debtorAccount.values[].value string · pattern ^(?!GB)[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$ yes IBAN shape (no spaces), never a GB IBAN.
outcomes array of TestJourneyOutcome · 1–8 items yes
verification enum: BROWSER | API | BLOCKED yes How the journey has been proven — end to end in a browser, over the API only, or blocked at the bank's sandbox.
verificationNote string | null · ≤300 chars yes
sourceUrl string | null · ≤300 chars yes The bank's public page the values come from; null only for the platform's mock banks.
updatedAt string (date-time) yes

TestJourneyCredential

Field Type Required Description
label string · ≤80 chars · ≥1 chars yes
fields array of TestJourneyField · 1–6 items yes
note string | null · ≤300 chars yes

TestJourneyField

Field Type Required Description
name string · ≤40 chars · ≥1 chars yes
value string · ≤120 chars · ≥1 chars yes

TestJourneyOutcome

Field Type Required Description
scenario enum: SUCCESS | DECLINE | CANCEL | UPSTREAM_ERROR | TIMEOUT yes
how string · ≤300 chars · ≥1 chars yes What the tester does — for example the sandbox amount to pay.
endsIn enum: CREATED | BANK_SELECTED | PROCESSING | INITIATED | FAILED yes The PaymentStatus the scenario ends in.
failureReason FailureReason | null yes Set only when endsIn is FAILED.

StatusCeiling

A subset of PaymentStatus: the highest status this bank reports on the success path, for the caller's environment. Compare it directly against a payment's status — when they are equal the bank will report nothing further on the success path. INITIATED — poll or await the webhook to INITIATED | FAILED. PROCESSING — the bank accepts the initiation and then goes quiet; PROCESSING MUST NOT be treated as success, and an untouched payment will end FAILED with failureReason EXPIRED at expiresAt, which is a platform timeout, not a bank rejection. FAILED is reachable for every bank regardless of this value. New values may be added and will only ever be HIGHER than INITIATED (e.g. SETTLED) — treat an unrecognised value as reaching finality; do not switch exhaustively.

One of: PROCESSING · INITIATED

ScaState

Masked metadata ONLY — never credential or OTP values

Field Type Required Description
scaStatus enum: RECEIVED | PSU_AUTHENTICATED | METHOD_SELECTED | CHALLENGE_ISSUED | FINALISED | FAILED yes
credentialRequirements array of object no
credentialRequirements[].field string no
credentialRequirements[].label string no
credentialRequirements[].secret boolean no
scaMethods array of object no
scaMethods[].methodId string no
scaMethods[].type string no
scaMethods[].name string no Masked, e.g. SMS to ***6789
challenge object no
challenge.type string no
challenge.displayText string no
attempts integer no
attemptsRemaining integer no
attemptFailed boolean no Present (true) when the last submission was rejected but attempts remain

SelectBankRedirectResult

REDIRECT / DECOUPLED banks. REDIRECT banks always carry authorisationUrl; DECOUPLED banks (no bank URL exists) carry decoupled instead — the PSU authorises inside the bank's own app and the message is the only hand-off.

Field Type Required Description
mvpId string yes
authorisationUrl string no
decoupled object no
decoupled.message string yes The ASPSP's instruction to the payer, verbatim (it may be in the bank's local language).
authModel enum: REDIRECT | DECOUPLED yes
status const "PROCESSING" yes

SelectBankEmbeddedHostedResult

EMBEDDED banks, scaMode HOSTED — the cross-journey handoff

Field Type Required Description
mvpId string yes
authModel const "EMBEDDED" yes
scaMode const "HOSTED" yes
scaUrl string yes
status const "PROCESSING" yes

SelectBankEmbeddedApiResult

EMBEDDED banks, scaMode API — the client drives SCA

Field Type Required Description
mvpId string yes
authModel const "EMBEDDED" yes
scaMode const "API" yes
sca ScaState yes See ScaState
status const "PROCESSING" yes

SelectBankDeclinedResult

The bank refused the payment at initiation (a definitive ASPSP decline, e.g. an ineligible debtor account). The payment is already terminal FAILED — no authorisation hand-off exists and the payment.finality webhook has been enqueued.

Field Type Required Description
mvpId string yes
status const "FAILED" yes
failureReason FailureReason yes See FailureReason
authModel enum: REDIRECT | DECOUPLED | EMBEDDED yes

SessionSelectBankRedirectResult

Field Type Required Description
authorisationUrl string no
decoupled object no
decoupled.message string yes DECOUPLED banks only — the ASPSP's instruction to the payer, shown by the hosted page in place of a redirect.
authModel enum: REDIRECT | DECOUPLED yes
scaLeavesSandbox boolean no Present (always true) only on sandbox payments at banks whose stub sandbox returns an SCA redirect targeting the bank's live production login. The hosted page shows an interstitial instead of auto-redirecting the PSU to the authorisationUrl.

SessionSelectBankEmbeddedResult

Field Type Required Description
authModel const "EMBEDDED" yes
sca ScaState yes See ScaState

SessionSelectBankDeclinedResult

The bank refused the payment at initiation — already terminal FAILED.

Field Type Required Description
status const "FAILED" yes
failureReason FailureReason yes See FailureReason
authModel enum: REDIRECT | DECOUPLED | EMBEDDED yes

WebhookAccount

An account on a webhook payload. Exactly one form is populated and the other fields are null: iban on EUR/SEK/RON, sortCode+accountNumber on GBP. Always present as an object; never omitted.

Field Type Required Description
iban string,null yes
sortCode string,null · pattern ^[0-9]{6}$ yes
accountNumber string,null · pattern ^[0-9]{8}$ yes

WebhookBeneficiary

Field Type Required Description
name string · ≤70 chars yes
iban string,null yes
sortCode string,null · pattern ^[0-9]{6}$ yes
accountNumber string,null · pattern ^[0-9]{8}$ yes

WebhookConsumer

Field Type Required Description
name string,null yes
iban string,null yes
sortCode string,null · pattern ^[0-9]{6}$ yes
accountNumber string,null · pattern ^[0-9]{8}$ yes