# BankConnector OpenAPI Specification — vendored for docs.bankconnector.com # Source: src/api/openapi.ts, built by scripts/build-docs-site.mjs. Do not edit by hand. openapi: 3.1.0 info: title: BankConnector API version: 0.1.0 description: | ## Quick start BankConnector converts canonical JSON payments to bank-specific ISO 20022 files and normalises incoming bank statements into one format. Try the whole flow in the hosted sandbox at **https://sandbox.bankconnector.com** — no signup, and no real money moves. ### Step 1: Get a sandbox API key One unauthenticated call provisions a complete, isolated sandbox — your own platform, a company, an admin user, an API key, and an active demo bank connection: ```http POST /sandbox/provision { "bankKey": "danske-dk" } → 201 { "apiKey": "key_…", "companyId": "comp_…", "baseUrl": "https://sandbox.bankconnector.com", … } ``` The raw key is shown ONCE — store it, and send it as the `X-API-Key` header on every request. **Your key identifies your platform**, so on every data-plane call you pass only `companyId` — you never send `platformId`. ### Step 2: Submit a payment ```http POST /journal/payments X-API-Key: key_… Idempotency-Key: 8f2c… (unique per payment; mandatory for host-to-host) { "companyId": "comp_…", "bankKey": "danske-dk", "payment": { "messageId": "MSG-20260619-0001", "initiatingParty": { "name": "Acme Corp", "organisationId": { "id": "ACME-1", "scheme": "CUST" } }, "payments": [{ "paymentId": "PMT-1", "paymentType": "sepa", "executionDate": "2026-06-20", "debtor": { "name": "Acme Corp", "country": "DK", "account": { "iban": "DK5000400440116243", "currency": "EUR" }, "agent": { "bic": "NDEADKKK" } }, "transactions": [{ "endToEndId": "E2E-1", "amount": "1000.00", "currency": "EUR", "creditor": { "name": "Supplier GmbH", "account": { "iban": "DE89370400440532013000" }, "agent": { "bic": "DEUTDEFF" } } }] }] } } ``` The `payment` object IS the canonical instruction (amounts are exact decimal **strings**); on this route it is WRAPPED under `payment` alongside `companyId` + `bankKey`. The flat 201 response includes `id`, `journalNo`, `status`, the generated `xml`, and `deliveryMode`. A retry with the same `Idempotency-Key` + body replays that 201 with an `Idempotency-Replayed: true` header. NOTE the envelope difference: `POST /validate/{bank}` and `POST /convert/{bank}` take that SAME canonical object as the raw top-level body (no `payment` wrapper, no companyId). See CANONICAL_INPUT.md for the full shape. ### Step 3: Read results ```http GET /journal/reconciliation → matched payments + bank confirmations (cleared / rejected / pending) GET /journal/list?unretrievedOnly=true → poll for documents you have not collected yet POST /inbound/payment-status → import a pain.002 status report POST /inbound/statement → import a camt.053 / 052 / 054 statement ``` --- > **Sandbox vs production:** the sandbox at https://sandbox.bankconnector.com is fully isolated — > the key, data and demo bank are sandbox-only and no real money can move. Going live (real bank > connections + approvals) is a separate step; the same request shapes apply. contact: email: johan@laymanadvisory.com servers: - url: https://sandbox.bankconnector.com description: 'Sandbox — the default target for every "Try it" call on this site, and where you should do all integration work first. The demo bank channel never moves real money by construction; a real bank connection here is safe from real sends only if your company is provisioned sandboxOnly — confirm that with your operator rather than assuming it from the host alone.' - url: 'https://{company}.bankconnector.com' description: Production — your company's branded subdomain. variables: company: default: your-company description: 'Your company''s subdomain, from your BankConnector operator.' tags: - name: Discovery description: Explore what banks and payment types are available - name: Conversion description: Validate and convert canonical payment JSON - name: Connections description: 'Bank connection setup, channel activation, and EBICS onboarding (session-authenticated, Admin only)' - name: Journal description: 'Submit payments + read the journal: the ERP integration surface (X-API-Key)' - name: Approvals description: 'Maker-checker approval policies + the pending-approval queue (session, Admin/Approver)' - name: Accounts description: Bank accounts registry + imported statements (session) - name: Webhooks description: 'Outbound event subscriptions (session, Admin)' - name: Auth description: 'Login, session, and 2FA. Login + demo are PUBLIC; the rest need a session.' - name: User Management description: 'Create, invite, and manage users within a company. Also covers workspace (platform) admin accounts.' - name: Platform description: Tenant dashboard + per-bank test payments - name: System description: Health and version x-tagGroups: - name: Quick Start tags: - Auth - User Management - Platform - System - name: Banks tags: - Discovery - Connections - name: Payments tags: - Conversion - Approvals - name: Incoming Data tags: - Journal - Accounts - name: Configuration tags: - Webhooks paths: /sandbox/provision: post: tags: - Getting started summary: Create a sandbox in one call (no signup) description: '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.' requestBody: required: false content: application/json: schema: type: object properties: bankKey: type: string description: Demo bank to connect (default `danske-dk`). See GET /profiles. example: danske-dk companyName: type: string description: Optional name for the created company. example: Acme Sandbox label: type: string description: Optional label for the API key. example: my-integration responses: '201': description: Sandbox provisioned content: application/json: schema: type: object properties: apiKey: type: string description: Raw API key — shown ONCE. Send as the X-API-Key header. example: key_1a2b… companyId: type: string example: comp_… platformId: type: string example: plat_… baseUrl: type: string example: https://sandbox.bankconnector.com bank: type: object properties: key: type: string name: type: string country: type: string firstPayment: type: object description: A ready-to-run POST /journal/payments request (headers + body). '404': description: Not a deployed sandbox (this endpoint does not exist in production). '429': description: Rate limited — too many sandboxes from this IP recently. /health: get: tags: - System summary: Liveness check responses: '200': description: Server is running content: application/json: schema: type: object properties: status: type: string example: ok /version: get: tags: - System summary: API version responses: '200': description: Current version content: application/json: schema: type: object properties: version: type: string example: 0.1.0 /ready: get: tags: - System summary: Readiness check description: '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: '200': description: Ready content: application/json: schema: type: object properties: ready: type: boolean example: true '503': description: Not ready (database unreachable or schema not migrated) content: application/json: schema: type: object properties: ready: type: boolean example: false error: type: string /fx/rates: get: tags: - System summary: Indicative FX reference rates description: '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: '200': description: Cached EUR-base rates content: application/json: schema: type: object properties: base: type: string example: EUR rates: type: object additionalProperties: type: number asOf: type: string nullable: true required: - base - rates /profiles: get: tags: - Discovery summary: List all banks with their offered payment types description: 'Returns every registered bank, its ISO 20022 version, and the payment types available for its country (with full metadata).' responses: '200': description: List of bank profiles content: application/json: schema: type: object properties: profiles: type: array items: $ref: '#/components/schemas/BankProfile' x-sandbox: true x-codeSamples: - lang: Shell label: cURL source: | curl -X GET https://your-host/profiles \ -H 'X-API-Key: YOUR_KEY' - lang: JavaScript label: Fetch source: | const res = await fetch("https://your-host/profiles", { method: "GET", headers: { "X-API-Key": "YOUR_KEY", }, }); const data = await res.json(); - lang: Python label: Python source: | import requests resp = requests.request( "GET", "https://your-host/profiles", headers={"X-API-Key": "YOUR_KEY"}, ) resp.raise_for_status() data = resp.json() /payment-types: get: tags: - Discovery summary: Full payment-type catalog description: 'All payment types in the shared catalog with ISO 20022 encoding, descriptions, and requirements.' responses: '200': description: Full catalog content: application/json: schema: type: object properties: paymentTypes: type: array items: $ref: '#/components/schemas/PaymentTypeEntry' x-sandbox: true /connectivity/profiles: get: tags: - Connections summary: List connectivity profiles (channel info per bank) description: 'Read-only metadata: which channel each bank uses (sftp / danske-ws / nordea-ca / bankconnect / ebics), its pre-known server/endpoint details, and SFTP wizard config. No auth required.' responses: '200': description: Connectivity profiles content: application/json: schema: type: object properties: items: type: array items: type: object x-sandbox: true '/platforms/{platformId}/companies': get: tags: - Workspace Admin summary: List the companies under a platform description: 'Workspace (platform) admin only. The company list a workspace admin manages: drives the GUI company list + company-switcher (POST /auth/company-context).' security: - SessionToken: [] parameters: - name: platformId in: path required: true schema: type: string responses: '200': description: Companies content: application/json: schema: type: object properties: items: type: array items: type: object properties: id: type: string name: type: string createdAt: type: string format: date-time nextCursor: type: string nullable: true '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: - Workspace Admin summary: Create a company under a platform description: 'Workspace (platform) admin only. Creates a new company (tenant) under the platform. Requires a **session** (workspace-admin login): not an API key. After creation, use `POST /auth/company-context` to enter the company and make company-scoped calls.' security: - SessionToken: [] parameters: - name: platformId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - name properties: name: type: string maxLength: 200 example: Acme Payments Ltd description: Display name for the company. responses: '201': description: Company created content: application/json: schema: type: object properties: company: type: object properties: id: type: string example: comp_… name: type: string createdAt: type: string format: date-time '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /auth/company-context: post: tags: - Workspace Admin summary: Enter a company context (platform admin acts AS a company) description: Workspace (platform) admin only. Sets the session's active company so the admin acts as an ADMIN of that company. The company must belong to the admin's platform. Cleared with DELETE. security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - companyId properties: companyId: type: string responses: '200': description: Context set content: application/json: schema: type: object properties: ok: type: boolean '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Unknown company in your platform content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' delete: tags: - Workspace Admin summary: Exit the company context (back to pure platform-admin scope) security: - SessionToken: [] responses: '200': description: Context cleared content: application/json: schema: type: object properties: ok: type: boolean '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/platforms/{platformId}/api-keys': get: tags: - Workspace Admin summary: 'List a platform''s API keys (metadata only: never the secret)' description: 'Workspace (platform) admin only. Returns active (non-revoked) keys with id, label, createdAt and lastUsedAt. The raw key is NEVER returned here: it is shown once, at creation.' security: - SessionToken: [] parameters: - name: platformId in: path required: true schema: type: string responses: '200': description: API keys (no secret) content: application/json: schema: type: object properties: items: type: array items: type: object properties: id: type: string label: type: string createdAt: type: string format: date-time lastUsedAt: type: string format: date-time nextCursor: type: string nullable: true '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: - Workspace Admin summary: 'Create an API key: the raw key is returned ONCE' description: Workspace (platform) admin only. Mints a new platform API key; the raw `key` value appears ONLY in this 201 response and is never stored or retrievable again. Store it securely. security: - SessionToken: [] parameters: - name: platformId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - label properties: label: type: string description: 'Human name, e.g. "Production ERP".' responses: '201': description: 'Key created: raw key shown once' content: application/json: schema: type: object properties: id: type: string key: type: string description: The raw API key. Shown ONCE. Never returned again. label: type: string createdAt: type: string format: date-time '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/platforms/{platformId}/api-keys/{keyId}': delete: tags: - Workspace Admin summary: Revoke an API key description: Workspace (platform) admin only. Soft-revokes the key (it stays for audit but stops authenticating immediately). security: - SessionToken: [] parameters: - name: platformId in: path required: true schema: type: string - name: keyId in: path required: true schema: type: string responses: '200': description: Revoked content: application/json: schema: type: object '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Unknown key for this platform content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/platforms/{platformId}/admins': get: tags: - User Management summary: List workspace admins for a platform description: Returns all workspace (platform) admin accounts. Workspace-admin session required. security: - SessionToken: [] parameters: - name: platformId in: path required: true schema: type: string responses: '200': description: Workspace admins content: application/json: schema: type: object properties: items: type: array items: type: object properties: id: type: string email: type: string name: type: string nextCursor: type: string nullable: true '403': description: Not a workspace admin content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: - User Management summary: Create a workspace admin description: 'Creates a new workspace (platform) admin account with a password. **Bootstrap note:** when the server is running in open/unauthenticated mode (no `BANKCONNECTOR_API_KEYS` set, e.g. first-time setup), this endpoint is accessible without a session: use it to create the very first admin. Once auth is enabled, an existing workspace-admin session is required.' requestBody: required: true content: application/json: schema: type: object required: - email - name - password properties: email: type: string format: email example: admin@acme.com name: type: string example: Alice Admin password: type: string description: Initial password. Minimum 8 characters. responses: '201': description: Admin created content: application/json: schema: type: object properties: id: type: string email: type: string name: type: string '400': description: Invalid request or duplicate email content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Not a workspace admin (when auth is enabled) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /users: get: tags: - User Management summary: List users in a company description: 'Returns all users in the company (name, email, roles, status). Readable by any authenticated company member.' security: - SessionToken: [] parameters: - name: platformId in: query required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Company users content: application/json: schema: type: object properties: users: type: array items: type: object properties: id: type: string email: type: string name: type: string roles: type: array items: type: string enum: - admin - approver - viewer status: type: string enum: - active - invited '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: - User Management summary: Create or invite a company user description: | **Company admin** only. Two modes: - **With `password`:** creates the user immediately (active). Use for programmatic provisioning. - **Without `password`:** sends an invite email. The user receives a link to `/auth/set-password` where they set their own password. The user must be given at least one role (`admin`, `approver`, and/or `viewer`). Users who will be assigned as payment approvers need the `approver` role. security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - email - name - roles properties: platformId: type: string companyId: type: string email: type: string format: email example: approver@acme.com name: type: string example: Bob Approver roles: type: array items: type: string enum: - admin - approver - viewer minItems: 1 example: - approver password: type: string description: 'If omitted, an invite email is sent and the user sets their own password.' responses: '201': description: User created or invite sent content: application/json: schema: type: object properties: id: type: string email: type: string name: type: string roles: type: array items: type: string invited: type: boolean description: true when an invite email was sent (no password provided) '400': description: 'Invalid request, duplicate email, or no valid roles' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Company admin required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /auth/set-password: post: tags: - User Management summary: 'Complete an invite: set password from email token (PUBLIC)' description: PUBLIC. Called when an invited user clicks their invite link. Validates the one-time token and sets the user's password. Any existing sessions for the user are revoked. The token comes from the invite email. requestBody: required: true content: application/json: schema: type: object required: - token - password properties: token: type: string description: The one-time invite token from the email link. password: type: string description: The user's chosen password. responses: '200': description: Password set content: application/json: schema: type: object properties: ok: type: boolean email: type: string '400': description: Invalid or expired token content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: 'Too many attempts: IP rate-limited' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /connections: get: tags: - Connections summary: List bank connections for a company security: - SessionToken: [] parameters: - name: platformId in: query required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Connection list content: application/json: schema: type: object properties: items: type: array items: $ref: '#/components/schemas/BankConnection' '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' post: tags: - Connections summary: 'Create (or find existing) bank connection: starts the setup wizard' description: 'Creates a bank connection for the given company, or returns the existing one if already set up. **Platform-admin note:** if you are authenticated as a workspace (platform) admin, you must enter a company context first via `POST /auth/company-context` before calling this endpoint. Without it you will receive: *"select a company context first."*' security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - bankKey properties: platformId: type: string companyId: type: string bankKey: $ref: '#/components/schemas/BankKey' environment: type: string enum: - test - production responses: '201': description: Connection created content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Admin required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/connections/{id}': get: tags: - Connections summary: Get a single connection + readiness security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: platformId in: query required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Connection + readiness content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '404': description: Not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/connections/{id}/signing-cert': put: tags: - Connections summary: Pin the bank's published signing certificate description: 'Stores the bank''s PUBLISHED X.509 signing certificate (PEM or base64-DER) on the connection so inbound Web Services signatures are verified against it (fail-closed on any mismatch). Admin only. Validates the cert parses (400 on bad input). Returns the cert subject + expiry so the operator can confirm they pinned the right cert; the raw PEM is never returned. GET /connections/{id} then surfaces signingCertSubject / signingCertExpiry.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - cert properties: platformId: type: string companyId: type: string cert: type: string description: The bank's published signing certificate — PEM (-----BEGIN CERTIFICATE-----) or base64-DER. responses: '200': description: Pinned content: application/json: schema: type: object properties: connectionId: type: string certSubject: type: string certExpiry: type: string format: date-time '400': description: Bad certificate or non-WS connection content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Admin required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/connections/{id}/activate-danske': post: tags: - Connections summary: Activate Danske EDI Web Services (automated cert enrolment) description: 'Calls the Danske bxd.fi activation endpoint with the one-time PIN, self-issues the signing + encryption certificates, and marks the connection active. Admin only.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - userId - pin properties: platformId: type: string companyId: type: string userId: type: string description: Agreement number (User ID) from Danske. pin: type: string description: One-time transfer key from Danske. responses: '200': description: Activated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '400': description: 'Activation failed (wrong PIN, network, etc.)' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Admin required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/connections/{id}/activate-nordea': post: tags: - Connections summary: Activate Nordea Corporate Access (HMAC cert enrolment via SMS code) description: 'Generates a Nordea-format signing CSR, sends the HMAC-signed enrolment request to Nordea''s Corporate Access endpoint using the SMS activation code, and stores the issued certificate. Admin only.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - holderName - signerId - country - activationCode properties: platformId: type: string companyId: type: string holderName: type: string description: Certificate-holder name (CN) as printed in the Nordea agreement. signerId: type: string description: Signer ID from the agreement. senderId: type: string country: type: string minLength: 2 maxLength: 2 activationCode: type: string description: 10-digit SMS activation code from Nordea. responses: '200': description: Activated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '400': description: Activation failed content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Admin required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/connections/{id}/activate-bankconnect': post: tags: - Connections summary: 'Activate Bank Connect (DK gateway: automated cert enrolment)' description: 'Enrols the signing certificate with the Bank Connect data central (BD/BEC/SDC), stores the issued certificate and bank public key, and marks the connection active. Data central and country are pre-filled from the bank profile. Admin only.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - organisationId - functionId - activationCode properties: platformId: type: string companyId: type: string organisationId: type: string description: Bank registration number. functionId: type: string description: Per-agreement routing code from the bank. activationCode: type: string description: One-time activation code from the bank. responses: '200': description: Activated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '400': description: Activation failed or bank has no Bank Connect data central content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Admin required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Connection not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/connections/{id}/generate-pgp': post: tags: - Connections summary: Generate our PGP key pair (SFTP channel) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: PGP key pair generated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/bank-key': post: tags: - Connections summary: Save the bank's PGP public key (SFTP channel) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - armoredKey properties: platformId: type: string companyId: type: string armoredKey: type: string description: ASCII-armored PGP public key block. responses: '200': description: Bank key saved content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/server-info': post: tags: - Connections summary: 'Save SFTP server info (host, port, paths, fingerprint)' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - host properties: platformId: type: string companyId: type: string host: type: string port: type: integer default: 22 uploadPath: type: string downloadPath: type: string hostFingerprint: type: string responses: '200': description: Server info saved content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/sftp-user': post: tags: - Connections summary: Set SFTP username and generate SSH key pair (single combined call) description: 'Sets the SFTP username AND generates the SSH key pair in one call: you cannot pre-generate the key before you have the username. The `username` must be obtained from the bank first; only then can you call this endpoint. The response includes the connection + readiness state, from which you can retrieve the generated SSH public key to send to the bank for whitelisting.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - username properties: platformId: type: string companyId: type: string username: type: string description: 'SFTP username assigned by the bank. Required: the SSH key generation and username registration are a single atomic operation.' responses: '200': description: Username set + SSH key generated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/approvers': post: tags: - Connections summary: Assign approver users to a connection description: 'Assigns which users may approve payments on this connection. **Approvers and approval policy are separate:** the policy (under `POST /approvals/policies`) defines the rule (how many approvers, any amount thresholds); this endpoint assigns the eligible users per connection. Both must be in place for a connection to reach go-live. Note: users must first exist in the company: create them with `POST /users` before assigning them as approvers.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - userIds properties: platformId: type: string companyId: type: string userIds: type: array items: type: string description: User IDs of company members with the Approver role. Must already exist in the company. responses: '200': description: Approvers assigned content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '422': description: One or more user IDs are not valid approvers in this company content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/connections/{id}/activate': post: tags: - Connections summary: Mark SFTP connection active (after the bank has whitelisted the SSH key) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Connection activated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/intro-email-sent': post: tags: - Connections summary: Mark intro email as sent (tracks wizard progress) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Updated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/ws-environment': post: tags: - Connections summary: Toggle a Web Services connection between TEST and PRODUCTION description: Requires an already-activated connection (certificates issued); does not re-run PKI. security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - environment properties: platformId: type: string companyId: type: string environment: type: string enum: - TEST - PRODUCTION responses: '200': description: Environment updated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/covered-banks': post: tags: - Connections summary: Set which member banks a shared connection covers description: For a shared (group) connection. The store clamps the set to the bank's group and always includes the connection's own bank. security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - coveredBankKeys properties: platformId: type: string companyId: type: string coveredBankKeys: type: array items: type: string description: Bank keys this connection serves (clamped to the bank's group). responses: '200': description: Covered banks updated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/ebics-params': post: tags: - Connections summary: 'Save EBICS connection parameters (Host ID, Partner ID, User ID, …)' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - url - hostId - partnerId - userId properties: platformId: type: string companyId: type: string url: type: string format: uri description: EBICS server URL. hostId: type: string description: Bank-assigned Host ID. partnerId: type: string description: Customer ID (PartnerID) from the bank. userId: type: string description: Subscriber ID (UserID) from the bank. protocolVersion: type: string example: H005 signatureVersion: type: string example: A006 subscriberMode: type: string enum: - single - multi responses: '200': description: EBICS params saved content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/ebics-generate-keys': post: tags: - Connections summary: 'Generate EBICS subscriber keys (authentication, encryption, signature)' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Keys generated content: application/json: schema: $ref: '#/components/schemas/ConnectionWithReadiness' '401': description: Sign in required '403': description: Admin required '/connections/{id}/intro-email': get: tags: - Connections summary: Render the bank intro email (draft to send to the bank contact) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: platformId in: query required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Rendered email (subject + body) content: application/json: schema: type: object '404': description: Not found '/connections/{id}/ebics-letter': get: tags: - Connections summary: 'Download the EBICS initialisation letter (PDF, base64-encoded)' description: 'Returns the signed INI/HIA initialisation letter as a base64-encoded PDF. Print, sign, and send to the bank to complete EBICS subscriber activation.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: platformId in: query required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: PDF as base64 content: application/json: schema: type: object properties: pdfBase64: type: string filename: type: string '404': description: Not found or keys not yet generated '/banks/{bankKey}/setup-info': get: tags: - Connections summary: Per-bank setup descriptor for the wizard UI description: 'Returns the channel kind, requirements text, field definitions, pre-known server/endpoint details, and SFTP wizard screens for a bank. Used by the setup wizard to render the correct flow.' parameters: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' responses: '200': description: Setup descriptor content: application/json: schema: type: object '404': description: Unknown bank x-sandbox: true '/banks/{bankKey}/settings': get: tags: - Connections summary: Get per-company settings for a specific bank security: - SessionToken: [] parameters: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' - name: platformId in: query required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Bank settings (null if none saved) content: application/json: schema: type: object post: tags: - Connections summary: Update per-company settings for a specific bank description: 'Payment-affecting fields (agreementId, chargeBearer, executionDateOffsetDays, approvalPolicyId) require Admin. The `selected` pin is open to any member.' security: - SessionToken: [] parameters: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' requestBody: required: true content: application/json: schema: type: object properties: platformId: type: string companyId: type: string agreementId: type: string chargeBearer: type: string enum: - SLEV - SHAR - CRED - DEBT executionDateOffsetDays: type: integer approvalPolicyId: type: string selected: type: boolean responses: '200': description: Settings saved content: application/json: schema: type: object '401': description: Sign in required '403': description: Admin required for payment-affecting fields /bank-settings: get: tags: - Connections summary: List all per-company bank settings in one call security: - SessionToken: [] parameters: - name: platformId in: query required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: All saved bank settings for this company content: application/json: schema: type: object properties: settings: type: array items: type: object /bank-requests: post: tags: - Platform summary: Request a bank we don't support yet description: Any signed-in user (or anonymous) can submit a BIC or bank name. The request is queued for the development team. requestBody: required: true content: application/json: schema: type: object required: - query properties: query: type: string description: BIC code or bank name. email: type: string format: email responses: '201': description: Request recorded content: application/json: schema: type: object properties: ok: type: boolean request: type: object properties: id: type: string query: type: string createdAt: type: string format: date-time '400': description: Empty query get: tags: - Platform summary: List all bank requests (Admin only) security: - SessionToken: [] responses: '200': description: All requests content: application/json: schema: type: object properties: items: type: array items: type: object '401': description: Sign in required '403': description: Admin required '/banks/{bankKey}/test-scenarios': get: tags: - Platform summary: Available one-click Test Payment scenarios for a bank description: '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: - name: bankKey in: path required: true schema: type: string responses: '200': description: Offered scenarios content: application/json: schema: type: object properties: scenarios: type: array items: type: object x-sandbox: true '/banks/{bankKey}/test-payment': post: tags: - Platform summary: Send a one-click test payment description: '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: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - scenario - account properties: platformId: type: string companyId: type: string scenario: type: string enum: - domestic - sepa - international - urgent environment: type: string enum: - test - production account: type: object properties: name: type: string iban: type: string other: type: object properties: id: type: string scheme: type: string currency: type: string country: type: string responses: '201': description: Test payment journaled content: application/json: schema: type: object '400': description: Invalid request '401': description: Sign in required '/banks/{bankKey}/payment-types': get: tags: - Discovery summary: Payment types offered by a specific bank parameters: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' example: nordea-dk responses: '200': description: Bank key/name + offered payment types with full metadata content: application/json: schema: type: object properties: bankKey: type: string description: 'The bank''s registry key (consistent with /banks/{bankKey}/status).' bank: type: string deprecated: true description: Deprecated alias of bankKey — kept one release. bankName: type: string countryCode: type: string paymentTypes: type: array items: type: object '404': description: Unknown bank content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-sandbox: true '/validate/{bankKey}': post: tags: - Conversion summary: Validate a payment instruction (no conversion) description: '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: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' example: nordea-dk requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PaymentInstruction' responses: '200': description: 'Validation outcome (valid or not: check `valid`)' content: application/json: schema: $ref: '#/components/schemas/ValidationOutcome' '400': description: Malformed JSON body content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Unknown bank key content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-sandbox: true x-codeSamples: - lang: Shell label: cURL source: | 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"}}}]}]}' - lang: JavaScript label: Fetch source: | 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(); - lang: Python label: Python source: | 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() '/convert/{bankKey}': post: tags: - Conversion summary: Validate then convert to bank-specific pain.001 XML description: '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: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' example: nordea-dk - name: validate in: query required: false schema: type: boolean description: 'If true, XSD-validate the generated XML before returning it.' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PaymentInstruction' responses: '200': description: Bank-specific pain.001 XML content: application/xml: schema: type: string '400': description: Malformed JSON body content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Unknown bank key content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: 'Validation failed: returns all issues at once' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-sandbox: true x-codeSamples: - lang: Shell label: cURL source: | 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"}}}]}]}' - lang: JavaScript label: Fetch source: | 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 - lang: Python label: Python source: | 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 '/convert-async/{bankKey}': post: tags: - Conversion summary: Enqueue a background conversion (returns a job id) description: '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: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' example: nordea-dk requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PaymentInstruction' responses: '202': description: Conversion enqueued content: application/json: schema: type: object properties: jobId: type: string status: type: string bankKey: type: string autoPaymentType: type: string required: - jobId - status - bankKey '400': description: 'Malformed JSON body, ?validate=true (unsupported on the async path), or unsupported painVersion' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Unknown bank key content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: 'Validation failed: returns all issues at once' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/convert-jobs/{id}': get: tags: - Conversion summary: Poll a background conversion job description: '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: - name: id in: path required: true schema: type: string example: job_abc123 responses: '200': description: 'Job status, or the converted XML once the job has succeeded' content: application/json: schema: type: object application/xml: schema: type: string '404': description: Conversion job not found (or not owned by the caller) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /journal/payments: post: tags: - Journal summary: 'Submit a payment: convert + journal (+ deliver if host-to-host)' description: '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).' security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: Idempotency-Key in: header required: false schema: type: string minLength: 8 maxLength: 255 description: 'Unique 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.' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/JournalPaymentRequest' responses: '201': description: 'Payment recorded + journaled. A replay of a prior idempotent submit ALSO returns 201, additionally carrying the response header `Idempotency-Replayed: true`.' content: application/json: schema: $ref: '#/components/schemas/JournalDocument' example: 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: deliveryMode: host-to-host createdAt: 2026-06-20T10:31:00Z delivery: status: queued '400': description: '`idempotency_key_required` — no Idempotency-Key (header or body) and no `payment.messageId` on a host-to-host submit' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Scope mismatch (cross-tenant) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': description: 'Idempotency conflict (same key, different body) or an in-flight retry' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: 'Validation failed: all issues at once' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: 'Rate limited (production): see Retry-After' content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-codeSamples: - lang: Shell label: cURL source: | 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"}}}]}]}}' - lang: JavaScript label: Fetch source: | 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(); - lang: Python label: Python source: | 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() /journal/payments/preview: post: tags: - Journal summary: 'Preview a payment: convert + validate, return XML (no journal, no send)' description: '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.' security: - ApiKeyAuth: [] - SessionToken: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/JournalPaymentRequest' responses: '200': description: Preview generated content: application/json: schema: type: object properties: xml: type: string painVersion: type: string preview: type: boolean '400': description: Missing fields content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: Validation / XSD failed content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /journal/ingest: post: tags: - Journal summary: 'Ingest a bank file (camt.053/054, pain.002, MT940) → normalised JSON' description: Upload a raw bank statement/status file; it is parsed to the one normalised shape and journaled. Requires authentication. security: - ApiKeyAuth: [] - SessionToken: [] requestBody: required: true content: application/json: schema: type: object properties: companyId: type: string description: Your company id. platformId is inferred from your API key — do not send it. bankKey: type: string content: type: string description: Raw file contents required: - companyId - content responses: '201': description: File ingested + normalised content: application/json: schema: $ref: '#/components/schemas/JournalDocument' example: 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 '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: Unrecognised / unparseable file content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /journal/documents: get: tags: - Journal summary: List journal document summaries (newest first) description: 'Tenant-scoped list of payments + statements (metadata only: no decrypted payload). Filter by direction/type.' security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string - name: direction in: query required: false schema: type: string enum: - inbound - outbound - name: type in: query required: false schema: type: string example: camt.053 - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 200 description: Page size (enables keyset pagination via nextCursor). - name: cursor in: query required: false schema: type: string description: Opaque cursor from a prior page's `nextCursor`. (`before` is accepted as a deprecated alias.) responses: '200': description: Document summaries content: application/json: schema: type: object properties: items: type: array items: $ref: '#/components/schemas/JournalDocument' nextCursor: type: string nullable: true '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /journal/export: post: tags: - Journal summary: Export full document payloads by journal number (admin / API key) description: '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.' security: - ApiKeyAuth: [] - SessionToken: [] requestBody: required: true content: application/json: schema: type: object properties: companyId: type: string description: Your company id. platformId is inferred from your API key — do not send it. journalNos: type: array items: type: string required: - companyId - journalNos responses: '200': description: Exported documents + any notFound journal numbers content: application/json: schema: type: object properties: exported: type: array items: type: object notFound: type: array items: type: string '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Admin required (human session) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /accounts: get: tags: - Accounts summary: List bank accounts for a company (balances masked) security: - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string responses: '200': description: Accounts with latest balances content: application/json: schema: type: object properties: items: type: array items: type: object '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /accounts/statements: get: tags: - Accounts summary: List imported camt.053 statements for an account description: Per-account statement history (the parsed NormalisedStatement summaries) used by the Statements screen. security: - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string - name: account in: query required: false schema: type: string description: IBAN or account id to filter to. responses: '200': description: Statements + any detected sequence gaps content: application/json: schema: type: object properties: account: type: string nullable: true statements: type: array items: type: object gaps: type: array items: type: object description: Possible missing statements detected by the continuity check. '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /approvals/pending: get: tags: - Approvals summary: List payments awaiting approval security: - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string responses: '200': description: Pending approval requests content: application/json: schema: type: object properties: requests: type: array items: type: object '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/approvals/{id}/approve': post: tags: - Approvals summary: Approve a pending payment (maker ≠ checker enforced) description: An approver other than the maker approves the request; once the policy's required approvals are met the payment proceeds to delivery. May require a 2FA code/backup code if step-up is configured. security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string reason: type: string code: type: string description: 2FA TOTP code (if step-up required). backupCode: type: string responses: '200': description: Approval recorded (and payment dispatched if fully approved) content: application/json: schema: type: object '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Maker cannot approve own payment / not an approver content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/approvals/{id}/reject': post: tags: - Approvals summary: Reject a pending payment security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string reason: type: string responses: '200': description: Rejection recorded content: application/json: schema: type: object '401': description: Sign in required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /approvals/policies: get: tags: - Approvals summary: List approval policies for a company security: - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string responses: '200': description: Policies content: application/json: schema: type: object properties: policies: type: array items: type: object '401': description: Sign in required post: tags: - Approvals summary: Create an approval policy (Admin) description: Defines required approver count + amount thresholds. Admin only. security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - name properties: platformId: type: string companyId: type: string name: type: string requiredApprovals: type: integer minimum: 1 thresholdAmount: type: string currency: type: string responses: '201': description: Policy created content: application/json: schema: type: object '401': description: Sign in required '403': description: Admin required '/approvals/policies/{id}': post: tags: - Approvals summary: Edit a DRAFT approval policy (Admin) description: 'Edit a policy''s name / required approver count / thresholds. Works only on a DRAFT version — an Active version is immutable (409); change it via /approvals/policies/{id}/propose then co-sign.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string name: type: string requiredApprovals: type: integer minimum: 1 thresholdAmount: type: string currency: type: string responses: '200': description: Updated draft policy content: application/json: schema: type: object properties: policy: type: object '401': description: Sign in required '403': description: Admin required '409': description: Version is not a draft / conflict '/approvals/policies/{id}/lock': post: tags: - Approvals summary: Activate (lock) a draft approval policy (Admin) description: Locks a DRAFT version → Active and immutable. Runs the quorum check (a dual policy needs ≥2 named approvers); fails 409 if it can't be met. An Active version is changed only via propose + co-sign. security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Policy activated content: application/json: schema: type: object properties: policy: type: object '401': description: Sign in required '403': description: Admin required '409': description: Quorum not met / conflict '/approvals/policies/{id}/propose': post: tags: - Approvals summary: Propose a change to an Active policy (Admin) description: 'Clones the Active version to an editable draft v(n+1). Edit the draft (POST /approvals/policies/{draftId}), then submit it for activation via `POST /approvals/changes/{changeId}/submit` and have a second Admin co-sign via `POST /approvals/changes/{changeId}/cosign`. The old version is retained forever once superseded.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '201': description: Draft version created content: application/json: schema: type: object properties: policy: type: object '401': description: Sign in required '403': description: Admin required '409': description: Base not Active / change already in progress /approvals/changes: get: tags: - Approvals summary: List approval-policy change requests (Admin) description: The dual-co-sign change requests for the company. `?status=pending` (default) | activated | cancelled. security: - SessionToken: [] parameters: - name: status in: query required: false schema: type: string enum: - pending - activated - cancelled responses: '200': description: Change requests content: application/json: schema: type: object properties: changes: type: array items: type: object '401': description: Sign in required '403': description: Admin required '/approvals/changes/{changeId}/{action}': post: tags: - Approvals summary: Submit / co-sign / cancel a policy change (Admin) description: 'action=submit — the proposer submits the draft for activation (counts as the FIRST of 2 co-signs). action=cosign — a SECOND, distinct Admin co-signs: the new version goes Active, the base is Superseded, and attached banks re-point to the new version. action=cancel — any Admin cancels a pending change (the draft is discarded; the base stays Active). A 2nd co-sign by the same Admin is refused (409).' security: - SessionToken: [] parameters: - name: changeId in: path required: true schema: type: string - name: action in: path required: true schema: type: string enum: - submit - cosign - cancel requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Change updated content: application/json: schema: type: object properties: change: type: object policy: type: object supersededId: type: string affectedBanks: type: array items: type: string '401': description: Sign in required '403': description: Admin required '409': description: Same-admin co-sign / conflict /journal/list: get: tags: - Journal summary: 'Paginated journal feed (cursor): the ERP read + collect surface' description: '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`.' security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string description: Your company id. (platformId is inferred from your API key — do not send it.) - name: cursor in: query required: false schema: type: string description: Opaque cursor from a prior page's `nextCursor`. - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 1000 default: 500 description: 'Page size — default 500, max 1000 (larger than the interactive /journal/documents feed, for bulk ERP pulls).' - name: direction in: query required: false schema: type: string enum: - inbound - outbound - name: type in: query required: false schema: type: string example: camt.053 - name: unretrievedOnly in: query required: false schema: type: boolean default: false description: 'When `true`, returns ONLY documents not yet collected (the standard poll-then-collect loop).' responses: '200': description: A page of documents + counts + nextCursor content: application/json: schema: type: object properties: total: type: integer description: Total documents matching the filter (not just this page). unretrieved: type: integer description: How many matching documents are still uncollected. nextCursor: type: string nullable: true description: Pass back as `cursor` to fetch the next page; null when there are no more. items: type: array items: type: object properties: journalNo: type: string example: OUT-000123 direction: type: string enum: - inbound - outbound example: outbound type: type: string example: pain.001 sourceFormat: type: string example: pain.001.001.09 bankKey: type: string example: nordea-dk summary: type: string example: SEPA — 1 payment status: type: string example: converted collected: type: boolean description: Whether this document has been collected (marked retrieved) yet. collectedAt: type: string format: date-time nullable: true description: When it was first collected (null if not yet). createdAt: type: string format: date-time expiresAt: type: string format: date-time nullable: true required: - total - unretrieved - items example: 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 '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/journal/documents/{id}': get: tags: - Journal summary: Fetch one journal document with its full (decrypted) payload description: 'Returns the document including its canonical/normalised payload and, for seeded/generated docs, the raw `xml` for download. Tenant-scoped.' security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Document + payload content: application/json: schema: $ref: '#/components/schemas/JournalDocument' '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Not found (or not in this tenant's scope) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/journal/documents/{id}/payments': get: tags: - Journal summary: Per-payment lines within an outbound batch (the drawer) security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: 'Per-line payments (payee, amount, endToEndId, status)' content: application/json: schema: type: object properties: payments: type: array items: type: object '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/journal/documents/{id}/file': get: tags: - Journal summary: Download the generated pain.001 XML for an outbound document description: '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.' security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: The pain.001 XML (attachment) content: application/xml: schema: type: string '404': description: No downloadable file for this document content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /journal/payment-types: get: tags: - Journal summary: Tally of outbound payments grouped by payment type description: '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.' security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string responses: '200': description: Per-type counts + total content: application/json: schema: type: object properties: counts: type: object additionalProperties: type: integer total: type: integer sampled: type: integer '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /journal/reconciliation: get: tags: - Journal summary: 'Reconciliation view: correlate outbound payments with pain.002 + camt evidence' description: '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.' security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string responses: '200': description: Per-payment reconciliation + evidence content: application/json: schema: type: object properties: items: type: array items: type: object properties: documentId: type: string journalNo: type: string messageId: type: string state: type: string enum: - cleared - rejected - cancelled - pending evidence: type: object '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /journal/events: get: tags: - Journal summary: Activity-log events (audit trail) for a company security: - ApiKeyAuth: [] - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string responses: '200': description: Events newest-first content: application/json: schema: type: object properties: items: type: array items: type: object '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /inbound/payment-status: post: tags: - Journal summary: Push a pain.002 status report → reconcile the outbound payment description: Accepts a raw pain.002 (or normalised status); matches it to the original outbound by MsgId/endToEndId and advances the payment status. Authenticated. security: - ApiKeyAuth: [] - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - companyId - content properties: companyId: type: string description: Your company id. platformId is inferred from your API key — do not send it. bankKey: type: string content: type: string description: Raw pain.002 XML. responses: '200': description: Reconciled (status advanced) content: application/json: schema: type: object '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: Unparseable / unmatched status content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /inbound/statement: post: tags: - Journal summary: 'Parse a camt.053/054 / MT940 statement → normalised JSON (preview only, not stored)' description: '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 (one per account); ALL are returned in the `items` list envelope.' security: - ApiKeyAuth: [] - SessionToken: [] requestBody: required: true content: text/plain: schema: type: string description: Raw camt.053/054 XML or MT940 text. responses: '200': description: Normalised statement(s) content: application/json: schema: type: object properties: items: type: array items: type: object '400': description: Empty body or unrecognised format content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: Unparseable file content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /webhooks: get: tags: - Webhooks summary: List webhook subscriptions security: - SessionToken: [] parameters: - name: companyId in: query required: true schema: type: string responses: '200': description: Subscriptions content: application/json: schema: type: object properties: items: type: array items: type: object '401': description: Sign in required post: tags: - Webhooks summary: Create a webhook subscription (Admin) description: 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. security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId - url properties: platformId: type: string companyId: type: string url: type: string format: uri eventTypes: type: array items: type: string minItems: 1 description: 'Event names to subscribe to, e.g. ["payment.sent", "payment.rejected"]. Omit to receive all events; an explicitly-empty array is rejected.' responses: '201': description: Subscription created (returns the signing secret once) content: application/json: schema: type: object '401': description: Sign in required '403': description: Admin required '/webhooks/{id}': delete: tags: - Webhooks summary: Delete a webhook subscription (Admin) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Deleted '401': description: Sign in required '403': description: Admin required '404': description: Not found '/webhooks/{id}/enable': post: tags: - Webhooks summary: Enable a disabled webhook (Admin) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Webhook enabled content: application/json: schema: type: object '401': description: Sign in required '403': description: Admin required '404': description: Not found '/webhooks/{id}/disable': post: tags: - Webhooks summary: Disable a webhook without deleting it (Admin) security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Webhook disabled content: application/json: schema: type: object '401': description: Sign in required '403': description: Admin required '404': description: Not found '/webhooks/{id}/test': post: tags: - Webhooks summary: Fire a signed test event to the webhook URL (Admin) description: Sends a signed ping to the endpoint and returns the delivery result. security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: - platformId - companyId properties: platformId: type: string companyId: type: string responses: '200': description: Delivery result of the test event content: application/json: schema: type: object '401': description: Sign in required '403': description: Admin required '404': description: Not found '/webhooks/{id}/deliveries': get: tags: - Webhooks summary: List delivery attempts for a webhook (Admin) description: '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.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string - name: cursor in: query required: false schema: type: string description: Opaque cursor from the previous page's `nextCursor`. (`after` is accepted as a deprecated alias.) - name: limit in: query required: false schema: type: integer default: 50 maximum: 200 responses: '200': description: A page of delivery attempts content: application/json: schema: type: object properties: items: type: array items: $ref: '#/components/schemas/WebhookDelivery' nextCursor: type: string nullable: true description: Last delivery id in this page when a full page was returned; null when no more. '401': description: Sign in required '403': description: Admin required '404': description: Webhook not found '/webhooks/{id}/deliveries/{deliveryId}/redeliver': post: tags: - Webhooks summary: Replay a single delivery (Admin) description: 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. security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: deliveryId in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Re-enqueued content: application/json: schema: type: object properties: redelivered: type: boolean deliveryId: type: string description: Id of the NEW delivery row. '401': description: Sign in required '403': description: Admin required '404': description: Delivery not found / not redeliverable '/webhooks/{id}/redeliver-failed': post: tags: - Webhooks summary: Bulk-replay failed deliveries since a timestamp (Admin) description: '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.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string - name: since in: query required: true schema: type: string format: date-time description: ISO timestamp — only failed deliveries created at/after this are replayed. - name: cursor in: query required: false schema: type: string description: Opaque cursor from a prior page's `nextCursor` to continue draining. responses: '200': description: Re-enqueued count + drain cursor content: application/json: schema: type: object properties: count: type: integer nextCursor: type: string nullable: true '400': description: Missing/invalid since '401': description: Sign in required '403': description: Admin required '404': description: Webhook not found '/webhooks/{id}/re-enable': post: tags: - Webhooks summary: Re-enable an auto-disabled webhook (Admin) description: 'Clears an auto-disabled state: resets `consecutiveFailures` to 0 and clears `disabledAt`/`disabledReason`. 400 if the webhook is not currently disabled.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string responses: '200': description: Re-enabled content: application/json: schema: type: object properties: webhookId: type: string enabledAt: type: string format: date-time '400': description: Webhook is not disabled '401': description: Sign in required '403': description: Admin required '404': description: Webhook not found '/webhooks/{id}/rotate-secret': post: tags: - Webhooks summary: Rotate the signing secret with an overlap window (Admin) description: '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.' security: - SessionToken: [] parameters: - name: id in: path required: true schema: type: string - name: companyId in: query required: true schema: type: string requestBody: required: false content: application/json: schema: type: object properties: overlapHours: type: integer default: 24 minimum: 1 maximum: 72 description: How long the old secret stays valid. responses: '200': description: Rotated — newSecret shown once content: application/json: schema: type: object properties: webhookId: type: string newSecret: type: string description: The new signing secret (whsec_…). Distribute to the receiver; not retrievable again. overlapUntil: type: string format: date-time '400': description: Invalid body '401': description: Sign in required '403': description: Admin required '404': description: Webhook not found /auth/login: post: tags: - Auth summary: 'Log in (PUBLIC): returns a session token + sets the session cookie' description: 'PUBLIC. Exchanges subdomain + email + password for a signed session. If 2FA is enabled, returns a step-up challenge instead (complete it at POST /auth/2fa).' requestBody: required: true content: application/json: schema: type: object required: - subdomain - email - password properties: subdomain: type: string example: demo email: type: string format: email password: type: string responses: '200': description: Logged in (token) OR a 2FA step-up challenge content: application/json: schema: type: object properties: token: type: string twoFactorRequired: type: boolean '401': description: Invalid credentials content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-codeSamples: - lang: Shell label: cURL source: | curl -X POST https://your-host/auth/login \ -H 'Content-Type: application/json' \ -d '{"subdomain":"demo","email":"you@acme.com","password":"…"}' - lang: JavaScript label: Fetch source: | const res = await fetch("https://your-host/auth/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({"subdomain":"demo","email":"you@acme.com","password":"…"}), }); const data = await res.json(); - lang: Python label: Python source: | import requests resp = requests.request( "POST", "https://your-host/auth/login", json={"subdomain":"demo","email":"you@acme.com","password":"…"}, ) resp.raise_for_status() data = resp.json() /2fa/setup: post: tags: - Auth summary: Begin 2FA enrollment (signed-in user) — returns a TOTP secret + otpauth URI description: Generates a NOT-yet-active TOTP secret for the signed-in user and returns its otpauth:// URI (render as a QR). Activate it with POST /2fa/confirm. Session-only. security: - SessionToken: [] responses: '200': description: Pending secret + otpauth URI content: application/json: schema: type: object properties: secret: type: string otpauthUri: type: string issuer: type: string '401': description: Sign in first content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /2fa/confirm: post: tags: - Auth summary: Confirm 2FA enrollment with a TOTP code — returns one-time backup codes description: Verifies a code against the pending secret and activates 2FA. The backup codes are returned EXACTLY ONCE — surface + warn. Session-only; throttled per user. security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - code properties: code: type: string description: 6-digit TOTP code responses: '200': description: Enabled + one-time backup codes content: application/json: schema: type: object properties: enabled: type: boolean backupCodes: type: array items: type: string '400': description: Setup not started content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': description: Already enabled content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: Code didn't match content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Too many attempts (Retry-After) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /2fa/disable: post: tags: - Auth summary: Disable 2FA (requires a fresh code; blocked when the company forces 2FA) description: Turns off the signed-in user's 2FA after verifying a fresh TOTP or backup code; refused (403) when the company requires 2FA. Revokes the user's OTHER sessions. Session-only; throttled. security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object properties: code: type: string backupCode: type: string responses: '200': description: Disabled content: application/json: schema: type: object properties: enabled: type: boolean '403': description: Company requires 2FA content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '409': description: Not enabled content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '422': description: A valid code is required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Too many attempts (Retry-After) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /company/require-2fa: post: tags: - Auth summary: 'Admin: set the company-wide 2FA requirement' description: 'Admin-only. When enabled, every user in the company must enroll in 2FA. Distinct from the per-user `enabled` state — the response field is `require2fa` (company policy).' security: - SessionToken: [] requestBody: required: true content: application/json: schema: type: object required: - enabled properties: enabled: type: boolean responses: '200': description: Updated company policy content: application/json: schema: type: object properties: require2fa: type: boolean '403': description: Admin role required content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /auth/me: get: tags: - Auth summary: Current principal (who am I) security: - SessionToken: [] responses: '200': description: The signed-in user + roles + company content: application/json: schema: type: object '401': description: Not signed in content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /auth/logout: post: tags: - Auth summary: Log out (invalidates the session) security: - SessionToken: [] responses: '200': description: Logged out /auth/platform-info: get: tags: - Auth summary: Public platform branding for the login page description: 'Returns the branding for the platform resolved from the request''s subdomain (no auth — called before login). On a known subdomain: the platform''s display name + subdomain. Otherwise the generic BankConnector branding. `logoUrl` is reserved for a later phase (null for now).' responses: '200': description: Platform branding content: application/json: schema: type: object required: - platformId - displayName - subdomain - logoUrl properties: platformId: type: string nullable: true displayName: type: string subdomain: type: string nullable: true logoUrl: type: string nullable: true /auth/2fa: post: tags: - Auth summary: Complete a 2FA login step-up (PUBLIC) description: PUBLIC. Submits the TOTP (or backup) code against the challenge from POST /auth/login to obtain the session. requestBody: required: true content: application/json: schema: type: object required: - challenge - code properties: challenge: type: string description: The challenge id from /auth/login. code: type: string backupCode: type: string responses: '200': description: Session granted content: application/json: schema: type: object properties: token: type: string '401': description: Invalid code content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '/banks/{bankKey}/status': get: tags: - Discovery summary: Whether a bank is supported + its channel/payment readiness (PUBLIC) parameters: - name: bankKey in: path required: true schema: $ref: '#/components/schemas/BankKey' responses: '200': description: Bank support status content: application/json: schema: type: object '404': description: Unknown bank content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-sandbox: true components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: 'Machine / ERP integration credential. Tenant scope is derived from the key: never from the request body.' SessionToken: type: apiKey in: header name: X-Session-Token description: Signed-in human session (the web app). The browser uses an httpOnly cookie + CSRF instead. schemas: BankKey: type: string enum: - bnp-paribas-bh - bnp-paribas-kw - bnp-paribas-my - bnp-paribas-qa - bnp-paribas-sa - bnp-paribas-vn - bnz-nz - bpi-ph - banca-intensa-rs - cba-au - cba-nz - citibank-in - citibank-my - citibank-ph - citibank-th - citibank-tw - citibank-vn - deutsche-bank-in - deutsche-bank-pt - hsbc-ae - hsbc-au - hsbc-cn - hsbc-eg - hsbc-hk - hsbc-id - hsbc-in - hsbc-jp - hsbc-kr - hsbc-mx - hsbc-my - hsbc-nz - hsbc-ph - hsbc-sg - hsbc-tr - hsbc-tw - handelsbanken-cn - handelsbanken-hk - handelsbanken-sg - jp-morgan-chase-au - otp-bank-me - seb-sg - standard-bank-za - standard-chartered-my - standard-chartered-sg - unicredit-ba - unicredit-rs - bnp-paribas-br - citibank-us - deutsche-bank-us - hsbc-br - hsbc-us - handelsbanken-us - jp-morgan-chase-ca - scotia-ca - tdcanadatrust-ca - abn-amro-be - alior-bank-pl - bnp-paribas-bg - bnp-paribas-cz - bnp-paribas-es - bnp-paribas-gb - bnp-paribas-hu - bank-of-america-it - csob-cz - ceska-sporitelna-cz - citibank-fr - citibank-gb - commerzbank-cz - commerzbank-hu - commerzbank-sk - credit-lyonnaise-fr - dsk-bank-bg - danske-bank-be - danske-bank-ie - deutsche-bank-be - deutsche-bank-fr - deutsche-bank-hu - dnb-gb - dnb-se - eik-banki-fo - eika-alliance-no - groenbank-gl - hsbc-be - hsbc-es - hsbc-fr - hsbc-ie - hsbc-it - handelsbanken-ee - handelsbanken-fi - handelsbanken-gb - handelsbanken-lt - handelsbanken-lu - handelsbanken-lv - ing-fr - jp-morgan-chase-be - jp-morgan-chase-gb - jp-morgan-chase-lu - natwest-gb - nordoya-spar-fo - otp-bank-hr - pbz-hr - bank-pekao-pl - raiffeisen-bank-cz - seb-fi - seb-gb - seb-no - societe-generale-lu - sparoresund-se - sparebank-1-alliance-no - swedbank-fi - swedbank-no - unicredit-bg - unicredit-it - unicredit-ro - unicredit-sk - werhahn-bank-de - zagrebacka-hr - danske-dk - deutsche-bank-de - nordea-dk - bbva-es - caixabank-es - pko-bank-polski-pl - vub-banka-sk - danske-fi - danske-gb - danske-pl - seb-ee - seb-lv - seb-lt - swedbank-ee - swedbank-lv - swedbank-lt - sr-bank-no - ing-nl - swedbank-se - svb-us - seb-se - dnb-no - hsbc-gb - jpmorgan-us - nordea-se - nordea-no - nordea-fi - danske-se - danske-no - abn-amro-nl - nord-lb-de - aareal-bank-de - apobank-de - bank-fuer-sozialwirtschaft-de - carl-plump-de - loebbecke-de - bayernlb-de - berliner-sparkasse-de - sparkasse-bremen-de - hsbc-de - haspa-de - hauck-aufhaeuser-de - berenberg-de - jyske-bank-de - helaba-de - naspa-de - nordea-de - olb-de - postbank-de - procredit-de - saarlb-de - santander-de - societe-generale-de - sparkasse-darmstadt-de - sparkasse-fuerth-de - sparkasse-heidelberg-de - sparkasse-koelnbonn-de - suedwestbank-de - wells-fargo-us - sparkasse-en-de - barclays-gb - bnp-paribas-fr - bnp-be - bnp-pl - bnp-it - bnp-lu - berliner-volksbank-de - landesbank-bw-de - ing-de - erste-group-at - raiffeisen-bank-international-at - bank-austria-at - oberbank-at - postfinance-ch - raiffeisen-ch - zurcher-kantonalbank-ch - abn-amro-de - bnp-de - danske-de - handelsbanken-de - seb-de - deutsche-at - commerzbank-nl - danske-nl - deutsche-nl - hsbc-nl - handelsbanken-nl - volksbank-raiffeisenbank-de - ubs-ch - rbc-ca - deutsche-bank-br - sydbank-dk - bmo-ca - jyske-bank-dk - spar-nord-dk - arbejdernes-dk - ringkjobing-dk - sjaelland-fyn-dk - commerzbank-de - credit-agricole-fr - handelsbanken-se - rabobank-nl - de-volksbank-nl - triodos-nl - van-lanschot-nl - nibc-nl - bng-nl - nwb-nl - bunq-nl - bank-mendes-gans-nl - credit-mutuel-fr - credit-suisse-ch - cic-fr - arkea-fr - cibc-ca - unicredit-de - unicredit-cz - societe-generale-fr - lloyds-gb - standard-chartered-gb - op-financial-group-fi - ing-be - ing-pl - ing-ro - ing-hu - ing-cz - ing-bg - bank-of-america-us - us-bank-us - truist-us - pnc-us - mbank-pl - aib-ie - eurobank-gr - splitska-hr - andelskassen-faelleskassen-dk - danske-andelskassers-bank-dk - den-jyske-sparekasse-dk - faster-andelskasse-dk - frorup-andelskasse-dk - froslev-mollerup-sparekasse-dk - fynske-bank-dk - gronlandsbanken-gl - hvidbjerg-bank-dk - lollands-bank-dk - laegernes-bank-dk - merkur-andelskasse-dk - mons-bank-dk - nykredit-bank-dk - vestjyskbank-dk - foroya-banki-fo - banknordik-dk - betri-banki-fo - borbjerg-sparekasse-dk - dragsholm-sparekasse-dk - sydjysk-sparekasse-dk - klim-sparekasse-dk - lan-og-spar-bank-dk - middelfart-sparekasse-dk - nordoya-sparikassi-fo - rise-sparekasse-dk - ronde-sparekasse-dk - sparekassen-balling-dk - sparekassen-bredebro-dk - sparekassen-danmark-dk - sparekassen-kronjylland-dk - spks-for-nr-nebel-og-omegn-dk - sparekassen-thy-dk - suduroyar-sparikassi-fo - sonderha-horsted-sparekasse-dk - djurslands-bank-dk - kreditbanken-dk - nordfyns-bank-dk - skjern-bank-dk example: nordea-dk description: 'A registered bank profile key, `-` (e.g. danske-dk).' JournalPaymentRequest: type: object required: - companyId - bankKey - payment description: 'platformId is INFERRED from your API key — do not send it. Pass only `companyId`, `bankKey`, and the `payment` object.' properties: companyId: type: string description: The company this payment belongs to. (platformId is inferred from your API key — do not send it.) bankKey: $ref: '#/components/schemas/BankKey' payment: $ref: '#/components/schemas/PaymentInstruction' environment: type: string enum: - test - production description: Which connection delivers the payment. Defaults to production; pass "test" to dispatch over the bank's test channel. validate: type: boolean description: Also XSD-validate the generated XML. idempotencyKey: type: string description: Alternative to the Idempotency-Key header. JournalDocument: type: object properties: id: type: string journalNo: type: string example: OUT-000123 platformId: type: string companyId: type: string direction: type: string enum: - inbound - outbound example: outbound description: '"outbound" for a pain.001 you submit; "inbound" for a bank file you receive (camt.05x / pain.002).' type: type: string example: pain.001 bankKey: type: string environment: type: string enum: - test - production status: type: string example: converted summary: type: string transactionCount: type: integer totals: type: array items: type: object properties: currency: type: string amount: type: string xml: type: string description: The generated bank file (pain.001 XML). Returned by POST /journal/payments and single-document fetches; omitted from list summaries. deliveryMode: type: string enum: - host-to-host - file-return description: How an outbound payment is delivered. createdAt: type: string format: date-time BankConnection: type: object properties: id: type: string bankKey: $ref: '#/components/schemas/BankKey' bankName: type: string example: Nordea Danmark environment: type: string enum: - test - production channelType: type: string enum: - sftp - webservice - ebics active: type: boolean steps: type: object description: Setup wizard progress flags. properties: introEmailSent: type: boolean pgpGenerated: type: boolean bankPublicKeyAdded: type: boolean serverInfoAdded: type: boolean sftpUserAdded: type: boolean description: 'true once both the SFTP username and SSH key pair have been set. These are a single combined operation via POST /connections/{id}/sftp-user.' approversAssigned: type: boolean certsIssued: type: boolean bankCertAdded: type: boolean ebicsKeysGenerated: type: boolean ebicsBankKeysFetched: type: boolean wsConfigAdded: type: boolean sshPublicKey: type: string description: Public half of the SSH key (for SFTP). pgpPublicKey: type: string description: Public half of our PGP key (for SFTP). approverUserIds: type: array items: type: string createdAt: type: string format: date-time ConnectionWithReadiness: type: object properties: connection: $ref: '#/components/schemas/BankConnection' readiness: type: object description: 'Current go-live checklist: which prerequisites are met and which are missing.' properties: ready: type: boolean missing: type: array items: type: string score: type: integer minimum: 0 maximum: 100 ValidationIssue: type: object required: - code - level - message properties: code: type: string example: BANK_BIC_REQUIRED level: type: string enum: - generic - bank - payment-type example: bank description: Which validation tier raised the issue. A `BANK_*` code is level `bank`; a `GEN_*` code is `generic`; a payment-type rule is `payment-type`. severity: type: string enum: - error - warning description: Defaults to "error" when absent. "warning" issues don't block the payment. message: type: string example: Bank "nordea-dk" requires a BIC for the debtor. path: type: string example: 'payments[0].debtor.agent.bic' bank: type: string example: nordea-dk paymentType: type: string example: sepa ValidationOutcome: type: object required: - valid - issues properties: valid: type: boolean issues: type: array items: $ref: '#/components/schemas/ValidationIssue' ErrorResponse: type: object required: - error properties: error: type: object required: - code - message properties: code: type: string description: 'Stable, snake_case, non-localised error identifier. Branch on this.' example: validation_failed message: type: string description: Human-readable text. May change; do not branch on it. details: type: object description: 'Optional structured context. For validation errors, contains `issues`.' properties: issues: type: array items: $ref: '#/components/schemas/ValidationIssue' WebhookDelivery: type: object description: One webhook delivery attempt-record. properties: id: type: string example: whd_… webhookId: type: string example: wh_… platformId: type: string companyId: type: string eventType: type: string example: payment.sent payload: type: object description: The original event envelope (type/message/at/data). status: type: string enum: - delivered - failed - pending httpStatus: type: integer nullable: true lastError: type: string nullable: true attempts: type: integer deliveredAt: type: string format: date-time nullable: true createdAt: type: string format: date-time updatedAt: type: string format: date-time OrganisationId: type: object required: - id properties: id: type: string maxLength: 35 example: DK12345678 scheme: type: string maxLength: 35 example: CUST issuer: type: string maxLength: 35 Account: type: object properties: iban: type: string example: DK5000400440116243 currency: type: string minLength: 3 maxLength: 3 example: EUR other: type: object required: - id - scheme properties: id: type: string maxLength: 34 example: 5050-1055 scheme: type: string maxLength: 35 example: BGNR description: Exactly one of `iban` or `other` must be provided. Agent: type: object properties: bic: type: string example: DEUTDEFF clearing: type: object required: - system - memberId properties: system: type: string maxLength: 35 example: GBDSC memberId: type: string maxLength: 35 example: '401234' name: type: string maxLength: 140 Party: type: object required: - name - account properties: name: type: string maxLength: 140 example: Acme Corp ApS country: type: string minLength: 2 maxLength: 2 example: DK account: type: object properties: iban: type: string example: DK5000400440116243 currency: type: string minLength: 3 maxLength: 3 example: EUR other: type: object required: - id - scheme properties: id: type: string maxLength: 34 example: 5050-1055 scheme: type: string maxLength: 35 example: BGNR description: Exactly one of `iban` or `other` must be provided. agent: type: object properties: bic: type: string example: DEUTDEFF clearing: type: object required: - system - memberId properties: system: type: string maxLength: 35 example: GBDSC memberId: type: string maxLength: 35 example: '401234' name: type: string maxLength: 140 organisationId: type: object required: - id properties: id: type: string maxLength: 35 example: DK12345678 scheme: type: string maxLength: 35 example: CUST issuer: type: string maxLength: 35 CreditorReference: type: object required: - value properties: type: type: string enum: - scor - kid - ocr - fik value: type: string maxLength: 35 example: RF18539007547034 ReferredDocument: type: object required: - type properties: type: type: string enum: - invoice - credit-note number: type: string maxLength: 35 example: INV-2026-588 date: type: string format: date example: 2026-05-01 amount: type: string example: '1600.00' pattern: '^\d{1,15}\.\d{2}$' Remittance: type: object properties: unstructured: type: array maxItems: 10 items: type: string maxLength: 140 description: Free-text advice lines (Ustrd). Auto-derived from documents if omitted. documents: type: array items: $ref: '#/components/schemas/ReferredDocument' description: 'Invoices and credit notes: each emitted as a structured Strd block.' creditorReference: type: object required: - value properties: type: type: string enum: - scor - kid - ocr - fik value: type: string maxLength: 35 example: RF18539007547034 additionalInfo: type: array items: type: string maxLength: 35 Transaction: type: object required: - endToEndId - amount - currency - creditor properties: endToEndId: type: string maxLength: 35 example: INV-2026-588 instructionId: type: string maxLength: 35 amount: type: string example: '1500.00' pattern: '^\d{1,15}\.\d{2}$' currency: type: string minLength: 3 maxLength: 3 example: EUR creditor: $ref: '#/components/schemas/Party' remittance: type: object properties: unstructured: type: array maxItems: 10 items: type: string maxLength: 140 description: Free-text advice lines (Ustrd). Auto-derived from documents if omitted. documents: type: array items: $ref: '#/components/schemas/ReferredDocument' description: 'Invoices and credit notes: each emitted as a structured Strd block.' creditorReference: type: object required: - value properties: type: type: string enum: - scor - kid - ocr - fik value: type: string maxLength: 35 example: RF18539007547034 additionalInfo: type: array items: type: string maxLength: 35 Payment: type: object required: - paymentId - executionDate - debtor - transactions properties: paymentId: type: string maxLength: 35 example: PMT-0001 paymentType: type: string enum: - sepa - sepa-instant - domestic - international - split - fik - kid - bankgiro - plusgiro - bacs - chaps - faster-payment - ach - wire - ch-domestic - ch-qr - eft - ca-wire - br-domestic - boleto - pix example: sepa executionDate: type: string format: date example: 2026-06-04 debtor: $ref: '#/components/schemas/Party' transactions: type: array minItems: 1 items: $ref: '#/components/schemas/Transaction' PaymentInstruction: type: object required: - messageId - initiatingParty - payments properties: messageId: type: string maxLength: 35 example: MSG-20260601-0001 creationDateTime: type: string format: date-time example: 2026-06-01T10:30:00Z initiatingParty: type: object required: - name properties: name: type: string maxLength: 140 example: Acme Corp ApS organisationId: type: object required: - id properties: id: type: string maxLength: 35 example: DK12345678 scheme: type: string maxLength: 35 example: CUST issuer: type: string maxLength: 35 payments: type: array minItems: 1 items: $ref: '#/components/schemas/Payment' PaymentTypeEntry: type: object required: - code - label - description - requires - channel properties: code: type: string example: sepa label: type: string example: SEPA Credit Transfer description: type: string requires: type: array items: type: string channel: type: string example: SEPA / TARGET2 isoEncoding: type: object properties: serviceLevel: type: string example: SEPA localInstrument: type: string example: INST categoryPurpose: type: string example: TAXS BankProfile: type: object required: - key - bankName - countryCode - painVersion - paymentTypes properties: key: $ref: '#/components/schemas/BankKey' bankName: type: string example: Nordea Danmark countryCode: type: string example: DK painVersion: type: string enum: - pain.001.001.03 - pain.001.001.09 bankBic: type: string example: NDEADKKK paymentTypes: type: array items: type: object required: - code - label - description - requires - channel properties: code: type: string example: sepa label: type: string example: SEPA Credit Transfer description: type: string requires: type: array items: type: string channel: type: string example: SEPA / TARGET2 isoEncoding: type: object properties: serviceLevel: type: string example: SEPA localInstrument: type: string example: INST categoryPurpose: type: string example: TAXS x-demo-api-key: demo_sandbox_public_key_readonly