sandbox.bankconnector.com API Reference

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

GET/profiles
List all banks with their offered payment types

Returns every registered bank, its ISO 20022 version, and the payment types available for its country (with full metadata).

Responses
StatusDescription
200List of bank profiles
Example response (200)
{
  "profiles": [
    {
      "key": "nordea-dk",
      "bankName": "Nordea Danmark",
      "countryCode": "DK",
      "painVersion": "pain.001.001.03",
      "bankBic": "NDEADKKK",
      "paymentTypes": [
        {
          "code": "sepa",
          "label": "SEPA Credit Transfer",
          "description": "string",
          "requires": [],
          "channel": "SEPA / TARGET2",
          "isoEncoding": {}
        }
      ]
    }
  ]
}
Code samples
curl -X GET https://your-host/profiles \
  -H 'X-API-Key: YOUR_KEY'
const res = await fetch("https://your-host/profiles", {
  method: "GET",
  headers: {
    "X-API-Key": "YOUR_KEY",
  },
});
const data = await res.json();
import requests

resp = requests.request(
  "GET", "https://your-host/profiles",
  headers={"X-API-Key": "YOUR_KEY"},
)
resp.raise_for_status()
data = resp.json()
GET/payment-types
Full payment-type catalog

All payment types in the shared catalog with ISO 20022 encoding, descriptions, and requirements.

Responses
StatusDescription
200Full catalog
Example response (200)
{
  "paymentTypes": [
    {
      "code": "sepa",
      "label": "SEPA Credit Transfer",
      "description": "string",
      "requires": [
        "string"
      ],
      "channel": "SEPA / TARGET2",
      "isoEncoding": {
        "serviceLevel": "SEPA",
        "localInstrument": "INST",
        "categoryPurpose": "TAXS"
      }
    }
  ]
}
GET/banks/{bankKey}/payment-types
Payment types offered by a specific bank
Parameters
NameInRequiredDescription
bankKey BankKeypathyes
Responses
StatusDescription
200Bank key/name + offered payment types with full metadata
404Unknown bank
Example response (200)
{
  "bankKey": "string",
  "bank": "string",
  "bankName": "string",
  "countryCode": "string",
  "paymentTypes": [
    {}
  ]
}
GET/banks/{bankKey}/status
Whether a bank is supported + its channel/payment readiness (PUBLIC)
Parameters
NameInRequiredDescription
bankKey BankKeypathyes
Responses
StatusDescription
200Bank support status
404Unknown bank

Conversion

Validate and convert canonical payment JSON

POST/validate/{bankKey}
Validate a payment instruction (no conversion)

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.

Parameters
NameInRequiredDescription
bankKey BankKeypathyes
Request body required
PropertyTypeRequiredDescription
messageIdstringyes
creationDateTimestringno
initiatingPartyobjectyes
paymentsPayment[]yes
Example
{
  "messageId": "MSG-20260601-0001",
  "creationDateTime": "2026-06-01T10: 30: 00Z",
  "initiatingParty": {
    "name": "Acme Corp ApS",
    "organisationId": {
      "id": "DK12345678",
      "scheme": "CUST",
      "issuer": "string"
    }
  },
  "payments": [
    {
      "paymentId": "PMT-0001",
      "paymentType": "sepa",
      "executionDate": "2026-06-04",
      "debtor": {
        "name": "Acme Corp ApS",
        "country": "DK",
        "account": {
          "iban": "DK5000400440116243",
          "currency": "EUR",
          "other": {}
        },
        "agent": {
          "bic": "DEUTDEFF",
          "clearing": {},
          "name": "string"
        },
        "organisationId": {
          "id": "DK12345678",
          "scheme": "CUST",
          "issuer": "string"
        }
      },
      "transactions": [
        {
          "endToEndId": "INV-2026-588",
          "instructionId": "string",
          "amount": "1500.00",
          "currency": "EUR",
          "creditor": {},
          "remittance": {}
        }
      ]
    }
  ]
}
Responses
StatusDescription
200Validation outcome (valid or not: check `valid`)
400Malformed JSON body
404Unknown bank key
Example response (200)
{
  "valid": false,
  "issues": [
    {
      "code": "BANK_BIC_REQUIRED",
      "level": "bank",
      "severity": "error",
      "message": "Bank \"nordea-dk\" requires a BIC for the debtor.",
      "path": "payments[0].debtor.agent.bic",
      "bank": "nordea-dk",
      "paymentType": "sepa"
    }
  ]
}
Code samples
curl -X POST https://your-host/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/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/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()
POST/convert/{bankKey}
Validate then convert to bank-specific pain.001 XML

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.

