Validation reference
When a payment is rejected, the reasons come back as a list of issues in error.details.issues, under 422 and under 400 alike, so one parse handles both. This is what the engine sends for a USD payment on a SEPA rail:
{
"code": "PAYMENT_TYPE_SEPA_EUR_ONLY",
"level": "payment-type",
"bank": "deutsche-bank-be",
"paymentType": "sepa",
"path": "payments[0].transactions[0].currency",
"message": "SEPA is a EUR scheme; got USD. Use \"international\" for non-EUR cross-border payments."
}
severityiswarningfor a warning and omitted for an error. An issue with noseverityblocks the payment; a warning never does.levelis the stage that raised the issue:generic,bankorpayment-type.bankandpaymentTypeappear when the rule belongs to one.
Validation has three levels and returns every issue at once. The exception is a schema failure: if the JSON does not match the canonical schema you get only GENERIC_SCHEMA_* issues, because nothing else can run.
- Generic rules: schema, currency, amount, dates, references.
- Bank rules: what one bank requires, such as a BIC or a national account-number check.
- Payment-type rules: what one payment type requires, such as SEPA being EUR-only.
A schema failure is 422 on BankConnector-Version: 2026-08-25 and later, and 400 on 2026-06-01 and with no version header (see the changelog). A body that is not JSON at all is 400. POST /api/validate/{bank} reports instead of refusing: it answers 200 with valid: false and the issues, and only an older version pin gets the 400 for a schema failure.
An unknown field is stripped, not rejected. The only signal is the warning UNKNOWN_FIELD_DROPPED, with the path of the field that was dropped.
POST /api/convert/{bank}?validate=true also validates the generated pain.001 against its schema. A failure is a 422 whose issues are XSD_INVALID.
Generic rules
| Code | Severity | Meaning |
|---|---|---|
GENERIC_SCHEMA_* | error | the JSON failed the canonical schema; one code per field, such as GENERIC_SCHEMA_AMOUNT_REQUIRED |
GENERIC_AMOUNT_POSITIVE | error | amount is 0 or less |
GENERIC_AMOUNT_PRECISION | error | a non-zero sub-unit the currency cannot hold ("1000.50" in JPY). "1000.00" is fine |
GENERIC_CURRENCY_CODE_INVALID | error | not an active ISO 4217 code |
GENERIC_COUNTRY_CODE_INVALID | error | a country on any party is not an assigned ISO 3166-1 alpha-2 code. UK is accepted and sent as GB |
GENERIC_PAYMENT_ID_DUPLICATE | error | paymentId used twice in one request |
GENERIC_END_TO_END_ID_DUPLICATE | error | endToEndId used twice in one request |
GENERIC_INSTRUCTION_ID_DUPLICATE | error | instructionId used twice in one request |
GENERIC_CREDITOR_REFERENCE_{RF,KID,OCR,OGM,VIITE,KENMERK}_INVALID | error | a typed creditor reference fails its own format or check digit |
GENERIC_IBAN_{FR,NL,BE}_* | error | a French, Dutch or Belgian IBAN fails the national structure or check key inside it |
GENERIC_IBAN_{NL,DE}_BANK_CODE_UNKNOWN | warning | the bank code in the IBAN is not on the national list; the payment is sent as given |
GENERIC_BIC_IBAN_COUNTRY_MISMATCH | warning | the BIC's country differs from the IBAN's |
GENERIC_EXECUTION_DATE_{PAST,FAR_FUTURE,NON_BANKING} | warning | the date is in the past, more than a year ahead, or not a banking day |
GENERIC_EXECUTION_DATE_AFTER_CUTOFF | warning | the date is today and the bank's published cut-off has passed (see below) |
GENERIC_MIXED_CURRENCY_BATCH | warning | more than one currency in one payment |
The id checks cover one request, not earlier ones. Banks de-duplicate on these ids and can silently drop a repeat, so the engine refuses it first. For repeat submissions across requests see Idempotency.
Date rules use the company's timezone, not UTC. A call that names no single company is judged in UTC.
Cut-off times
A bank stops taking same-day orders at a deadline it publishes. GENERIC_EXECUTION_DATE_AFTER_CUTOFF is a warning, never a refusal: it names the deadline and the current time in the bank's zone. It only exists for a bank whose cut-off BankConnector holds, so its absence does not mean you are inside the deadline. Check with your bank.
Profile warnings
A bank profile is BankConnector's description of one bank: its character set, field lengths and payment types.
| 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 replaced to fit the bank's character set (é becomes e) |
PROFILE_NAME_CHARS_DROPPED | warning | a mixed-script name lost characters with no Latin form ("王伟 Ltd" becomes "Ltd"); send the Latin name |
PROFILE_ID_CHARSET_INVALID | warning | an id has characters outside the SEPA set. Ids are sent unchanged, so the bank may reject or alter them |
PROFILE_NATIVE_CHARSET_UNSUPPORTED | warning | text outside the character set the bank's own documentation states; nothing is filtered |
SEPA_CHARSET_UNSUPPORTED | warning | a bank that does not transliterate would send non-SEPA characters verbatim on a SEPA payment |
SEPA_INSTRPRTY_IGNORED | warning | priority: "urgent" on a SEPA payment; the scheme has no such field, so it goes at normal speed or is refused |
SEPA_INSTRPRTY_URGENT_CLEARING | warning | priority: "urgent" on a SEPA payment at a bank whose guide reads it as a request for urgent clearing |
SEPA_RGLTRYRPTG_UNSUPPORTED | warning | regulatoryReporting on a SEPA payment; strict SEPA banks ignore or reject the element |
Two profile codes are errors:
PROFILE_NAME_UNREPRESENTABLE: a required name becomes empty after transliteration (CJK only, for example).SEPA_ID_SLASH_INVALID: on a SEPA payment, an id starts or ends with/or contains//.
Transliteration runs before truncation and can lengthen text (ä becomes ae), so a name that looks short can still be truncated.
Bank rules
Per-bank rules are not listed here. Each issue's own message names the bank's requirement, and GET /api/banks/{bank}/payment-types returns what that bank expects before you build against it. The codes you meet most:
BANK_BIC_REQUIRED: this bank requires the debtor BIC.BANK_DEBTOR_COUNTRY_REQUIRED: this bank requiresdebtor.country.BANK_BBAN_CHECKSUM_INVALID: a national account number fails its check digit.REGULATORY_REPORTING_REQUIRED(error): a cross-border payment out of Norway above NOK 100 000 or Sweden above SEK 150 000, in that currency, with noregulatoryReporting.details[].code. The rule and its code list come fromregulatoryReportingonGET /api/banks/{bank}/payment-types.REGULATORY_REPORTING_EXPECTED(warning): the same duty on a payment in a third currency. The engine holds no exchange rates and compares the bare number, so EUR 50 000 out of Norway raises nothing. Convert it yourself.BANK_ADDRESS_DERIVATION_*: only when the bank settings turn onderiveHybridAddress, which takes the postcode and town from an address line for DE, AT, FI, FR, NL, GB, JE, GG and IM. When it cannot do that with certainty the payment is refused and the issue says why. SupplyingpostalAddress.townNameavoids all of them.
Payment-type rules
Examples only; a bank offers about 26 types in all. GET /api/banks/{bank}/payment-types lists the ones it offers.
| Type | Key requirements |
|---|---|
sepa | creditor IBAN; EUR only; debtor account inside the SEPA area |
sepa-instant | as SEPA; no scheme maximum, only a bank's own published ceiling |
international | creditor account, and the creditor bank as a BIC or as a clearing id plus bank name and country |
fik (DK) | valid FIK reference and creditor number; DKK only |
kid (NO) | 4-25 characters with a MOD10 or MOD11 check; NOK only |
bankgiro / plusgiro (SE) | check-digit-validated number; bankgiro is SEK only |
bacs / chaps / faster-payment (UK) | 6-digit sort code and 8-digit account; GBP only; Faster up to GBP 1,000,000 |
ach (US) | 9-digit routing number with ABA checksum; USD only |
wire (US) | creditor bank by BIC alone, or by routing or clearing id with its name; ABA checksum only when a routing number is given; USD only |
ch-domestic / ch-qr (CH) | CH or LI creditor IBAN; CHF or EUR; QR adds a 27-digit reference check |
eft (CA) | routing and account; CAD only |
If you omit paymentType, the engine picks one: POST /api/convert/{bank} reports it in the X-Auto-Payment-Type header and POST /api/convert-async/{bank} in autoPaymentType. If it cannot (PAYMENT_TYPE_AUTO_UNRESOLVED) or the bank does not offer the type (PAYMENT_TYPE_NOT_OFFERED), you get an error; set the type explicitly when in doubt. PAYMENT_TYPE_SEPA_DEBTOR_OUTSIDE_SEPA means a SEPA payment's debtor IBAN is from outside the SEPA area; use international.
Where warnings are returned
A warning never blocks. It reports a file the bank will accept while doing something other than what you asked, such as sending an urgent payment at normal speed. Warnings arrive on success responses too:
| Call | Success response | Where the warnings are |
|---|---|---|
POST /api/validate/{bank} | 200 JSON | issues, mixed with any errors; filter on severity |
POST /api/journal/payments | 201 JSON | warnings, omitted when there are none |
POST /api/journal/payments/preview | 200 JSON | warnings |
POST /api/convert-async/{bank} | 202 JSON | warnings |
POST /api/convert/{bank} | 200 XML (the file) | the X-Validation-Warnings header |
warnings holds the same issue objects as issues, filtered to warnings. The convert header carries codes only, as CODE=count pairs: X-Validation-Warnings: SEPA_INSTRPRTY_IGNORED=1,PROFILE_NAME_TRUNCATED=3. For the messages and paths, post the same body to POST /api/validate/{bank}.
Handling issues in your client
// Rejected: a rule failure is 422, and a schema failure is 400 on an older version pin.
if (res.status === 422 || res.status === 400) {
const { error } = await res.json();
const issues = error.details?.issues ?? [];
const blocking = issues.filter((i: any) => i.severity !== "warning");
const warnings = issues.filter((i: any) => i.severity === "warning");
// Surface `blocking` to whoever owns the payment data; log `warnings`.
}
// Accepted: the payment went through; log anything it warned about.
if (res.status === 201) {
const { journalNo, warnings = [] } = await res.json();
for (const w of warnings) console.warn(`${journalNo} ${w.code} @ ${w.path}: ${w.message}`);
}
Treat errors as "fix and resubmit" and warnings as "worth logging". A truncation or non-banking-day warning is often the first sign of a data-quality problem upstream.
A bank with no rails refuses the payment
A bank profile with no payment types has no rail a payment could use. A payment to it is refused with the error PAYMENT_TYPE_BANK_OFFERS_NO_PAYMENT_TYPES and no file is produced. The fault is in the bank profile, not in your payment, and setting paymentType does not help. When a connection is created for such a bank, BankConnector emits a connection.no-payment-rails event once (see Receiving results).