{
  "openapi": "3.1.0",
  "info": {
    "title": "BankConnector API",
    "version": "0.1.0",
    "description": "## Quick start\n\nBankConnector converts canonical JSON payments to bank-specific ISO 20022 files and\nnormalises incoming bank statements into one format. Try the whole flow in the hosted\nsandbox at **https://sandbox.bankconnector.com** — no signup, and no real money moves.\n\n### Step 1: Get a sandbox API key\n\nOne unauthenticated call provisions a complete, isolated sandbox — your own platform, a\ncompany, an admin user, an API key, and an active demo bank connection:\n\n```http\nPOST /sandbox/provision\n{ \"bankKey\": \"danske-dk\" }\n→ 201 { \"apiKey\": \"key_…\", \"companyId\": \"comp_…\", \"baseUrl\": \"https://sandbox.bankconnector.com\", … }\n```\n\nThe raw key is shown ONCE — store it, and send it as the `X-API-Key` header on every\nrequest. **Your key identifies your platform**, so on every data-plane call you pass only\n`companyId` — you never send `platformId`.\n\n### Step 2: Submit a payment\n\n```http\nPOST /journal/payments\nX-API-Key: key_…\nIdempotency-Key: 8f2c…   (unique per payment; mandatory for host-to-host)\n{\n  \"companyId\": \"comp_…\", \"bankKey\": \"danske-dk\",\n  \"payment\": {\n    \"messageId\": \"MSG-20260619-0001\",\n    \"initiatingParty\": { \"name\": \"Acme Corp\", \"organisationId\": { \"id\": \"ACME-1\", \"scheme\": \"CUST\" } },\n    \"payments\": [{\n      \"paymentId\": \"PMT-1\", \"paymentType\": \"sepa\", \"executionDate\": \"2026-06-20\",\n      \"debtor\": { \"name\": \"Acme Corp\", \"country\": \"DK\", \"account\": { \"iban\": \"DK5000400440116243\", \"currency\": \"EUR\" }, \"agent\": { \"bic\": \"NDEADKKK\" },\n        \"organisationId\": { \"id\": \"CAW-12345\", \"scheme\": \"BANK\" } },\n      \"transactions\": [{ \"endToEndId\": \"E2E-1\", \"amount\": \"1000.00\", \"currency\": \"EUR\",\n        \"creditor\": { \"name\": \"Supplier GmbH\", \"account\": { \"iban\": \"DE89370400440532013000\" }, \"agent\": { \"bic\": \"DEUTDEFF\" } } }]\n    }]\n  }\n}\n```\n\nThe `payment` object IS the canonical instruction (amounts are exact decimal **strings**); on\nthis route it is WRAPPED under `payment` alongside `companyId` + `bankKey`. The flat 201 response\nincludes `id`, `journalNo`, `status`, the generated `xml`, and `deliveryMode`. A retry with the\nsame `Idempotency-Key` + body replays that 201 with an `Idempotency-Replayed: true` header. NOTE the\nenvelope difference: `POST /validate/{bank}` and `POST /convert/{bank}` take that SAME canonical object\nas the raw top-level body (no `payment` wrapper, no companyId). See CANONICAL_INPUT.md for the full shape.\n\n### Step 3: Read results\n\n```http\nGET  /journal/reconciliation              → matched payments + bank confirmations (cleared / rejected / pending)\nGET  /journal/list?unretrievedOnly=true   → poll for documents you have not collected yet\nPOST /inbound/payment-status              → import a pain.002 status report\nPOST /inbound/statement                   → import a camt.053 / 052 / 054 statement\n```\n\n---\n\n> **Sandbox vs production:** the sandbox at https://sandbox.bankconnector.com is fully isolated —\n> the key, data and demo bank are sandbox-only and no real money can move. Going live (real bank\n> 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."
          }
        }
      }
    },
    "/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 \\\n  -H 'X-API-Key: YOUR_KEY'"
          },
          {
            "lang": "JavaScript",
            "label": "Fetch",
            "source": "const res = await fetch(\"https://your-host/profiles\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"YOUR_KEY\",\n  },\n});\nconst data = await res.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresp = requests.request(\n  \"GET\", \"https://your-host/profiles\",\n  headers={\"X-API-Key\": \"YOUR_KEY\"},\n)\nresp.raise_for_status()\ndata = 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
      }
    },
    "/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)."
                    },
                    "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
      }
    },
    "/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
      }
    },
    "/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Liveness check",
        "description": "Always 200 while the process is up. `status` is strictly serviceability (QA-11) — a serving box says \"ok\" even when non-fatal startup config warnings exist; those ride in the advisory `warnings` field as stable machine codes. Use /ready for the orchestrator serviceability probe.",
        "responses": {
          "200": {
            "description": "Server is running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Advisory startup-warning CODES (never messages/secrets); absent when none."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Log in (PUBLIC): sets the httpOnly session cookie",
        "description": "PUBLIC. Exchanges subdomain + email + password for a signed session, delivered ONLY as the httpOnly `bc_session` cookie (the raw token is never in the response body; programmatic callers should use API keys). 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 (session cookie set) OR a 2FA step-up challenge",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "csrfToken": {
                      "type": "string",
                      "description": "Echo in X-CSRF-Token on unsafe requests."
                    },
                    "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 \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"subdomain\":\"demo\",\"email\":\"you@acme.com\",\"password\":\"…\"}'"
          },
          {
            "lang": "JavaScript",
            "label": "Fetch",
            "source": "const res = await fetch(\"https://your-host/auth/login\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\"subdomain\":\"demo\",\"email\":\"you@acme.com\",\"password\":\"…\"}),\n});\nconst data = await res.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresp = requests.request(\n  \"POST\", \"https://your-host/auth/login\",\n  json={\"subdomain\":\"demo\",\"email\":\"you@acme.com\",\"password\":\"…\"},\n)\nresp.raise_for_status()\ndata = 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 (delivered ONLY as the httpOnly `bc_session` cookie — the raw token is never in the response body).",
        "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 (session cookie set)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "csrfToken": {
                      "type": "string",
                      "description": "Echo in X-CSRF-Token on unsafe requests."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/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, plus any operator-provisioned environment keys (source:\"env\") — identity only, no dates, and no API revocation (remove them from BANKCONNECTOR_API_KEYS and restart). 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"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "db",
                              "env"
                            ],
                            "description": "QA-2: \"db\" = self-service key (revocable via DELETE); \"env\" = operator-provisioned via BANKCONNECTOR_API_KEYS (read-only identity; revoke by editing the env map and restarting)."
                          },
                          "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"
                }
              }
            }
          }
        }
      }
    },
    "/client-errors": {
      "post": {
        "tags": [
          "Platform"
        ],
        "summary": "Report a browser-side crash",
        "description": "REACT-6(b) / ADR-0047. The React island error boundary posts here when a render throws, so a crash on a payments or approvals screen is not invisible to the operator. Tenant-scoped by the SESSION — the body's tenant is never trusted.\n\nALWAYS answers 202, even when nothing is stored: a report is best-effort telemetry, the browser cannot act on the difference, and an error reporter that returns errors is a second failure on an already-broken screen. `recorded` says which happened.\n\nOFF BY DEFAULT: unless BANKCONNECTOR_CLIENT_ERROR_CAPTURE=1 the payload is not even parsed. Per ADR-0047 it stays off in production until the DPA and privacy notice cover the processing activity. When on, payloads are encrypted at rest, purged after 30 days, and readable only by an audited operator via GET /ops/client-errors. Credentials and national identity numbers are stripped before storage; other detail is retained deliberately, so a report identifies the record that broke.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "island": {
                    "type": "string",
                    "description": "Island label from the mounter, e.g. \"approvals-queue\"."
                  },
                  "route": {
                    "type": "string",
                    "description": "Route in view. Query string is dropped by client AND server."
                  },
                  "message": {
                    "type": "string"
                  },
                  "stack": {
                    "type": "string"
                  },
                  "componentStack": {
                    "type": "string",
                    "description": "React component stack."
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. `recorded` distinguishes stored from dropped (capture off, no session, empty).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "recorded": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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"
          }
        }
      }
    },
    "/platforms/{platformId}/companies/{companyId}": {
      "patch": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Update a company (rename / sandboxOnly flag) — workspace admin",
        "description": "Partial update: `name` renames the company; `sandboxOnly` flips the pilot safety flag (a sandbox-only company can never activate a production connection or deliver a production payment). Platform-admin only.",
        "parameters": [
          {
            "name": "platformId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "companyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "sandboxOnly": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Updated company",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Nothing to update",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Not a workspace admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/platforms/{platformId}/approval-settings": {
      "get": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Read the platform's approval-governance settings",
        "parameters": [
          {
            "name": "platformId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Approval settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Patch the platform's approval-governance settings",
        "description": "Sanitised partial patch of `approvalMode` / `forbidSelfApproval` / `externalApprovalAuthority`. Unknown fields are stripped; an empty effective patch is a 400.",
        "parameters": [
          {
            "name": "platformId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Updated settings",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Nothing to update",
            "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.",
        "parameters": [
          {
            "name": "platformId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "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:\n\n- **With `password`:** creates the user immediately (active). Use for programmatic provisioning.\n- **Without `password`:** sends an invite email. The user receives a link to `/auth/set-password` where they set their own password.\n\nThe 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"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/settings": {
      "patch": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Update a user's settings blob (partial)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "platformId": {
                    "type": "string"
                  },
                  "companyId": {
                    "type": "string"
                  },
                  "settings": {
                    "type": "object"
                  }
                },
                "required": [
                  "platformId",
                  "companyId",
                  "settings"
                ]
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Updated user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/roles": {
      "put": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Replace a user's role set",
        "description": "PUT = full replacement of the role array (there is no partial role patch).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "platformId": {
                    "type": "string"
                  },
                  "companyId": {
                    "type": "string"
                  },
                  "roles": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "platformId",
                  "companyId",
                  "roles"
                ]
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Updated user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/deactivate": {
      "post": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Deactivate a user (command — keeps data, revokes access)",
        "description": "A state command, deliberately POST (ADR-0011 keeps commands as POST): keeps PII for audit, revokes live sessions, and reports approval-quorum impact.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "User deactivated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/reinvite": {
      "post": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Re-send a user's invite (command)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Invite re-sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/users/{id}/reset-password": {
      "post": {
        "tags": [
          "Tenancy"
        ],
        "summary": "Trigger a password reset for a user (command)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "security": [
          {
            "SessionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Reset issued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/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
      }
    },
    "/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}/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"
                    },
                    "maxItems": 200,
                    "description": "User IDs of company members with the Approver role. Must already exist in the company. Max 200 (enforced server-side): each id costs a user lookup, so the list is bounded rather than unbounded input behind admin auth."
                  }
                }
              }
            }
          }
        },
        "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}/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"
          }
        }
      }
    },
    "/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}/guide-feedback": {
      "post": {
        "tags": [
          "Connections"
        ],
        "summary": "Report that a bank's setup guide is wrong",
        "description": "Sends the customer's note about a bank's setup guide to BankConnector. The bank, its data central, the company and the platform are attached server-side from the route + session — the body carries only the message, so a caller cannot file feedback as another company.",
        "security": [
          {
            "SessionToken": []
          }
        ],
        "parameters": [
          {
            "name": "bankKey",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/BankKey"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "platformId",
                  "companyId",
                  "message"
                ],
                "properties": {
                  "platformId": {
                    "type": "string"
                  },
                  "companyId": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 4000,
                    "description": "What was confusing, or what the bank did differently."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sent": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Empty or over-long message"
          },
          "401": {
            "description": "Not signed in"
          },
          "404": {
            "description": "Unknown bank"
          },
          "502": {
            "description": "The feedback could not be mailed — retry"
          }
        }
      }
    },
    "/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"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Connections"
        ],
        "summary": "Update per-company settings for a specific bank (partial patch)",
        "description": "Payment-affecting fields (agreementId, debtorAgreementId, chargeBearer, executionDateOffsetDays, approvalPolicyId, painVersion, batchBooking, authorisationCode, authorisationProprietary) require Admin. The `selected` pin is open to any member. `debtorAgreementId` is the bank's second agreement identifier (e.g. the Nordea CAW/CCM agreement number) — auto-filled into payments[i].debtor.organisationId (scheme BANK) when the payment omits it; `agreementId` fills initiatingParty.organisationId (scheme CUST). `batchBooking` and the authorisation pair are per-AGREEMENT pain.001 facts (PmtInf/BtchBookg and GrpHdr/Authstn): omit or send null to keep the bank profile's default. `authorisationCode` (ISO Authorisation1Code) and `authorisationProprietary` are mutually exclusive — sending both is a 400.",
        "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"
                  },
                  "debtorAgreementId": {
                    "type": "string"
                  },
                  "chargeBearer": {
                    "type": "string",
                    "enum": [
                      "SLEV",
                      "SHAR",
                      "CRED",
                      "DEBT"
                    ]
                  },
                  "executionDateOffsetDays": {
                    "type": "integer"
                  },
                  "approvalPolicyId": {
                    "type": "string"
                  },
                  "painVersion": {
                    "type": "string",
                    "enum": [
                      "pain.001.001.03",
                      "pain.001.001.09"
                    ],
                    "nullable": true
                  },
                  "batchBooking": {
                    "type": "boolean",
                    "nullable": true
                  },
                  "authorisationCode": {
                    "type": "string",
                    "enum": [
                      "AUTH",
                      "FDET",
                      "FSUM",
                      "ILEV"
                    ],
                    "nullable": true
                  },
                  "authorisationProprietary": {
                    "type": "string",
                    "maxLength": 128,
                    "nullable": true
                  },
                  "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"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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-op": {
      "post": {
        "tags": [
          "Connections"
        ],
        "summary": "Renew the OP (Finland) Web Services signing certificate",
        "description": "Issues a fresh keypair + CSR (same subject as the current certificate: CN=WS username, C=FI) to OP's Certificate Service — the CertApplicationRequest is signed with the CURRENT key (guide §4.1.4), so NO transfer key is needed while the current certificate is valid — and commits the new key + certificate atomically on success, refusing a certificate whose public key does not match the CSR (OP silently returns copies of prior certificates). Run before the ~2-year expiry (alerts at 30/14/7 days; the daily auto-renew sweep also acts inside the 30-day horizon): a LAPSED OP certificate cannot be renewed at all — the customer starts over with a NEW transfer key from OP. 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"
                }
              }
            }
          },
          "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}/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": [
                      "technical",
                      "per-user"
                    ],
                    "description": "One shared technical subscriber, or one EBICS subscriber per approver."
                  },
                  "veuOnly": {
                    "type": "boolean",
                    "description": "Per-user only: this agreement routes every order through VEU (refuses multi-signature uploads) — delivery uploads one ES and adds the rest via HVE."
                  },
                  "uploaderUserId": {
                    "type": "string",
                    "description": "Per-user only: DESIGNATED uploader (app user id) — which subscriber transports payment uploads and acts for VEU orders. Must be a user in this company; the subscriber must be ACTIVE at delivery time (delivery fails with a clear error otherwise). Empty string clears the designation (default: first active subscriber). May be a non-approver transport-class subscriber for veuOnly agreements."
                  }
                }
              }
            }
          }
        },
        "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}/ebics-fetch-htd": {
      "post": {
        "tags": [
          "Connections"
        ],
        "summary": "Fetch the bank's record of the EBICS agreement (HTD/HKD) and store the discovery snapshot",
        "description": "Runs EBICS HTD (technical subscriber) or HKD (per-user mode) against the bank and persists the result on the connection: accounts on the agreement, permitted order types/BTFs with bank-side signature quorums, and each subscriber's bank-side status + signature class. Requires the bank keys (run ebics-fetch-hpb first). Pre-activation returns notActivatedYet instead of failing, so it can be polled as an activation check. In per-user mode, approvers the bank reports as ready are advanced to active automatically.",
        "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": "Discovery result (fetched flag, message, and the parsed agreement data when available)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionWithReadiness"
                }
              }
            }
          },
          "401": {
            "description": "Sign in required"
          },
          "403": {
            "description": "Admin required"
          }
        }
      }
    },
    "/connections/{id}/ebics-renew": {
      "post": {
        "tags": [
          "Connections"
        ],
        "summary": "Rotate the EBICS subscriber keys in-band (HCS)",
        "description": "Sends an HCS order: the NEW three keys (signature/authentication/encryption), authorised by the CURRENT signature key — no new INI letter or bank re-activation. Two-phase and key-loss safe: the new keyset is staged before sending and promoted only on a confirmed accept; an ambiguous (post-commit) outcome keeps both keysets and the next call resolves automatically by probing which keyset the bank accepts. Per-user mode rotates one approver at a time (pass userId).",
        "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"
                  },
                  "userId": {
                    "type": "string",
                    "description": "Per-user mode: the approver whose subscriber to rotate."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renewal outcome (renewed | ambiguous | resolved-kept-old | resolved-promoted | unresolved)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionWithReadiness"
                }
              }
            }
          },
          "401": {
            "description": "Sign in required"
          },
          "403": {
            "description": "Admin required"
          }
        }
      }
    },
    "/connections/{id}/ebics-veu-pending": {
      "post": {
        "tags": [
          "Connections"
        ],
        "summary": "List the orders pending signature at the bank (EBICS VEU / HVU)",
        "description": "Read-only: queries the bank's VEU queue via HVU using the first active per-user subscriber as transport. Returns order type + order number, signature progress (done/required), and whether each order is still signable. Per-user connections only. Normally empty - payments arrive fully signed; a veuOnly agreement or an interrupted co-sign parks orders here.",
        "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": "Pending orders (veuPending array on the connection response)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionWithReadiness"
                }
              }
            }
          },
          "401": {
            "description": "Sign in required"
          },
          "403": {
            "description": "Admin required"
          }
        }
      }
    },
    "/connections/{id}/ebics-suspend": {
      "post": {
        "tags": [
          "Connections"
        ],
        "summary": "Emergency-suspend the subscriber's EBICS access (SPR)",
        "description": "Sends an authenticated SPR order telling the bank to immediately suspend this subscriber's access — the compromised-key kill switch. One-directional: restoring access requires re-onboarding (new INI/HIA + letter). Also deactivates the connection locally so polling/delivery stops. Per-user mode suspends the named (or first active) approver.",
        "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"
                  },
                  "userId": {
                    "type": "string",
                    "description": "Per-user mode: the approver to suspend."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suspend result (bank return code + message)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionWithReadiness"
                }
              }
            }
          },
          "401": {
            "description": "Sign in required"
          },
          "403": {
            "description": "Admin required"
          }
        }
      }
    },
    "/connections/{id}/ebics-veu-complete": {
      "post": {
        "tags": [
          "Connections"
        ],
        "summary": "Complete an interrupted VEU co-sign for a pending order (HVE)",
        "description": "Re-drives the remaining approvers' HVE signatures for an order the bank is holding. Identify the payment by documentId, or by the pending order's dataDigest (matched server-side to the sent pain.001). Digest re-checked via HVD before signing — never co-signs a mismatched order.",
        "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"
                  },
                  "documentId": {
                    "type": "string",
                    "description": "The sent payment (its frozen pain.001 is signed)."
                  },
                  "orderDigest": {
                    "type": "string",
                    "description": "The pending order's dataDigest, to resolve the payment."
                  },
                  "orderId": {
                    "type": "string",
                    "description": "The bank order number to select the pending order."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Co-sign results (per approver) + executionReady",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionWithReadiness"
                }
              }
            }
          },
          "401": {
            "description": "Sign in required"
          },
          "403": {
            "description": "Admin required"
          }
        }
      }
    },
    "/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"
          }
        }
      }
    },
    "/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 \\\n  -H 'X-API-Key: YOUR_KEY' \\\n  -H 'Content-Type: application/json' \\\n  -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\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"YOUR_KEY\",\n    \"Content-Type\": \"application/json\",\n  },\n  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\"}}}]}]}),\n});\nconst data = await res.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresp = requests.request(\n  \"POST\", \"https://your-host/validate/nordea-dk\",\n  headers={\"X-API-Key\": \"YOUR_KEY\"},\n  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\"}}}]}]},\n)\nresp.raise_for_status()\ndata = 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 \\\n  -H 'X-API-Key: YOUR_KEY' \\\n  -H 'Content-Type: application/json' \\\n  -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\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"YOUR_KEY\",\n    \"Content-Type\": \"application/json\",\n  },\n  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\"}}}]}]}),\n});\nconst data = await res.text();  // pain.001 XML"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresp = requests.request(\n  \"POST\", \"https://your-host/convert/nordea-dk\",\n  headers={\"X-API-Key\": \"YOUR_KEY\"},\n  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\"}}}]}]},\n)\nresp.raise_for_status()\ndata = 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. A permanently-failed (dead) job returns 200 with a `failure` object — NOT the reserved `error` envelope key, which always means the REQUEST itself failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "running",
                        "succeeded",
                        "dead"
                      ]
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "failure": {
                      "type": "object",
                      "description": "Present only when status is `dead` (the job failed permanently).",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "conversion_failed"
                        },
                        "message": {
                          "type": "string"
                        },
                        "details": {
                          "type": "object",
                          "properties": {
                            "lastError": {
                              "type": "string",
                              "description": "Bounded first line of the worker error."
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "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 \\\n  -H 'X-API-Key: YOUR_KEY' \\\n  -H 'Content-Type: application/json' \\\n  -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\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"YOUR_KEY\",\n    \"Content-Type\": \"application/json\",\n  },\n  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\"}}}]}]}}),\n});\nconst data = await res.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresp = requests.request(\n  \"POST\", \"https://your-host/journal/payments\",\n  headers={\"X-API-Key\": \"YOUR_KEY\"},\n  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\"}}}]}]}},\n)\nresp.raise_for_status()\ndata = 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": "retrieved",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Filter by fetch state (CM-19): `false` = not yet fetched (awaiting fetch), `true` = already fetched. This is the document's `retrieved` flag, not a `status` value."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free-text search (case-insensitive, matched literally — `%` and `_` are not wildcards). Matches the document summary, messageId, journalNo, bankKey, status, type, and the rendered `<amount> <currency>` totals text."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "rejected"
            },
            "description": "Exact document status (e.g. `pending-approval`, `sent`, `executed`, `rejected`). Exact match, not a search."
          },
          {
            "name": "amountMin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "100.00"
            },
            "description": "Lower bound on any of the document's totals, inclusive. Decimal string, compared in EXACT minor units (never as a float). ⚠️ Applied to the returned PAGE by the service, not pushed into SQL — so with a page size N you get the matching subset OF THAT PAGE; keep paging via `cursor` for the rest. A non-numeric value is IGNORED (no 400)."
          },
          {
            "name": "amountMax",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "5000.00"
            },
            "description": "Upper bound on any of the document's totals, inclusive. Same semantics as `amountMin`."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-07-01"
            },
            "description": "Only documents created on or after this day (`YYYY-MM-DD`, inclusive, server timezone)."
          },
          {
            "name": "dateTo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-07-31"
            },
            "description": "Only documents created on or before this day (`YYYY-MM-DD`, **inclusive** — the whole day is covered)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            },
            "description": "Page size (enables keyset pagination via nextCursor — on filtered searches too)."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque cursor from a prior page's `nextCursor`."
          }
        ],
        "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"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                }
              }
            }
          }
        }
      }
    },
    "/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"
                }
              }
            }
          }
        }
      }
    },
    "/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. Keyset-paginated over the outbound payments (default 500, max 1000): a non-null `nextCursor` means MORE payments exist — pass it back as `?cursor=` to walk the rest.",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "SessionToken": []
          }
        ],
        "parameters": [
          {
            "name": "companyId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 500
            },
            "description": "Page size over the outbound payments."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque keyset cursor from a previous response's `nextCursor`."
          }
        ],
        "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"
                          },
                          "bankKey": {
                            "type": "string"
                          },
                          "summary": {
                            "type": "string"
                          },
                          "totals": {
                            "type": "array",
                            "description": "Per-currency totals for the payment. `amount` is a decimal STRING.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "amount": {
                                  "type": "string"
                                },
                                "currency": {
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "status": {
                            "type": "string",
                            "description": "The payment's journal status."
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "cleared",
                              "rejected",
                              "cancelled",
                              "pending"
                            ]
                          },
                          "evidence": {
                            "type": "object"
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Non-null ⇒ the result was truncated at `limit` — more payments exist."
                    }
                  }
                }
              }
            }
          },
          "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",
        "description": "Keyset-paginated newest-first (default page 5000 — the historical scan bound — max 5000): a non-null `nextCursor` means the result was TRUNCATED and more events exist — pass it back as `?cursor=` to walk the rest.",
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "SessionToken": []
          }
        ],
        "parameters": [
          {
            "name": "companyId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5000,
              "default": 5000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque keyset cursor from a previous response's `nextCursor`."
          }
        ],
        "responses": {
          "200": {
            "description": "Events newest-first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Non-null ⇒ truncated at `limit` — more events exist."
                    }
                  }
                }
              }
            }
          },
          "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"
                }
              }
            }
          }
        }
      }
    },
    "/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": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "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/bank-rosters": {
      "get": {
        "tags": [
          "Approvals"
        ],
        "summary": "Who may approve payments on each covered bank",
        "description": "One row per COVERED BANK (`danske-dk`) — never per connection: one connection may serve several country banks. `bound: false` means the bank inherits the company default (which is also what a bank added tomorrow gets); `sharedWith` names the other banks in the query that resolve to the same policy.",
        "security": [
          {
            "SessionToken": []
          }
        ],
        "parameters": [
          {
            "name": "companyId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bankKeys",
            "in": "query",
            "required": true,
            "description": "Comma-separated covered bank keys (repeatable).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One roster row per covered bank",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "bankKey": {
                            "type": "string"
                          },
                          "policyId": {
                            "type": "string"
                          },
                          "policyName": {
                            "type": "string"
                          },
                          "builtIn": {
                            "type": "boolean"
                          },
                          "bound": {
                            "type": "boolean"
                          },
                          "approverUserIds": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "requiredApprovals": {
                            "type": "integer"
                          },
                          "sharedWith": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No bank keys named",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Sign in required"
          }
        }
      },
      "post": {
        "tags": [
          "Approvals"
        ],
        "summary": "Set who may approve payments on covered banks (Admin)",
        "description": "ALL-OR-NOTHING across the set: the ratchet judges each bank, but if ANY bank's roster shrinks nothing moves until a second Admin co-signs the one request naming them all. 200 = in force now; 202 = accepted and NOT in force, `change` is the request awaiting that signature. No policy is ever edited in place — each bank is moved to a policy carrying the wanted roster on its current rule, so a policy shared with other banks is left untouched.",
        "security": [
          {
            "SessionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "platformId",
                  "companyId",
                  "targets"
                ],
                "properties": {
                  "platformId": {
                    "type": "string"
                  },
                  "companyId": {
                    "type": "string"
                  },
                  "targets": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "bankKey",
                        "approverUserIds"
                      ],
                      "properties": {
                        "bankKey": {
                          "type": "string"
                        },
                        "approverUserIds": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "In force now",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "banks": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pending": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted — NOT in force; awaiting a second Admin's co-sign",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "banks": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "change": {
                      "type": "object"
                    },
                    "pending": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed roster set"
          },
          "401": {
            "description": "Sign in required"
          },
          "403": {
            "description": "Admin required"
          },
          "409": {
            "description": "Refused by the ratchet (e.g. only one Admin) / a change is already awaiting co-sign",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/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"
          }
        }
      }
    },
    "/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`."
          },
          {
            "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"
          }
        }
      }
    }
  },
  "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-ch",
          "hsbc-cn",
          "hsbc-dk",
          "hsbc-eg",
          "hsbc-hk",
          "hsbc-id",
          "hsbc-in",
          "hsbc-jp",
          "hsbc-kr",
          "hsbc-mx",
          "hsbc-my",
          "hsbc-nz",
          "hsbc-ph",
          "hsbc-pl",
          "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-ie",
          "bnp-paribas-nl",
          "bnp-paribas-sg",
          "bnp-paribas-us",
          "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",
          "eika-alliance-no",
          "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",
          "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",
          "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",
          "postfinance-test-ch",
          "cic-ch",
          "berner-kantonalbank-ch",
          "st-galler-kantonalbank-ch",
          "basellandschaftliche-kantonalbank-ch",
          "thurgauer-kantonalbank-ch",
          "schwyzer-kantonalbank-ch",
          "aargauische-kantonalbank-ch",
          "banque-cantonale-de-fribourg-ch",
          "banque-cantonale-de-geneve-ch",
          "banca-dello-stato-ticino-ch",
          "valiant-ch",
          "bank-cler-ch",
          "bcv-ch",
          "graubuendner-kantonalbank-ch",
          "hypothekarbank-lenzburg-ch",
          "zuger-kantonalbank-ch",
          "urner-kantonalbank-ch",
          "luzerner-kantonalbank-ch",
          "basler-kantonalbank-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",
          "foroya-banki-fo",
          "betri-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",
          "sparekassen-thy-dk",
          "suduroyar-sparikassi-fo",
          "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",
              "converted-only"
            ],
            "description": "How an outbound payment is delivered. \"converted-only\" (renamed from the retired \"file-return\" in QA-14) means the document was converted/recorded but has no live host-to-host connection to deliver over — nothing is sent."
          },
          "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",
            "description": "Identification scheme. Common codes: CUST (bank-assigned customer/agreement id), BANK, TXID (tax number — CNPJ/CPF/NIP; formatting punctuation is stripped on the wire)."
          },
          "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
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "example": "DE",
            "description": "Agent country → FinInstnId/PstlAdr/Ctry. Several priority rails require it; where a bank profile enables BIC-derivation it is auto-filled from BIC positions 5-6 on non-SEPA rails when omitted."
          }
        }
      },
      "PostalAddress": {
        "type": "object",
        "description": "Structured postal address (ISO 20022 PstlAdr, v1.1). Emitted where the bank/rail requires it.",
        "properties": {
          "streetName": {
            "type": "string",
            "maxLength": 70
          },
          "buildingNumber": {
            "type": "string",
            "maxLength": 16
          },
          "postCode": {
            "type": "string",
            "maxLength": 16
          },
          "townName": {
            "type": "string",
            "maxLength": 35
          },
          "countrySubDivision": {
            "type": "string",
            "maxLength": 35
          },
          "addressLines": {
            "type": "array",
            "maxItems": 7,
            "items": {
              "type": "string",
              "maxLength": 70
            }
          }
        }
      },
      "RegulatoryReporting": {
        "type": "object",
        "description": "Regulatory reporting (ISO 20022 RgltryRptg, v1.1). Required by some corridors — e.g. cross-border CNY purpose codes (/BUSINESS/…), the Philippines /ORDERRES/PH// purpose-of-payment, Polish central-bank codes 51/53/71.",
        "properties": {
          "debitCreditReportingIndicator": {
            "type": "string",
            "enum": [
              "CRED",
              "DEBT",
              "BOTH"
            ]
          },
          "authority": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 140
              },
              "country": {
                "type": "string",
                "minLength": 2,
                "maxLength": 2
              }
            }
          },
          "details": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "maxLength": 35
                },
                "country": {
                  "type": "string",
                  "minLength": 2,
                  "maxLength": 2
                },
                "code": {
                  "type": "string",
                  "maxLength": 10
                },
                "amount": {
                  "type": "string",
                  "pattern": "^\\d{1,15}\\.\\d{2}$"
                },
                "info": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "maxLength": 35
                  }
                }
              }
            }
          }
        }
      },
      "SplitPayment": {
        "type": "object",
        "required": [
          "vatAmount",
          "taxId",
          "invoiceRef"
        ],
        "description": "Polish split payment / MPP (v1.1). MANDATORY on the `split` payment type: the VAT amount, creditor tax id (NIP) and invoice reference are packed into the regulated remittance layout (/VAT/…/IDC/…/INV/…). A split payment without a VAT amount is rejected by the bank — HSBC PL charges 50 EUR per occurrence.",
        "properties": {
          "vatAmount": {
            "type": "string",
            "pattern": "^\\d{1,15}\\.\\d{2}$",
            "example": "2300.00",
            "description": "VAT portion — must be ≤ the transaction amount."
          },
          "taxId": {
            "type": "string",
            "maxLength": 14,
            "example": "1234567890",
            "description": "Creditor tax id (Polish NIP)."
          },
          "invoiceRef": {
            "type": "string",
            "maxLength": 35,
            "example": "FV/2026/07/0042",
            "description": "Invoice reference; \"PRZEKAZANIE WLASNE\" for own-VAT-account transfers."
          }
        }
      },
      "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"
          },
          "postalAddress": {
            "$ref": "#/components/schemas/PostalAddress"
          },
          "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
              },
              "country": {
                "type": "string",
                "minLength": 2,
                "maxLength": 2,
                "example": "DE",
                "description": "Agent country → FinInstnId/PstlAdr/Ctry. Several priority rails require it; where a bank profile enables BIC-derivation it is auto-filled from BIC positions 5-6 on non-SEPA rails when omitted."
              }
            }
          },
          "organisationId": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string",
                "maxLength": 35,
                "example": "DK12345678"
              },
              "scheme": {
                "type": "string",
                "maxLength": 35,
                "example": "CUST",
                "description": "Identification scheme. Common codes: CUST (bank-assigned customer/agreement id), BANK, TXID (tax number — CNPJ/CPF/NIP; formatting punctuation is stripped on the wire)."
              },
              "issuer": {
                "type": "string",
                "maxLength": 35
              }
            }
          }
        }
      },
      "UltimateParty": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 140,
            "example": "Acme Subsidiary GmbH"
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "example": "DE"
          },
          "postalAddress": {
            "$ref": "#/components/schemas/PostalAddress"
          },
          "organisationId": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "string",
                "maxLength": 35,
                "example": "DK12345678"
              },
              "scheme": {
                "type": "string",
                "maxLength": 35,
                "example": "CUST",
                "description": "Identification scheme. Common codes: CUST (bank-assigned customer/agreement id), BANK, TXID (tax number — CNPJ/CPF/NIP; formatting punctuation is stripped on the wire)."
              },
              "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"
          },
          "ultimateDebtor": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UltimateParty"
              }
            ],
            "description": "ADVANCED — omit unless you know you need it. The party the payment is really made ON BEHALF OF, when that is NOT the account holder being debited (e.g. a payments factory paying a supplier for a subsidiary; a client-money account paying out for an underlying client). Overrides the payment-level `ultimateDebtor` for THIS transaction only. If it is the same party as the debtor, leave it out — some bank schemes reject or flag that."
          },
          "ultimateCreditor": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UltimateParty"
              }
            ],
            "description": "ADVANCED — omit unless you know you need it. The party the money is ultimately FOR, when that is NOT the account being credited (e.g. paying an agent's collection account on behalf of the end beneficiary). Rare: 14 of 350 real bank files use it."
          },
          "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
                }
              }
            }
          },
          "purposeCode": {
            "type": "string",
            "maxLength": 10,
            "example": "GDDS",
            "description": "Purpose of payment — one business fact; the engine speaks the bank's language for you: ISO Purp/Cd for ≤4-char codes, or the corridor wrapper where required (PH /ORDERRES/PH//<code>, cross-border CNY /BUSINESS/… translated from the ISO code). Only needed where a corridor/regulation demands a purpose."
          },
          "regulatoryReporting": {
            "$ref": "#/components/schemas/RegulatoryReporting"
          },
          "splitPayment": {
            "$ref": "#/components/schemas/SplitPayment"
          },
          "instructionForDebtorAgent": {
            "type": "string",
            "maxLength": 140,
            "description": "ADVANCED — omit unless your bank has told you to send it. A free-text instruction to YOUR OWN bank (ISO InstrForDbtrAgt) about how to settle this transaction. The engine already derives this where a rail requires it (e.g. a Brazilian boleto barcode), so set it only on an explicit bank instruction."
          }
        }
      },
      "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",
              "domestic-express",
              "salary",
              "kid",
              "bankgiro",
              "plusgiro",
              "bacs",
              "chaps",
              "faster-payment",
              "ach",
              "wire",
              "ch-domestic",
              "ch-qr",
              "eft",
              "ca-wire",
              "br-domestic",
              "boleto",
              "pix"
            ],
            "example": "sepa"
          },
          "priority": {
            "type": "string",
            "enum": [
              "normal",
              "urgent"
            ],
            "description": "\"urgent\" emits ISO InstrPrty HIGH for time-critical / same-day clearing."
          },
          "chargeBearer": {
            "type": "string",
            "enum": [
              "DEBT",
              "CRED",
              "SHAR",
              "SLEV"
            ],
            "description": "Who pays the bank charges (ISO ChrgBr). OPTIONAL — the engine applies each bank's correct default (and forces bank-mandated values); set only to express a deliberate commercial choice. SWIFT/MT aliases OUR/BEN/SHA accepted."
          },
          "localInstrument": {
            "type": "string",
            "maxLength": 35,
            "description": "ADVANCED override (ISO LclInstrm/Cd) — omit it. The engine derives the correct rail codes from your paymentType and the bank profile; set only on explicit bank instruction."
          },
          "categoryPurpose": {
            "type": "string",
            "maxLength": 4,
            "description": "ADVANCED override (ISO CtgyPurp/Cd) — omit it. Derived from your paymentType and the bank profile; set only on explicit bank instruction."
          },
          "executionDate": {
            "type": "string",
            "format": "date",
            "example": "2026-06-04"
          },
          "debtor": {
            "$ref": "#/components/schemas/Party"
          },
          "ultimateDebtor": {
            "allOf": [
              {
                "$ref": "#/components/schemas/UltimateParty"
              }
            ],
            "description": "ADVANCED — omit unless you know you need it. The party the payment is really made ON BEHALF OF, when that is NOT the account holder being debited (e.g. a payments factory paying for a subsidiary). Applies to every transaction in this payment unless a transaction sets its own `ultimateDebtor`. If it names the same party as `debtor`, leave it out — the SEPA scheme permits it only when it DIFFERS from the debtor."
          },
          "transactions": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Transaction"
            }
          }
        }
      },
      "PaymentInstruction": {
        "type": "object",
        "required": [
          "messageId",
          "initiatingParty",
          "payments"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "enum": [
              "1.0",
              "1.1"
            ],
            "description": "Canonical contract version; defaults to the current version when omitted."
          },
          "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",
                    "description": "Identification scheme. Common codes: CUST (bank-assigned customer/agreement id), BANK, TXID (tax number — CNPJ/CPF/NIP; formatting punctuation is stripped on the wire)."
                  },
                  "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"
}