Validation Reference
Audience: 🔌 Integration Client
When a payment is rejected 422, the reasons come back as a list of issues in error.details.issues. Each issue looks like:
{
"code": "PAYMENT_TYPE_SEPA_EUR_ONLY",
"level": "payment-type",
"severity": "error",
"message": "SEPA payments must be in EUR.",
"path": "payments[0].transactions[0].currency"
}
severityiserror(blocks) orwarning(informational — the payment still proceeds). It's omitted when it would beerror(the default); checkvalid/ the absence oferror-severity issues to know if a payment can proceed.leveltells you which stage raised it:generic,bank, orpayment-type.
Validation runs in four levels, and stops at the first structural failure:
- Structural (schema) — if the JSON doesn't match the canonical schema, you get only
GENERIC_SCHEMAissues and nothing else runs. This is the 400/422 line: malformed body →400; schema-invalid canonical payment →422. - Generic rules — currency, amount, dates, references (below).
- Bank rules — per-bank requirements (BIC required, national account checksums, etc.).
- Payment-type rules — per scheme (SEPA EUR-only, KID checksum, etc.).
Generic rules (apply to every payment)
| Code | Severity | Meaning |
|---|---|---|
GENERIC_SCHEMA | error | JSON failed the canonical schema |
GENERIC_AMOUNT_POSITIVE | error | amount ≤ 0 (so "0.00" fails here) |
GENERIC_AMOUNT_PRECISION | error | more decimals than the currency allows (JPY/ISK 0, KWD 3) |
GENERIC_CURRENCY_CODE_INVALID | error | not an active ISO 4217 code |
GENERIC_COUNTRY_CODE_INVALID | error | debtor country not ISO 3166 |
GENERIC_PAYMENT_ID_DUPLICATE | error | paymentId reused across payments |
GENERIC_END_TO_END_ID_DUPLICATE | error | endToEndId reused in the batch |
GENERIC_CREDITOR_REFERENCE_RF_INVALID | error | RF reference fails ISO 11649 mod-97 |
GENERIC_CREDITOR_REFERENCE_KID_INVALID | error | KID fails MOD10/MOD11 |
GENERIC_BIC_IBAN_COUNTRY_MISMATCH | warning | BIC country ≠IBAN country |
GENERIC_EXECUTION_DATE_PAST | warning | date is in the past |
GENERIC_EXECUTION_DATE_FAR_FUTURE | warning | more than a year ahead |
GENERIC_EXECUTION_DATE_NON_BANKING | warning | weekend/holiday; suggests next banking day |
GENERIC_MIXED_CURRENCY_BATCH | warning | more than one currency in one payment |
The duplicate-id errors matter: banks de-duplicate on paymentId and endToEndId and may silently drop repeats, so BankConnector blocks them up front. Keep them unique within a batch.
Profile warnings (character set / truncation)
| Code | Severity | Meaning |
|---|---|---|
PROFILE_NAME_TRUNCATED | warning | a name was shortened to the bank's limit |
PROFILE_REMITTANCE_TRUNCATED | warning | remittance text was shortened |
PROFILE_CHARSET_TRANSLITERATED | warning | characters were transliterated to the SEPA set |
PROFILE_NAME_UNREPRESENTABLE | error | a required name transliterates to empty (e.g. CJK-only) |
Remember the order: transliterate, then truncate. Transliteration can expand text (ä→ae, ß→ss), so a name that looks short can still truncate.
Bank-rule examples
Different banks require different things. Common ones:
BANK_BIC_REQUIRED— this bank requires the debtor BIC.BANK_BIC_VALUE_INVALID— the debtor BIC doesn't match the expected institution.BANK_INITIATING_PARTY_ID_REQUIRED— aninitiatingParty.organisationIdis required.BANK_DEBTOR_COUNTRY_REQUIRED/BANK_DEBTOR_ACCOUNT_CURRENCY_REQUIRED.BANK_BBAN_FORMAT_INVALID/BANK_BBAN_CHECKSUM_INVALID— national account number checks (Norwegian MOD-11, Danish structure, Polish NRB mod-97).
Query GET /banks/<bank>/payment-types and the bank's setup info to see what a given bank expects before you build against it.
Payment-type rules (by scheme)
| Type | Key requirements |
|---|---|
sepa | creditor IBAN required; EUR only |
sepa-instant | as SEPA + amount ≤ €100,000 |
international | creditor account and creditor BIC required |
fik (DK) | valid FIK reference (kortart 71 + Luhn) |
kid (NO) | 2–25 digits, MOD10/MOD11 |
bankgiro / plusgiro (SE) | Luhn-checked, digit-length bounded |
bacs / chaps / faster-payment (UK) | 6-digit sort code + 8-digit account; GBP only; Faster ≤ £1,000,000 |
ach / wire (US) | 9-digit routing + ABA checksum; USD only |
ch-domestic / ch-qr (CH) | creditor IBAN + CHF/EUR; QR adds a 27-digit reference check |
eft (CA) | routing + account; CAD only |
If you omit paymentType, the engine attempts to infer it and returns autoSelected in the response. If it can't (PAYMENT_TYPE_AUTO_UNRESOLVED) or the type isn't offered by that bank (PAYMENT_TYPE_NOT_OFFERED), you get an error — set it explicitly when in doubt.
Handling issues in your client
if (res.status === 422) {
const { error } = await res.json();
const blocking = (error.details?.issues ?? []).filter((i: any) => i.severity !== "warning");
const warnings = (error.details?.issues ?? []).filter((i: any) => i.severity === "warning");
// Surface `blocking` to whoever owns the payment data; log `warnings`.
}
Treat errors as "fix and resubmit," and warnings as "worth logging, but it went through." Don't discard warnings — a truncation or non-banking-day warning is often the first sign of a data-quality issue upstream.