Build / API reference
API reference
Every endpoint an integration writes code against: discovery, conversion, the journal, accounts, and webhooks, generated from the OpenAPI spec. Base URL https://sandbox.bankconnector.com; the sandbox demo bank never moves real money by construction, and a real bank connection is only money-safe if your company is provisioned sandboxOnly (see Going live). One-time operator setup lives in the Operator API reference.
Download the spec: openapi.json, openapi.yaml. The full spec covers both tracks.
Discovery
Explore what banks and payment types are available
/profilesReturns every registered bank, its ISO 20022 version, and the codes of the payment types available for its country. Resolve a code against GET /payment-types for the whole catalog, or GET /banks/{bankKey}/payment-types for one bank's entries with their ISO encoding.
| Status | Description |
|---|---|
| 200 | List of bank profiles |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"key": "string",
"bankName": "string",
"formerNames": [
"string"
],
"acceptedDebtorBics": [
"string"
],
"countryCode": "string",
"painVersion": "string",
"evidenceTier": "live",
"unsupported": {
"reason": "string",
"contactSupport": false
},
"brand": "string",
"paymentTypes": [
"string"
]
}
]
}curl -X GET https://your-host/api/profiles \
-H 'X-API-Key: YOUR_KEY'const res = await fetch("https://your-host/api/profiles", {
method: "GET",
headers: {
"X-API-Key": "YOUR_KEY",
},
});
const data = await res.json();import requests
resp = requests.request(
"GET", "https://your-host/api/profiles",
headers={"X-API-Key": "YOUR_KEY"},
)
resp.raise_for_status()
data = resp.json()/profiles/membersGermany's Sparkassen and cooperative banks (Volksbanken, Raiffeisenbanken) are served by two group profiles, sparkasse-de and volksbank-raiffeisenbank-de, except the members with a profile of their own. Each item is one institution with its name, seat, BIC and bank codes, and bankKey, the profile that serves it. A Sparkasse also carries ebics, its EBICS server and, where known, its Host ID. Built from the Bundesbank bank-code file, the DSGV list of Sparkassen and BaFin's register of credit institutions; an institution that is not listed is not served by a group profile.
| Status | Description |
|---|---|
| 200 | Every member institution |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"name": "string",
"town": "string",
"bic": "string",
"bankCode": "string",
"otherBankCodes": [
"string"
],
"formerBankCodes": [
"string"
],
"countryCode": "string",
"bankKey": "string",
"ebics": {
"url": "string",
"hostId": "string"
}
}
]
}/payment-typesAll payment types in the shared catalog with ISO 20022 encoding, descriptions, and requirements.
| Status | Description |
|---|---|
| 200 | Full catalog |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"code": "string",
"label": "string",
"description": "string",
"requires": [
"string"
],
"channel": "string",
"isoEncoding": {
"serviceLevel": "string",
"localInstrument": "string",
"categoryPurpose": "string"
}
}
]
}/banks/{bankKey}/payment-types| Name | In | Required | Description |
|---|---|---|---|
bankKey BankKey | path | yes |
| Status | Description |
|---|---|
| 200 | Bank key/name + offered payment types with full metadata |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | Unknown bank |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"code": "string",
"label": "string",
"description": "string",
"requires": [
"string"
],
"channel": "string",
"isoEncoding": {
"serviceLevel": "string",
"serviceLevelProprietary": "string",
"localInstrument": "string",
"localInstrumentProprietary": "string",
"categoryPurpose": "string"
}
}
],
"bankKey": "string",
"bankName": "string",
"countryCode": "string",
"regulatoryReporting": {
"country": "string",
"authority": {
"name": "string",
"country": "string"
},
"register": "string",
"threshold": {
"amount": 0,
"currency": "string"
},
"scope": "string",
"codeDigits": 0,
"codeList": {
"publication": "string",
"url": "string",
"codes": [
{
"code": "string",
"label": "string"
}
]
},
"infoText": "expected",
"summary": "string",
"sources": [
"string"
]
}
}/banking-days/adjustReturns date unchanged with moved: false when it is a banking day for this bank and currency, and otherwise the nearest banking day in direction, with why the requested date is closed. A banking day is one the payment preview raises no calendar warning for: not a weekend, not a bank holiday in the bank's country, and open in the currency's settlement calendar (TARGET2 for EUR) and, when creditorCountry is given, in that country's. next is the first later date that clears all of them, the date the preview's own GENERIC_EXECUTION_DATE_NON_BANKING warning suggests for the same bank, currency and creditor; previous is the banking day before, for a date that must not be overshot, such as the last day of a cash discount. The date is never clamped to today. Needs banks:read on an application install's grant.
| Name | In | Required | Description |
|---|---|---|---|
bankKey string | query | yes | The debtor's bank, as `GET /profiles` lists it (e.g. `danske-dk`). Its country's bank holidays close the day. |
currency string | query | yes | ISO 4217 code of the payment currency, upper case. EUR adds the TARGET2 calendar; DKK, SEK, NOK, GBP and CHF add their own country's. |
date string | query | yes | The payment date to check, `YYYY-MM-DD`, between 2000-01-01 and 2099-12-31. |
direction enum | query | yes | Which way to move a date that is not a banking day: `next` for the first banking day after it, `previous` for the last one before it (a cash-discount last day). A banking day is returned unchanged either way. |
creditorCountry string | query | no | ISO 3166-1 alpha-2 country of the creditor's account (the IBAN's first two letters), upper case. Optional: when given, that country's bank holidays close the day too, as they do at preview. |
| Status | Description |
|---|---|
| 200 | The banking day |
| 400 | A parameter is missing or invalid (`invalid_parameter`): an unknown `bankKey`, a currency or country that is not an upper-case ISO code, a date that is not a real `YYYY-MM-DD` day between 2000 and 2099, or a `direction` other than `next`/`previous`. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"date": "string",
"moved": false,
"reason": "weekend",
"holidayName": "string"
}/banks/liveOne item per bank and environment where the organisation has at least one active bank connection. A connection that is deactivated or deleted drops its bank from the list on the next read, and a connection activated today is listed before any statement has arrived. A bank live in both test and production is listed twice. The items carry the bank key, its name and the environment only, never the connection itself. Needs banks:read on an application install's grant; an install reads its own organisation and needs no companyId.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The live banks, ordered by bank key and then environment |
| 400 | No organisation to read: `companyId` is required for this caller (`scope_required`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"bankKey": "string",
"bankName": "string",
"environment": "test",
"live": false
}
]
}/banks/{bankKey}/status| Name | In | Required | Description |
|---|---|---|---|
bankKey BankKey | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | Bank support status |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | Unknown bank |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"bankKey": "string",
"deliveryMode": "converted-only",
"hostToHost": false
}Conversion
Validate and convert canonical payment JSON
/validate/{bankKey}Runs all three validation levels (generic → bank → payment-type) and returns every issue at once. Always returns HTTP 200: check valid in the response body. Use this to check a payment before sending it.
| Name | In | Required | Description |
|---|---|---|---|
bankKey BankKey | path | yes | |
companyId string | query | no | The company whose timezone the execution-date rules judge in. Optional: without it, and without a selected company, those rules use UTC. |
| Property | Type | Required | Description |
|---|---|---|---|
schemaVersion | enum | no | Canonical contract version; defaults to the current version when omitted. |
messageId | string | yes | |
creationDateTime | string | no | |
initiatingParty | object | yes | |
payments | Payment[] | yes |
{
"schemaVersion": "1.0",
"messageId": "MSG-20260601-0001",
"creationDateTime": "2026-06-01T10: 30: 00Z",
"initiatingParty": {
"name": "Acme Corp ApS",
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
},
"secondOrganisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"payments": [
{
"paymentId": "PMT-0001",
"paymentType": "sepa",
"priority": "normal",
"chargeBearer": "DEBT",
"localInstrument": "string",
"localInstrumentProprietary": "string",
"categoryPurpose": "string",
"executionDate": "2026-06-04",
"debtor": {
"name": "Acme Corp ApS",
"country": "DK",
"postalAddress": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string",
"countrySubDivision": "string",
"addressLines": []
},
"account": {
"iban": "DK5000400440116243",
"currency": "EUR",
"other": {},
"rollNumber": "ROLL-1234567"
},
"agent": {
"bic": "DEUTDEFF",
"clearing": {},
"name": "string",
"country": "DE"
},
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"serviceUserNumber": "123456",
"ultimateDebtor": {
"name": "Acme Subsidiary GmbH",
"country": "DE",
"postalAddress": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string",
"countrySubDivision": "string",
"addressLines": []
},
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"transactions": [
{
"endToEndId": "INV-2026-588",
"instructionId": "string",
"amount": "1500.00",
"currency": "EUR",
"creditor": {},
"ultimateDebtor": {},
"ultimateCreditor": {},
"instructionForDebtorAgent": "string",
"instructionForCreditorAgent": "string",
"remittance": {},
"purposeCode": "GDDS",
"regulatoryReporting": {},
"splitPayment": {},
"hmrcRtiReference": "AB1",
"resendOf": {}
}
]
}
]
}| Status | Description |
|---|---|
| 200 | Validation outcome (valid or not: check `valid`) |
| 400 | Malformed JSON body |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | Unknown bank key |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"valid": false,
"issues": [
{
"code": "string",
"level": "generic",
"severity": "error",
"message": "string",
"path": "string",
"bank": "string"
}
]
}curl -X POST https://your-host/api/validate/nordea-dk \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]}'const res = await fetch("https://your-host/api/validate/nordea-dk", {
method: "POST",
headers: {
"X-API-Key": "YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]}),
});
const data = await res.json();import requests
resp = requests.request(
"POST", "https://your-host/api/validate/nordea-dk",
headers={"X-API-Key": "YOUR_KEY"},
json={"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]},
)
resp.raise_for_status()
data = resp.json()/convert/{bankKey}Validates the input (all three levels) then converts it to the bank's specific ISO 20022 pain.001 format. Returns XML on success. Add ?validate=true to additionally validate the generated XML against the official ISO 20022 XSD. This is the stateless engine. It does not apply per-company bank settings (charge bearer, execution-date offset). Pass ?painVersion= to choose the output version here; per-company settings are applied only on the delivery path, POST /journal/payments. Use that route to see a payment exactly as it will be submitted.
| Name | In | Required | Description |
|---|---|---|---|
bankKey BankKey | path | yes | |
validate boolean | query | no | If true, XSD-validate the generated XML before returning it. |
painVersion string | query | no | The pain.001 version to produce, where the bank supports more than one. A version the bank does not support is a 400 `unsupported_pain_version` listing the supported ones in `details.supported`. The sandbox ignores it: every file it builds is pain.001.001.09, whatever the bank. |
companyId string | query | no | The company whose timezone the execution-date rules judge in. Optional: without it, and without a selected company, those rules use UTC. |
| Property | Type | Required | Description |
|---|---|---|---|
schemaVersion | enum | no | Canonical contract version; defaults to the current version when omitted. |
messageId | string | yes | |
creationDateTime | string | no | |
initiatingParty | object | yes | |
payments | Payment[] | yes |
{
"schemaVersion": "1.0",
"messageId": "MSG-20260601-0001",
"creationDateTime": "2026-06-01T10: 30: 00Z",
"initiatingParty": {
"name": "Acme Corp ApS",
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
},
"secondOrganisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"payments": [
{
"paymentId": "PMT-0001",
"paymentType": "sepa",
"priority": "normal",
"chargeBearer": "DEBT",
"localInstrument": "string",
"localInstrumentProprietary": "string",
"categoryPurpose": "string",
"executionDate": "2026-06-04",
"debtor": {
"name": "Acme Corp ApS",
"country": "DK",
"postalAddress": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string",
"countrySubDivision": "string",
"addressLines": []
},
"account": {
"iban": "DK5000400440116243",
"currency": "EUR",
"other": {},
"rollNumber": "ROLL-1234567"
},
"agent": {
"bic": "DEUTDEFF",
"clearing": {},
"name": "string",
"country": "DE"
},
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"serviceUserNumber": "123456",
"ultimateDebtor": {
"name": "Acme Subsidiary GmbH",
"country": "DE",
"postalAddress": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string",
"countrySubDivision": "string",
"addressLines": []
},
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"transactions": [
{
"endToEndId": "INV-2026-588",
"instructionId": "string",
"amount": "1500.00",
"currency": "EUR",
"creditor": {},
"ultimateDebtor": {},
"ultimateCreditor": {},
"instructionForDebtorAgent": "string",
"instructionForCreditorAgent": "string",
"remittance": {},
"purposeCode": "GDDS",
"regulatoryReporting": {},
"splitPayment": {},
"hmrcRtiReference": "AB1",
"resendOf": {}
}
]
}
]
}| Status | Description |
|---|---|
| 200 | Bank-specific pain.001 XML. A payment that is VALID but raised non-blocking warnings still converts, warnings never block, and its response carries the `X-Validation-Warnings` header. |
| 400 | Malformed JSON body |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | Unknown bank key |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Validation failed: returns all issues at once |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
curl -X POST https://your-host/api/convert/nordea-dk \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]}'const res = await fetch("https://your-host/api/convert/nordea-dk", {
method: "POST",
headers: {
"X-API-Key": "YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]}),
});
const data = await res.text(); // pain.001 XMLimport requests
resp = requests.request(
"POST", "https://your-host/api/convert/nordea-dk",
headers={"X-API-Key": "YOUR_KEY"},
json={"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]},
)
resp.raise_for_status()
data = resp.text # pain.001 XML/convert-async/{bankKey}Non-blocking version of POST /convert/{bankKey}: validates the input, enqueues a background conversion, and returns 202 with a jobId immediately. Poll GET /convert-jobs/{id} for the result. ?validate=true is not supported here (the background worker cannot run the XSD). Use the synchronous /convert/{bankKey}?validate=true instead.
| Name | In | Required | Description |
|---|---|---|---|
bankKey BankKey | path | yes | |
validate boolean | query | no | Not available on the async path: `true` is a 400 `validate_unsupported_on_async`. Use `POST /convert/{bankKey}?validate=true` for an XSD-validated conversion. |
painVersion string | query | no | The pain.001 version to produce, where the bank supports more than one. A version the bank does not support is a 400 `unsupported_pain_version` listing the supported ones in `details.supported`. The sandbox ignores it: every file it builds is pain.001.001.09, whatever the bank. |
companyId string | query | no | The company whose timezone the execution-date rules judge in. Optional: without it, and without a selected company, those rules use UTC. |
| Property | Type | Required | Description |
|---|---|---|---|
schemaVersion | enum | no | Canonical contract version; defaults to the current version when omitted. |
messageId | string | yes | |
creationDateTime | string | no | |
initiatingParty | object | yes | |
payments | Payment[] | yes |
{
"schemaVersion": "1.0",
"messageId": "MSG-20260601-0001",
"creationDateTime": "2026-06-01T10: 30: 00Z",
"initiatingParty": {
"name": "Acme Corp ApS",
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
},
"secondOrganisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"payments": [
{
"paymentId": "PMT-0001",
"paymentType": "sepa",
"priority": "normal",
"chargeBearer": "DEBT",
"localInstrument": "string",
"localInstrumentProprietary": "string",
"categoryPurpose": "string",
"executionDate": "2026-06-04",
"debtor": {
"name": "Acme Corp ApS",
"country": "DK",
"postalAddress": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string",
"countrySubDivision": "string",
"addressLines": []
},
"account": {
"iban": "DK5000400440116243",
"currency": "EUR",
"other": {},
"rollNumber": "ROLL-1234567"
},
"agent": {
"bic": "DEUTDEFF",
"clearing": {},
"name": "string",
"country": "DE"
},
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"serviceUserNumber": "123456",
"ultimateDebtor": {
"name": "Acme Subsidiary GmbH",
"country": "DE",
"postalAddress": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string",
"countrySubDivision": "string",
"addressLines": []
},
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"transactions": [
{
"endToEndId": "INV-2026-588",
"instructionId": "string",
"amount": "1500.00",
"currency": "EUR",
"creditor": {},
"ultimateDebtor": {},
"ultimateCreditor": {},
"instructionForDebtorAgent": "string",
"instructionForCreditorAgent": "string",
"remittance": {},
"purposeCode": "GDDS",
"regulatoryReporting": {},
"splitPayment": {},
"hmrcRtiReference": "AB1",
"resendOf": {}
}
]
}
]
}| Status | Description |
|---|---|
| 202 | Conversion enqueued. `warnings` is present only when the (valid) payment raised non-blocking warnings: they are returned here, on the enqueue, rather than on the job poll, whose success body is the XML file. |
| 400 | Malformed JSON body, ?validate=true (unsupported on the async path), or unsupported painVersion |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | The credential carries no organisation scope, so the job could not be fenced to an owner and the conversion is refused. An API key bound to an organisation is the async path's credential; the synchronous POST /convert/{bankKey} returns the XML directly and stores nothing. |
| 404 | Unknown bank key |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Validation failed: returns all issues at once |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"jobId": "string",
"status": "pending",
"bankKey": "string",
"autoPaymentType": "string",
"unknownFieldsDropped": [
"string"
],
"warnings": [
{
"code": "string",
"level": "generic",
"severity": "error",
"message": "string",
"path": "string",
"bank": "string"
}
]
}/convert-jobs/{id}Returns the status of a job created by POST /convert-async/{bankKey}. While pending/running the body carries the job status; once succeeded it returns the converted pain.001 XML. A job is readable only by the tenant that created it (others get 404; never a cross-tenant leak).
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes |
| Status | Description |
|---|---|
| 200 | Terminal state: the converted XML once the job has succeeded. A permanently-failed (dead) job returns 200 with a `failure` object. Not the reserved `error` envelope key, which always means the REQUEST itself failed. |
| 202 | Job still pending or running, poll again. Same job-status body as the 200. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | Conversion job not found (or not owned by the caller) |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"jobId": "string",
"status": "pending",
"createdAt": "string",
"updatedAt": "string",
"failure": {
"code": "string",
"message": "string",
"details": {
"lastError": "string"
}
}
}/payment-types/suggestInfers which of the bank's payment types fits one canonical payment, from its currency, amount, creditor country and urgency. Returns the suggested code (selected, null when nothing fits), a confidence of high, medium or low, a plain-language reason and the other viable types in alternatives. Nothing is stored and no payment is created. A body that is not JSON answers 400; a payment with the wrong structure answers 422 on the current API version and 400 on older ones.
| Property | Type | Required | Description |
|---|---|---|---|
bankKey | string | yes | |
payment | object | yes |
{
"bankKey": "string",
"payment": {
"debtor": {
"country": "string",
"account": {
"iban": "string",
"other": {
"id": "string",
"scheme": "string"
}
},
"agent": {
"clearing": {
"system": "string"
}
}
},
"transactions": [
{
"amount": "string",
"currency": "string",
"creditor": {
"country": "string",
"account": {},
"agent": {}
},
"remittance": {
"creditorReference": {}
}
}
]
}
}| Status | Description |
|---|---|
| 200 | The suggestion |
| 400 | `validation_failed`: the body is not JSON, or `bankKey` or `payment` is missing |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | `bank_not_found`: no bank with that key |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | `validation_failed`: `payment` is structurally wrong |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"bankKey": "string",
"selected": "string",
"confidence": "high",
"reason": "string",
"alternatives": [
{
"code": "string",
"reason": "string"
}
]
}Journal
Submit payments + read the journal: the ERP integration surface (X-API-Key)
/journal/paymentsThe primary ERP integration call. Validates and converts the canonical payment for bankKey, records it in the journal, and routes it through approval and delivery. Send only companyId, bankKey and payment.
On host-to-host, payment.messageId is the payment's identity whether or not you also send an Idempotency-Key: the server keys the submission on msgid:<messageId>, so a new key never turns a messageId it has already seen into a second payment. An application install paired for an ERP company (erp_company_id at GET /oauth/authorize) is checked across every install of that company, not the organisation, so another company of it may use the same messageId; a messageId the organisation-wide check holds is still refused for it. An Idempotency-Key is mandatory for host-to-host only where there is no messageId to key on (a missing key then returns 400 idempotency_key_required). Send a unique key per logical payment so a network-timeout retry cannot double-submit. A retry with the same key and body is answered from the recorded payment (same record, xml and deliveryMode, current status) with the header Idempotency-Replayed: true. The per-run reports (approval, delivery, warnings, autoFilled, unknownFieldsDropped) are not guaranteed on a retry; read the document or its approval by journalNo for those. The same key with a different body → 409. A re-submission under a payment.messageId whose original payment is rejected, cancelled or rejected-by-approver is refused with 409 message_id_already_used; issue a new messageId. Requires authentication (X-API-Key for ERP integrations).
| Name | In | Required | Description |
|---|---|---|---|
Idempotency-Key string | header | no | Unique per company per logical payment. On host-to-host, `payment.messageId` is the payment's identity whether or not this key is sent. Mandatory only where there is no `messageId` (else 400 `idempotency_key_required`). Same key + same body is answered from the recorded payment (same record, `xml`, `deliveryMode`, current `status`, `Idempotency-Replayed: true`). The per-run reports (`approval`, `delivery`, `warnings`, `autoFilled`, `unknownFieldsDropped`) are not guaranteed on a retry; read the document or its approval by `journalNo` for those. Same key + different body → 409. |
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | The company this payment belongs to. |
bankKey | BankKey | yes | |
payment | PaymentInstruction | yes | |
environment | enum | no | Which connection delivers the payment. Defaults to production; pass "test" to dispatch over the bank's test channel. |
validate | boolean | no | Also XSD-validate the generated XML. |
idempotencyKey | string | no | Alternative to the Idempotency-Key header. |
maker | object | no | Who initiated this payment, for a session-less (API-key/ERP) caller only. A signed-in session's maker identity is always the session user and this is ignored for it. `reference` is resolved against this company's users; an unresolved reference is kept only as a display label and is not a verified maker for approval purposes. |
source | object | no | The ERP journal this payment came from, as your ERP names it, shown beside the payment's own id on the Payments list and the approval screen, e.g. "PAY-0031 · Payments 2026-09-28". Display only: it is not part of the payment and not part of the idempotency comparison, so a retry of the same payment under a renamed journal replays the original (it never answers 409). Optional; either field may be omitted. |
erpNotes | object[] | no | Facts your ERP records about this batch that an auditor must be able to read later. `payee-account-changed`: the payee's bank account differs from the one their last payment went to, and your ERP showed that warning to `shownTo` at `shownAt`. Each note is written into the payment's hash-chained `payment.converted` journal event and onto its approval request, where the approver sees it. `from` and `to` are recorded masked to their first two and last four characters. Never part of the payment: not in the bank file and not part of the idempotency comparison, so a retry with different notes replays the original answer. Optional. Text fields are trimmed, 1-140 characters, with no control, bidirectional-override or invisible (zero-width, LRM/RLM/ALM) characters; a note that breaks a rule is a 400. |
{
"companyId": "string",
"bankKey": "nordea-dk",
"payment": {
"schemaVersion": "1.0",
"messageId": "MSG-20260601-0001",
"creationDateTime": "2026-06-01T10: 30: 00Z",
"initiatingParty": {
"name": "Acme Corp ApS",
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
},
"secondOrganisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"payments": [
{
"paymentId": "PMT-0001",
"paymentType": "sepa",
"priority": "normal",
"chargeBearer": "DEBT",
"localInstrument": "string",
"localInstrumentProprietary": "string",
"categoryPurpose": "string",
"executionDate": "2026-06-04",
"debtor": {
"name": "Acme Corp ApS",
"country": "DK",
"postalAddress": {},
"account": {},
"agent": {},
"organisationId": {}
},
"serviceUserNumber": "123456",
"ultimateDebtor": {
"name": "Acme Subsidiary GmbH",
"country": "DE",
"postalAddress": {},
"organisationId": {}
},
"transactions": [
{}
]
}
]
},
"environment": "test",
"validate": false,
"idempotencyKey": "string",
"maker": {
"reference": "string",
"name": "string"
},
"source": {
"reference": "string",
"name": "string"
},
"erpNotes": [
{
"kind": "payee-account-changed",
"paymentRef": "string",
"from": "string",
"to": "string",
"shownTo": "string",
"shownAt": "2026-01-01T00: 00: 00Z"
}
]
}| Status | Description |
|---|---|
| 201 | Payment recorded and journaled. A replay of a prior idempotent submit also returns 201, with the response header `Idempotency-Replayed: true`. `warnings` is present only when the accepted payment raised non-blocking validation warnings; they never block a submit or change this status code. |
| 400 | `idempotency_key_required`: no Idempotency-Key (header or body) and no `payment.messageId` on a host-to-host submit |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 402 | Two billing gates share this status. Both apply to production submits only, never to a `test`-environment submit or to `POST /journal/payments/preview`. `trial_payments_exhausted`: this company has already sent its 10 free lifetime production payments (docs/product/PRICING.md) and has no active package; `error.details` carries `productionPaymentsSent`, `freeLimit` and `packagesUrl` (GET /billing/packages for current prices). `billing_past_due`: an established account 30+ days past due on an unpaid invoice; `error.details` carries `pastDueSince` and `graceDays`. A payment already approved and in flight is never affected. |
| 403 | Scope mismatch (cross-tenant) |
| 409 | `conflict`: idempotency conflict (same key, different body) or an in-flight retry. `message_id_already_used`: the payment previously submitted with this `payment.messageId` is terminal and unsuccessful (`rejected`, `cancelled` or `rejected-by-approver`), so this re-submission was not attempted. A `messageId` is used once: `details` carries `originalJournalNo`, `originalStatus` and `remedy: "new_message_id"`. Issue a new `messageId` for a new attempt; a new `Idempotency-Key` does not help. A re-submission while the original is still in flight (or already `executed`) is answered from the recorded payment, without a guarantee of the per-run reports. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Validation failed: all issues at once |
| 429 | Rate limited (production): see Retry-After |
{
"id": "doc_9f3a",
"journalNo": "OUT-000123",
"companyId": "comp_123",
"bankKey": "nordea-dk",
"environment": "production",
"direction": "outbound",
"type": "pain.001",
"status": "converted",
"summary": "SEPA · 1 payment",
"transactionCount": 1,
"totals": [
{
"currency": "EUR",
"amount": "1000.00"
}
],
"xml": "<?xml version=\"1.0\"?><Document xmlns=\"urn:iso:std:iso: 20022:tech:xsd:pain.001.001.09\">…</Document>",
"deliveryMode": "host-to-host",
"createdAt": "2026-06-20T10: 31: 00Z",
"delivery": {
"status": "queued"
}
}// Submit a payment through the engine: validated, recorded in the journal, dispatched to the bank.
// The SDK auto-generates an Idempotency-Key, so a network retry can never create a duplicate payment.
import { BankConnector, BankConnectorApiError } from "@bankconnector/sdk";
export async function submitPayment(client: BankConnector) {
try {
const result = await client.submitPayment({
companyId: "YOUR_COMPANY_ID",
bankKey: "nordea-dk",
payment: {
messageId: "M-2026-0001",
initiatingParty: { name: "Acme ApS" },
payments: [
{
paymentId: "P-1",
paymentType: "domestic",
executionDate: "2026-08-20",
debtor: { name: "Acme ApS", account: { iban: "DK5000400440116243" } },
transactions: [
{
endToEndId: "INV-2026-0042",
amount: "1500.00",
currency: "DKK",
creditor: { name: "Supplier A/S", country: "DK", account: { iban: "DK9520000123456789" } },
},
],
},
],
},
});
console.log(`Recorded ${result.journalNo}, status ${result.status}`);
} catch (err) {
if (err instanceof BankConnectorApiError && err.code === "validation_failed") {
// Every issue at once: fix in one pass rather than one 422 at a time.
console.error("Rejected:", (err.details as { issues: { code: string; message: string }[] }).issues);
return;
}
throw err;
}
}
// new BankConnector({ baseUrl: "https://your-host", apiKey: process.env.BANKCONNECTOR_API_KEY })curl -X POST https://your-host/api/journal/payments \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"companyId":"YOUR_COMPANY_ID","bankKey":"nordea-dk","payment":{"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]}}'const res = await fetch("https://your-host/api/journal/payments", {
method: "POST",
headers: {
"X-API-Key": "YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({"companyId":"YOUR_COMPANY_ID","bankKey":"nordea-dk","payment":{"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]}}),
});
const data = await res.json();import requests
resp = requests.request(
"POST", "https://your-host/api/journal/payments",
headers={"X-API-Key": "YOUR_KEY"},
json={"companyId":"YOUR_COMPANY_ID","bankKey":"nordea-dk","payment":{"messageId":"M-1","creationDateTime":"2026-06-11T10:00:00Z","initiatingParty":{"name":"Acme"},"payments":[{"paymentId":"P-1","paymentType":"sepa","executionDate":"2026-06-12","debtor":{"name":"Acme","account":{"iban":"DK5000400440116243"}},"transactions":[{"endToEndId":"E2E-1","amount":"100.00","currency":"EUR","creditor":{"name":"Beta","country":"DE","account":{"iban":"DE89370400440532013000"}}}]}]}},
)
resp.raise_for_status()
data = resp.json()/journal/payments/previewRead-only. Runs the same settings-fill, validate and convert (+ XSD) pipeline as POST /journal/payments and returns the generated bank file. It does not journal, approve or dispatch, and needs no live host-to-host connection. Use it to inspect the exact bank file a payment would produce before submitting it. Requires authentication.
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | The company this payment belongs to. |
bankKey | BankKey | yes | |
payment | PaymentInstruction | yes | |
environment | enum | no | Which connection delivers the payment. Defaults to production; pass "test" to dispatch over the bank's test channel. |
validate | boolean | no | Also XSD-validate the generated XML. |
idempotencyKey | string | no | Alternative to the Idempotency-Key header. |
maker | object | no | Who initiated this payment, for a session-less (API-key/ERP) caller only. A signed-in session's maker identity is always the session user and this is ignored for it. `reference` is resolved against this company's users; an unresolved reference is kept only as a display label and is not a verified maker for approval purposes. |
source | object | no | The ERP journal this payment came from, as your ERP names it, shown beside the payment's own id on the Payments list and the approval screen, e.g. "PAY-0031 · Payments 2026-09-28". Display only: it is not part of the payment and not part of the idempotency comparison, so a retry of the same payment under a renamed journal replays the original (it never answers 409). Optional; either field may be omitted. |
erpNotes | object[] | no | Facts your ERP records about this batch that an auditor must be able to read later. `payee-account-changed`: the payee's bank account differs from the one their last payment went to, and your ERP showed that warning to `shownTo` at `shownAt`. Each note is written into the payment's hash-chained `payment.converted` journal event and onto its approval request, where the approver sees it. `from` and `to` are recorded masked to their first two and last four characters. Never part of the payment: not in the bank file and not part of the idempotency comparison, so a retry with different notes replays the original answer. Optional. Text fields are trimmed, 1-140 characters, with no control, bidirectional-override or invisible (zero-width, LRM/RLM/ALM) characters; a note that breaks a rule is a 400. |
{
"companyId": "string",
"bankKey": "nordea-dk",
"payment": {
"schemaVersion": "1.0",
"messageId": "MSG-20260601-0001",
"creationDateTime": "2026-06-01T10: 30: 00Z",
"initiatingParty": {
"name": "Acme Corp ApS",
"organisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
},
"secondOrganisationId": {
"id": "DK12345678",
"scheme": "CUST",
"issuer": "string"
}
},
"payments": [
{
"paymentId": "PMT-0001",
"paymentType": "sepa",
"priority": "normal",
"chargeBearer": "DEBT",
"localInstrument": "string",
"localInstrumentProprietary": "string",
"categoryPurpose": "string",
"executionDate": "2026-06-04",
"debtor": {
"name": "Acme Corp ApS",
"country": "DK",
"postalAddress": {},
"account": {},
"agent": {},
"organisationId": {}
},
"serviceUserNumber": "123456",
"ultimateDebtor": {
"name": "Acme Subsidiary GmbH",
"country": "DE",
"postalAddress": {},
"organisationId": {}
},
"transactions": [
{}
]
}
]
},
"environment": "test",
"validate": false,
"idempotencyKey": "string",
"maker": {
"reference": "string",
"name": "string"
},
"source": {
"reference": "string",
"name": "string"
},
"erpNotes": [
{
"kind": "payee-account-changed",
"paymentRef": "string",
"from": "string",
"to": "string",
"shownTo": "string",
"shownAt": "2026-01-01T00: 00: 00Z"
}
]
}| Status | Description |
|---|---|
| 200 | Preview generated. Carries the same `warnings` a real submit would return (present only when the payment raised non-blocking warnings). |
| 400 | Missing fields |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Validation / XSD failed |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"xml": "string",
"painVersion": "pain.001.001.03",
"preview": false,
"warnings": [
{
"code": "string",
"level": "generic",
"severity": "error",
"message": "string",
"path": "string",
"bank": "string",
"paymentType": "string"
}
],
"autoSelected": [
{
"paymentId": "string",
"selected": "string",
"confidence": "string",
"reason": "string"
}
],
"autoFilled": [
"string"
],
"unknownFieldsDropped": [
"string"
],
"whitelist": {
"applicable": false,
"outcome": "cleared",
"checks": [
{
"nip": "string",
"nrb": "string",
"date": "string",
"status": "listed",
"requestId": "string",
"error": "string"
}
],
"conversions": [
{
"nrb": "string",
"amount": "string",
"currency": "string",
"date": "string",
"rate": {
"table": "string",
"no": "string",
"effectiveDate": "string",
"mid": "string"
},
"amountPln": "string",
"reachesThreshold": false,
"error": "string"
}
]
},
"approval": {
"required": false
}
}/journal/payee-verificationsOpt-in. Builds the bank file exactly as POST /journal/payments/preview does and uploads it once as a verification-only file: at OP Web Services, FileType pain.001.001.09 VOP (or .03 VOP). The bank checks each payee's name against the account and pays nothing: no journal document is written, no approval runs, nothing is billed, and the file never counts as a sent payment. The bank may charge per check under your own agreement with it. Euro transfers only. The results arrive later as a payee-verification report: a file.received event whose document (type other, source format vop:pain.002.001.10) lists each payee by end-to-end id as match, close-match (with the name the payee's bank holds), no-match or not-possible; read it with GET /journal/documents/{id}. Requires authentication.
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | Your company id. |
bankKey | string | yes | The bank, e.g. `op-financial-group-fi`. |
payment | - | yes | The canonical payment instruction whose payees are checked (CANONICAL_INPUT.md). |
environment | enum | no | Which connection sends the file. Defaults to `production`. |
{
"companyId": "string",
"bankKey": "string",
"environment": "test"
}| Status | Description |
|---|---|
| 202 | The bank accepted the file for checking. `messageId` is the verification file's own MsgId (your instruction's `messageId` comes back as `requestMessageId`); the report names it as `originalMessageId`. |
| 400 | Missing fields, or the bank refused the file (`bank_rejected`) |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | No active connection to the bank in that environment (`connection_not_found`) |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The connection's channel takes no verification-only file (`payee_verification_unsupported`), its test side is for development only (`production_only_channel`), a non-euro transfer, or validation / XSD failed |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
| 502 | The bank could not be reached, or its answer could not be verified (`bank_unavailable`); sending again is safe |
{
"messageId": "string",
"requestMessageId": "string",
"bankKey": "string",
"environment": "test",
"transactionCount": 0,
"endToEndIds": [
"string"
],
"reference": "string",
"painVersion": "string",
"xml": "string",
"sentAt": "2026-01-01T00: 00: 00Z"
}/journal/payments/batchFor a batch sent to POST /journal/payments whose answer never arrived (a timeout, a closed connection). Returns outcome: found with each payment's end-to-end id and state when the engine recorded the batch, purged when it recorded it and the document has since passed retention, processing while a submission of it holds its key without having recorded it, and not-found only when nothing holds the batch and no submission of it is running. Only not-found means the payments may be sent again, under a new messageId. Every outcome is a 200; the endpoint never answers 404 for an unknown batch, so a 404 from a proxy or a wrong path cannot be read as not-found. not-found speaks for the moment of the read: ask once your own send has ended, not while it may still be on its way. Tenant-scoped. Needs statements:read on an application install's grant, the scope of GET /journal/reconciliation. An install finds only a batch it submitted, or another install paired for the same ERP company (erp_company_id) did: a messageId another company's held batch carries reads processing while that submission holds the key, then not-found. An install paired for an ERP company asks under that company's key, so another company's batch under the same messageId reads not-found to it.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
messageId string | query | yes | The batch's `messageId`, as sent in the payment body to `POST /journal/payments`. The engine identifies a submission by its messageId whether or not an `Idempotency-Key` was sent, so a key that differs from the messageId does not find the batch. Surrounding spaces are ignored, as they are on submission. |
| Status | Description |
|---|---|
| 200 | What the engine holds for the batch |
| 400 | `messageId` is missing or longer than 35 characters (`invalid_parameter`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"messageId": "string",
"outcome": "found",
"documentId": "string",
"journalNo": "string",
"status": "string",
"environment": "test",
"bankKey": "string",
"createdAt": "2026-01-01T00: 00: 00Z",
"payments": [
{
"index": 0,
"payee": "string",
"amount": "string",
"currency": "string",
"endToEndId": "string",
"recon": {
"state": "cleared",
"evidence": {
"source": "pain.002",
"summary": "string",
"date": "string",
"amount": "string",
"currency": "string",
"reason": "string",
"addtlInf": "string",
"documentId": "string"
}
}
}
],
"recon": {
"state": "cleared",
"evidence": {
"source": "pain.002",
"summary": "string",
"date": "string",
"amount": "string",
"currency": "string",
"reason": "string",
"addtlInf": "string",
"documentId": "string"
}
}
}/journal/ingestUpload a raw bank statement/status file; it is parsed to the one normalised shape and journaled. Requires authentication.
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | Your company id. |
bankKey | string | no | |
content | string | yes | Raw file contents |
environment | enum | no |
{
"companyId": "string",
"bankKey": "string",
"content": "string",
"environment": "test"
}| Status | Description |
|---|---|
| 200 | Dedupe hit: this content was already ingested. Returns the existing document unchanged; no events or status advances re-run. |
| 201 | File ingested and normalised: every statement block of the file was ingested. A file whose blocks did not all end the same way answers 207. |
| 207 | Partial: the file's statement blocks did not all end the same way (some ingested, some deduped, some quarantined as belonging to another company), or a file-level alert says the file was not whole (a per-account block that opened and never closed). The body is the 201's, the primary document plus the `ingest` block, and `ingest.statements` says what happened to every block. A camt.053 carries one block per account, so this status distinguishes "3 of 4 accounts imported" from a clean import. |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Unrecognised / unparseable file, or `incomplete_statement`: the file is truncated, or states an amount or credit/debit indicator that could not be read. Nothing from the file is stored. In the second case `error.details.parseIssues` names each field. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"id": "string",
"journalNo": "string",
"companyId": "string",
"bankKey": "string",
"environment": "test",
"direction": "inbound",
"type": "pain.001",
"sourceFormat": "string",
"summary": "string",
"status": "string",
"messageId": "string",
"importSource": "manual",
"transactionCount": 0,
"totals": [
{
"currency": "string",
"amount": "string"
}
],
"paymentType": "string",
"source": {
"reference": "string",
"name": "string"
},
"idempotencyKey": "string",
"collected": false,
"collectedAt": "string",
"createdAt": "string",
"expiresAt": "string",
"deduped": false,
"ingest": {
"statementCount": 0,
"ingested": 0,
"deduped": 0,
"quarantined": 0,
"partial": false,
"statements": [
{
"index": 0,
"kind": "ingested",
"documentId": "string",
"journalNo": "string",
"account": "string",
"message": "string"
}
],
"alerts": [
{
"kind": "string",
"message": "string",
"reason": "string",
"account": "string",
"unclosedBlockCount": 0,
"survivingStatementCount": 0
}
]
}
}/journal/documentsThe console's list. An ERP polls GET /journal/list?uncollectedOnly=true for documents, or GET /journal/events?since= for events. Tenant-scoped list of payments + statements (metadata only, no payload). Filter by direction/type. Each row names its bank environment. An application install lists only the outbound payments its ERP company submitted, from this install or another paired with the same erp_company_id; inbound documents are listed as for any caller, except that on a production server it lists no statement (camt.052/053/054) received from a bank's test environment.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
direction enum | query | no | |
type enum | query | no | |
collected boolean | query | no | Filter by collection state: `false` = you have not collected it yet, `true` = already collected. This is the item's `collected` flag, not a `status` value. |
q string | query | no | Free-text search (case-insensitive, matched literally, `%` and `_` are not wildcards). Matches the document summary, messageId, journalNo, bankKey, status, type, and the rendered `<amount> <currency>` totals text. |
status string | query | no | One or more exact document statuses (e.g. `pending-approval`, `sent`, `executed`, `rejected`, `rejected-by-approver`), comma-separated for an or match (e.g. `converted,sent`). Exact match per value, not a search. |
amountMin string | query | no | Lower bound on any of the document's totals, inclusive. Decimal string, compared in exact minor units. Applied to the returned page by the service, not in the query, so with page size N you get the matching subset of that page; keep paging via `cursor` for the rest. A non-numeric value is ignored (no 400). |
amountMax string | query | no | Upper bound on any of the document's totals, inclusive. Same semantics as `amountMin`. |
dateFrom string | query | no | Only documents created on or after this day (`YYYY-MM-DD`, inclusive, server timezone). |
dateTo string | query | no | Only documents created on or before this day (`YYYY-MM-DD`, **inclusive**; the whole day is covered). |
accountId string | query | no | Only documents for one registered account. Its opaque id (`acct_…`), the same identifier `GET /accounts/statements` takes, not the IBAN. An id this company has no such account for returns an empty page, not an error. |
unresolvedOnly boolean | query | no | `true` leaves out an outbound batch whose every refused line was re-issued (`resendOf`) and the re-issue executed: the rows that carry `refusalsResolved`. The batch's `status` is not changed. Like `amountMin`, it is applied to the returned page, so keep paging via `cursor`. |
limit integer | query | no | Page size. Without it a page holds up to 1000 documents (500 on a filtered search). Every page carries `nextCursor`, `null` on the last one; pass it back as `cursor` for the next page. |
cursor string | query | no | Opaque cursor from a prior page's `nextCursor`. A malformed cursor is a 400 `invalid_cursor`. |
| Status | Description |
|---|---|
| 200 | Document summaries |
| 400 | A `direction`, `type` or `limit` that is not valid (`invalid_parameter`), or a malformed `cursor` (`invalid_cursor`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"id": "string",
"journalNo": "string",
"direction": "inbound",
"type": "pain.001",
"sourceFormat": "string",
"bankKey": "string",
"bankName": "string",
"environment": "test",
"summary": "string",
"status": "string",
"deliveryMode": "converted-only",
"importSource": "manual",
"transactionCount": 0,
"totals": [
{
"currency": "string",
"amount": "string"
}
],
"paymentType": "string",
"payeeName": "string",
"source": {
"reference": "string",
"name": "string"
},
"accountIban": "string",
"accountCurrency": "string",
"refusalsResolved": 0,
"collected": false,
"collectedAt": "string",
"createdAt": "string"
}
],
"nextCursor": "string",
"total": 0
}/journal/exportBulk extraction of full pain.001/camt payloads (account numbers and amounts), optionally filtered within a statement. Admin-only for human sessions; the ERP API key is the intended consumer. Every returned document is marked collected in the same database transaction that reads it. Safe to retry: a repeated call with the same journalNos returns every matching document again regardless of its collected state (wasAlreadyCollected tells you which), so a response your client never received (timeout, dropped connection) does not lose the data. If you track your backlog only via GET /journal/list?uncollectedOnly=true, keep the journalNos from a call until you confirm you received its response: they are marked collected, and so drop off that list, whether or not the response arrived.
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | Your company id. |
journalNos | string[] | yes | |
filter | - | no |
{
"companyId": "string",
"journalNos": [
"string"
]
}| Status | Description |
|---|---|
| 200 | Exported documents + any notFound journal numbers |
| 207 | Partial hit: some of the requested journalNos exist and were exported, but at least one did not: check `notFound` for which. Same body as 200. Branch on the status code: a 200-only client would treat this as full success and miss the unexported documents. |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required (human session) |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"exported": [
{
"journalNo": "string",
"documentId": "string",
"direction": "inbound",
"type": "pain.001",
"sourceFormat": "string",
"bankKey": "string",
"createdAt": "string",
"wasAlreadyCollected": false,
"transactionCount": 0
}
],
"notFound": [
"string"
]
}/journal/listTenant-scoped, newest-first list of journal documents with a stable opaque cursor for forward pagination. Metadata only, no payload. For ERP polling, pass uncollectedOnly=true to fetch only documents you have not collected yet, then mark them collected via POST /journal/export. total counts all documents; uncollected counts how many are still uncollected. Each item carries collected (boolean), collectedAt and environment; an ERP that books statements skips the test ones, which a bank's test system can send for a real account. An application install lists and counts only the outbound payments its ERP company submitted, from this install or another paired with the same erp_company_id, and on a production server no statement (camt.052/053/054) received from a bank's test environment.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | Your company id. |
cursor string | query | no | Opaque cursor from a prior page's `nextCursor`. |
limit integer | query | no | Page size: default 500, max 1000 (larger than the interactive /journal/documents feed, for bulk ERP pulls). |
direction enum | query | no | |
type enum | query | no | |
uncollectedOnly boolean | query | no | When `true`, returns only documents not yet collected (the standard poll-then-collect loop). |
| Status | Description |
|---|---|
| 200 | A page of documents + counts + nextCursor |
| 400 | A `direction`, `type` or `limit` that is not valid (`invalid_parameter`), or a malformed `cursor` (`invalid_cursor`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"total": 42,
"uncollected": 3,
"nextCursor": null,
"items": [
{
"journalNo": "OUT-000123",
"direction": "outbound",
"type": "pain.001",
"sourceFormat": "pain.001.001.09",
"bankKey": "nordea-dk",
"environment": "production",
"summary": "SEPA · 1 payment",
"status": "converted",
"collected": false,
"collectedAt": null,
"createdAt": "2026-06-20T10: 31: 00Z",
"expiresAt": null
}
]
}// The ERP pull loop: the async iterator walks every uncollected journal document, following `nextCursor`
// until the server says there is no more.
import { BankConnector } from "@bankconnector/sdk";
export async function pollStatements(client: BankConnector) {
for await (const doc of client.journalListAll({ direction: "inbound", uncollectedOnly: true })) {
console.log(`${doc.journalNo} ${doc.type} ${doc.summary}`);
// Fetch + import the full document into your ERP here, then it is marked collected by the export call.
}
}/journal/documents/{id}Returns the document including its canonical/normalised payload and, for seeded/generated docs, the raw xml for download. Tenant-scoped. For an application install, an outbound payment neither it nor another install paired with the same erp_company_id submitted is a 404, here and on /file and /payments. On a production server, so is a statement (camt.052/053/054) received from a bank's test environment, here and on /file, /export.csv and /export.camt053.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | Document + payload |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | Not found (or not in this tenant's scope) |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"id": "string",
"journalNo": "string",
"companyId": "string",
"bankKey": "string",
"environment": "test",
"direction": "inbound",
"type": "pain.001",
"sourceFormat": "string",
"summary": "string",
"status": "string",
"messageId": "string",
"importSource": "manual",
"transactionCount": 0,
"totals": [
{
"currency": "string",
"amount": "string"
}
],
"paymentType": "string",
"source": {
"reference": "string",
"name": "string"
},
"idempotencyKey": "string",
"collected": false,
"collectedAt": "string",
"createdAt": "string",
"expiresAt": "string",
"inputTrace": {
"filled": [
{
"path": "string",
"source": "bank-settings",
"setting": "string"
}
],
"warnings": [
{
"code": "string",
"level": "generic",
"severity": "error",
"message": "string",
"path": "string",
"bank": "string",
"paymentType": "string"
}
],
"unknownFieldsDropped": [
"string"
]
},
"entryRecon": [
{
"index": 0,
"state": "matched",
"journalNo": "string",
"lineCount": 0,
"line": {
"number": 0,
"payee": "string"
},
"source": {
"reference": "string",
"name": "string"
}
}
],
"payeeVerification": {
"reportDocumentId": "string",
"final": false,
"reportedAt": "string",
"notices": [
{
"statusCode": "string",
"text": "string"
}
],
"transactions": [
{
"endToEndId": "string",
"result": "match",
"statusCode": "string",
"byOmission": false,
"suggestedName": "string",
"reasonCode": "string",
"note": "string"
}
],
"unreadEntries": 0
},
"csvSpecification": [
{
"index": 0,
"kind": "items",
"itemCount": 0,
"notification": {
"id": "string",
"journalNo": "string"
}
}
]
}/journal/documents/{id}/payments| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | Per-line payments (payee, amount, endToEndId, status) |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"documentId": "string",
"journalNo": "string",
"messageId": "string",
"status": "string",
"deliveryMode": "converted-only",
"bankKey": "string",
"bankName": "string",
"createdAt": "string",
"payments": [
{
"index": 0,
"payee": "string",
"amount": "string",
"currency": "string",
"endToEndId": "string",
"recon": {
"state": "cleared",
"evidence": {
"source": "pain.002",
"summary": "string",
"date": "string",
"amount": "string",
"currency": "string",
"reason": "string",
"addtlInf": "string",
"documentId": "string"
}
}
}
],
"approval": {
"id": "string",
"status": "pending",
"paymentRef": "string",
"payeeName": "string",
"currency": "string",
"currencies": [
"string"
],
"maxTransaction": "string",
"journalTotal": "string",
"requiredApprovals": 0,
"requireOneFromEachGroup": false,
"orderedApproval": false,
"groupCount": 0,
"approvals": [
{
"userName": "string",
"at": "string",
"authorityLapsed": "deactivated"
}
],
"rejections": [
{
"userName": "string",
"at": "string",
"reason": "string"
}
],
"canApprove": false,
"blockReason": "string",
"twoFactorRequired": false
},
"recon": {
"state": "cleared",
"evidence": {
"source": "pain.002",
"summary": "string",
"date": "string",
"amount": "string",
"currency": "string",
"reason": "string",
"addtlInf": "string",
"documentId": "string"
}
},
"refusals": {
"refused": 0,
"resolved": 0,
"resends": [
{
"endToEndId": "string",
"documentId": "string",
"journalNo": "string",
"resendEndToEndId": "string",
"reissuedBy": "other_sender",
"state": "cleared",
"resolved": false
}
]
},
"stepTimes": {
"submittedAt": "string",
"sentAt": "string"
},
"awaitingBankValidation": {
"portal": "string"
}
}/journal/documents/{id}/signer-releaseFor an outbound payment at awaiting-signer-release: the bank ran its payee check, holds the file, and waits for the customer's own signer to release or cancel it there. This reads the bank's pending order (HVU/HVD) over the EBICS subscriber that signs the release (the signer's own on a per-approver connection, the company's one subscriber on an at-upload connection) and returns the order as the bank shows it, with an acknowledgement digest of exactly what was shown. The POST must echo that digest, so a decision is bound to the content the signer read. Requires the Approver role on a person's session; a machine credential is refused.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The bank's payee-check result, the held order and the acknowledgement digest |
| 401 | Sign in required: there is no valid session. |
| 403 | Not an Approver, or the signer has no active EBICS subscriber on this bank channel |
| 409 | The payment is not awaiting a signer's release, or the bank no longer holds an order bound to it (`veu_order_not_bound`) |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"documentId": "string",
"journalNo": "string",
"payeeCheck": {
"reportDocumentId": "string",
"final": false,
"reportedAt": "string",
"notices": [
{
"statusCode": "string",
"text": "string"
}
],
"transactions": [
{
"endToEndId": "string",
"result": "match",
"statusCode": "string",
"byOmission": false,
"suggestedName": "string",
"reasonCode": "string",
"note": "string"
}
],
"unreadEntries": 0
},
"bankDisplay": {
"base64": "string",
"text": "string",
"sha256": "string"
},
"folder": "bound",
"order": {
"orderType": "string",
"orderId": "string"
},
"acknowledgement": "string",
"canRelease": false,
"canCancel": false,
"refusal": {
"code": "not_found",
"message": "string"
}
}/journal/documents/{id}/signer-releaseThe signer's decision on a held payment, signed at the bank under the signer's own EBICS subscriber on a per-approver connection, or the company's one subscriber on an at-upload connection. acknowledgement must equal the digest the GET returned: a mismatch is 409 content_changed, because the bank's view changed since the signer read it. The decision is recorded in the journal before the bank act, and takes the same second-factor step-up as approving a payment. Requires the Approver role on a person's session.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes |
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | no | |
decision | enum | yes | |
acknowledgement | string | yes | The `acknowledgement` digest returned by GET /journal/documents/{id}/signer-release. |
code | string | no | 2FA TOTP code (if step-up required). |
backupCode | string | no |
{
"companyId": "string",
"decision": "release",
"acknowledgement": "string",
"code": "string",
"backupCode": "string"
}| Status | Description |
|---|---|
| 200 | The recorded decision and the bank's answer to the release or cancellation |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Sign in required: there is no valid session. |
| 403 | Not an Approver, second factor not verified, or no active EBICS subscriber for the signer |
| 409 | `content_changed` (the acknowledgement no longer matches what the bank shows), or the payment is not awaiting release |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"decision": "release",
"status": "string",
"transactionId": "string"
}/journal/documents/{id}/fileReturns the stored file as an application/xml attachment. For an outbound payment that is the pain.001 we generated (?version=approved returns the submit-time file instead of the delivered one). For an inbound camt.052/053/054 it is the bank's original file, byte for byte as it arrived.
The inbound statement file is retained for 30 days by default (configurable per deployment) and is then destroyed; the statement itself, its balances and the audit trail are kept for the full document retention period. A request after that answers 404 with error.code raw_file_expired and a message naming the expiry date. That is the retention policy working, not a fault: treat the statement JSON from GET /journal/documents/{id} as the durable record, and re-request the file from the bank if you need the original bytes. A 404 with not_found means the document isn't in scope or stored no file at all. Downloading does not mark the document collected.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
version enum | query | no | `approved` returns the file as it stood when the payment was submitted, what the approver reviewed, for a payment whose delivered file was rebuilt at release to carry approver codes. Omit it for the file that went to the bank; for every other document the two are the same file. |
| Status | Description |
|---|---|
| 200 | The stored XML (attachment). The generated pain.001 outbound, the bank's original camt inbound |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | No downloadable file for this document: `not_found` (out of scope, or no file was stored) or `raw_file_expired` (an inbound statement whose raw bank file passed its retention window) |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
/journal/documents/{id}/export.csvReturns one camt.053 statement as the versioned bankconnector-statement-v1 CSV profile: UTF-8 with BOM, ; delimited, . decimal, ISO dates, one record per transaction, debit amounts negative. The column list and the profile's invariants are in docs/reference/ERP_STATEMENT_CSV.md. There is no format parameter: other clients read the JSON of GET /journal/documents/{id}. 404 if the document isn't in scope or isn't a camt.053. camt.052 and camt.054 describe movements the day's camt.053 books again, so exporting them could post the same transaction twice. One exception, never beside the total: on a Finnish account, a combined reference-payment entry is carried as the lines of the camt.054 that itemises it when they sum to it exactly, flagged item in the last column, specification. Which entries are itemised is decided at the statement's first export and repeated by every later one. 409 statement_specification_expired when an itemised entry's camt.054 has passed its retention period. Downloading does not mark the document collected.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The statement as CSV (attachment) |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | No bank statement to export for this document |
| 409 | `statement_specification_expired`: the statement's CSV carries a Finnish combined entry as the items of a camt.054 that has passed its retention period, so the file can no longer be reproduced line for line |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
/journal/documents/{id}/export.camt053Returns one stored bank statement as an ISO 20022 camt.053.001.02 file, the version ERP statement importers read, whatever format the bank sent it in (camt.053.001.08, MT940, …). It is rendered from the statement the journal keeps for the document's whole retention period, so it works after the bank's original file has expired (/file then answers raw_file_expired). The same document always gives the same bytes, so an ERP's duplicate detection keeps working across downloads.
The file carries the account, statement id, sequence, period and balances, and per entry the amount, direction, status, booking and value dates, the bank's references, the bank transaction code, and per transaction its references, parties and accounts, remittance (unstructured and structured), charges, currency exchange, purpose and return reason. An entry the bank booked as one batch is written as one Ntry with its transactions when the stored statement proves the grouping; otherwise each transaction is its own Ntry. A text longer than the ISO element allows is cut to fit; the JSON of GET /journal/documents/{id} keeps it whole. A pending or future-dated entry is written PDNG and one with an unrecognised status INFO, never BOOK.
404 if the document isn't in scope or isn't a camt.053: camt.052 and camt.054 describe movements the day's camt.053 books again, so importing them as a statement could post the same transaction twice. Downloading does not mark the document collected.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The statement as camt.053.001.02 (attachment) |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | No bank statement to export for this document |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
/journal/payment-typesA tally of your outbound payments grouped by payment type, weighted by transaction count. An application install tallies only the payments its ERP company submitted, from this install or another paired with the same erp_company_id.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | Per-type counts + total |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"counts": {},
"total": 0,
"sampled": 0
}/journal/audit/verifyRe-walks every event in the company's activity log (the rows GET /journal/events serves) and checks each event's hash and its link to the one before. Read-only: it never writes to the log.
A broken chain is answered 200, not an error. ok: false with brokenAt naming the first event that failed (index is its position in the walk, oldest first; reason is content-altered, broken-link or retired-key). Read ok, never the status code alone, to decide whether the chain is intact. length is how many events were checked. genesisAnchored: false means the oldest events are gone: purgeVerified then says whether a signed retention checkpoint proves that was a legitimate purge (true) or not (false); it is null when nothing was truncated.
The walk covers the company's whole history, so its cost grows with the log; it shares the per-caller rate limit of the other heavy journal reads. An application install reaches it with payments:read, the scope that reads the log itself, and only for its own company.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The chain's verdict: intact (`ok: true`) or broken (`ok: false` + `brokenAt`) |
| 401 | Authentication required |
| 403 | An application install without `payments:read`, or a request naming another company |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"ok": false,
"length": 0,
"genesisAnchored": false,
"brokenAt": {
"index": 0,
"eventId": "string",
"reason": "content-altered"
},
"purgeVerified": false
}/journal/documents/{id}/raw-pain002Returns the inbound pain.002 exactly as the bank sent it, as an application/xml attachment. This keeps the bank's own account of a rejection for the whole life of the document: unlike the raw camt statement files it is not removed after a retention window. 404 not_found when the document is not in scope, is not an inbound pain.002, or stored no file. Downloading does not mark the document collected.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The stored pain.002 XML, as the bank sent it (attachment) |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | No stored raw pain.002 for this document |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
/journal/reconciliationReturns each outbound payment's state (cleared, rejected or pending) with the matching evidence, by matching inbound pain.002 status reports and camt.053/054 statement entries 1:1. Keyset-paginated over the outbound payments (default 500, max 1000): a non-null nextCursor means more payments exist. Pass it back as ?cursor=. An application install reads only the payments its ERP company submitted, from this install or another paired with the same erp_company_id, matched against the organisation's evidence. A payment an approver rejected (status: rejected-by-approver) also carries rejection: who rejected it, when, and the reason they gave.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
limit integer | query | no | Page size over the outbound payments. |
cursor string | query | no | Opaque keyset cursor from a previous response's `nextCursor`. |
| Status | Description |
|---|---|
| 200 | Per-payment reconciliation + evidence |
| 400 | A `limit` that is not valid (`invalid_parameter`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"documentId": "string",
"journalNo": "string",
"messageId": "string",
"bankKey": "string",
"summary": "string",
"totals": [
{
"currency": "string",
"amount": "string"
}
],
"status": "string",
"createdAt": "2026-01-01T00: 00: 00Z",
"state": "cleared",
"evidence": {
"source": "pain.002",
"summary": "string",
"date": "string",
"amount": "string",
"currency": "string",
"reason": "string",
"addtlInf": "string",
"documentId": "string"
},
"rejection": {
"reason": "string",
"rejectedBy": {
"name": "string"
},
"rejectedAt": "2026-01-01T00: 00: 00Z"
}
}
],
"nextCursor": "string"
}/journal/eventsKeyset-paginated newest-first (default and maximum page size 5000): a non-null nextCursor means more events exist. Pass it back as ?cursor=.
?since=<seq> is a second paging mode for a poller with no public endpoint of its own (webhooks unreachable). It walks the same rows oldest-first by the seq every item carries, so since=<the last seq you saved> resumes where you left off, even after a crash, a restart or a week offline. ?since= and ?cursor= are mutually exclusive (400 if both are given).
?connectionId=<id> narrows the feed to one bank channel: that connection's own lifecycle events plus every event recorded against a bank the connection serves (a shared connection covers a whole bank group), in this connection's environment. A company can hold a test and a production connection for the same bank, so an event stamped with the other environment is excluded. Events with no environment appear on both. It composes with either paging mode.
?from=/?to= bound the feed by event time, inclusive at both ends. Both are instants, not calendar days, because the server does not know your timezone. They combine with cursor, since and connectionId, and a from later than to is a 400.
?notify= narrows the feed to events that concern a person. action is what somebody in the company must do something about (an approval waiting, a delivery that failed, a certificate about to expire); watch is what has consequences nobody there can clear (a statement gap, a double debit, a revoked integration); any is both. Sign-ins and the rest of the audit record are in none of them. An unrecognised value is a 400. It combines with limit, cursor, since, connectionId, from and to.
An application install reads only its own part of the trail. An organisation can pair several ERP companies, one install each, so a request authenticated by an install's access token returns only (a) the events about the payments its ERP company submitted, from this install or another paired with the same erp_company_id (conversion, approval, delivery, the bank's status reports, the SLA and retention alerts), (b) the organisation-wide changes every company works under: approval-policy changes (policy., bank.policy-changed, org.self-approval-rule-changed, org.erp-approval-changed) and bank-channel changes (connection., bank.sends-blocked, bank.sends-unblocked, bankkey.), and (c) when the install holds statements:read, the events about the organisation's inbound camt.052, camt.053 and camt.054 statements: file.received, statement., account.statement-gap and account.statement-integrity-gap. Every install of the organisation reads every statement, except on a production server one received from a bank's test environment, which its statement lists leave out too. It never returns another install's payments, sign-ins, user administration, status-report files or their pain002.unmatched-* warnings, and no parameter widens that. seq values in it have gaps where other rows were left out, which ?since= handles. An event about a payment whose document has passed the journal retention (90 days) is outside an install's view. To start a newly paired company at its pairing, pass from= the grant.createdAt that GET /oauth/userinfo returns. Every other caller (a signed-in person, an API key) reads the organisation's whole trail.
A file.received event's metadata carries the documentId of the file, its type (camt.053, pain.002, …) and, for a statement, accountIban: the account masked to its first two and last four characters, as on every statement event.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
limit integer | query | no | |
cursor string | query | no | Opaque keyset cursor from a previous response's `nextCursor`. Mutually exclusive with `since`. |
connectionId string | query | no | Return only this bank channel's events: its own events plus its banks' events in this connection's environment (see the endpoint description). 404 `connection_not_found` if the id is not one of your connections. Combines with `limit`, `cursor` and `since`. |
since integer | query | no | Resume the feed oldest-first after this `seq` (0 starts from the beginning). Pass the highest `seq` you have processed; a non-null `nextCursor` in the response is the next `since` value, and more events are already waiting. A null `nextCursor` means you are caught up. Mutually exclusive with `cursor`. |
from string | query | no | Return only events at or after this instant (inclusive). An ISO-8601 timestamp, not a calendar day, because the server does not know your timezone. Combines with `limit`, `cursor`, `since` and `connectionId`. 400 `invalid_parameter` if `from` is later than `to`. |
to string | query | no | Return only events at or before this instant (inclusive). See `from`. |
notify enum | query | no | Return only events that concern a person: `action` (somebody must do something), `watch` (consequences nobody there can clear) or `any` (both). The in-app notification bell reads this. 400 `invalid_parameter` on any other value. |
lang enum | query | no | Language of each item's `display` text. `display` is derived at read time and the stored `message` is never changed. |
| Status | Description |
|---|---|
| 200 | Events newest-first |
| 400 | A `limit` that is not valid (`invalid_parameter`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"id": "string",
"companyId": "string",
"type": "string",
"message": "string",
"actorUserId": "string",
"metadata": {},
"createdAt": "string",
"expiresAt": "string",
"prevHash": "string",
"hash": "string",
"chainFormat": 0,
"seq": 0,
"display": {
"event": "string",
"text": "string",
"reference": "string"
}
}
],
"nextCursor": "string"
}/inbound/payment-statusAccepts a raw pain.002 (or normalised status), matches it to the original outbound payment by MsgId/endToEndId and advances the payment status.
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | Your company id. |
bankKey | string | no | |
content | string | yes | Raw pain.002 XML. |
{
"companyId": "string",
"bankKey": "string",
"content": "string"
}| Status | Description |
|---|---|
| 200 | Reconciled (status advanced) |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Unparseable / unmatched status |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"messageId": "string",
"createdAt": "string",
"originalMessageId": "string",
"sourceFormat": "pain.002.001.02",
"groupStatus": "accepted",
"groupStatusCode": "string",
"groupReasons": [
{
"code": "string",
"description": "string",
"originator": "string"
}
],
"originalTxCount": 0,
"paymentGroups": [
{
"paymentInfoId": "string",
"status": "accepted",
"statusCode": "string",
"reasons": [
{
"code": "string",
"description": "string",
"originator": "string"
}
],
"transactions": [
{
"endToEndId": "string",
"paymentInfoId": "string",
"instructionId": "string",
"originalAmount": "string",
"currency": "string",
"status": "accepted",
"statusCode": "string",
"reasons": [],
"bankRef": "string",
"acctSvcrRef": "string",
"stsId": "string"
}
]
}
],
"rejectedTransactions": [
{
"endToEndId": "string",
"paymentInfoId": "string",
"instructionId": "string",
"originalAmount": "string",
"currency": "string",
"status": "accepted",
"statusCode": "string",
"reasons": [
{
"code": "string",
"description": "string",
"originator": "string"
}
],
"bankRef": "string",
"acctSvcrRef": "string",
"stsId": "string"
}
],
"acceptedTransactions": [
{
"endToEndId": "string",
"paymentInfoId": "string",
"instructionId": "string",
"originalAmount": "string",
"currency": "string",
"status": "accepted",
"statusCode": "string",
"reasons": [
{
"code": "string",
"description": "string",
"originator": "string"
}
],
"bankRef": "string",
"acctSvcrRef": "string",
"stsId": "string"
}
]
}/inbound/statementParses the raw file body into normalised JSON without storing it (use POST /journal/ingest to store). A camt.053 may carry several <Stmt> (one per account); all are returned in the items envelope. When this exact file is already in the journal, alreadyImported names the document it was filed as. POST /journal/ingest deduplicates on the same key.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | Normalised statement(s) |
| 400 | Empty body or unrecognised format |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Unparseable file, or `incomplete_statement`: the file is truncated, or states an amount or credit/debit indicator that could not be read. In the second case `error.details.parseIssues` names each field, and POST /journal/ingest refuses the same file. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"messageId": "string",
"createdAt": "string",
"sequenceNumber": "string",
"statementId": "string",
"sourceFormat": "string",
"accountIban": "string",
"accountOther": "string",
"accountCurrency": "string",
"accountOwner": "string",
"fromDate": "string",
"toDate": "string",
"entrySummary": {
"totalCount": 0,
"totalSum": "string",
"totalDirection": "credit",
"creditCount": 0,
"creditSum": "string",
"debitCount": 0,
"debitSum": "string"
},
"balances": [
{
"type": "string",
"amount": "string",
"currency": "string",
"direction": "credit",
"date": "string",
"creditLineIncluded": false,
"creditLineAmount": "string",
"availability": []
}
],
"closingBalance": {
"type": "string",
"amount": "string",
"currency": "string",
"direction": "credit",
"date": "string",
"creditLineIncluded": false,
"creditLineAmount": "string",
"availability": [
{}
]
},
"openingBalance": {
"type": "string",
"amount": "string",
"currency": "string",
"direction": "credit",
"date": "string",
"creditLineIncluded": false,
"creditLineAmount": "string",
"availability": [
{}
]
},
"transactions": [
{
"bankRef": "string",
"endToEndId": "string",
"instructionId": "string",
"amount": "string",
"currency": "string",
"direction": "credit",
"status": "booked",
"bookingDate": "string",
"valueDate": "string",
"paymentDate": "string",
"counterpartyName": "string",
"counterpartyIban": "string",
"counterpartyAccount": "string",
"counterpartyAccountScheme": "string",
"counterpartyRegNo": "string",
"counterpartyAccountNo": "string",
"counterpartyBic": "string",
"counterpartyCountry": "string",
"counterpartyAddress": "string",
"remittance": "string",
"remittanceLines": [],
"creditorReference": "string",
"creditorReferenceType": "string",
"creditorReferenceScheme": "string",
"fik": {},
"creditorReferences": [],
"referredDocuments": [],
"txCodeDomain": "string",
"txCodeFamily": "string",
"txCodeProprietary": "string",
"txCodeProprietaryIssuer": "string",
"textKeySupplement": "string",
"entryTxCodeProprietary": "string",
"entryTxCodeProprietaryIssuer": "string",
"originalCurrency": "string",
"originalAmount": "string",
"chargeAmount": "string",
"chargeCurrency": "string",
"entryInfo": "string",
"transactionInfo": "string",
"txCodeSubFamily": "string",
"paymentInfoId": "string",
"originalMessageId": "string",
"batchTransactionCount": 0,
"ultimateDebtorName": "string",
"ultimateCreditorName": "string",
"exchangeRate": "string",
"exchangeSourceCurrency": "string",
"exchangeTargetCurrency": "string",
"counterValueAmount": "string",
"counterValueCurrency": "string",
"purposeCode": "string",
"mandateId": "string",
"chargeBearer": "string",
"clearingSystemReference": "string",
"proprietaryReference": "string",
"counterpartyOrganisationId": "string",
"counterpartyOrganisationIdScheme": "string",
"counterpartyPersonId": "string",
"entryReference": "string",
"isReversal": false,
"returnReasonCode": "string",
"returnReasonInfo": "string",
"originalBankRef": "string",
"interbankSettlementDate": "string",
"entryBankRef": "string",
"additionalInfoMessageId": "string",
"counterpartyBankCode": "string",
"isBatchBooking": false,
"rail": "bacs",
"fasterPaymentId": "string",
"gvc": "string",
"primanota": "string"
}
],
"droppedCurrencies": [
"string"
]
}
],
"alreadyImported": {
"journalNo": "string",
"importedAt": "string"
}
}Accounts
Bank accounts registry + imported statements (session)
/accounts| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
limit integer | query | no | |
cursor string | query | no |
| Status | Description |
|---|---|
| 200 | Accounts with latest balances |
| 400 | A `limit` that is not a number (`invalid_parameter`), or a malformed `cursor` (`invalid_cursor`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"id": "string",
"companyId": "string",
"iban": "string",
"accountOther": "string",
"country": "string",
"currency": "string",
"bankKey": "string",
"bankName": "string",
"name": "string",
"connected": false,
"lastReceivedData": "string",
"latestBalance": "string",
"latestBalanceCurrency": "string",
"latestBalanceDate": "string",
"createdAt": "string",
"updatedAt": "string",
"link": {
"linkedBy": "iban-tail",
"linkedAt": "string",
"reason": "string"
}
}
],
"nextCursor": "string",
"total": 0
}/accounts/{id}Sets the account's label, shown first on each row of the Bank accounts table. Any signed-in user with access to the account may set it. Send null or an empty string to clear it; the account then shows its masked account number. Statement ingest never overwrites a name set here.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Property | Type | Required | Description |
|---|---|---|---|
name | string | null | yes |
{
"name": "string"
}| Status | Description |
|---|---|
| 200 | The updated account |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Sign in required: there is no valid session. |
| 403 | An API key, application install or other machine credential is refused (`forbidden`): this operation needs a signed-in person. |
| 404 | No such account for this company |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"account": {
"id": "string",
"companyId": "string",
"iban": "string",
"accountOther": "string",
"country": "string",
"currency": "string",
"bankKey": "string",
"bankName": "string",
"name": "string",
"connected": false,
"lastReceivedData": "string",
"latestBalance": "string",
"latestBalanceCurrency": "string",
"latestBalanceDate": "string",
"createdAt": "string",
"updatedAt": "string",
"link": {
"linkedBy": "iban-tail",
"linkedAt": "string",
"reason": "string"
}
}
}/accounts/{id}/linkMerges an account known only by its domestic number into an IBAN account at the same bank, so one account carries both identifiers and its balance. Company admins only, because a link changes which balance an approver's funds check reads. Refused when the accounts are at different banks or hold different currencies. Undo with POST /accounts/{id}/unlink.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Property | Type | Required | Description |
|---|---|---|---|
ibanAccountId | string | yes |
{
"ibanAccountId": "string"
}| Status | Description |
|---|---|
| 200 | The linked account |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Sign in required: there is no valid session. |
| 403 | Admin required: a session without the Admin role, an API key or an application install is refused (`admin_required`). |
| 404 | No such account for this company |
| 409 | The accounts cannot be linked (different bank or currency, or already linked) |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"account": {
"id": "string",
"companyId": "string",
"iban": "string",
"accountOther": "string",
"country": "string",
"currency": "string",
"bankKey": "string",
"bankName": "string",
"name": "string",
"connected": false,
"lastReceivedData": "string",
"latestBalance": "string",
"latestBalanceCurrency": "string",
"latestBalanceDate": "string",
"createdAt": "string",
"updatedAt": "string",
"link": {
"linkedBy": "iban-tail",
"linkedAt": "string",
"reason": "string"
}
},
"removedAccountId": "string"
}/accounts/{id}/unlinkSplits a linked account back into its IBAN account and its local-number account, and records that the two must not be linked again automatically. The IBAN account's balance is cleared until a statement for it arrives. Safe to retry: either resulting account id answers with the same pair. Company admins only.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The two accounts after the split |
| 401 | Sign in required: there is no valid session. |
| 403 | Admin required: a session without the Admin role, an API key or an application install is refused (`admin_required`). |
| 404 | No such account for this company |
| 409 | The account is not linked (`not_linked`), or it was linked to a different local account number while the undo waited (`conflict`: nothing changed, so re-read the accounts and retry) |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"iban": {
"id": "string",
"companyId": "string",
"iban": "string",
"accountOther": "string",
"country": "string",
"currency": "string",
"bankKey": "string",
"bankName": "string",
"name": "string",
"connected": false,
"lastReceivedData": "string",
"latestBalance": "string",
"latestBalanceCurrency": "string",
"latestBalanceDate": "string",
"createdAt": "string",
"updatedAt": "string",
"link": {
"linkedBy": "iban-tail",
"linkedAt": "string",
"reason": "string"
}
},
"local": {
"id": "string",
"companyId": "string",
"iban": "string",
"accountOther": "string",
"country": "string",
"currency": "string",
"bankKey": "string",
"bankName": "string",
"name": "string",
"connected": false,
"lastReceivedData": "string",
"latestBalance": "string",
"latestBalanceCurrency": "string",
"latestBalanceDate": "string",
"createdAt": "string",
"updatedAt": "string",
"link": {
"linkedBy": "iban-tail",
"linkedAt": "string",
"reason": "string"
}
}
}/accounts/statementsPer-account statement history, one page at a time, newest period first. A page is extended to the end of its last period, so it can hold more than limit statements; a period holding more than limit statements is split across pages. A non-null nextCursor means older statements exist. Pass it back as ?cursor=. On a production server a statement that arrived from a bank's test environment is not listed, nor counted in gaps.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
account string | query | yes | |
limit integer | query | no | Statements per page, counted before the page is extended to the end of its period. A page holds fewer than twice `limit`. |
cursor string | query | no | The previous answer's `nextCursor`. A malformed value is 400 `invalid_cursor`. |
| Status | Description |
|---|---|
| 200 | One page of statements + the detected sequence gaps for that page |
| 400 | `account_required`, `invalid_parameter` or `invalid_cursor` |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 404 | Unknown account id for this company |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"documentId": "string",
"fromDate": "string",
"toDate": "string",
"sequenceNumber": "string",
"opening": {
"amount": "string",
"currency": "string",
"direction": "string",
"date": "string"
},
"closing": {
"amount": "string",
"currency": "string",
"direction": "string",
"date": "string"
},
"txCount": 0
}
],
"nextCursor": "string",
"account": "string",
"gaps": [
{
"kind": "balance",
"reason": "string",
"afterDocumentId": "string",
"beforeDocumentId": "string"
}
]
}Webhooks
Outbound event subscriptions (session, Admin)
/webhooks| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | Subscriptions |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"id": "string",
"companyId": "string",
"url": "string",
"eventTypes": [
"string"
],
"description": "string",
"enabled": false,
"createdAt": "string",
"secretHint": "string",
"lastStatus": "delivered",
"lastAttemptAt": "string",
"lastError": "string",
"consecutiveFailures": 0,
"disabledAt": "string",
"disabledReason": "string",
"pendingSecretExpiresAt": "string",
"transport": "https",
"emailTo": "string",
"filterPredicate": {
"bankKey": "string",
"currency": "string",
"minAmount": 0,
"maxAmount": 0
},
"deliverySequence": 0,
"health": {
"windowHours": 0,
"delivered": 0,
"failed": 0,
"pending": 0,
"skippedDisabled": 0,
"skippedUnsubscribed": 0
},
"unknownEventTypes": [
"string"
]
}
],
"retryPolicy": {
"maxAttempts": 0,
"firstBackoffMs": 0,
"maxBackoffMs": 0,
"backoffJitter": 0,
"autoDisableAfterFailures": 0
}
}/webhooksRegisters an HTTPS endpoint to receive signed event callbacks. Admin only. The subscription field is eventTypes (an array of event names); a typo like events is rejected (400) rather than silently subscribing to everything, and so is a misspelt event NAME inside the array (e.g. payment.snet): delivery matches by exact string, so an unknown value would otherwise be stored and never fire. Omit eventTypes entirely (or send ["*"]) to receive every event in the published catalog; a type outside the catalog reaches only a subscription that names it.
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | |
url | string | yes | |
eventTypes | enum[] | no | Event names to subscribe to, e.g. ["payment.sent", "payment.rejected"], or ["*"] for every event in the published catalog. Omit for the same as "*". A type outside the catalog reaches only a subscription that names it. An explicitly-empty array is rejected; an unknown value 400s naming it. |
description | string | no | |
transport | enum | no | |
emailTo | string | no | |
filterPredicate | object | null | no |
{
"companyId": "string",
"url": "string",
"eventTypes": [
"*"
],
"description": "string",
"transport": "https",
"emailTo": "string",
"filterPredicate": {
"bankKey": "string",
"currency": "string",
"minAmount": 0,
"maxAmount": 0
}
}| Status | Description |
|---|---|
| 201 | Subscription created (returns the signing secret once) |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"endpoint": {
"id": "string",
"companyId": "string",
"url": "string",
"eventTypes": [
"string"
],
"description": "string",
"enabled": false,
"createdAt": "string",
"secretHint": "string",
"lastStatus": "delivered",
"lastAttemptAt": "string",
"lastError": "string",
"consecutiveFailures": 0,
"disabledAt": "string",
"disabledReason": "string",
"pendingSecretExpiresAt": "string",
"transport": "https",
"emailTo": "string",
"filterPredicate": {
"bankKey": "string",
"currency": "string",
"minAmount": 0,
"maxAmount": 0
},
"deliverySequence": 0,
"health": {
"windowHours": 0,
"delivered": 0,
"failed": 0,
"pending": 0,
"skippedDisabled": 0,
"skippedUnsubscribed": 0
},
"unknownEventTypes": [
"string"
]
},
"secret": "string"
}// Verify a webhook delivery REALLY came from BankConnector: the control that stops a forged
// "payment.sent" being accepted. Verify over the RAW body bytes, before any JSON parsing.
import { verifyWebhookSignature, SIGNATURE_HEADER } from "@bankconnector/sdk";
export function handleWebhook(rawBody: string, headers: Record<string, string | undefined>): { ok: boolean } {
const signature = headers[SIGNATURE_HEADER];
const secret = process.env.BANKCONNECTOR_WEBHOOK_SECRET; // the `secret` returned ONCE by POST /webhooks
if (!signature || !secret) return { ok: false };
if (!verifyWebhookSignature(secret, signature, rawBody, { replayProtection: true })) {
return { ok: false }; // discard silently: do not tell a forger why
}
const event = JSON.parse(rawBody) as { type: string; data: unknown };
console.log(`verified ${event.type}`);
return { ok: true };
}/webhooks/{id}Returns one webhook subscription's detail (includes deliverySequence).
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The webhook subscription |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Not found |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"endpoint": {
"id": "string",
"companyId": "string",
"url": "string",
"eventTypes": [
"string"
],
"description": "string",
"enabled": false,
"createdAt": "string",
"secretHint": "string",
"lastStatus": "delivered",
"lastAttemptAt": "string",
"lastError": "string",
"consecutiveFailures": 0,
"disabledAt": "string",
"disabledReason": "string",
"pendingSecretExpiresAt": "string",
"transport": "https",
"emailTo": "string",
"filterPredicate": {
"bankKey": "string",
"currency": "string",
"minAmount": 0,
"maxAmount": 0
},
"deliverySequence": 0,
"health": {
"windowHours": 0,
"delivered": 0,
"failed": 0,
"pending": 0,
"skippedDisabled": 0,
"skippedUnsubscribed": 0
},
"unknownEventTypes": [
"string"
]
}
}/webhooks/{id}Soft-deletes this webhook endpoint. The endpoint stops receiving events immediately and refuses enable/redeliver, but its delivery history stays readable via GET /webhooks/{id}/deliveries until each row's own 30-day retention expires.
?confirm=<id> is REQUIRED unconditionally, unlike DELETE /connections/{id}, there is no "test" tier of webhook endpoint whose deletion is cheap to reverse; without it the request is refused with 400 confirmation_required.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
confirm string | query | yes | The endpoint's own id. Always required for this route. |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
| Status | Description |
|---|---|
| 200 | Deleted |
| 400 | Confirmation required: re-send with ?confirm=<id> |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Not found |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"removed": false
}/webhooks/{id}/enable| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | no |
{
"companyId": "string"
}| Status | Description |
|---|---|
| 200 | Webhook enabled |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Not found |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"endpoint": {
"id": "string",
"companyId": "string",
"url": "string",
"eventTypes": [
"string"
],
"description": "string",
"enabled": false,
"createdAt": "string",
"secretHint": "string",
"lastStatus": "delivered",
"lastAttemptAt": "string",
"lastError": "string",
"consecutiveFailures": 0,
"disabledAt": "string",
"disabledReason": "string",
"pendingSecretExpiresAt": "string",
"transport": "https",
"emailTo": "string",
"filterPredicate": {
"bankKey": "string",
"currency": "string",
"minAmount": 0,
"maxAmount": 0
},
"deliverySequence": 0,
"health": {
"windowHours": 0,
"delivered": 0,
"failed": 0,
"pending": 0,
"skippedDisabled": 0,
"skippedUnsubscribed": 0
},
"unknownEventTypes": [
"string"
]
}
}/webhooks/{id}/disable| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | no |
{
"companyId": "string"
}| Status | Description |
|---|---|
| 200 | Webhook disabled |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Not found |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"endpoint": {
"id": "string",
"companyId": "string",
"url": "string",
"eventTypes": [
"string"
],
"description": "string",
"enabled": false,
"createdAt": "string",
"secretHint": "string",
"lastStatus": "delivered",
"lastAttemptAt": "string",
"lastError": "string",
"consecutiveFailures": 0,
"disabledAt": "string",
"disabledReason": "string",
"pendingSecretExpiresAt": "string",
"transport": "https",
"emailTo": "string",
"filterPredicate": {
"bankKey": "string",
"currency": "string",
"minAmount": 0,
"maxAmount": 0
},
"deliverySequence": 0,
"health": {
"windowHours": 0,
"delivered": 0,
"failed": 0,
"pending": 0,
"skippedDisabled": 0,
"skippedUnsubscribed": 0
},
"unknownEventTypes": [
"string"
]
}
}/webhooks/{id}/testSends a signed ping to the endpoint and returns the delivery result.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | no |
{
"companyId": "string"
}| Status | Description |
|---|---|
| 200 | Delivery result of the test event |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Not found |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"ok": false,
"queued": false
}/webhooks/{id}/deliveriesPer-attempt delivery log so no event is silently lost. Newest first, keyset-paginated: pass cursor = the previous page's nextCursor to continue. limit defaults to 50, max 200.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
cursor string | query | no | The previous page's `nextCursor`, opaque. A malformed one is 400 `invalid_cursor`. |
limit integer | query | no |
| Status | Description |
|---|---|
| 200 | A page of delivery attempts |
| 400 | A `limit` that is not a number (`invalid_parameter`), or a malformed `cursor` (`invalid_cursor`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Webhook not found |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"items": [
{
"id": "whd_…",
"webhookId": "wh_…",
"companyId": "string",
"eventType": "payment.sent",
"payload": {},
"status": "delivered",
"httpStatus": 0,
"lastError": "string",
"attempts": 0,
"deliveredAt": "2026-01-01T00: 00: 00Z",
"createdAt": "2026-01-01T00: 00: 00Z",
"updatedAt": "2026-01-01T00: 00: 00Z",
"sequence": 0,
"expiresAt": "2026-01-01T00: 00: 00Z",
"sentAt": "2026-01-01T00: 00: 00Z"
}
],
"nextCursor": "string"
}/webhooks/{id}/deliveries/{deliveryId}/redeliverRe-enqueues the original event payload, as a new delivery attempt (a new row; the original failure record is preserved). A fresh signature is computed at send time. 404 if the delivery is unknown or not in a failed/delivered state.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
deliveryId string | path | yes | |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
| Status | Description |
|---|---|
| 200 | Re-enqueued |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Delivery not found / not redeliverable |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"redelivered": false,
"deliveryId": "string"
}/webhooks/{id}/redeliver-failedRe-enqueues failed deliveries for the webhook created at/after since. Bounded: a single call replays at most a fixed cap (500); while more remain, nextCursor is non-null. Pass it back as cursor to drain the rest. Idempotent: a replay id is derived from the source delivery, so a repeated/overlapping window cannot double-enqueue.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
since string | query | yes | ISO timestamp: only failed deliveries created at/after this are replayed. |
cursor string | query | no | The previous call's `nextCursor`, opaque, to continue draining. A malformed one is 400 `invalid_cursor`. |
| Status | Description |
|---|---|
| 200 | Re-enqueued count + drain cursor |
| 400 | A missing or invalid `since` (`invalid_since`), or a malformed `cursor` (`invalid_cursor`). |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Webhook not found |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"count": 0,
"nextCursor": "string"
}/webhooks/{id}/re-enableClears an auto-disabled state: resets consecutiveFailures to 0 and clears disabledAt/disabledReason. 400 if the webhook is not currently disabled.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
| Status | Description |
|---|---|
| 200 | Re-enabled |
| 400 | Webhook is not disabled |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Webhook not found |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"webhookId": "string",
"enabledAt": "string"
}/webhooks/{id}/rotate-secretGenerates a new signing secret and returns it once. The previous secret is retained as a 'pending' secret and both signatures verify during the overlap window (overlapHours, default 24, max 72), so the receiver can update its key without dropping in-flight deliveries. Outbound deliveries are immediately signed with the new secret. After the window (housekeeping) the old secret is dropped.
| Name | In | Required | Description |
|---|---|---|---|
id string | path | yes | |
companyId string | query | no | The organisation this request acts on. Read from here OR from `companyId` in the JSON body (this query parameter wins when both are sent). Required, in one of the two places, for an API key, a signed-in session, a workspace administrator or a system admin; an application install or a delegated user falls back to its own company when neither is given. |
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | no | |
overlapHours | number | no | How long the old secret stays valid. |
{
"companyId": "string",
"overlapHours": 0
}| Status | Description |
|---|---|
| 200 | Rotated: newSecret shown once |
| 400 | Invalid body |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Admin required: a session without the Admin role is refused (`forbidden`). |
| 404 | Webhook not found |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"webhookId": "string",
"newSecret": "string",
"overlapUntil": "string"
}/billing/webhooks/stripeVerifies the Stripe-Signature header over the raw request body and records the event idempotently. Not part of the integrator surface, configured once, in the Stripe Dashboard, against this platform's own webhook signing secret.
| Name | In | Required | Description |
|---|---|---|---|
Stripe-Signature string | header | yes | HMAC signature over the raw body, minted by Stripe. |
| Status | Description |
|---|---|
| 200 | Accepted (fresh event, redelivery, or a recognised-but-unhandled event type) |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Missing or invalid Stripe-Signature |
| 403 | Caller IP is not on the published Stripe webhook allowlist |
| 404 | Billing not configured on this deployment |
| 409 | An earlier delivery of the same event is still being handled; Stripe's retry is answered once that delivery finishes, or after it is taken to have died |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"received": false
}/email/webhooks/brevo/marketingA marketing unsubscribe, or a spam complaint about a campaign, withdraws the person's marketing-email consent unless they gave a newer one; a hard bounce flags their address. Authenticated by the bearer token configured on the webhook in Brevo. Not part of the integrator surface. Answers 200 to every accepted event, including event types it does not act on.
| Name | In | Required | Description |
|---|---|---|---|
Authorization string | header | yes | `Bearer <token>`: the token configured on the webhook in Brevo (BREVO_WEBHOOK_TOKEN). |
| Status | Description |
|---|---|
| 200 | Accepted |
| 400 | Body is not a Brevo event or an array of events |
| 401 | Missing or wrong bearer token |
| 403 | Caller IP is not on BREVO_WEBHOOK_IP_ALLOWLIST |
| 404 | The receiver is not configured on this deployment |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
| 500 | An event could not be recorded; Brevo may deliver it again |
{
"received": false
}/email/webhooks/brevo/transactionalA hard bounce or spam complaint flags the person's address for admins to see; email keeps being sent to it. Authenticated by the bearer token configured on the webhook in Brevo. Not part of the integrator surface. Answers 200 to every accepted event, including event types it does not act on.
| Name | In | Required | Description |
|---|---|---|---|
Authorization string | header | yes | `Bearer <token>`: the token configured on the webhook in Brevo (BREVO_WEBHOOK_TOKEN). |
| Status | Description |
|---|---|
| 200 | Accepted |
| 400 | Body is not a Brevo event or an array of events |
| 401 | Missing or wrong bearer token |
| 403 | Caller IP is not on BREVO_WEBHOOK_IP_ALLOWLIST |
| 404 | The receiver is not configured on this deployment |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
| 500 | An event could not be recorded; Brevo may deliver it again |
{
"received": false
}Workspace
Company dashboard, API keys, and per-bank test payments
/banks/{bankKey}/test-scenariosLists the canned test-payment scenarios this bank can send (domestic, sepa, international, urgent), tailored to its country and offered payment types: each with its label, amount, currency and the chosen payment type.
| Name | In | Required | Description |
|---|---|---|---|
bankKey string | path | yes | |
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | Offered scenarios |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"scenarios": [
{
"id": "domestic",
"label": "string",
"description": "string",
"amount": "string",
"currency": "string",
"creditorName": "string",
"paymentType": "string"
}
]
}/banks/{bankKey}/test-paymentBuilds a schema-valid canonical payment for the chosen scenario from the debtor account supplied, then runs it through the normal convert → approval → deliver path over the chosen environment (defaults to the test channel). Requires a signed-in session.
| Name | In | Required | Description |
|---|---|---|---|
bankKey BankKey | path | yes |
| Property | Type | Required | Description |
|---|---|---|---|
companyId | string | yes | |
scenario | enum | yes | |
environment | enum | no | |
account | object | yes | |
executionDate | string | no | The requested execution date (YYYY-MM-DD). Defaults to the company's own today when omitted. |
{
"companyId": "string",
"scenario": "domestic",
"environment": "test",
"account": {
"name": "string",
"iban": "string",
"other": {
"id": "string",
"scheme": "string"
},
"currency": "string",
"country": "string"
},
"executionDate": "string"
}| Status | Description |
|---|---|
| 201 | Test payment journaled |
| 400 | Invalid request |
| 401 | Sign in required: there is no valid session. |
| 403 | Admin required: a session without the Admin role, an API key or an application install is refused (`admin_required`). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"id": "string",
"journalNo": "string",
"companyId": "string",
"bankKey": "string",
"environment": "test",
"direction": "inbound",
"type": "pain.001",
"sourceFormat": "string",
"summary": "string",
"status": "string",
"messageId": "string",
"transactionCount": 0,
"totals": [
{
"currency": "string",
"amount": "string"
}
],
"paymentType": "string",
"source": {
"reference": "string",
"name": "string"
},
"collected": false,
"collectedAt": "string",
"idempotencyKey": "string",
"createdAt": "string",
"expiresAt": "string",
"deduped": false,
"xml": "string",
"deliveryMode": "string",
"autoSelected": [
{
"paymentId": "string",
"selected": "string",
"confidence": "string",
"reason": "string"
}
],
"autoFilled": [
"string"
],
"unknownFieldsDropped": [
"string"
],
"warnings": [
{
"code": "string",
"level": "generic",
"severity": "error",
"message": "string",
"path": "string",
"bank": "string",
"paymentType": "string"
}
],
"approval": {
"id": "string",
"companyId": "string",
"documentId": "string",
"paymentRef": "string",
"source": {
"reference": "string",
"name": "string"
},
"erpNotes": [
{
"kind": "payee-account-changed",
"paymentRef": "string",
"from": "string",
"to": "string",
"shownTo": "string",
"shownAt": "string"
}
],
"makerUserId": "string",
"makerName": "string",
"makerClaimedEmail": "string",
"maxTransaction": "string",
"journalTotal": "string",
"currency": "string",
"currencies": [
"string"
],
"payeeName": "string",
"payeeCount": 0,
"fundingAccount": {
"iban": "string",
"accountOther": "string",
"bankKey": "string",
"accounts": [
{
"iban": "string",
"accountOther": "string",
"totals": []
}
]
},
"policyVersionId": "string",
"requiredApprovals": 0,
"allowSelfApproval": false,
"approverLimits": {},
"requireOneFromEachGroup": false,
"orderedApproval": false,
"groups": [
{
"id": "string",
"name": "string",
"userIds": [
"string"
]
}
],
"approverUserIds": [
"string"
],
"approvals": [],
"rejections": [],
"status": "pending",
"createdAt": "string",
"updatedAt": "string"
},
"delivery": {
"queued": false,
"externalApproval": {
"authority": "string",
"at": "string"
}
},
"whitelist": {
"applicable": false,
"outcome": "cleared",
"checks": [
{
"nip": "string",
"nrb": "string",
"date": "string",
"status": "listed",
"requestId": "string",
"error": "string"
}
],
"conversions": [
{
"nrb": "string",
"amount": "string",
"currency": "string",
"date": "string",
"rate": {
"table": "string",
"no": "string",
"effectiveDate": "string",
"mid": "string"
},
"amountPln": "string",
"reachesThreshold": false,
"error": "string"
}
]
}
}System
Health and version
/healthAlways 200 while the process is up. status is strictly serviceability. A serving box says "ok" even when non-fatal startup config warnings exist; those ride in the advisory warnings field as stable machine codes. Use /ready for the orchestrator serviceability probe.
| Status | Description |
|---|---|
| 200 | Server is running |
{
"status": "string",
"warnings": [
"string"
]
}/readyReadiness probe for orchestrators (distinct from /health liveness): 200 when the app can serve. The database is reachable and migrated when one is configured, else 503 so the load balancer keeps traffic away. Public, like /health.
| Status | Description |
|---|---|
| 200 | Ready |
| 503 | Not ready: frontend bundle missing, schema not migrated, or database unreachable. `error.details.reason` is one of `frontend_bundle_missing`, `schema_not_migrated`, `database_unreachable` so a caller can branch on which; `error.details.ready` is always `false`. |
{
"ready": false
}/fx/ratesGlobal (non-tenant) indicative EUR-base FX rates for converting balances to a common currency. Read-only, kept fresh by a daily job (seeded so it works offline). Shape: { base, rates, asOf }.
| Status | Description |
|---|---|
| 200 | Cached EUR-base rates |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"base": "string",
"rates": {},
"asOf": "string"
}Getting started
/sandbox/provisionSelf-serve onboarding: sandbox only, no sign-in. Send your email and we email you a 6-digit code; the answer is 202 with a verificationId. Send both to POST /sandbox/provision/verify (or open the link in the email) and we create a complete, isolated sandbox: your own company, an API key and an active demo bank connection. Everything created is sandbox-only (no real payments can move). It creates no dashboard sign-in: the web dashboard is reached by signing up at /signup. An email address has one sandbox at a time. Rate-limited per IP and per email address; unused sandboxes are removed after 30 days. An ERP add-on's pairing request (clientId + PKCE) without an email is still answered at once with 201 while the sandbox allows it, for add-on releases that do not ask for an email yet.
| Property | Type | Required | Description |
|---|---|---|---|
email | string | no | Your email address. We email a 6-digit code to it; the sandbox and its API key are created when you send the code to `POST /sandbox/provision/verify`. |
source | string | no | Where the request comes from: `docs`, `website` or `start`. Anything else is recorded as `api`. |
bankKey | string | no | Demo bank to connect (default `danske-dk`). See GET /profiles. |
companyName | string | no | Optional name for the created company. |
label | string | no | Optional label for the API key. |
clientId | string | no | ERP add-on pairing: the add-on's OAuth `client_id` (a first-party ERP add-on, e.g. `uniconta-addon`). With the three PKCE/redirect fields below, the response carries `pairing.code`: redeem it at `POST /oauth/token` (`grant_type=authorization_code`, same `redirect_uri`, your `code_verifier`). No browser, no consent screen: the sandbox is new and cannot move money. |
codeChallenge | string | no | PKCE S256 code challenge (with `clientId`). |
codeChallengeMethod | string | no | Must be `S256`. |
redirectUri | string | no | The add-on's loopback redirect URI, bound to the code (with `clientId`). |
ibans | string[] | no | The ERP company's own IBANs, registered as accounts of the new sandbox company so the demo bank's statements for them reach it. An IBAN another sandbox already holds refuses the call with 409 `iban_held_by_another_organisation`. |
externalRef | string | no | The ERP company this sandbox is for (e.g. `uniconta:<companyId>`), with `clientId` only. For an hour, a retry with the same `externalRef` and the same `codeChallenge` answers 200 with the same sandbox and a fresh `pairing.code` (no API key, which is in the first answer only); a different challenge gets 409 `sandbox_already_provisioned`. |
{
"email": "dev@example.com",
"source": "docs",
"bankKey": "danske-dk",
"companyName": "Acme Sandbox",
"label": "my-integration",
"clientId": "uniconta-addon",
"codeChallenge": "string",
"codeChallengeMethod": "string",
"redirectUri": "http://127.0.0.1: 53682/callback",
"ibans": [
"string"
],
"externalRef": "string"
}| Status | Description |
|---|---|
| 200 | An ERP company's sandbox replayed (same `externalRef` and code challenge, no `email`): the company, its IBANs and a fresh pairing code. No credential of the company. |
| 201 | Sandbox provisioned (an ERP add-on's pairing request without an `email`). |
| 202 | Code emailed: send it with `verificationId` to `POST /sandbox/provision/verify`. |
| 400 | `missing_fields` (no `email`), or an invalid email, body, IBAN or pairing request (unknown client, redirect or PKCE). |
| 404 | Not a deployed sandbox (this endpoint does not exist in production). |
| 409 | `iban_held_by_another_organisation` (retry without those IBANs) or `sandbox_already_provisioned` (this `externalRef` was provisioned recently with a different code challenge). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Rate limited: too many sandboxes from this IP, or codes to this address, recently. |
| 502 | `email_send_failed`: the code could not be emailed. Try again. |
| 503 | `email_transport_unavailable`: this server cannot send email. |
{
"message": "string",
"companyId": "string",
"bank": {
"key": "string",
"name": "string",
"country": "string"
},
"sandboxIban": "string",
"ibans": [
"string"
],
"pairing": {
"clientId": "string",
"code": "string",
"redirectUri": "string",
"scopes": [
"string"
],
"expiresIn": 0
}
}/sandbox/provision/verifyRedeem the 6-digit code POST /sandbox/provision emailed, with the verificationId its answer carried. Answers 201 with the sandbox: the raw API key (shown once), the company and a ready-to-run first payment, plus pairing when an ERP add-on asked for one. The key belongs to the sandbox's company: later requests name that companyId, and any other company's id is refused. A code works for 60 minutes and takes 5 tries; wrong codes from one IP also lock that IP out for a while.
| Property | Type | Required | Description |
|---|---|---|---|
verificationId | string | yes | The `verificationId` from the `202` answer of `POST /sandbox/provision`. |
code | string | yes | The 6-digit code from the email. |
{
"verificationId": "string",
"code": "482913"
}| Status | Description |
|---|---|
| 200 | An ERP company's sandbox replayed (same `externalRef` and code challenge): the company, its IBANs and a fresh pairing code. No credential of the company. |
| 201 | Sandbox provisioned |
| 400 | `missing_fields`: `verificationId` and `code` are both required. |
| 404 | Not a deployed sandbox (this endpoint does not exist in production). |
| 409 | `sandbox_exists_for_email` (this address already has a sandbox), `iban_held_by_another_organisation` or `sandbox_already_provisioned`. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | `sandbox_code_invalid`: wrong, expired or already used. `details.attemptsLeft` is 0 when only a new code helps. |
| 429 | Too many wrong codes from this IP recently. |
{
"message": "string",
"companyId": "string",
"bank": {
"key": "string",
"name": "string",
"country": "string"
},
"sandboxIban": "string",
"ibans": [
"string"
],
"pairing": {
"clientId": "string",
"code": "string",
"redirectUri": "string",
"scopes": [
"string"
],
"expiresIn": 0
}
}/signupSelf-serve production onboarding: production, and a deployed sandbox (where the organisation is a sandbox tenant, removed once idle). Creates your organisation and invites every administrator you list, then emails each of them a verification link. Nothing usable (no password, no API key) exists until an administrator completes POST /signup/verify with the token from their email. administrators[0] becomes the account that verification mints an API key for. The organisation always starts on the platform's default approval policy (2 distinct approvers, no self-approval): configure anything else afterwards from Approval policies. Rate-limited per IP.
| Property | Type | Required | Description |
|---|---|---|---|
administrators | object[] | yes | One or more administrators to invite. `administrators[0]` is the signup owner. The row `POST /signup/verify` recognises via its Terms-of-Service acceptance and mints an API key for. Every entry is created with `["admin","approver"]` roles. Since the organisation's default approval policy needs 2 distinct approvers, listing only one administrator here is legal but leaves the organisation unable to release a payment until a second one is invited (from Settings → Team, after verifying). |
companyName | string | yes | The new organisation's registered name. Stored as its name and as the legal name its invoices are made out to (`GET /company/legal-identity`). |
registrationNumber | string | no | The organisation's national registration number, required unless a `vatNumber` stands in for it. Where the form asks for a VAT number and the registration number gives none, the country's full VAT number sent here, prefix included (`IE6388047V`), with no `vatNumber`, is the `vatNumber`, and no registration number is recorded. CVR (DK), Y-tunnus (FI), organisationsnummer (SE), organisasjonsnummer (NO) or its equivalent. A number whose country has a check digit must pass it and is stored in the register's form (digits only); any other country's is stored as typed (trimmed). The signup decides on the answer `POST /signup/register-lookup` sealed for the same country and number (`registerAnswer`); when none is sent, it is older than an hour, or the register gave no answer in it, the register is asked again. A number the register does not know, an inactive company and a wrong check digit are refused with 422; a register that still gives no answer is refused with 503 (try again). A country with no register lookup, a number VIES does not list as a VAT number, and a register that confirms the number without naming the company go on with the name and address as sent, and the organisation's Admins can correct the number afterwards (`PATCH /company/registration-number`); a number the register confirmed is changed by support only. The name and address are stored as sent. The same value `PUT /company/registration-number` writes and `GET /auth/me` reads back. A Danish organisation's 8-digit CVR number is also stored as its VAT number, `DK` + CVR, unless VIES answered that `DK` + CVR is no VAT number. A signup without one stores none, and the organisation's Admin adds it once afterwards (`PATCH /company/registration-number`). |
registeredAddress | object | yes | The organisation's registered address, stored as its legal identity (`GET /company/legal-identity`). `country` is the organisation's country and the one member required; the others are optional and can be added later on Company settings. |
termsAccepted | boolean | yes | Must be `true`: the acceptance, on behalf of the organisation, of the Customer Agreement (https://bankconnector.com/legal/customer-agreement.html) and the Data Processing Agreement (https://bankconnector.com/legal/dpa.html), version 2026-10-14 (Customer Agreement 1.2, Data Processing Agreement 1.1), that this call records. |
marketingConsent | boolean | no | Optional. `true` only when the person signing up ticked the (unticked by default) box to receive product news by email; it records that consent for the first administrator and no one else. Absent or `false` records nothing. It can be changed at any time from `PUT /me/marketing-consent`. |
programToken | string | no | Optional. The token from a partner's registration link (`signup/p/<token>`), or the link's name (`signup/<name>`, any case); either resolves via `GET /signup/program/{token}`. It attributes the new organisation to that partner's program and applies the program's capability settings; by itself it grants the partner no access to the organisation (see `partnerAccess`), and the organisation is created exactly where a direct signup's would be. A token that names no program is rejected with 422 rather than ignored. Never sent together with `partnerId`. Omit the field entirely for a direct signup. |
partnerId | string | no | Optional. The partner the customer chose on the general signup, an `id` from `GET /signup/partners`. It links the new organisation to that partner exactly as `programToken` does, and grants nothing by itself. An id that list does not offer is rejected with 422 (`details.field: partnerId`), as is a body carrying both `partnerId` and `programToken`. Omit it for no partner. |
partnerAccess | boolean | no | The customer's explicit answer to giving the linked partner access. REQUIRED whenever a partner is linked (by `programToken`, `partnerId`, or an `erpPartner` number a listed partner holds) and rejected with 422 (`details.field: partnerAccess`) when missing or when no partner is linked, an `erpPartner` number no listed partner holds included; it is never defaulted. `true` gives the partner Administer access, starting when the signup owner confirms their email (`POST /signup/verify` with their own link), and records the owner as the person who gave it; a signup that is never confirmed gives nothing, nor does one whose organisation has by then decided that partner's access itself or changed its partner, and the customer can remove the access at any time. What the customer is told it means: See whether your bank connections are working. Also see your payments and bank statements, without changing anything. Also set up and change your bank connections. A partner can never approve a payment, change approval rules or add users. `false` keeps the partner linked with no access. |
erpPartner | object | no | Optional. The customer's partner as their ERP knows it, for an ERP add-on that cannot know the partner's `id`. A number held by a partner the general signup lists (the one `GET /signup/erp-partner/uniconta/{pid}` names) links the new organisation to that partner exactly as `partnerId` does, and grants nothing by itself. Any other number, one nobody holds or one a partner not on that list holds, links no partner and is not refused: the signup goes on as one with no partner, and answers the same for both, so it does not tell which numbers an unlisted partner holds. Ask the customer which partner they work with instead (`partnerId`). The add-on reads the outcome afterwards from `GET /onboarding/partner`. Ignored when `programToken` or `partnerId` is sent. A malformed number is rejected with 422 (`details.field: erpPartner.uniconta`). |
source | string | no | Optional. `erp:<slug>`: the ERP add-on whose setup wizard sent the customer here (the signup page reads it from the URL fragment). Recorded on the signup for attribution; it grants nothing and selects no program. A source naming an ERP BankConnector has an add-on for (`erp:uniconta`) also decides the organisation's product, ERP Add-on for that ERP, and `product` is then ignored. A malformed value is rejected with 422 (`details.field: source`). |
product | string | no | Optional. How the organisation will use BankConnector: `direct-api` (its own systems call this API) or `erp-addon` (through an add-on in its accounting system, named in `productErp`). Omitted, the organisation starts on `direct-api`. Ignored when `source` names an ERP add-on. An Admin can change it in settings until the organisation chooses a package (`PATCH /company/product`). Anything else, `embedded` included, is rejected with 422 (`details.field: product`). |
productErp | string | no | The ERP for `product: erp-addon`: `uniconta`. Required with `erp-addon` and rejected with any other product, with 422 (`details.field: productErp`). |
authorization | object | no | Optional. An ERP add-on's pending OAuth authorization request. The request half of the consent it would otherwise open the browser at (`POST /oauth/authorize/decision`'s members, camelCase). It is checked as the consent screen checks it: a registered client, a redirect that client registered (an add-on's `http://127.0.0.1` loopback path, any port), S256 PKCE and scopes the client is registered for; anything else is rejected with 422 (`details.field: authorization`) before anything is created. When the first administrator verifies within 60 minutes of this call, `POST /signup/verify` answers with `authorize.url`: the consent screen for this request, where they finish signing in (setting up two-factor authentication if needed) and approve. Send it only when `administrators[0]` is the person at the add-on's computer, the loopback is there. |
registerAnswer | string | no | |
vatNumber | string | no | Optional. The VAT number `POST /signup/register-lookup` looked the company up by, for an EU country whose registration number gives no VAT number (not Denmark, Finland or Sweden) and whose VIES answer names the company (not Germany or Spain); ignored for any other country. Sent, VIES's answer for it decides the signup in place of the registration number's: the sealed `registerAnswer` for it when under an hour old, else VIES is asked again. A number VIES does not list is refused with 422 on `vatNumber`, and VIES giving no answer with 503. Stored as the organisation's VAT number, in VIES's form, when VIES confirms it. A value that is not a VAT number of the country, or carries another country's prefix, is refused with 422 on `vatNumber`. |
language | enum | no | Optional. `da` when the form was shown in Danish, else `en`, the default. Every refusal a person can meet is worded in it. The signup keeps it: each administrator's confirmation email, and a retry of one, is written in it, and the terms acceptance is recorded with the sentence the owner ticked, in it. The agreements accepted are the same English documents in either language |
{
"administrators": [
{
"email": "owner@example.com",
"name": "Jane Owner"
},
{
"email": "cofounder@example.com",
"name": "Alex Cofounder"
}
],
"companyName": "Acme ApS",
"registrationNumber": "13748136",
"registeredAddress": {
"streetName": "Havnegade",
"buildingNumber": "14",
"postCode": "1058",
"townName": "København",
"country": "DK",
"addressLines": [
"c/o Revisorhuset"
]
},
"termsAccepted": false,
"marketingConsent": false,
"programToken": "string",
"partnerId": "string",
"partnerAccess": false,
"erpPartner": {
"uniconta": 3819
},
"source": "erp:uniconta",
"product": "direct-api",
"productErp": "uniconta",
"authorization": {
"clientId": "uniconta-addon",
"redirectUri": "http://127.0.0.1: 49152/callback",
"scope": "payments:submit payments:read",
"state": "string",
"codeChallenge": "string",
"codeChallengeMethod": "string",
"erpCompanyId": "string"
},
"registerAnswer": "string",
"vatNumber": "NL805734958B01",
"language": "en"
}| Status | Description |
|---|---|
| 202 | Signup accepted: check email to verify |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 409 | An administrator's email address is already another organisation's login (`conflict`, `details.index` and `details.field: administrators.<i>.email` name the row). An address has one login, and a login belongs to one organisation, so the row needs a different address. A refused signup creates and removes nothing. The owner's address (`administrators[0]`) is not refused when it holds only an earlier signup that nobody has confirmed: once every other row is accepted, the earlier signup is removed when the address was its owner (its administrators this signup also names are invited here), and the address leaves it when it was another of its administrators; that address's earlier link stops working. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Missing/invalid field, terms not accepted, no administrators, more than 10 administrators, a duplicate administrator email, a missing registered-address country (`details.field: registeredAddress.country`), a missing company number (`details.field: registrationNumber`; a `vatNumber` stands in for it in a country whose form asks for one), a number whose check digit is wrong, that the register does not know, or whose company the register lists as inactive (`details.field: registrationNumber`), a `vatNumber` VIES does not list (`details.field: vatNumber`), a `programToken` that names no program, a malformed `source`, or an `authorization` request the consent screen would refuse (`details.field: authorization`). |
| 429 | Rate limited: too many signups from this IP recently. |
| 503 | Signup is unavailable: no email transport is configured on this instance (`invites_unavailable`), or the company register gave no answer for the number (`service_unavailable`, `details.field: registrationNumber` or `vatNumber`): nothing was created, and the same signup can be sent again. |
{
"message": "string",
"email": "string",
"administratorsInvited": 0
}/signup/register-lookupChecks the number's check digit and asks the country's company register about it, so the signup form can fill in the company's name and address. Norway is looked up in Brønnøysundregistrene; Denmark in the Danish company register (CVR), every company whether VAT-registered or not, with the EU VAT register (VIES) asked beside it for the VAT registration and as the answer when CVR gives none; the answer comes as soon as CVR's does, without company.vatRegistered, and POST /signup records VIES's answer on it (waiting at most VIES's own 12-second timeout); Finland in the Patent and Registration Office's Business Information System (PRH), and in VIES when PRH gives no answer; Sweden in VIES, as the VAT number the registration number gives (DK + CVR, FI + the Business ID's digits, SE + organisationsnummer + 01). lookup.register names the register that answered. VIES gives no name or address where the member state does not disclose them; not_vat_registered means VIES does not list that VAT number. For an EU country whose registration number gives no VAT number (not Denmark, Finland or Sweden) and whose VIES answer names the company (not Germany or Spain), a vatNumber sent is asked about in VIES instead of the registration number, which may then be left out; lookup.number is that VAT number in VIES's form. Any other country answers not_covered. Every outcome answers 200 with the register's answer and the sentence the form shows. The answer is sealed in registerAnswer for an hour, and POST /signup decides on it without asking the register again (see there for which answers stop a signup). Production and deployed sandbox only, unauthenticated, and rate-limited per IP together with POST /partners/register/lookup.
| Property | Type | Required | Description |
|---|---|---|---|
country | string | yes | ISO 3166-1 alpha-2: where the company is registered. |
registrationNumber | string | no | The company's registration number: CVR, organisasjonsnummer, organisationsnummer. Required unless a `vatNumber` is looked up instead. The country's full VAT number sent here, prefix included, with no `vatNumber`, is looked up as the `vatNumber` (as for `POST /signup`). |
vatNumber | string | no | Optional. The company's VAT number, with or without its country prefix, for a country where it is looked up instead of the registration number (see above); ignored for any other country. |
language | enum | no | Optional. `da` or `en`, the default: the language `message` and any refusal are worded in |
{
"country": "string",
"registrationNumber": "string",
"vatNumber": "NL805734958B01",
"language": "en"
}| Status | Description |
|---|---|
| 200 | The register's answer. |
| 400 | The body is not valid JSON. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | No valid country (`details.field: registeredAddress.country`), no number (`details.field: registrationNumber`), or a `vatNumber` that is not a VAT number of the country (`details.field: vatNumber`). |
| 429 | Rate limited: too many lookups from this IP recently. |
{
"lookup": {
"outcome": "active",
"country": "string",
"number": "string",
"register": "string",
"lookedUpAt": "string",
"company": {
"name": "string",
"address": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string"
},
"legalForm": "string",
"inactiveReasons": [
"deleted"
],
"vatRegistered": false
},
"consultationNumber": "string"
},
"message": "string",
"registerAnswer": "string"
}/signup/company-searchThe signup form's company-name search, run as the person types: at most eight active companies in the country's company register whose name matches, the likeliest first, each with its registration number and town. Searched for Denmark (CVR, on a server with Erhvervsstyrelsen's CVR access), Norway (Enhetsregisteret), Finland (PRH), the United Kingdom (Companies House, on a server with its API key) and France (Recherche d'entreprises). Look the picked number up with POST /signup/register-lookup, whose sealed answer is what POST /signup decides on. Production and deployed sandbox only, unauthenticated, and rate-limited per IP on its own budget.
| Property | Type | Required | Description |
|---|---|---|---|
country | string | yes | ISO 3166-1 alpha-2; `DK`, `NO`, `FI`, `GB`, `FR` and `SE` are searched. |
name | string | yes | The name as typed so far; the last word may be cut short. |
language | enum | no |
{
"country": "string",
"name": "novo nordis",
"language": "en"
}| Status | Description |
|---|---|
| 200 | The matching companies; an empty list when none matches. |
| 400 | The body is not valid JSON. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Fewer than two letters (`details.field: name`), or a country this server cannot search by name (`details.field: country`). |
| 429 | Rate limited: too many searches from this IP recently. |
| 503 | CVR gave no answer (`service_unavailable`); give the number instead. |
{
"items": [
{
"registrationNumber": "string",
"name": "string",
"townName": "string"
}
]
}/signup/partnersThe partners an operator switched on for the general signup, so a customer who signs up without a partner link can name the partner who referred them. Returns an id and a display name for each and nothing else. Production and deployed sandbox only, unauthenticated, and rate-limited per IP with the registration-link lookup.
| Status | Description |
|---|---|
| 200 | The listed partners; an empty list when none is switched on. |
| 404 | Self-service signup is not open on this deployment. |
| 429 | Rate limited: too many lookups from this IP recently. |
{
"items": [
{
"id": "string",
"displayName": "string"
}
]
}/signup/erp-partner/uniconta/{pid}The signup page an ERP add-on's link opens (#erpPartner.uniconta=<pid>) calls this to name the partner it asks the customer about. Returns the display name of the holder only when the general signup lists that partner (GET /signup/partners), and nothing else. Production and deployed sandbox only, unauthenticated, and rate-limited per IP with the registration-link lookup. POST /signup resolves erpPartner.uniconta itself; this answer decides nothing.
| Name | In | Required | Description |
|---|---|---|---|
pid string | path | yes | The partner firm's Uniconta partner number (`_Belongs2Pid`), a whole number from 1. |
| Status | Description |
|---|---|
| 200 | The partner holding this number |
| 404 | No partner the general signup lists holds this number. The same answer for a number nobody holds, one an unlisted, inactive or deleted partner holds, and a malformed one; also when self-service signup is not open on this deployment. |
| 429 | Rate limited: too many lookups from this IP recently. |
{
"displayName": "string"
}/signup/program/{token}A partner hands out a registration link (signup/p/<token>). This resolves that token to the branding the signup page renders, production only, and unauthenticated, because it is called before anyone has an account. It returns branding only. Pass the same token back to POST /signup as programToken to have the organisation attributed to that program. Rate-limited per IP, separately from signup.
| Name | In | Required | Description |
|---|---|---|---|
token string | path | yes | The opaque token from the partner's registration link, or the name of a named link (any case). |
| Status | Description |
|---|---|
| 200 | The program this link names |
| 404 | No program holds this token (the same answer for a link that never existed and one that has been rotated away), or this is not a production deployment. |
| 429 | Rate limited: too many lookups from this IP recently. |
{
"programId": "string",
"displayName": "string"
}/signup/verifyRedeems the token emailed by POST /signup, sets your password, and. The first time this succeeds for a signup's owner, mints the organisation's API key. Returns 200 with no credential for any other invite token (this route is not a substitute for POST /auth/set-password).
| Property | Type | Required | Description |
|---|---|---|---|
token | string | yes | The token from the verification email link. |
password | string | yes | The password to set for this account. |
language | enum | no | Optional. `da` or `en`, the default: the language a refusal of the link or the password is worded in |
{
"token": "string",
"password": "string",
"language": "en"
}| Status | Description |
|---|---|
| 200 | Password set (not a signup-owner token, or key generation could not complete). |
| 201 | Signup verified: organisation activated |
| 400 | Missing token/password, or the token is invalid/expired/already used. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | Rate limited: too many attempts from this IP recently. |
{
"ok": false,
"email": "string",
"apiKeyPending": false,
"awaitingApproval": false,
"twofaRequired": false,
"twofaToken": "string",
"forcedEnroll": false,
"expiresAt": "string",
"user": {
"id": "string",
"companyId": "string",
"email": "string",
"name": "string",
"roles": [
"admin"
],
"status": "active",
"settings": {
"locale": "string",
"timezone": "string",
"dateFormat": "iso",
"notifications": {
"paymentNeedsApproval": false,
"paymentApproved": false,
"paymentRejected": false,
"statementReceived": false,
"erpDecisionReceipt": false
}
},
"totpEnabledAt": "string",
"emailDeliveryFlag": {
"kind": "hard_bounce",
"at": "string"
},
"createdAt": "string"
},
"csrfToken": "string"
}/partners/registerA firm applies to become a partner: its partner type, its details, its administrators (the contact person first, then any colleagues) and where its customers operate, plus its acceptance of the BankConnector Partner Agreement (version 1.2, https://bankconnector.com/legal/partner-agreement.html). The contact is emailed a confirmation link, where they choose their password; nothing reaches BankConnector until it is opened, and the firm cannot sign in until BankConnector approves the application. The other administrators are invited on approval. The number is looked up in the country's company register (see POST /partners/register/lookup): an invalid number, one the register does not know, or a company it lists as inactive is refused with 422 on the number sent and nothing is recorded. VIES not listing the VAT number (not_vat_registered) is not refused: the firm types its name and address. A company the register names takes its legal name and address from the register, and legalName and address may be left out; sent anyway, they are the name and address the applicant corrected, they replace the register's on the application, and the register's version stays in its registerLookup. firmKind, firmKindOther, customerCount, erpSystems and erpSystemsOther are not taken: a body carrying any of them is refused with 422 naming that field. Production and deployed sandbox only, unauthenticated, and rate-limited per IP with signup.
| Property | Type | Required | Description |
|---|---|---|---|
legalName | string | no | Required unless the register found the company; its name is used when it did, unless one is sent here. |
vatNumber | string | no | The firm's company registration number (CVR, SIREN, …). Required unless a valid `euVatNumber` is sent, which any EU country other than Denmark, Finland and Sweden accepts in its place. A French SIREN or a Czech IČO is asked about in VIES as the VAT number it gives. The full VAT number of a country whose registration number gives none, prefix included (`IE6388047V`), is the `euVatNumber`. A value starting `enc:` is refused with 400 `validation_failed`. |
euVatNumber | string | no | The firm's VAT number, for an EU country other than Denmark, Finland and Sweden; ignored for any other country. VIES is asked about it instead of the registration number. It stands in for `vatNumber`, and is recorded as the firm's number when no `vatNumber` is sent. VIES confirms a German or Spanish number without the firm's name and address, which are then typed. Refused with 422 on `euVatNumber` when it is not a VAT number of the country, and when neither number is sent for a country that takes it. |
country | string | yes | ISO 3166-1 alpha-2: where the firm is registered. |
address | object | no | Required unless the register found the company with an address; its address is used when it did, unless one is sent here. |
phone | string | yes | |
website | string | yes | |
administrators | object[] | yes | The firm's administrators: the contact person first. Each address must be distinct. |
partnerType | string | yes | `uniconta-partner`: Uniconta partner. You set up BankConnector for your Uniconta customers. `api-partner`: API partner. A consultancy or IT firm that connects its customers' own systems to their banks for them. `software-vendor`: Software vendor. A software company whose own product, such as an ERP or accounting system or a platform, gives all its users bank connections. |
customerRegions | string[] | yes | Where the firm's customers operate. |
agreementAccepted | boolean | yes | Must be `true`. |
agreementVersion | string | yes | The agreement version the applicant read; must be the current one (`1.2`). |
registerAnswer | string | no | Optional. The `registerAnswer` `POST /partners/register/lookup` gave for this country and number, sent back unchanged. Within the hour, the application decides on that answer and the register is not asked again; a missing, altered, expired or other number's answer, or one where the register could not be reached, has the register asked again. |
language | enum | no | Optional. `da` when the form was shown in Danish, else `en`, the default. Every refusal a person can meet is worded in it. The application keeps it: the confirmation email, a re-sent one and the receipt are written in it, and the agreement acceptance is recorded with the sentence the contact ticked, in it. The Partner Agreement is the same English document in either language |
firmKind | - | no | Not taken: refused with 422 naming the field when present. |
firmKindOther | - | no | Not taken: refused with 422 naming the field when present. |
customerCount | - | no | Not taken: refused with 422 naming the field when present. |
erpSystems | - | no | Not taken: refused with 422 naming the field when present. |
erpSystemsOther | - | no | Not taken: refused with 422 naming the field when present. |
{
"legalName": "string",
"vatNumber": "string",
"euVatNumber": "NL805734958B01",
"country": "string",
"address": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string",
"addressLines": [
"string"
]
},
"phone": "string",
"website": "string",
"administrators": [
{
"name": "string",
"email": "string"
}
],
"partnerType": "string",
"customerRegions": [
"string"
],
"agreementAccepted": false,
"agreementVersion": "string",
"registerAnswer": "string",
"language": "en"
}| Status | Description |
|---|---|
| 202 | Application recorded: the contact confirms it from the emailed link. |
| 400 | The body is not valid JSON. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 409 | An administrator's email address already belongs to a BankConnector login; `details.field` names it. |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | A missing or invalid field; `details.field` names it. The number sent (`vatNumber`, else `euVatNumber`) when it is invalid, unknown to the register, or the company is not active. |
| 429 | Rate limited: too many signups or applications from this IP recently. |
| 502 | The confirmation email could not be sent; try again in a few minutes. |
| 503 | No email can be sent on this instance. |
{
"message": "string",
"email": "string"
}/partners/register/lookupChecks the number's check digit and asks the country's company register about it. Norway is looked up in Brønnøysundregistrene; Denmark in the Danish company register (CVR) alone, and the firm's VAT registration is not asked (company.vatRegistered is null); Finland in the Patent and Registration Office's Business Information System (PRH), and in VIES when PRH gives no answer; France in the government's API Recherche d'entreprises by SIREN, likewise with VIES as the fallback; Czechia in ARES, the Ministry of Finance's register, by IČO, likewise; Ireland in the Companies Registration Office's open data by company number, and Switzerland in the Federal Statistical Office's UID register by UID, each alone; Poland in the Ministry of Finance's VAT taxpayer white list by NIP, with VIES as the fallback; Sweden in VIES, as the VAT number the registration number gives. A euVatNumber for another EU country is asked about in VIES instead, and may be sent without vatNumber (see POST /partners/register). Any other country answers not_covered. Every outcome answers 200 with the register's answer, sealed in registerAnswer, and the sentence the form shows. Nothing is recorded; POST /partners/register decides on the sealed answer when it is sent back, else asks the register again. Production and deployed sandbox only, unauthenticated, and rate-limited per IP.
| Property | Type | Required | Description |
|---|---|---|---|
country | string | yes | ISO 3166-1 alpha-2: where the firm is registered. |
vatNumber | string | no | The firm's company registration number, as for `POST /partners/register`. |
euVatNumber | string | no | The firm's VAT number, as for `POST /partners/register`. |
language | enum | no | Optional. `da` or `en`, the default: the language `message` and any refusal are worded in |
{
"country": "string",
"vatNumber": "string",
"euVatNumber": "NL805734958B01",
"language": "en"
}| Status | Description |
|---|---|
| 200 | The register's answer. |
| 400 | The body is not valid JSON. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | No country, no number, or a `euVatNumber` that is not one of the country; `details.field` names it. |
| 429 | Rate limited: too many lookups from this IP recently. |
{
"lookup": {
"outcome": "active",
"country": "string",
"number": "string",
"register": "string",
"lookedUpAt": "string",
"company": {
"name": "string",
"address": {
"streetName": "string",
"buildingNumber": "string",
"postCode": "string",
"townName": "string"
},
"legalForm": "string",
"inactiveReasons": [
"deleted"
],
"vatRegistered": false
},
"consultationNumber": "string"
},
"message": "string",
"registerAnswer": "string"
}/partners/register/resendThe contact of an application still waiting for its email address to be confirmed is sent a fresh confirmation link. It replaces the link sent before, which is refused from then on, and expires as the first one did. Answers 202 with the same body for every address: one with no application, a colleague the form named, and the contact of an application already confirmed, approved or declined, none of which is changed. Throttled per IP and per address; a throttled request gets the same 202. Production and deployed sandbox only, unauthenticated.
| Property | Type | Required | Description |
|---|---|---|---|
email | string | yes | The contact's address, as given on the application. |
{
"email": "string"
}| Status | Description |
|---|---|
| 202 | Accepted: the neutral answer, identical for every address. |
| 400 | The body is not valid JSON. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | Not an email address (`details.field: email`). |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
| 503 | No email can be sent on this instance. |
{
"message": "string"
}/partners/register/verifyRedeems the token from the confirmation email and sets the contact's password. A password the policy refuses leaves the token unspent. The application then waits for BankConnector's review; the firm's contact cannot sign in until it is approved. Single use. Production and deployed sandbox only.
| Property | Type | Required | Description |
|---|---|---|---|
token | string | yes | The token from the confirmation email link. |
password | string | yes | The contact's password, checked against the password policy. |
language | enum | no | Optional. `da` or `en`, the default: the language a refusal of the link or the password is worded in. The receipt email is written in the language the application was made in |
{
"token": "string",
"password": "string",
"language": "en"
}| Status | Description |
|---|---|
| 200 | Confirmed: the application is with BankConnector for review. |
| 400 | The link is invalid, expired or already used, or the password breaks the policy. |
| 404 | Not a production or deployed-sandbox server (this endpoint does not exist elsewhere). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | No password was given (`details.field: password`). |
| 429 | Rate limited: too many attempts from this IP recently. |
{
"message": "string",
"firmName": "string",
"firstName": "string",
"partnerType": "uniconta-partner"
}/onboarding/statusWhat an organisation has set up so far, so an ERP add-on's setup wizard can show what is done and what is next while the Admin completes the browser-only steps. Read-only, and it carries states, never secrets: the organisation (whether it requires two-factor authentication and whether approving in the ERP is on, and demo: true only for a demo organisation, the same answer GET /oauth/userinfo gives: on a deployed sandbox, one its self-serve provisioning created; on any non-production server, a demo seed organisation or one the server's operator lists in DEMO_ORGANISATION_IDS; never in production), each bank connection's bank key, display name and status, each active approval policy with whether it names its approvers, and each user's name, email, roles, status and two-factor enrolment. Readable by an Admin's own ERP-add-on sign-in holding the identity scope setup:read, by the add-on's company connection (an application install) holding onboarding:read, and by an Admin in the browser; refused to an install without onboarding:read, an API key, a non-Admin and a sign-in without setup:read.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The organisation's setup status. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Not an Admin, not a caller this status is for, an install without `onboarding:read` (`insufficient_scope`), or a sign-in without `setup:read`. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"organisation": {
"id": "string",
"name": "string",
"require2fa": false,
"erpApproval": {
"enabled": false
},
"demo": false
},
"connections": [
{
"bankKey": "string",
"displayName": "string",
"status": "string"
}
],
"policies": [
{
"id": "string",
"name": "string",
"namesApprovers": false,
"approverUserIds": [
"string"
]
}
],
"users": [
{
"id": "string",
"name": "string",
"email": "string",
"roles": [
"string"
],
"status": "invited",
"twoFactorEnrolled": false
}
]
}/onboarding/partnerThe partner (reseller) the organisation is on: partnerId, name and erpPartnerIds.uniconta, the partner firm's number in Uniconta (null when not known). partner is null when the organisation has no partner. Ids and names only, never the partner's contact details. Readable by any authenticated member, and by an application install holding onboarding:read. A partner set, changed or cleared by BankConnector is the answer from the next request on.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Status | Description |
|---|---|
| 200 | The organisation's partner, or `partner: null`. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Forbidden: the credential's role, scope or grant does not allow this operation. |
| 429 | Too many requests from this network (`rate_limited`); wait `Retry-After` seconds. |
{
"partner": {
"partnerId": "string",
"name": "string",
"erpPartnerIds": {
"uniconta": 0
}
}
}/onboarding/partner/uniconta-pid-proposalsThe ERP add-on's company connection proposes the Uniconta partner number (User.Pid) of a reseller login it saw connect, as the number of THIS organisation's partner. It sets nothing: BankConnector checks the number and confirms or rejects it, and until it is confirmed GET /onboarding/partner and a signup's erpPartner.uniconta read only the number the partner already holds. status is the number's state for this organisation's partner: pending (waiting to be checked), confirmed (the partner's number) or rejected; proposing a number again changes nothing. The answer does not depend on whether another partner holds the number. Only an application install holding partner:propose. An organisation may have three numbers waiting at once.
| Name | In | Required | Description |
|---|---|---|---|
companyId string | query | no | The organisation this request acts on. Required with an API key. A signed-in company user, an application install or a delegated user falls back to its own company when this is omitted; a workspace administrator falls back to the company selected with `POST /auth/company-context`. |
| Property | Type | Required | Description |
|---|---|---|---|
unicontaPid | integer | yes |
{
"unicontaPid": 0
}| Status | Description |
|---|---|
| 200 | The proposed number and its state for this organisation's partner. |
| 400 | The body is not JSON (`validation_failed`). Before `BankConnector-Version: 2026-10-20` a body that fails the route's schema is a 400 too. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Not an application install, or an install without `partner:propose` |
| 409 | The organisation has no partner |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The body is JSON but fails the route's schema, or names a field the route does not read (`validation_failed`, one entry per problem in `details.issues`). Pin `BankConnector-Version: 2026-10-20` to get 422; earlier pins answer 400. |
| 429 | The organisation already has three numbers waiting to be checked |
{
"proposal": {
"unicontaPid": 0,
"status": "pending"
}
}/demo/resetIn one transaction, deletes the organisation's payments, bank answers, statements, audit trail, approval requests, stored Idempotency-Key replies, the claims that de-duplicate payments and bank files, its webhook delivery rows and its queued webhook and payment deliveries, and clears each bank account's statement state. It then sends one opening camt.053 per account, dated yesterday (UTC), through its Demo Bank connection, opening at the balance the account's first statement opened at. Users, sign-ins, two-factor authentication, grants, installs, approval policies, settings, bank connections, webhook endpoints and the bank accounts themselves stay. The audit trail starts again with the reset's own demo.reset event, and the reset is also recorded on the platform's own audit trail. Calling it again after a failure finishes the reset. Only for an application install's token holding payments:submit, and only for a demo organisation (organisation.demo on GET /oauth/userinfo) that is not under a legal hold: never in production. A refusal deletes nothing. The request has no body.
| Status | Description |
|---|---|
| 200 | The organisation's demo data was reset and its opening statements sent. |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Not an install token, the server is in production mode (`demo_reset_disabled_in_production`), or the organisation is not a demo organisation (`not_a_demo_organisation`). |
| 409 | The organisation is under a legal hold (`legal_hold`). |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"reset": false,
"erased": {
"documents": 0,
"events": 0,
"accounts": 0,
"approvalRequests": 0,
"idempotencyKeys": 0,
"webhookDeliveries": 0,
"queuedJobs": 0
},
"openingStatements": {
"date": "string",
"accounts": [
{
"account": "string",
"currency": "string",
"openingBalance": "string"
}
],
"ingested": 0
}
}/demo/statementsStores a camt.053 bank statement for a demo organisation and lists it with its other statements (GET /journal/documents?direction=inbound&type=camt.053), so a demo company opens on statements whose balances chain: the Demo Bank's next statement for the account opens at the closing balance of the latest one the server holds. The file is read and de-duplicated as POST /journal/ingest reads it, and the answer is the same. Only for an application install's token holding payments:submit, only on the sandbox, only for a demo organisation (organisation.demo on GET /oauth/userinfo), only for a camt.053, and only when every statement in it is for a bank account the organisation already holds, in that account's currency. A refusal stores nothing.
| Property | Type | Required | Description |
|---|---|---|---|
content | string | yes | The camt.053 file's contents. |
{
"content": "string"
}| Status | Description |
|---|---|
| 200 | This file was already stored. Returns the existing document unchanged. |
| 201 | The statement was stored. |
| 207 | Some of the file's statement blocks were stored and some were not. |
| 400 | The file is not a bank file the server reads (`unrecognised_format`) |
| 401 | Authentication required: the credential is missing, invalid or expired. |
| 403 | Not an install token, an install without `payments:submit` (`insufficient_scope`), a production server (`demo_statements_sandbox_only`), not a demo organisation (`not_a_demo_organisation`), not a camt.053 (`demo_upload_not_a_statement`), or a statement for an account the organisation does not hold (`demo_statement_account_not_held`). |
| 413 | The request body is larger than this operation accepts (`payload_too_large`). |
| 422 | The statement could not be read whole (`incomplete_statement`, `validation_failed`) |
| 429 | Too many requests from this caller or this network (`rate_limited`); wait `Retry-After` seconds. |
{
"id": "string",
"journalNo": "string",
"companyId": "string",
"bankKey": "string",
"environment": "test",
"direction": "inbound",
"type": "pain.001",
"sourceFormat": "string",
"summary": "string",
"status": "string",
"messageId": "string",
"importSource": "manual",
"transactionCount": 0,
"totals": [
{
"currency": "string",
"amount": "string"
}
],
"paymentType": "string",
"source": {
"reference": "string",
"name": "string"
},
"idempotencyKey": "string",
"collected": false,
"collectedAt": "string",
"createdAt": "string",
"expiresAt": "string",
"deduped": false,
"ingest": {
"statementCount": 0,
"ingested": 0,
"deduped": 0,
"quarantined": 0,
"partial": false,
"statements": [
{
"index": 0,
"kind": "ingested",
"documentId": "string",
"journalNo": "string",
"account": "string",
"message": "string"
}
],
"alerts": [
{
"kind": "string",
"message": "string",
"reason": "string",
"account": "string",
"unclosedBlockCount": 0,
"survivingStatementCount": 0
}
]
}
}