Parameters
NameInRequiredDescription
bankKey BankKeypathyes
validate booleanquerynoIf true, XSD-validate the generated XML before returning it.
Request body required
PropertyTypeRequiredDescription
messageIdstringyes
creationDateTimestringno
initiatingPartyobjectyes
paymentsPayment[]yes
Example
{
  "messageId": "MSG-20260601-0001",
  "creationDateTime": "2026-06-01T10: 30: 00Z",
  "initiatingParty": {
    "name": "Acme Corp ApS",
    "organisationId": {
      "id": "DK12345678",
      "scheme": "CUST",
      "issuer": "string"
    }
  },
  "payments": [
    {
      "paymentId": "PMT-0001",
      "paymentType": "sepa",
      "executionDate": "2026-06-04",
      "debtor": {
        "name": "Acme Corp ApS",
        "country": "DK",
        "account": {
          "iban": "DK5000400440116243",
          "currency": "EUR",
          "other": {}
        },
        "agent": {
          "bic": "DEUTDEFF",
          "clearing": {},
          "name": "string"
        },
        "organisationId": {
          "id": "DK12345678",
          "scheme": "CUST",
          "issuer": "string"
        }
      },
      "transactions": [
        {
          "endToEndId": "INV-2026-588",
          "instructionId": "string",
          "amount": "1500.00",
          "currency": "EUR",
          "creditor": {},
          "remittance": {}
        }
      ]
    }
  ]
}
Responses
StatusDescription
200Bank-specific pain.001 XML
400Malformed JSON body
404Unknown bank key
422Validation failed: returns all issues at once
Code samples
curl -X POST https://your-host/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/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 XML
import requests

resp = requests.request(
  "POST", "https://your-host/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
POST/convert-async/{bankKey}
Enqueue a background conversion (returns a job id)

Non-blocking sibling 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 async worker can't run the XSD) — use the synchronous /convert/{bankKey}?validate=true instead.

Parameters
NameInRequiredDescription
bankKey BankKeypathyes
Request body required
PropertyTypeRequiredDescription
messageIdstringyes
creationDateTimestringno
initiatingPartyobjectyes
paymentsPayment[]yes
Example
{
  "messageId": "MSG-20260601-0001",
  "creationDateTime": "2026-06-01T10: 30: 00Z",
  "initiatingParty": {
    "name": "Acme Corp ApS",
    "organisationId": {
      "id": "DK12345678",
      "scheme": "CUST",
      "issuer": "string"
    }
  },
  "payments": [
    {
      "paymentId": "PMT-0001",
      "paymentType": "sepa",
      "executionDate": "2026-06-04",
      "debtor": {
        "name": "Acme Corp ApS",
        "country": "DK",
        "account": {
          "iban": "DK5000400440116243",
          "currency": "EUR",
          "other": {}
        },
        "agent": {
          "bic": "DEUTDEFF",
          "clearing": {},
          "name": "string"
        },
        "organisationId": {
          "id": "DK12345678",
          "scheme": "CUST",
          "issuer": "string"
        }
      },
      "transactions": [
        {
          "endToEndId": "INV-2026-588",
          "instructionId": "string",
          "amount": "1500.00",
          "currency": "EUR",
          "creditor": {},
          "remittance": {}
        }
      ]
    }
  ]
}
Responses
StatusDescription
202Conversion enqueued
400Malformed JSON body, ?validate=true (unsupported on the async path), or unsupported painVersion
404Unknown bank key
422Validation failed: returns all issues at once
Example response (202)
{
  "jobId": "string",
  "status": "string",
  "bankKey": "string",
  "autoPaymentType": "string"
}
GET/convert-jobs/{id}
Poll a background conversion job

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).

Parameters
NameInRequiredDescription
id stringpathyes
Responses
StatusDescription
200Job status, or the converted XML once the job has succeeded
404Conversion job not found (or not owned by the caller)

Journal

Submit payments + read the journal: the ERP integration surface (X-API-Key)

POST/journal/payments
Submit a payment: convert + journal (+ deliver if host-to-host)

