# 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}/renew-danske':
    post:
      tags:
        - Connections
      summary: Renew the Danske EDI Web Services certificates
      description: Issues fresh signing + encryption keypairs/CSRs to Danske's PKI via RenewCertificate — the request is enveloped-signed with the current signing certificate and XML-encrypted to the bank certificate (PKI WS spec §7) — and commits the new keys + certificates atomically on success. Run before the 2-year certificate expiry (the cert-expiring alerts fire at 30/14/7 days). A failed renewal leaves the current credentials untouched. 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
              properties:
                platformId:
                  type: string
                companyId:
                  type: string
      responses:
        '200':
          description: Renewed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionWithReadiness'
        '400':
          description: Renewal failed (current credentials left untouched)
          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}/renew-nordea':
    post:
      tags:
        - Connections
      summary: Renew the Nordea Corporate Access signing certificate
      description: 'Issues a fresh keypair + CSR (same subject as the current certificate) to Nordea''s Certificate Service — the CertApplicationRequest is signed with the current certificate in place of the HMAC (Certificate Management §3.1.2), so no new SMS activation code is needed while the current certificate is valid — and commits the new key + certificate atomically on success. Run before the 2-year expiry (alerts at 30/14/7 days). A failed renewal leaves the current credentials untouched. 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
              properties:
                platformId:
                  type: string
                companyId:
                  type: string
      responses:
        '200':
          description: Renewed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionWithReadiness'
        '400':
          description: Renewal failed (current credentials left untouched)
          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}/renew-bankconnect':
    post:
      tags:
        - Connections
      summary: Renew the Bank Connect customer certificate
      description: 'Issues a fresh keypair + CSR (CN=functionId) to the Bank Connect data central via renewCustomerCertificate, signed with the current certificate, and commits the new key + certificate atomically on success. Run before the 3-year certificate expiry (the cert-expiring alerts fire at 30/14/7 days); the bank revokes the previous certificate 48 hours after a successful renewal. 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
              properties:
                platformId:
                  type: string
                companyId:
                  type: string
      responses:
        '200':
          description: Renewed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectionWithReadiness'
        '400':
          description: Renewal failed (current credentials left untouched)
          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: <?xml version="1.0"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09">…</Document>
                deliveryMode: host-to-host
                createdAt: 2026-06-20T10:31:00Z
                delivery:
                  status: queued
        '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 <Stmt> (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, `<bank>-<country>` (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