Error catalogue
Every error uses the Berlin Group tppMessages envelope:
{
"tppMessages": [
{ "category": "ERROR", "code": "CONSENT_EXPIRED", "text": "The consent has expired." }
]
}
code is stable and safe to branch on. text is a short sentence for a human
and may change without notice — do not parse it.
path — which field was wrong
When a refusal is about a specific field in your request body, we name it:
{
"tppMessages": [
{
"category": "ERROR",
"code": "FORMAT_ERROR",
"path": "creditorAccount.iban",
"text": "creditorAccount.iban is not a valid IBAN."
}
]
}
Dot-notated, matching the request body. code tells you what kind of problem
it is; path tells you where. Between them a FORMAT_ERROR is actionable —
you can highlight the offending field for your own user — without reading
text, which you are told not to parse.
path is absent when no single field is at fault, and that absence is
meaningful rather than an omission. A refusal that names no field is one you
cannot fix by correcting a value: a compliance outcome, a header problem, a
state conflict, or a PAYMENT_FAILED whose cause is deliberately not
disclosed. Treat “no path” as “do not retry a variation of this request”.
Codes we emit
| HTTP | Code | When | What to do |
|---|---|---|---|
| 400 | FORMAT_ERROR |
A malformed request: missing X-Request-ID, a bad IBAN, an unparseable amount or date |
Fix the request. The text says what is wrong; this class of error is safe to surface to your own logs. |
| 400 | PRODUCT_INVALID |
A currency other than EUR | We are EUR-only. Do not retry. |
| 400 | PRODUCT_UNKNOWN |
A payment product we do not offer | Use sepa-credit-transfers or instant-sepa-credit-transfers. |
| 400 | SERVICE_INVALID |
A bank-offered global access flag (availableAccounts, allPsd2) |
Request dedicated accounts by IBAN instead. |
| 400 | PAYMENT_FAILED |
Initiation refused | See below — the cause is deliberately not disclosed. |
| 401 | CERTIFICATE_MISSING |
No client certificate reached us | Check your mTLS configuration and that you are on the XS2A host. |
| 401 | CERTIFICATE_INVALID |
The certificate could not be parsed or validated | Check the chain and that the QWAC is intact. |
| 401 | CERTIFICATE_EXPIRED |
Outside its validity window | Rotate. Do not wait for expiry — see certificates. |
| 401 | CONSENT_EXPIRED |
Past validUntil |
Create a new consent and re-authenticate the customer. |
| 401 | CONSENT_INVALID |
The consent does not cover this request: wrong scope, wrong account, or no longer active | Re-read the consent. If it was revoked, ask the customer for a new one. |
| 403 | CONSENT_UNKNOWN |
No consent addressable under that identifier for you | Do not retry with variations — see below. |
| 403 | ROLE_INVALID |
Your certificate lacks the PSD2 role this service needs | Nothing to retry; the role is in the certificate. |
| 403 | SERVICE_BLOCKED |
Your registration is not currently permitted | See certificates. |
| 404 | RESOURCE_UNKNOWN |
No such payment or authorisation for you | Check the identifier. Consents never 404 — see below. |
| 409 | REQUEST_ID_REUSED |
You reused an X-Request-ID with a different request body |
Generate a fresh UUID. We refuse rather than returning the first result, so a changed instruction is never mistaken for a completed one. Remembered for 48 hours. |
| 405 | CANCELLATION_INVALID |
The payment can no longer be cancelled | The funds have moved. Nothing to retry. |
| 429 | ACCESS_EXCEEDED |
The four-per-day unattended cap is reached | Wait for the daily reset, or make the request attended by sending PSU-IP-Address when the customer is genuinely present. |
Refusals that are identical on purpose
Three pairs are byte-identical, and no amount of retrying will separate them:
- An unknown consent id and another provider’s consent — both
CONSENT_UNKNOWN. - An account outside the consent and an account belonging to someone else —
both
CONSENT_INVALID. - Every payment-initiation failure — insufficient funds, unknown debtor
account, a creditor IBAN we cannot route to, a creditor account belonging to
the debtor’s own customer, limit breach, compliance hold — all
PAYMENT_FAILEDwith the same sentence.
Each pair would otherwise be an oracle: ask a question, learn a fact about a customer that no consent covers. Treat them as one condition.
Consents never return 404
403 CONSENT_UNKNOWN subsumes “not found” on every consent operation. An id
that never existed and one belonging to another provider answer identically —
which is the whole point, since a distinguishable 404 would let you enumerate
which consent ids are real.
So the consent operations declare no 404 at all, and a 404 branch on them is
dead code. The only 404 in this interface is RESOURCE_UNKNOWN on a payment
or on the templated authorisation read, where the id is one you were given
rather than one you could guess.
Distinguishing our failures from your refusals
A 4xx means the interface understood you and said no. A 5xx means we broke.
Only 5xx counts against our published availability figures, and your alerting
should draw the same line — a rise in CONSENT_EXPIRED is a renewal problem in
your integration, not an outage in ours.
| Code | Status | Meaning |
|---|---|---|
INTERNAL_SERVER_ERROR |
500 | We failed. Says nothing about your request — replay it unchanged after a backoff. |
Do not require a 5xx to parse. The application emits the usual
tppMessages envelope, but a failure above it — load balancer, TLS
termination, a timeout that never reaches the application — produces whatever
that layer emits, which may be HTML or empty. 502, 503 and 504 mean the same
thing as 500 and are handled the same way. Branch on the status code.
Retry guidance
| Situation | Retry? |
|---|---|
| 5xx or a connection failure | Yes, with backoff. Payment initiation is idempotent on X-Request-ID. |
ACCESS_EXCEEDED |
Not today, unless the request becomes attended. |
| Any other 4xx | No. Fix the request, the consent, or the certificate. |