The primary ERP integration call. Validates + converts the canonical payment for bankKey, records it in the journal, and routes it through approval + delivery. Pass only companyId (+ bankKey + payment) — platformId is inferred from your API key; do not send it. Idempotency-Key is MANDATORY for host-to-host (a missing key returns 400 idempotency_key_required, unless the payment.messageId is supplied and used as the fallback dedupe key). Send a unique key per logical payment so a network-timeout retry can never double-submit. A retry with the SAME key + SAME body replays the original 201 response and carries the header Idempotency-Replayed: true; the same key with a DIFFERENT body → 409. Requires authentication (X-API-Key for ERP integrations).

Parameters
NameInRequiredDescription
Idempotency-Key stringheadernoUnique per company per logical payment. MANDATORY for host-to-host (else 400 `idempotency_key_required`, unless `payment.messageId` is provided). Same key + same body replays the original 201 (with `Idempotency-Replayed: true`); same key + different body → 409.
Request body required
PropertyTypeRequiredDescription
companyIdstringyesThe company this payment belongs to. (platformId is inferred from your API key — do not send it.)
bankKeyBankKeyyes
paymentPaymentInstructionyes
environmentenumnoWhich connection delivers the payment. Defaults to production; pass "test" to dispatch over the bank's test channel.
validatebooleannoAlso XSD-validate the generated XML.
idempotencyKeystringnoAlternative to the Idempotency-Key header.
Example
{
  "companyId": "string",
  "bankKey": "nordea-dk",
  "payment": {
    "messageId": "MSG-20260601-0001",
    "creationDateTime": "2026-06-01T10: 30: 00Z",
    "initiatingParty": {
      "name": "Acme Corp ApS",
      "organisationId": {
        "id": "DK12345678",
        "scheme": "CUST",
        "issuer": "string"
      }
    },
    "payments": [
      {
        "paymentId": "PMT-0001",
        "paymentType": "sepa",
        "executionDate": "2026-06-04",
        "debtor": {
          "name": "Acme Corp ApS",
          "country": "DK",
          "account": {},
          "agent": {},
          "organisationId": {}
        },
        "transactions": [
          {}
        ]
      }
    ]
  },
  "environment": "test",
  "validate": false,
  "idempotencyKey": "string"
}
Responses
StatusDescription
201Payment recorded + journaled. A replay of a prior idempotent submit ALSO returns 201, additionally carrying the response header `Idempotency-Replayed: true`.
400`idempotency_key_required` — no Idempotency-Key (header or body) and no `payment.messageId` on a host-to-host submit
401Authentication required
403Scope mismatch (cross-tenant)
409Idempotency conflict (same key, different body) or an in-flight retry
422Validation failed: all issues at once
429Rate limited (production): see Retry-After
Example response (201)
{
  "id": "doc_9f3a",
  "journalNo": "OUT-000123",
  "platformId": "plat_123",
  "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"
  }
}
Code samples
curl -X POST https://your-host/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/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/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()
POST/journal/payments/preview
Preview a payment: convert + validate, return XML (no journal, no send)

Read-only. Runs the same settings-fill → validate → convert (+ XSD) pipeline as POST /journal/payments and returns the generated bank file, but 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.

Request body required
PropertyTypeRequiredDescription
companyIdstringyesThe company this payment belongs to. (platformId is inferred from your API key — do not send it.)
bankKeyBankKeyyes
paymentPaymentInstructionyes
environmentenumnoWhich connection delivers the payment. Defaults to production; pass "test" to dispatch over the bank's test channel.
validatebooleannoAlso XSD-validate the generated XML.
idempotencyKeystringnoAlternative to the Idempotency-Key header.
Example
{
  "companyId": "string",
  "bankKey": "nordea-dk",
  "payment": {
    "messageId": "MSG-20260601-0001",
    "creationDateTime": "2026-06-01T10: 30: 00Z",
    "initiatingParty": {
      "name": "Acme Corp ApS",
      "organisationId": {
        "id": "DK12345678",
        "scheme": "CUST",
        "issuer": "string"
      }
    },
    "payments": [
      {
        "paymentId": "PMT-0001",
        "paymentType": "sepa",
        "executionDate": "2026-06-04",
        "debtor": {
          "name": "Acme Corp ApS",
          "country": "DK",
          "account": {},
          "agent": {},
          "organisationId": {}
        },
        "transactions": [
          {}
        ]
      }
    ]
  },
  "environment": "test",
  "validate": false,
  "idempotencyKey": "string"
}
Responses
StatusDescription
200Preview generated
400Missing fields
401Authentication required
422Validation / XSD failed
Example response (200)
{
  "xml": "string",
  "painVersion": "string",
  "preview": false
}
POST/journal/ingest
Ingest a bank file (camt.053/054, pain.002, MT940) → normalised JSON

Upload a raw bank statement/status file; it is parsed to the one normalised shape and journaled. Requires authentication.

Request body required
PropertyTypeRequiredDescription
companyIdstringyesYour company id. platformId is inferred from your API key — do not send it.
bankKeystringno
contentstringyesRaw file contents
Example
{
  "companyId": "string",
  "bankKey": "string",
  "content": "string"
}
Responses
StatusDescription
201File ingested + normalised
401Authentication required
422Unrecognised / unparseable file
Example response (201)
{
  "id": "doc_a1b2",
  "journalNo": "IN-000042",
  "companyId": "comp_123",
  "bankKey": "nordea-dk",
  "environment": "production",
  "direction": "inbound",
  "type": "camt.053",
  "status": "imported",
  "summary": "Statement — 3 entries",
  "transactionCount": 3,
  "totals": [
    {
      "currency": "EUR",
      "amount": "4200.00"
    }
  ],
  "createdAt": "2026-06-20T08: 15: 00Z"
}
GET/journal/documents
List journal document summaries (newest first)

Tenant-scoped list of payments + statements (metadata only: no decrypted payload). Filter by direction/type.

Parameters
NameInRequiredDescription
companyId stringqueryyes
direction enumqueryno
type stringqueryno
limit integerquerynoPage size (enables keyset pagination via nextCursor).
cursor stringquerynoOpaque cursor from a prior page's `nextCursor`. (`before` is accepted as a deprecated alias.)
Responses
StatusDescription
200Document summaries
401Authentication required
Example response (200)
{
  "items": [
    {
      "id": "string",
      "journalNo": "OUT-000123",
      "platformId": "string",
      "companyId": "string",
      "direction": "outbound",
      "type": "pain.001",
      "bankKey": "string",
      "environment": "test",
      "status": "converted",
      "summary": "string",
      "transactionCount": 0,
      "totals": [
        {
          "currency": "string",
          "amount": "string"
        }
      ],
      "xml": "string",
      "deliveryMode": "host-to-host",
      "createdAt": "2026-01-01T00: 00: 00Z"
    }
  ],
  "nextCursor": "string"
}
POST/journal/export
Export full document payloads by journal number (admin / API key)

Bulk extraction of full pain.001/camt payloads (account numbers + amounts), optionally filtered within a statement. Admin-only for human sessions; the ERP API key is the intended consumer.

Request body required
PropertyTypeRequiredDescription
companyIdstringyesYour company id. platformId is inferred from your API key — do not send it.
journalNosstring[]yes
Example
{
  "companyId": "string",
  "journalNos": [
    "string"
  ]
}
Responses
StatusDescription
200Exported documents + any notFound journal numbers
401Authentication required
403Admin required (human session)
Example response (200)
{
  "exported": [
    {}
  ],
  "notFound": [
    "string"
  ]
}
GET/journal/list
Paginated journal feed (cursor): the ERP read + collect surface

Tenant-scoped, newest-first list of journal documents with a stable opaque cursor for forward pagination. Metadata only (no decrypted payload). The flagship ERP polling pattern: pass unretrievedOnly=true to fetch only documents you have not collected yet, then mark them collected via POST /journal/export. total counts all documents; unretrieved counts how many are still uncollected. Each item carries collected (boolean) and collectedAt so a poller can track exactly what it has pulled. platformId is inferred from your API key — pass only companyId.

Parameters
NameInRequiredDescription
companyId stringqueryyesYour company id. (platformId is inferred from your API key — do not send it.)
cursor stringquerynoOpaque cursor from a prior page's `nextCursor`.
limit integerquerynoPage size — default 500, max 1000 (larger than the interactive /journal/documents feed, for bulk ERP pulls).
direction enumqueryno
type stringqueryno
unretrievedOnly booleanquerynoWhen `true`, returns ONLY documents not yet collected (the standard poll-then-collect loop).
Responses
StatusDescription
200A page of documents + counts + nextCursor
401Authentication required
Example response (200)
{
  "total": 42,
  "unretrieved": 3,
  "nextCursor": null,
  "items": [
    {
      "journalNo": "OUT-000123",
      "direction": "outbound",
      "type": "pain.001",
      "sourceFormat": "pain.001.001.09",
      "bankKey": "nordea-dk",
      "summary": "SEPA — 1 payment",
      "status": "converted",
      "collected": false,
      "collectedAt": null,
      "createdAt": "2026-06-20T10: 31: 00Z",
      "expiresAt": null
    }
  ]
}
GET/journal/documents/{id}
Fetch one journal document with its full (decrypted) payload

Returns the document including its canonical/normalised payload and, for seeded/generated docs, the raw xml for download. Tenant-scoped.

Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
Responses
StatusDescription
200Document + payload
401Authentication required
404Not found (or not in this tenant's scope)
Example response (200)
{
  "id": "string",
  "journalNo": "OUT-000123",
  "platformId": "string",
  "companyId": "string",
  "direction": "outbound",
  "type": "pain.001",
  "bankKey": "string",
  "environment": "test",
  "status": "converted",
  "summary": "string",
  "transactionCount": 0,
  "totals": [
    {
      "currency": "string",
      "amount": "string"
    }
  ],
  "xml": "string",
  "deliveryMode": "host-to-host",
  "createdAt": "2026-01-01T00: 00: 00Z"
}
GET/journal/documents/{id}/payments
Per-payment lines within an outbound batch (the drawer)
Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
Responses
StatusDescription
200Per-line payments (payee, amount, endToEndId, status)
401Authentication required
Example response (200)
{
  "payments": [
    {}
  ]
}
GET/journal/documents/{id}/file
Download the generated pain.001 XML for an outbound document

Returns the stored payment file as an application/xml attachment. 404 if the document isn't in scope, isn't outbound, or has no generated XML. Downloading does NOT mark the document retrieved.

Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
Responses
StatusDescription
200The pain.001 XML (attachment)
404No downloadable file for this document
GET/journal/payment-types
Tally of outbound payments grouped by payment type

Lightweight GROUP BY over the plaintext summary column (no payload decrypt), weighted by transaction count: a tally of your outbound payments grouped by payment type.

Parameters
NameInRequiredDescription
companyId stringqueryyes
Responses
StatusDescription
200Per-type counts + total
401Authentication required
Example response (200)
{
  "counts": {},
  "total": 0,
  "sampled": 0
}
GET/journal/reconciliation
Reconciliation view: correlate outbound payments with pain.002 + camt evidence

For each outbound payment, returns its real-world state (cleared / rejected / pending) and the matching evidence, by correlating inbound pain.002 status reports and camt.053/054 statement entries 1:1.

Parameters
NameInRequiredDescription
companyId stringqueryyes
Responses
StatusDescription
200Per-payment reconciliation + evidence
401Authentication required
Example response (200)
{
  "items": [
    {
      "documentId": "string",
      "journalNo": "string",
      "messageId": "string",
      "state": "cleared",
      "evidence": {}
    }
  ]
}
GET/journal/events
Activity-log events (audit trail) for a company
Parameters
NameInRequiredDescription
companyId stringqueryyes
Responses
StatusDescription
200Events newest-first
401Authentication required
Example response (200)
{
  "items": [
    {}
  ]
}
POST/inbound/payment-status
Push a pain.002 status report → reconcile the outbound payment

Accepts a raw pain.002 (or normalised status); matches it to the original outbound by MsgId/endToEndId and advances the payment status. Authenticated.

Request body required
PropertyTypeRequiredDescription
companyIdstringyesYour company id. platformId is inferred from your API key — do not send it.
bankKeystringno
contentstringyesRaw pain.002 XML.
Example
{
  "companyId": "string",
  "bankKey": "string",
  "content": "string"
}
Responses
StatusDescription
200Reconciled (status advanced)
401Authentication required
422Unparseable / unmatched status
POST/inbound/statement
Parse a camt.053/054 / MT940 statement → normalised JSON (preview only, not stored)

Stateless parse-only preview: the raw file body is normalised and returned but NOT journaled (use POST /journal/ingest to store). A camt.053 may carry several <Stmt> (one per account); ALL are returned in the items list envelope.

Responses
StatusDescription
200Normalised statement(s)
400Empty body or unrecognised format
422Unparseable file
Example response (200)
{
  "items": [
    {}
  ]
}

Accounts

Bank accounts registry + imported statements (session)

GET/accounts
List bank accounts for a company (balances masked)
Parameters
NameInRequiredDescription
companyId stringqueryyes
Responses
StatusDescription
200Accounts with latest balances
401Sign in required
Example response (200)
{
  "items": [
    {}
  ]
}
GET/accounts/statements
List imported camt.053 statements for an account

Per-account statement history (the parsed NormalisedStatement summaries) used by the Statements screen.

Parameters
NameInRequiredDescription
companyId stringqueryyes
account stringqueryno
Responses
StatusDescription
200Statements + any detected sequence gaps
401Sign in required
Example response (200)
{
  "account": "string",
  "statements": [
    {}
  ],
  "gaps": [
    {}
  ]
}

Webhooks

Outbound event subscriptions (session, Admin)

GET/webhooks
List webhook subscriptions
Parameters
NameInRequiredDescription
companyId stringqueryyes
Responses
StatusDescription
200Subscriptions
401Sign in required
Example response (200)
{
  "items": [
    {}
  ]
}
POST/webhooks
Create a webhook subscription (Admin)

Registers 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. Omit eventTypes entirely to receive all events.

Request body required
PropertyTypeRequiredDescription
platformIdstringyes
companyIdstringyes
urlstringyes
eventTypesstring[]noEvent names to subscribe to, e.g. ["payment.sent", "payment.rejected"]. Omit to receive all events; an explicitly-empty array is rejected.
Example
{
  "platformId": "string",
  "companyId": "string",
  "url": "string",
  "eventTypes": [
    "string"
  ]
}
Responses
StatusDescription
201Subscription created (returns the signing secret once)
401Sign in required
403Admin required
DELETE/webhooks/{id}
Delete a webhook subscription (Admin)
Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
Responses
StatusDescription
200Deleted
401Sign in required
403Admin required
404Not found
POST/webhooks/{id}/enable
Enable a disabled webhook (Admin)
Parameters
NameInRequiredDescription
id stringpathyes
Request body required
PropertyTypeRequiredDescription
platformIdstringyes
companyIdstringyes
Example
{
  "platformId": "string",
  "companyId": "string"
}
Responses
StatusDescription
200Webhook enabled
401Sign in required
403Admin required
404Not found
POST/webhooks/{id}/disable
Disable a webhook without deleting it (Admin)
Parameters
NameInRequiredDescription
id stringpathyes
Request body required
PropertyTypeRequiredDescription
platformIdstringyes
companyIdstringyes
Example
{
  "platformId": "string",
  "companyId": "string"
}
Responses
StatusDescription
200Webhook disabled
401Sign in required
403Admin required
404Not found
POST/webhooks/{id}/test
Fire a signed test event to the webhook URL (Admin)

Sends a signed ping to the endpoint and returns the delivery result.

Parameters
NameInRequiredDescription
id stringpathyes
Request body required
PropertyTypeRequiredDescription
platformIdstringyes
companyIdstringyes
Example
{
  "platformId": "string",
  "companyId": "string"
}
Responses
StatusDescription
200Delivery result of the test event
401Sign in required
403Admin required
404Not found
GET/webhooks/{id}/deliveries
List delivery attempts for a webhook (Admin)

Per-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.

Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
cursor stringquerynoOpaque cursor from the previous page's `nextCursor`. (`after` is accepted as a deprecated alias.)
limit integerqueryno
Responses
StatusDescription
200A page of delivery attempts
401Sign in required
403Admin required
404Webhook not found
Example response (200)
{
  "items": [
    {
      "id": "whd_…",
      "webhookId": "wh_…",
      "platformId": "string",
      "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"
    }
  ],
  "nextCursor": "string"
}
POST/webhooks/{id}/deliveries/{deliveryId}/redeliver
Replay a single delivery (Admin)

Re-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.

Parameters
NameInRequiredDescription
id stringpathyes
deliveryId stringpathyes
companyId stringqueryyes
Responses
StatusDescription
200Re-enqueued
401Sign in required
403Admin required
404Delivery not found / not redeliverable
Example response (200)
{
  "redelivered": false,
  "deliveryId": "string"
}
POST/webhooks/{id}/redeliver-failed
Bulk-replay failed deliveries since a timestamp (Admin)

Re-enqueues failed deliveries for the webhook created at/after since. BOUNDED: a single call replays at most a fixed cap (500); when a full page is returned, 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.

Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
since stringqueryyesISO timestamp — only failed deliveries created at/after this are replayed.
cursor stringquerynoOpaque cursor from a prior page's `nextCursor` to continue draining.
Responses
StatusDescription
200Re-enqueued count + drain cursor
400Missing/invalid since
401Sign in required
403Admin required
404Webhook not found
Example response (200)
{
  "count": 0,
  "nextCursor": "string"
}
POST/webhooks/{id}/re-enable
Re-enable an auto-disabled webhook (Admin)

Clears an auto-disabled state: resets consecutiveFailures to 0 and clears disabledAt/disabledReason. 400 if the webhook is not currently disabled.

Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
Responses
StatusDescription
200Re-enabled
400Webhook is not disabled
401Sign in required
403Admin required
404Webhook not found
Example response (200)
{
  "webhookId": "string",
  "enabledAt": "2026-01-01T00: 00: 00Z"
}
POST/webhooks/{id}/rotate-secret
Rotate the signing secret with an overlap window (Admin)

Generates 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.

Parameters
NameInRequiredDescription
id stringpathyes
companyId stringqueryyes
Request body
PropertyTypeRequiredDescription
overlapHoursintegernoHow long the old secret stays valid.
Example
{
  "overlapHours": 24
}
Responses
StatusDescription
200Rotated — newSecret shown once
400Invalid body
401Sign in required
403Admin required
404Webhook not found
Example response (200)
{
  "webhookId": "string",
  "newSecret": "string",
  "overlapUntil": "2026-01-01T00: 00: 00Z"
}

Platform

Tenant dashboard + per-bank test payments

GET/banks/{bankKey}/test-scenarios
Available one-click Test Payment scenarios for a bank

Lists 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.

Parameters
NameInRequiredDescription
bankKey stringpathyes
Responses
StatusDescription
200Offered scenarios
Example response (200)
{
  "scenarios": [
    {}
  ]
}
POST/banks/{bankKey}/test-payment
Send a one-click test payment

Builds 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.

Parameters
NameInRequiredDescription
bankKey BankKeypathyes
Request body required
PropertyTypeRequiredDescription
platformIdstringyes
companyIdstringyes
scenarioenumyes
environmentenumno
accountobjectyes
Example
{
  "platformId": "string",
  "companyId": "string",
  "scenario": "domestic",
  "environment": "test",
  "account": {
    "name": "string",
    "iban": "string",
    "other": {
      "id": "string",
      "scheme": "string"
    },
    "currency": "string",
    "country": "string"
  }
}
Responses
StatusDescription
201Test payment journaled
400Invalid request
401Sign in required

System

Health and version

GET/health
Liveness check
Responses
StatusDescription
200Server is running
Example response (200)
{
  "status": "ok"
}
GET/version
API version
Responses
StatusDescription
200Current version
Example response (200)
{
  "version": "0.1.0"
}
GET/ready
Readiness check

Readiness 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.

Responses
StatusDescription
200Ready
503Not ready (database unreachable or schema not migrated)
Example response (200)
{
  "ready": true
}
GET/fx/rates
Indicative FX reference rates

Global (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 }.

Responses
StatusDescription
200Cached EUR-base rates
Example response (200)
{
  "base": "EUR",
  "rates": {},
  "asOf": "string"
}

Getting started

POST/sandbox/provision
Create a sandbox in one call (no signup)

Self-serve onboarding — sandbox only. With NO authentication, provisions a complete, isolated sandbox: your own platform, a company, an admin user, an API key, and an active demo bank connection. Everything created is sandbox-only (no real money can move). Returns the raw API key (shown once) plus a ready-to-run first payment. Your key identifies the platform, so on later requests you pass only companyId. Rate-limited per IP; unused sandboxes are removed after 30 days.

Request body
PropertyTypeRequiredDescription
bankKeystringnoDemo bank to connect (default `danske-dk`). See GET /profiles.
companyNamestringnoOptional name for the created company.
labelstringnoOptional label for the API key.
Example
{
  "bankKey": "danske-dk",
  "companyName": "Acme Sandbox",
  "label": "my-integration"
}
Responses
StatusDescription
201Sandbox provisioned
404Not a deployed sandbox (this endpoint does not exist in production).
429Rate limited — too many sandboxes from this IP recently.
Example response (201)
{
  "apiKey": "key_1a2b…",
  "companyId": "comp_…",
  "platformId": "plat_…",
  "baseUrl": "https://sandbox.bankconnector.com",
  "bank": {
    "key": "string",
    "name": "string",
    "country": "string"
  },
  "firstPayment": {}
}