{"openapi":"3.1.0","info":{"title":"OxaBank Platform API","version":"0.1.0","description":"The single public surface of the OxaBank platform. Every capability is reached through here; nothing above this layer talks to the accounting core, the payment engine or any external provider directly. This document is the authoritative contract and the source from which all client SDKs are generated - it is written first and the code follows.\n\n**Rate limiting**: every request that carries a bearer token is counted against that credential, verified or not: `PLATFORM_RATE_LIMIT_BURST` at once (60), refilled at `PLATFORM_RATE_LIMIT_PER_MINUTE` (600). Past it, any path answers 429 `too-many-requests` with `Retry-After` in whole seconds and the correlation id, before the request reaches anything. Requests without a token (health, the contract, metrics, signed deliveries) are not counted. The limit is each process's own: several replicas limit each.\n","license":{"name":"Proprietary"},"x-published-by":"Oxagile","x-public-subset":"operations, sandbox and provider-facing paths left out"},"servers":[{"url":"https://api.your-bank.example/v1","description":"Your deployment: on your infrastructure, under your domain"}],"security":[{"oidc":[]}],"tags":[{"name":"tenants"},{"name":"approvals"},{"name":"audit"},{"name":"rules"},{"name":"proposals"},{"name":"statements"},{"name":"events"},{"name":"disputes"},{"name":"exchange"},{"name":"notifications"},{"name":"sessions"}],"paths":{"/devices/key":{"get":{"tags":["sessions"],"summary":"Whether this device has a key to confirm with","description":"Spec 044. The key this device registered, by its session's device, and whether the deployment requires every confirmation to be signed by it (PLATFORM_CONFIRMATION=device_key).\n","responses":{"200":{"description":"This device's key, or none","content":{"application/json":{"example":{"registered":true,"id":"01a10200-0000-7000-8000-000000000001","registered_at":"2026-10-03T12:00:00Z","required":false}}}},"401":{"description":"`signed-out`"}}},"post":{"tags":["sessions"],"summary":"Register this device's key","description":"Spec 044. The public half of a P-256 key the phone made in its secure hardware, usable only after the person's face or fingerprint: base64 of its SubjectPublicKeyInfo, DER. Bound to the tenant, the person and this session's device; one live key per device, another retiring it; the same key again changes nothing. Audited as device_key.registered.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["public_key"],"properties":{"public_key":{"type":"string","description":"base64 of a P-256 SubjectPublicKeyInfo","DER":null}}}}}},"responses":{"201":{"description":"The key registered","content":{"application/json":{"example":{"id":"01a10200-0000-7000-8000-000000000001","algorithm":"ES256","registered_at":"2026-10-03T12:00:00Z"}}}},"400":{"description":"Not a P-256 public key"},"401":{"description":"`signed-out`"}}}},"/passkeys":{"get":{"tags":["sessions"],"summary":"The customer's passkeys, and what the browser needs to add one","description":"Spec 046. The passkeys that confirm this customer's payments on the web; the relying party (PLATFORM_WEBAUTHN_RP_ID, absent where passkeys are not set up), this customer's registration challenge and user handle (base64url), and whether the deployment requires a verified key.\n","responses":{"200":{"description":"The customer's passkeys","content":{"application/json":{"example":{"available":true,"rp_id":"bank.example","registration_challenge":"q83vEjRWeJA","user_handle":"3q2-7w","required":false,"passkeys":[{"id":"01a10300-0000-7000-8000-000000000001","name":"MacBook","credential_id":"AAECAw","registered_at":"2026-10-03T12:00:00Z","last_used_at":null}]},"schema":{"type":"object","required":["available","rp_id","registration_challenge","user_handle","required","passkeys"],"properties":{"available":{"type":"boolean"},"rp_id":{"type":"string","nullable":true},"registration_challenge":{"type":"string"},"user_handle":{"type":"string"},"required":{"type":"boolean"},"passkeys":{"type":"array","items":{"$ref":"#/components/schemas/Passkey"}}}}}}},"401":{"description":"`signed-out`"}}},"post":{"tags":["sessions"],"summary":"Keep a passkey the customer's browser made","description":"Spec 046. What navigator.credentials.create returned, base64url: the client data (webauthn.create, this customer's registration challenge, one of PLATFORM_WEBAUTHN_ORIGINS) and the attestation object, whose authenticator data must be for the relying party, with the person present and verified, and carry an ES256 key. The attestation statement is not read. A credential is one customer's; the same one again changes nothing. Audited as passkey.registered.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["client_data_json","attestation_object"],"properties":{"name":{"type":"string","maxLength":64,"description":"What the customer calls it; Passkey when absent"},"client_data_json":{"type":"string","description":"base64url of response.clientDataJSON"},"attestation_object":{"type":"string","description":"base64url of response.attestationObject"}}}}}},"responses":{"201":{"description":"The passkey kept","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Passkey"}}}},"400":{"description":"Not a passkey for this bank, by this person, verified"},"401":{"description":"`signed-out`"},"403":{"description":"Passkeys are not set up for this bank"},"409":{"description":"The credential is someone else's"}}}},"/passkeys/{id}":{"delete":{"tags":["sessions"],"summary":"Remove one of the customer's passkeys","description":"Spec 046. It confirms nothing after. Audited as passkey.removed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Removed"},"401":{"description":"`signed-out`"},"404":{"description":"Not one of the customer's passkeys"}}}},"/fx-wires":{"get":{"tags":["transfers"],"summary":"What a wire abroad may be paid in, and the customer's wires in other currencies","description":"Spec 053. Offered where the FX provider is bound for it (PLATFORM_FX_WIRE_RAIL) and a rate provider is configured; `currencies` are what a payee abroad may be paid in, from a dollar account.\n","responses":{"200":{"description":"The offer and the wires","content":{"application/json":{"schema":{"type":"object","required":["available","sell_currency","currencies","wires"],"properties":{"available":{"type":"boolean"},"sell_currency":{"type":"string"},"currencies":{"type":"array","items":{"type":"string"}},"wires":{"type":"array","items":{"$ref":"#/components/schemas/FxWire"}}}},"example":{"available":true,"sell_currency":"USD","currencies":["EUR","GBP"],"wires":[{"id":"0199c000-0000-7000-8000-0000000000aa","state":"sent","said":"Sent by SWIFT. It usually arrives in 1 to 3 business days.","from_account":"11","payee":{"id":"0199c000-0000-7000-8000-000000000001","display_name":"Lena Schneider"},"sell_minor":50000,"sell_currency":"USD","buy_minor":42909,"buy_currency":"EUR","rate":"0.858058","spread_bps":50,"spread_minor":215,"rate_source":"European Central Bank reference rate","rate_as_of":"2026-10-02","reference":null,"authorize":{"amount":"500.00","currency":"USD","payee":"Lena Schneider, who receives 429.09 EUR","expires_in_minutes":10},"money_moved":true,"return_reason":null,"created_at":"2026-10-05T10:00:00Z","updated_at":"2026-10-05T10:01:00Z"}]}}}}}},"post":{"tags":["transfers"],"summary":"Quote a wire abroad in the payee's currency","description":"Spec 053. From the customer's own dollar account to a SWIFT payee with an address, at the platform's rate and spread (PLATFORM_EXCHANGE_SPREAD_BPS), as an exchange is quoted. The answer carries both figures and what to sign. Nothing moves until it is confirmed.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["from_account","payee_id","sell_minor","buy_currency"],"properties":{"from_account":{"type":"string"},"payee_id":{"type":"string","format":"uuid"},"sell_minor":{"type":"integer","minimum":1},"buy_currency":{"type":"string"},"reference":{"type":"string","maxLength":140,"nullable":true}}}}}},"responses":{"201":{"description":"Quoted, awaiting the customer's confirmation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FxWire"}}}},"400":{"description":"Not a SWIFT payee with an address","not a dollar account":null,"or a currency not offered":null},"404":{"description":"Not the customer's payee or account"},"409":{"description":"The rate is too old to quote on"},"503":{"description":"Not offered here"}}}},"/fx-wires/{id}":{"get":{"tags":["transfers"],"summary":"One of the customer's wires in another currency","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The wire","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FxWire"}}}},"404":{"description":"Not the customer's"}}}},"/fx-wires/{id}/confirm":{"post":{"tags":["transfers"],"summary":"Confirm a wire in another currency, and have it sent","description":"Spec 053. Confirmed as a transfer is (the kind is fx_wire and the id the wire's; the payee signed reads \"<payee>, who receives <amount> <currency>\"). The core debits the customer's dollars to PLATFORM_FX_WIRE_SETTLEMENT_ACCOUNT; the FX provider buys exactly what the payee receives and pays it by SWIFT, each once under the wire's key. A payment that cannot be made is refunded in full on a second person's approval (fx_wire_refund).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Sent, being sent, refused or returned, with why","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FxWire"}}}},"400":{"description":"The confirmation does not hold"},"409":{"description":"Already confirmed","or the quote has expired":null},"503":{"description":"The account could not be debited just now; nothing was sent"}}}},"/linked-accounts":{"get":{"tags":["transfers"],"summary":"The customer's accounts at other banks, to add money from","description":"Spec 048. Each by its holder, routing number and last four digits; whether pulling is offered here (PLATFORM_ACH_RAIL=column with a settlement account) and its limits in cents.\n","responses":{"200":{"description":"The linked accounts","content":{"application/json":{"schema":{"type":"object","required":["available","verification_required","max_minor","daily_minor","accounts"],"properties":{"available":{"type":"boolean"},"verification_required":{"type":"boolean","description":"Spec 054. Where the bank verifies (PLATFORM_MICRO_DEPOSIT_ACCOUNT), a pull is taken only from an account verified by its two small deposits.\n"},"max_minor":{"type":"integer","nullable":true},"daily_minor":{"type":"integer","nullable":true},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/LinkedAccount"}}}},"example":{"available":true,"verification_required":true,"max_minor":100000,"daily_minor":250000,"accounts":[{"id":"0199b000-0000-7000-8000-000000000001","holder_name":"Ana Rivera","routing_number":"021000021","last4":"6789","account_type":"checking","label":"From Ana Rivera ••6789","verified":false,"verification":{"id":"0199b000-0000-7000-8000-0000000000dd","linked_account_id":"0199b000-0000-7000-8000-000000000001","state":"sent","said":"Two deposits of less than 1.00 USD are on their way. They usually show on that account's statement in 1 to 3 business days; then say what they were. 3 tries left.","tries_left":3,"authorize":null,"created_at":"2026-10-05T09:00:00Z","updated_at":"2026-10-05T09:01:00Z"},"created_at":"2026-10-05T09:00:00Z"}]}}}}}},"post":{"tags":["transfers"],"summary":"Link the customer's own account at another US bank","description":"Spec 048. A valid ABA routing number, 4 to 17 digits, checking or savings. The same account again is the same link.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["holder_name","routing_number","account_number","account_type"],"properties":{"holder_name":{"type":"string","maxLength":127},"routing_number":{"type":"string"},"account_number":{"type":"string"},"account_type":{"type":"string","enum":["checking","savings"]}}}}}},"responses":{"201":{"description":"Linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkedAccount"}}}},"400":{"description":"Not a routing number, an account number, or a type"}}}},"/linked-accounts/{id}":{"delete":{"tags":["transfers"],"summary":"Unlink one; pulls already made keep naming it","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Unlinked"},"404":{"description":"Not one of the customer's"}}}},"/linked-accounts/{id}/micro-deposits":{"post":{"tags":["transfers"],"summary":"Verify a linked account with two small deposits","description":"Spec 054. Nothing is sent until it is confirmed. The answer carries what to sign: 0.00 USD, and \"Two small deposits from OxaBank to <holder> ••<last4>\". At most three verifications sent in 30 days per customer; one under way per account is that one again.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"Awaiting the customer's confirmation, or the one under way","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MicroDeposit"}}}},"400":{"description":"Three sent in 30 days"},"404":{"description":"Not one of the customer's linked accounts"},"409":{"description":"Already verified","or failed three times":null},"503":{"description":"Verification is not set up here"}}}},"/micro-deposits/{id}/confirm":{"post":{"tags":["transfers"],"summary":"Confirm, and have the two deposits sent","description":"Spec 054. Confirmed as a transfer is (the kind is micro_deposit and the id the verification's). The core moves the two together from PLATFORM_MICRO_DEPOSIT_ACCOUNT to the ACH settlement account; Column sends each under its own key. Posting, it may be confirmed again: the core is not asked twice. A deposit the other bank refuses or sends back closes the verification, and putting its cents back is a second person's approval (micro_deposit_return).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Sent, or refused or returned with why","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MicroDeposit"}}}},"400":{"description":"The confirmation does not hold"},"409":{"description":"Already sent","or expired":null},"503":{"description":"Not sent just now; confirm again"}}}},"/micro-deposits/{id}/check":{"post":{"tags":["transfers"],"summary":"Say the two amounts, in either order","description":"Spec 054. Right, the account is verified; wrong three times, the verification fails, and the account is removed and added again to start over.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amounts_minor"],"properties":{"amounts_minor":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"integer","minimum":1,"maximum":99}}}}}}},"responses":{"200":{"description":"What it came to","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/MicroDeposit"},{"type":"object","required":["outcome"],"properties":{"outcome":{"type":"string","enum":["verified","wrong","failed","not_open"]}}}]}}}},"400":{"description":"Not two amounts of 1 to 99 cents"},"404":{"description":"Not the customer's"}}}},"/pulls":{"get":{"tags":["transfers"],"summary":"The customer's pulls, the newest first","responses":{"200":{"description":"Up to 20","content":{"application/json":{"schema":{"type":"object","required":["pulls"],"properties":{"pulls":{"type":"array","items":{"$ref":"#/components/schemas/Pull"}}}},"example":{"pulls":[{"id":"0199b000-0000-7000-8000-0000000000aa","state":"sent","said":"Asked of your bank. Money pulled by ACH usually arrives in 1 to 3 business days.","linked_account_id":"0199b000-0000-7000-8000-000000000001","from":"From Ana Rivera ••6789","to_account":"4","amount_minor":2500,"currency":"USD","mandate":"I authorise OxaBank to debit 25.00 USD once, on or after 2026-10-05, from the checking account ending 6789 at routing number 021000021, held by Ana Rivera, and to credit it to my OxaBank account. I can cancel before it is sent. If it was not authorised, I can dispute it with my bank.","authorize":{"amount":"25.00","currency":"USD","payee":"From Ana Rivera ••6789","expires_in_minutes":10},"auth_method":"device_biometric","money_moved":false,"return_reason":null,"recurring_id":null,"due_on":null,"created_at":"2026-10-05T09:00:00Z","updated_at":"2026-10-05T09:01:00Z"}]}}}}}},"post":{"tags":["transfers"],"summary":"Say how much to pull in, from which linked account, to which dollar account","description":"Spec 048. Nothing happens until it is confirmed. Into the customer's own dollar account; refused above PLATFORM_ACH_PULL_MAX_MINOR, or when the customer's pulls of the last 24 hours would pass PLATFORM_ACH_PULL_DAILY_MINOR. The answer carries the authorisation text and the figures to sign.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["linked_account_id","to_account","amount_minor"],"properties":{"linked_account_id":{"type":"string","format":"uuid"},"to_account":{"type":"string"},"amount_minor":{"type":"integer","minimum":1}}}}}},"responses":{"201":{"description":"Awaiting the customer's authorisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pull"}}}},"400":{"description":"Over a limit","not a dollar account":null,"or nothing to pull":null},"404":{"description":"Not the customer's linked account or account"},"503":{"description":"Pulling is not set up here"}}}},"/pulls/{id}":{"get":{"tags":["transfers"],"summary":"One of the customer's pulls","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The pull","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pull"}}}},"404":{"description":"Not the customer's"}}}},"/backoffice/people":{"get":{"tags":["operators"],"summary":"Who the back office's ids are","description":"Spec 057. Each member of the operator's tenant (up to 500, operators first) by the identity provider's subject: an operator by the label they were added with, a customer by their client's name in the core where they have one. Operators only.\n","responses":{"200":{"description":"The tenant's people","content":{"application/json":{"schema":{"type":"object","required":["people"],"properties":{"people":{"type":"array","items":{"type":"object","required":["subject","name","role"],"properties":{"subject":{"type":"string"},"name":{"type":"string","nullable":true},"role":{"type":"string","enum":["operator","customer"]}}}}}}}}},"403":{"description":"Not an operator of this tenant"}}}},"/backoffice/lending/portfolio":{"get":{"tags":["lending","back-office"],"summary":"Every loan at its stage, and the portfolio at risk","description":"Spec 069, as Mambu shows a loan book: partial_application, pending_approval, approved, rejected, withdrawn, active, active_in_arrears, closed_obligations_met, written_off; what is owed on loans out and how much of it is on loans late more than 1, 30 and 90 days (PAR1/PAR30/PAR90, also in basis points); credit lines by state with the open limits.\n","responses":{"200":{"description":"The portfolio","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficePortfolio"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/loans/{id}":{"get":{"tags":["lending","back-office"],"summary":"One loan whole — its stage, schedule, payments and position","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The loan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeLoan"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such loan"}}}},"/backoffice/loans/{id}/write-off":{"post":{"tags":["lending","back-office"],"summary":"Propose writing a drawn loan off","description":"Spec 069. **This changes nothing.** Proposed with a reason for what it owes now (loan_write_off); a credit officer within their limit (25,000.00) or the credit committee approves, and it is written off for what it owes then. A payment that still comes in is still taken.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such loan"},"409":{"description":"Not a drawn loan still owing, or a write-off already waits"}}}},"/backoffice/settings/products":{"get":{"tags":["back-office"],"summary":"The savings and loan products the core offers","description":"Spec 071. As the core defines them: rate, amounts and terms, whether active, and how the core books them (none, cash, accrual). Made in the core; read here.\n","responses":{"200":{"description":"The products, savings first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeProducts"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/settings/calendars":{"get":{"tags":["back-office"],"summary":"The days each rail is shut in a year","parameters":[{"name":"year","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Each rail's rules and holidays","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeCalendars"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/settings/branches":{"get":{"tags":["back-office"],"summary":"The bank's branches, open and closed, with their staff and customers counted","responses":{"200":{"description":"The branches","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeBranches"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["back-office"],"summary":"Propose a branch","description":"Spec 071. **This adds nothing.** A code (3 to 12 capitals, digits or dashes), a name, a city, the core account its vault's cash is booked in, a currency (USD when none) and a reason (branch_change); a supervisor approves.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name","city","vault_account","reason"],"properties":{"code":{"type":"string"},"name":{"type":"string"},"city":{"type":"string"},"vault_account":{"type":"string"},"currency":{"type":"string"},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason, or a code, name, city or vault that does not read"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"The code is taken, or the same proposal already waits"}}}},"/backoffice/settings/branches/{id}":{"post":{"tags":["back-office"],"summary":"Propose renaming, moving the vault of, closing or opening again a branch","description":"Spec 071. **This changes nothing.** `closed` true closes it (refused while a till is open there), false opens it again; otherwise the name, city and vault given replace the ones there. A supervisor approves (branch_change). A closed branch opens no till.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"name":{"type":"string"},"city":{"type":"string"},"vault_account":{"type":"string"},"closed":{"type":"boolean"},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason, a vault that does not read, or nothing would change"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such branch"},"409":{"description":"Closed or open already, tills open there, or the same proposal already waits"}}}},"/backoffice/settings/authority":{"get":{"tags":["back-office","iam"],"summary":"The authority matrix with the bank's amounts","description":"Spec 071. For each role: how much it posts at the desk and how much it decides in each domain it decides, with the matrix's own amount and who set the bank's, when. `limit_minor` null is without limit.\n","responses":{"200":{"description":"The matrix","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeAuthority"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["back-office","iam"],"summary":"Propose an amount in the authority matrix","description":"Spec 071. **This changes nothing.** Only where the matrix gives the role an amount (a teller's posting, a head teller's or a branch manager's teller overrides, a credit officer's lending); whether a role decides a domain at all, or without limit, is not a setting. An IAM administrator proposes and another approves (authority_limit).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role","scope","limit_minor","reason"],"properties":{"role":{"type":"string"},"scope":{"type":"string","description":"A domain (teller, lending, …) or posting"},"limit_minor":{"type":"integer","format":"int64","minimum":1,"maximum":100000000000},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason, no such role or scope, no amount there to set, out of bounds, or the amount already"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"The same proposal already waits"}}}},"/backoffice/accounting/trial-balance":{"get":{"tags":["back-office"],"summary":"The chart of accounts with what was debited and credited to each","description":"Spec 070. Every general ledger account the core keeps, with its debits, credits and balance on its own side (assets and expenses debit, the rest credit) per currency, from the journal up to `to`; and per currency whether all debits equal all credits. Accountants and supervisors read it.\n","parameters":[{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"The trial balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTrialBalance"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/accounting/journal":{"get":{"tags":["back-office"],"summary":"What was booked, newest first","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"gl","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The journal (at most 500 entries)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeJournal"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["back-office"],"summary":"Propose a manual journal entry","description":"Spec 070. **This books nothing.** A debit and a credit of the same amount between two accounts that take manual entries, with a memo (manual_journal); another accountant or a supervisor approves, and the core books it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["debit_gl","credit_gl","amount_minor","currency","memo"],"properties":{"debit_gl":{"type":"string"},"credit_gl":{"type":"string"},"amount_minor":{"type":"integer","format":"int64","minimum":1},"currency":{"type":"string"},"memo":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No memo, no amount, the same account twice, or an account that takes no manual entries"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/accounting/closures":{"get":{"tags":["back-office"],"summary":"The days the books are closed up to, latest first","responses":{"200":{"description":"The closures","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeClosures"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["back-office"],"summary":"Propose closing the books up to a day","description":"Spec 070. **This closes nothing.** Only a day already over (books_closure); another accountant or a supervisor approves, and from then the core books nothing on or before it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["closing_date","comments"],"properties":{"closing_date":{"type":"string","format":"date"},"comments":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No comments, or a day not over"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"That closure already waits for approval"}}}},"/backoffice/reports/overview":{"get":{"tags":["back-office"],"summary":"What a manager reads first","description":"Spec 070. Customers by state and by branch with what they hold, the tills' cash by branch with what closed tills were over or short, what waits for approval by kind, and the staff's open and overdue tasks.\n","responses":{"200":{"description":"The overview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeOverview"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/customers/{subject}/accounts/{account}/actions":{"post":{"tags":["customers","accounts","back-office"],"summary":"Propose blocking, holding, releasing or closing a customer's account","description":"Spec 068, as Mambu's lock, block-and-seize and close. **This changes nothing.** Proposed with a reason (account_control); a branch manager, a compliance officer or a supervisor approves, and the core does it then. A hold sets part of the balance aside (`amount_minor`); a release lets one go (`hold_ref`, from the account's standing); only an empty account closes.\n","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"account","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","reason"],"properties":{"action":{"type":"string","enum":["block","unblock","block_debits","unblock_debits","hold","release","close"]},"amount_minor":{"type":"integer","minimum":1,"description":"For a hold"},"hold_ref":{"type":"string","description":"For a release"},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason, an unknown action, a hold with no amount, a release with no hold"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a customer the caller sees, or not their account"},"409":{"description":"A balance on an account to close, or the same change waiting"}}}},"/backoffice/customers/{subject}/state":{"post":{"tags":["customers","back-office"],"summary":"Propose a customer's state","description":"Spec 068. active, inactive, exited or blacklisted, with a reason (client_state); a branch manager, a compliance officer or a supervisor approves. An exited or blacklisted customer signs in no more (POST /sessions answers 403) and their sessions are ended when it is approved.\n","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["state","reason"],"properties":{"state":{"type":"string","enum":["active","inactive","exited","blacklisted"]},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason or no such state"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a customer the caller sees"},"409":{"description":"They are that already, or the same change waits"}}}},"/backoffice/customers/{subject}/notes":{"post":{"tags":["customers","back-office"],"summary":"Write a note on a customer","description":"Spec 068. At once; shown on the customer's page, newest first, with its author.","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":4000}}}}}},"responses":{"201":{"description":"Written","content":{"application/json":{"schema":{"type":"object","required":["id","author","body"],"properties":{"id":{"type":"string","format":"uuid"},"author":{"type":"string"},"body":{"type":"string"}}}}}},"400":{"description":"Empty or too long"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a customer the caller sees"}}}},"/backoffice/tasks":{"get":{"tags":["tasks","back-office"],"summary":"Tasks — the caller's, a customer's, or everyone's","description":"Spec 068. The open first, the soonest due first.","parameters":[{"name":"scope","in":"query","required":false,"schema":{"type":"string","enum":["mine","all"]}},{"name":"customer","in":"query","required":false,"schema":{"type":"string"}},{"name":"open","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"The tasks","content":{"application/json":{"schema":{"type":"object","required":["tasks"],"properties":{"tasks":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeTask"}}}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["tasks","back-office"],"summary":"Set a task, for oneself or another member of staff","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"detail":{"type":"string"},"due_on":{"type":"string","format":"date"},"assignee":{"type":"string","description":"A member of staff; the caller where none"},"customer":{"type":"string","description":"The customer it is about"}}}}}},"responses":{"201":{"description":"Set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTask"}}}},"400":{"description":"No title, or not for a member of staff"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/tasks/{id}/done":{"post":{"tags":["tasks","back-office"],"summary":"Mark a task done","description":"Spec 068. By whoever it is for, or whoever set it.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTask"}}}},"403":{"description":"Somebody else's"},"409":{"description":"Done already"}}}},"/backoffice/branches":{"get":{"tags":["operators","branches"],"summary":"The bank's branches","description":"Spec 067. Any member of staff.","responses":{"200":{"description":"The branches","content":{"application/json":{"schema":{"type":"object","required":["branches"],"properties":{"branches":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeBranch"}}}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/tills":{"get":{"tags":["tills","back-office"],"summary":"A branch's tills, open first, and its tellers","description":"Spec 067. Head tellers, branch managers, supervisors; a branch-scoped caller sees their own branch only.","parameters":[{"name":"branch","in":"query","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The branch's tills","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTills"}}}},"400":{"description":"No branch given and the caller works none"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["tills","back-office"],"summary":"Open a till for a teller, with a float","description":"Spec 067. A head teller opens a till for a teller of their branch; one open till per teller. The float is cash already in the branch; nothing moves in the core. Audited (till.opened).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["teller","opening_minor"],"properties":{"teller":{"type":"string"},"branch":{"type":"string","format":"uuid"},"opening_minor":{"type":"integer","minimum":0}}}}}},"responses":{"201":{"description":"Opened","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTill"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"Not a teller, not of this branch, or a till already open"}}}},"/backoffice/tills/mine":{"get":{"tags":["tills","back-office"],"summary":"The caller's open till and their posting limit","responses":{"200":{"description":"Their till, or null","content":{"application/json":{"schema":{"type":"object","required":["till","posting_limit_minor"],"properties":{"till":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/BackofficeTill"}]},"posting_limit_minor":{"type":"integer"}}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/tills/{id}":{"get":{"tags":["tills","back-office"],"summary":"A till and its entries","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The till","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTill"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not the caller's","nor of a branch they manage":null}}}},"/backoffice/tills/{id}/cash":{"post":{"tags":["tills","back-office"],"summary":"Top a drawer up from the vault, or take cash back","description":"Spec 067. Head tellers. Cash moving inside the branch; nothing moves in the core. Audited.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","amount_minor"],"properties":{"kind":{"type":"string","enum":["buy","sell"]},"amount_minor":{"type":"integer","minimum":1}}}}}},"responses":{"200":{"description":"The till after it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTill"}}}},"403":{"description":"Not a head teller"},"409":{"description":"The till is closed","or the drawer does not hold that much":null}}}},"/backoffice/tills/{id}/close":{"post":{"tags":["tills","back-office"],"summary":"Count and close a till","description":"Spec 067. Its teller or whoever manages its branch's tills. What it should hold (the float and every entry), what was counted and the difference are written down and audited (till.closed); a count that differs needs a note of eight characters or more.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["counted_minor"],"properties":{"counted_minor":{"type":"integer","minimum":0},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"Closed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeTill"}}}},"400":{"description":"A difference with no note"},"409":{"description":"Already closed"}}}},"/backoffice/me":{"get":{"tags":["operators"],"summary":"Who is signed in, their roles and what they may do","description":"Spec 066. Any member of staff. The roles carry the permissions; every operator route needs one (platform-iam's ROUTES) and refuses with 403 \"Your role does not allow this.\" otherwise.\n","responses":{"200":{"description":"The signed-in member of staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMe"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/iam/staff":{"get":{"tags":["operators","iam"],"summary":"The staff, their roles, the roles a bank gives, and changes waiting","description":"Spec 066. IAM administrators (and, as every read, auditors).","responses":{"200":{"description":"The staff","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeIamStaff"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/iam/staff/{subject}/roles":{"post":{"tags":["operators","iam"],"summary":"Propose giving or taking a member of staff's role","description":"Spec 066. **This changes nothing.** One IAM administrator proposes with a reason (role_change); another approves at POST /approvals/{id}/decision, and only an IAM administrator may. Nobody proposes or decides a change to their own roles. An auditor holds no other role and an IAM administrator none but that (409 with why). A person new to the staff needs a `label`; a customer of the bank cannot be given a role.\n","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role","grant","reason"],"properties":{"role":{"type":"string","enum":["teller","head_teller","branch_manager","service_agent","operations","credit_officer","credit_committee","compliance","accountant","supervisor","auditor","iam_admin"]},"grant":{"type":"boolean","description":"True gives the role","false takes it away":null},"label":{"type":"string","description":"The name of a person new to the staff"},"branch":{"type":"string","format":"uuid","description":"Spec 067: the branch they work, set with the role"},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed, awaiting another IAM administrator","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No such role","no reason":null,"or a new member with no name":null},"403":{"description":"Not an IAM administrator, or their own roles"},"404":{"description":"Taking a role from nobody"},"409":{"description":"Already held, not held, roles that may not go together, a customer, or the same change waiting"}}}},"/backoffice/customers":{"get":{"tags":["customers","back-office"],"summary":"Find a customer","description":"Spec 065. By any words of their name (the core's), the identity provider's subject (six characters or more), the core's client number, an account number or a card's last four. Empty lists the first 50. Operators only.\n","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Customers found, at most 50","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeCustomers"},"example":{"term":"eriksson","customers":[{"subject":"CiQwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMGN1MDESBWxvY2Fs","name":"Anna Maria Eriksson","client_id":"7","matched_by":"name"}]}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/customers/{subject}":{"get":{"tags":["customers","back-office"],"summary":"Everything the bank holds about a customer","description":"Spec 065. Profile and identity, screening, accounts with balances, the credit line and loans, cards, payments, crypto, disputes, support, devices, businesses, and what operators proposed and decided about them. A part that could not be read is named in `unavailable`; the rest is shown.\n","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeCustomer"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a customer of this tenant"}}}},"/backoffice/customers/{subject}/accounts/{account}/movements":{"get":{"tags":["customers","back-office"],"summary":"One of a customer's accounts, as its statement reads","description":"Spec 065. The account has to be one the customer holds, as the core says now.","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"account","in":"path","required":true,"schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"The account and its latest movements, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeCustomerMovements"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not this customer's account"},"503":{"description":"The core did not answer"}}}},"/backoffice/customers/{subject}/teller":{"post":{"tags":["customers","back-office"],"summary":"Propose a teller's movement for a customer","description":"Spec 065. **This moves nothing.** Cash in (from the bank's till, PLATFORM_TELLER_TILL_ACCOUNT, to one of their accounts), cash out (the other way), or between two of their accounts in one currency, with a reason. A second person approves (teller_movement) and the core moves it then. Cash out and between are checked against the balance now and again by the core.\n","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["movement","account","amount_minor","reason"],"properties":{"movement":{"type":"string","enum":["cash_in","cash_out","between"]},"account":{"type":"string","description":"Theirs; the one money comes from","or for cash in goes to":null},"to_account":{"type":"string","description":"For between","the other of theirs":null},"amount_minor":{"type":"integer","minimum":1},"reason":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"Spec 067. Posted at once, on the teller's own authority with the customer at the desk: within their posting limit, cash through their open till (audited teller.posted).\n","content":{"application/json":{"schema":{"type":"object","required":["posted","id","core_ref","note"],"properties":{"posted":{"type":"boolean","enum":[true]},"id":{"type":"string","format":"uuid"},"core_ref":{"type":"string"},"till_expected_minor":{"type":"integer","nullable":true},"note":{"type":"string"}}}}}},"202":{"description":"Above the teller's limit — proposed, for a head teller, manager or supervisor whose limit covers it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason, nothing to move, no till for cash, or two currencies"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a customer","or not their account":null},"409":{"description":"The account or the drawer does not hold that much, or no till is open for cash"}}}},"/backoffice/customers/{subject}/cards/{card}/freeze":{"post":{"tags":["customers","cards","back-office"],"summary":"Freeze a customer's card now","description":"Spec 065. At once, by one operator — protecting a customer does not wait for a colleague — at the issuer first, with the reason written to the audit chain (operator.card_frozen).\n","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"card","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"Frozen","content":{"application/json":{"schema":{"type":"object","required":["card_id","status"],"properties":{"card_id":{"type":"string","format":"uuid"},"status":{"type":"string"}}}}}},"400":{"description":"No reason"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not this customer's card"},"409":{"description":"The card cannot be frozen from where it is"}}}},"/backoffice/customers/{subject}/cards/{card}/unfreeze":{"post":{"tags":["customers","cards","back-office"],"summary":"Propose unfreezing a customer's card","description":"Spec 065. The card stays frozen until a different person approves (card_unfreeze).","parameters":[{"name":"subject","in":"path","required":true,"schema":{"type":"string"}},{"name":"card","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed, awaiting a second person","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"No reason"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not this customer's card"},"409":{"description":"The card is not frozen"}}}},"/credit-line":{"get":{"tags":["lending"],"summary":"The customer's revolving credit line, where it stands today","description":"Spec 056. Whether a line is offered here (PLATFORM_CREDIT_LINE_PRODUCT) and the customer's line: asked for, open, declined or refused. Open, what is drawn, available, owed today, the next instalment and anything overdue are read from the core, which keeps the line as a loan; the last draws and repayments with it.\n","responses":{"200":{"description":"The offer and the line","content":{"application/json":{"schema":{"type":"object","required":["available","line"],"properties":{"available":{"type":"boolean"},"line":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/CreditLine"}]}}},"example":{"available":true,"line":{"id":"0199d000-0000-7000-8000-0000000000c1","state":"open","said":"Open. Draw from it into your account, and pay it back at any time.","currency":"USD","asked_minor":300000,"limit_minor":200000,"drawn_minor":70000,"available_minor":130000,"payoff_minor":70412,"overdue_minor":0,"next_due":{"on":"2026-11-06","amount_minor":6619},"days_past_due":0,"closed_by":null,"closed_at":null,"annual_rate":"24","term_months":12,"account_id":"4","provenance":"live","movements":[{"id":"0199d000-0000-7000-8000-0000000000d1","kind":"draw","amount_minor":100000,"currency":"USD","account_id":"4","state":"done","said":"In your account.","authorize":null,"money_moved":true,"auth_method":"device_key","created_at":"2026-10-06T09:00:00Z"}],"agreement":{"id":"0199d000-0000-7000-8000-0000000000e1","state":"signed","sha256":"9f2c4d8e1a7b3c5f6e0d2b4a8c1e3f5a7b9d0c2e4f6a8b0d1c3e5f7a9b0c2d4e","signed_at":"2026-10-05T21:04:00Z","auth_method":"device_key","authorize":null,"pdf":"/v1/documents/0199d000-0000-7000-8000-0000000000e1/pdf","signed_pdf":"/v1/documents/0199d000-0000-7000-8000-0000000000e1/pdf?signed=true","created_at":"2026-10-05T21:00:00Z"},"autopay":{"id":"0199d000-0000-7000-8000-0000000000a1","state":"active","said":"On: the amount due each month, up to 200.00 USD, from account 4.","from_account":"4","cap_minor":20000,"authorize":null,"runs":[{"due_on":"2026-11-06","amount_minor":6619,"state":"done","said":"Paid."}],"created_at":"2026-10-06T09:00:00Z"},"created_at":"2026-10-05T09:00:00Z"}}}}}}},"post":{"tags":["lending"],"summary":"Ask for a credit line","description":"Spec 056. Into the customer's own dollar account, at least 100.00 USD. The bank's credit policy decides (013): approved, at the lesser of what was asked and what it allows, and its rate; referred, at what was asked and PLATFORM_CREDIT_LINE_RATE; declined, kept and said. Approved or referred, a second person opens it in the core (credit_line_opening). One asked for or open per customer.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["limit_minor","to_account","monthly_income_minor"],"properties":{"limit_minor":{"type":"integer","minimum":10000},"to_account":{"type":"string"},"monthly_income_minor":{"type":"integer","minimum":1},"monthly_obligations_minor":{"type":"integer","minimum":0}}}}}},"responses":{"201":{"description":"Asked for, or declined","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditLine"}}}},"400":{"description":"Under the least","not a dollar account":null,"or no income said":null},"404":{"description":"Not the customer's account"},"409":{"description":"A line already asked for or open, or one the bank closed or charged off still owes"},"503":{"description":"No line is offered here"}}},"delete":{"tags":["lending"],"summary":"Close the customer's own line","description":"Spec 059. Only when the core says nothing is owed on it; its autopay stops with it. A closed line is never drawn again (the core would open a paid-off loan again on the next draw, so the closing is the platform's).\n","responses":{"200":{"description":"Closed","content":{"application/json":{"schema":{"type":"object","required":["available","line"],"properties":{"available":{"type":"boolean"},"line":{"$ref":"#/components/schemas/CreditLine"}}}}}},"409":{"description":"Something is owed on it, closed already, or no open line"},"503":{"description":"The core did not answer","or no line is offered here":null}}}},"/credit-line/agreement":{"get":{"tags":["lending"],"summary":"The line's agreement, to read and sign","description":"Spec 060. Rendered once from the line's terms and kept as those bytes with their SHA-256. Unsigned, it carries what the customer confirms: the limit and \"OxaBank credit line agreement, SHA-256 …\" with the whole hash, confirmed as a payment is (the kind is line_agreement and the id the agreement's). A line is not drawn until it is signed.\n","responses":{"200":{"description":"The agreement","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineAgreement"},"example":{"id":"0199d000-0000-7000-8000-0000000000e1","state":"unsigned","sha256":"9f2c4d8e1a7b3c5f6e0d2b4a8c1e3f5a7b9d0c2e4f6a8b0d1c3e5f7a9b0c2d4e","signed_at":null,"auth_method":null,"authorize":{"amount":"3000.00","currency":"USD","payee":"OxaBank credit line agreement, SHA-256 9f2c4d8e1a7b3c5f6e0d2b4a8c1e3f5a7b9d0c2e4f6a8b0d1c3e5f7a9b0c2d4e"},"pdf":"/v1/documents/0199d000-0000-7000-8000-0000000000e1/pdf","signed_pdf":null,"created_at":"2026-10-05T21:00:00Z"}}}},"409":{"description":"No line the core keeps"}}}},"/credit-line/agreement/sign":{"post":{"tags":["lending"],"summary":"Sign the line's agreement","description":"Spec 060. The customer's confirmation over the limit and the agreement's SHA-256, under the same freshness and strength as a payment's; the signed copy, which says who signed, how, when and what, is kept beside it. Once.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Signed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineAgreement"}}}},"400":{"description":"The confirmation does not hold"},"409":{"description":"Signed already","or no line":null}}}},"/documents/{id}/pdf":{"get":{"tags":["lending"],"summary":"A document of the customer's own, as read and signed or the signed copy","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"signed","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"The PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Not the caller's","or not signed yet":null}}}},"/credit-line/autopay":{"post":{"tags":["lending"],"summary":"Pay the line's amount due automatically each month, up to a ceiling","description":"Spec 058. From the customer's own dollar account (the line's when none is named). Nothing is taken until it is confirmed, and then on each due day the core's amount due, never more than the ceiling — the figure the customer signs. One asked for or on per line.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["cap_minor"],"properties":{"cap_minor":{"type":"integer","minimum":1},"from_account":{"type":"string","nullable":true}}}}}},"responses":{"201":{"description":"Awaiting the customer's confirmation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineAutopay"}}}},"404":{"description":"Not the customer's account"},"409":{"description":"No open line","or one already asked for or on":null}}}},"/credit-line/autopay/{id}":{"delete":{"tags":["lending"],"summary":"Stop it; nothing more is taken","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Stopped","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineAutopay"}}}},"404":{"description":"Not the customer's"}}}},"/credit-line/autopay/{id}/confirm":{"post":{"tags":["lending"],"summary":"Confirm the standing authorisation","description":"Spec 058. Confirmed as a transfer is (the kind is line_autopay and the id the autopay's), over the ceiling and \"Your OxaBank credit line's amount due each month, from account …\". Each due day — the core's business day — is then taken once.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"On","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineAutopay"}}}},"400":{"description":"The confirmation does not hold"},"409":{"description":"Already on or stopped","or expired":null}}}},"/credit-line/movements":{"post":{"tags":["lending"],"summary":"Say what to draw from the line or pay back to it","description":"Spec 056. Nothing moves until it is confirmed. A draw goes into the customer's own dollar account (the line's when none is named) and is at most the limit less what is drawn, as the core says it now; a repayment comes from one and is at most what paying it off today takes.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","amount_minor"],"properties":{"kind":{"type":"string","enum":["draw","repayment"]},"amount_minor":{"type":"integer","minimum":1},"account_id":{"type":"string","nullable":true}}}}}},"responses":{"201":{"description":"Awaiting the customer's confirmation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineMovement"}}}},"400":{"description":"Over what is available","or over the payoff":null},"404":{"description":"Not the customer's account"},"409":{"description":"No open line"}}}},"/credit-line/movements/{id}/confirm":{"post":{"tags":["lending"],"summary":"Confirm a draw or a repayment, and have the core make it","description":"Spec 056. Confirmed as a transfer is (the kind is line_draw or line_repayment and the id the movement's). A draw waits on the line's signed agreement. The line is locked, what is drawn read from the core again, and a draw goes only if it still fits: the core would take one past the limit. Once per movement, under its key.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Done, or refused with why","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineMovement"}}}},"400":{"description":"The confirmation does not hold","or it no longer fits":null},"409":{"description":"Already done","or expired":null},"503":{"description":"The core could not be asked; confirm again","it is done once":null}}}},"/recurring-pulls":{"get":{"tags":["transfers"],"summary":"The customer's standing authorisations to pull every week or month","description":"Spec 055. Up to 20, the newest first; refused ones are not listed.","responses":{"200":{"description":"Whether pulling is offered here, and the standing authorisations","content":{"application/json":{"schema":{"type":"object","required":["available","recurring"],"properties":{"available":{"type":"boolean"},"recurring":{"type":"array","items":{"$ref":"#/components/schemas/RecurringPull"}}}},"example":{"available":true,"recurring":[{"id":"0199b000-0000-7000-8000-0000000000ee","state":"active","said":"50.00 USD every month. Next on 2026-11-05.","linked_account_id":"0199b000-0000-7000-8000-000000000001","from":"From Ana Rivera ••6789","to_account":"4","amount_minor":5000,"currency":"USD","every":"month","first_on":"2026-10-05","next_on":"2026-11-05","taken":1,"mandate":"I authorise OxaBank to debit 50.00 USD every month, starting on 2026-10-05, from the checking account ending 6789 at routing number 021000021, held by Ana Rivera, and to credit it to my OxaBank account, until I stop it. I can stop it in the app at any time before a debit is due. If a debit was not authorised, I can dispute it with my bank.","authorize":null,"auth_method":"device_key","created_at":"2026-10-05T09:00:00Z","stopped_at":null}]}}}}}},"post":{"tags":["transfers"],"summary":"Say what to pull, every week or month, from which day","description":"Spec 055. Nothing is taken until it is confirmed, and then only on the days it names. From a linked account (verified, where the bank verifies) into the customer's own dollar account; each pull at most PLATFORM_ACH_PULL_MAX_MINOR; the first day today or later, within a year. The answer carries the recurring authorisation's text and what to sign: the amount and \"<From holder ••last4>, every week|month from <first day>\".\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["linked_account_id","to_account","amount_minor","every","first_on"],"properties":{"linked_account_id":{"type":"string","format":"uuid"},"to_account":{"type":"string"},"amount_minor":{"type":"integer","minimum":1},"every":{"type":"string","enum":["week","month"]},"first_on":{"type":"string","format":"date"}}}}}},"responses":{"201":{"description":"Awaiting the customer's confirmation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringPull"}}}},"400":{"description":"Over the per-pull limit","unverified":null,"not a dollar account":null,"or not a schedule":null},"404":{"description":"Not the customer's linked account or account"},"503":{"description":"Pulling is not set up here"}}}},"/recurring-pulls/{id}":{"delete":{"tags":["transfers"],"summary":"Stop it; nothing more is taken","description":"Spec 055. Pulls already sent go on as any pull. Stopping twice is the same.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Stopped","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringPull"}}}},"404":{"description":"Not the customer's"}}}},"/recurring-pulls/{id}/confirm":{"post":{"tags":["transfers"],"summary":"Confirm the standing authorisation","description":"Spec 055. Confirmed as a transfer is (the kind is recurring_pull and the id the authorisation's): the recurring authorisation on file. Nothing is taken now. On each due day the platform starts one pull under it, for exactly its amount from exactly its account, never before the day and once per day; a pull over the 24-hour limit is refused that day and said.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringPull"}}}},"400":{"description":"The confirmation does not hold","or the account is not verified":null},"409":{"description":"Already confirmed or stopped","or expired":null}}}},"/pulls/{id}/confirm":{"post":{"tags":["transfers"],"summary":"Authorise the debit, and have it asked of the customer's bank","description":"Spec 048. Confirmed as a transfer is (the kind is pull and the id the pull's): this confirmation is the debit's authorisation on file. Column is asked for one ACH debit (WEB) under the pull's key. A settled debit is proposed as a credit to a second person (pull_credit); one returned after it was credited is proposed back (pull_reversal).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Sent, or refused with why","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pull"}}}},"400":{"description":"The confirmation does not hold","or over a limit":null},"409":{"description":"Already confirmed","or expired":null},"503":{"description":"The bank could not be asked; nothing was taken"}}}},"/sign-in":{"get":{"tags":["sessions"],"summary":"Where the apps send a person to sign in or sign up","description":"Spec 043. The identity provider by its own name, which is the issuer in its tokens and where browsers and apps reach it, the client each app signs in as (the authorization-code flow with PKCE, RFC 8252 on the phone), and whether this is a sandbox, where the sandbox's people can be signed in as by name. Asked before anybody is signed in; it says nothing of anybody.\n","security":[],"responses":{"200":{"description":"The provider and the apps' clients","content":{"application/json":{"example":{"issuer":"https://id.your-bank.example/realms/oxabank","clients":{"app":"oxabank-app","backoffice":"oxabank-backoffice"},"scope":"openid profile email","sandbox":true},"schema":{"type":"object","required":["issuer","clients","scope","sandbox"],"properties":{"issuer":{"type":"string"},"clients":{"type":"object","required":["app","backoffice"],"properties":{"app":{"type":"string"},"backoffice":{"type":"string"}}},"scope":{"type":"string"},"sandbox":{"type":"boolean"}}}}}},"503":{"description":"The identity provider did not answer"}}}},"/tenants":{"get":{"tags":["tenants"],"summary":"List the tenants the caller belongs to","description":"Every tenant the caller is a member of, by slug, and no other. A caller who belongs to several has to say which, and every route refuses to guess; this is where they find out which. Asked before there is a session, since a session is begun in one tenant: a valid token is all it needs. A token that belongs nowhere gets an empty list. Nothing of how the core addresses a tenant.\n","responses":{"200":{"description":"The caller's tenants","content":{"application/json":{"example":[{"id":"01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e10","slug":"oxabank","display_name":"OxaBank","status":"active"}],"schema":{"type":"array","items":{"$ref":"#/components/schemas/Tenant"}}}}},"401":{"description":"No token, or one the issuer did not sign"},"503":{"description":"No store"}}}},"/approvals":{"get":{"tags":["approvals"],"summary":"List approvals","parameters":[{"name":"state","in":"query","schema":{"$ref":"#/components/schemas/ApprovalState"}},{"$ref":"#/components/parameters/Offset"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalView"}}}},"description":"Bare array, newest proposed first. Not PagedApprovals - there is no total and no items wrapper."},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["approvals"],"summary":"Propose a change to a system of record","description":"Creates an approval in state `proposed`. Nothing is written to the system of record here. The proposer is taken from the token, never from the body.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewApproval"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalView"}}},"description":"Not in this group's list; noted because it returns the same ApprovalView (state always \"proposed\", decided_by null)."},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/approvals/{id}/decision":{"post":{"tags":["approvals"],"summary":"Approve or reject","description":"The decider is taken from the token. A decider identical to the proposer is refused with 409; role separation is enforced here, on the server, and cannot be bypassed by a client. Concurrent decisions resolve to exactly one winner.\n","parameters":[{"$ref":"#/components/parameters/PathId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decision"],"properties":{"decision":{"type":"string","enum":["approve","reject"]},"reason":{"type":"string","nullable":true,"description":"Optional; null accepted. Stored as rejection_reason on BOTH approve and reject."}}}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalView"}}},"description":"Re-read after execution, so for an approve the state is usually already `executed` or `failed`, not `approved`; for a reject it is `rejected`.\n"},"400":{"description":"decision is neither \"approve\" nor \"reject\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"Already decided, or the decider is the proposer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/audit":{"get":{"tags":["audit"],"summary":"Search the audit trail","description":"Searching by correlation id returns the whole journey of one payment (SC-003).","parameters":[{"name":"correlation_id","in":"query","schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/Offset"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}}}},"description":"Bare array, the tenant's WHOLE chain in ascending id order (or its entries for one correlation_id). offset/limit are ignored."},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/audit/verify":{"post":{"tags":["audit"],"summary":"Re-derive the hash chain and report the first divergence","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChainVerification"}}},"description":"Documented from the code"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/proposals/{id}":{"get":{"tags":["proposals"],"summary":"Retrieve an automated proposal with its evidence and provenance","description":"Who answered, which version, and the SHA-256 of exactly what was asked, so the enquiry can be reproduced. The caller's tenant only; anyone else's, or an id that is not a uuid, is 404.\n","parameters":[{"$ref":"#/components/parameters/PathId"}],"responses":{"200":{"description":"The proposal as recorded","content":{"application/json":{"example":{"id":"01a0f1c2-0000-7000-8000-000000000001","subject_type":"screening_match","subject_ref":"cust-1|ofac|12345@2026-09-30","classification":"likely_false_positive","explanation":"The names are close, but the dates of birth disagree: most likely two different people with similar names.","suggested_action":{"verdict":"cleared"},"confidence":0.8,"evidence":[{"code":"dob_conflict","detail":"Born 1970-03-02; the entry says 1981-11-05."}],"provenance":"live","provider":"rule-based","provider_version":"screening-rules-1","prompt_hash":"4f2a0c9e0b1d5e6a7c8b9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c","created_at":"2026-09-30T14:02:11Z"},"schema":{"$ref":"#/components/schemas/Proposal"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such proposal for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/events/replay":{"post":{"tags":["events"],"summary":"Replay a topic's published events from a chosen position","description":"Sends the tenant's published events for `topic` again, in outbox order, with their original ids, from `earliest`, `latest` or a position — a count of the tenant's published events, all topics together. Consumers deduplicate on the id, so replay converges rather than duplicating effects. Nothing is marked. At most `limit` events (default 1000) are looked at per call; `next` is where the following call resumes. Audited.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"},"example":{"topic":"platform.core","from":"earliest","limit":1000}}}},"responses":{"202":{"description":"Replayed","content":{"application/json":{"example":{"topic":"platform.core","sent":42,"from":0,"next":57},"schema":{"$ref":"#/components/schemas/BackofficeReplayResult"}}}},"400":{"description":"Not a topic the platform publishes to, or not a position"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"415":{"description":"Content-Type is not application/json (axum Json rejection, plain text)."},"422":{"description":"Body is JSON but not a ReplayBody: missing `topic`/`from`, `from` a negative or fractional number or a non-string non-number, `limit` negative (axum, plain text).\n"},"500":{"description":"The outbox could not be read (ReplayError::Store). Nothing sent, not audited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"The bus could not be reached or stopped answering; `next` says where to resume","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/BackofficeReplayInterrupted"},{"$ref":"#/components/schemas/Problem"}]}}}}}}},"/backoffice/events/dead-letters":{"get":{"tags":["events"],"summary":"What the relay gave up on","description":"Outbox rows dead-lettered after their bounded attempts, or at once when not decodable. Oldest first.\n","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int64","default":100,"minimum":1,"maximum":500},"description":"Clamped to 1..500 rather than refused; 0 or negatives become 1."}],"responses":{"200":{"description":"Dead letters","content":{"application/json":{"example":{"dead_letters":[{"id":"01a0e88f-2b58-7e09-094e-9720341ea93d","type":"transfer.posted","attempts":8,"last_error":"bus unavailable: connection refused","created_at":"2026-09-28T12:00:00Z"}]},"schema":{"$ref":"#/components/schemas/BackofficeDeadLetterList"}}}},"400":{"description":"`limit` is not an integer. axum Query rejection: plain-text body, not a Problem."},"403":{"$ref":"#/components/responses/OperatorsOnly"},"500":{"description":"The outbox could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"No database configured (Problem), or the identity provider is unreachable (plain text)."}}}},"/backoffice/events/dead-letters/{id}/requeue":{"post":{"tags":["events"],"summary":"Put a dead letter back in the queue","description":"Attempts cleared; the relay sends it like any other. Audited.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Back in the queue","content":{"application/json":{"example":{"id":"01a0e88f-2b58-7e09-094e-9720341ea93d","state":"pending"},"schema":{"$ref":"#/components/schemas/BackofficeDeadLetterRequeued"}}}},"400":{"description":"`id` is not a UUID (axum Path rejection, plain text), or the caller has several tenant memberships (Problem)."},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No dead letter with that id for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"No database configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/loans/mine":{"get":{"tags":["lending"],"summary":"The caller's own drawn loans, with where each stands today","description":"Every figure is derived at the moment of asking from the schedule frozen at disbursement and the repayments received. The platform holds no balance, so a client that cached one would cache a number that changes at midnight with no write.\n","responses":{"200":{"description":"Loans, each with a derived position or a statement that it is not drawn","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyLoans"}}}}}}},"/loans/{id}/schedule":{"get":{"tags":["lending"],"summary":"One loan's instalments, each marked paid, part-paid or due","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The frozen schedule and the position derived from it"},"404":{"description":"Not the caller's loan. Never 403 - saying an id exists is saying an id exists"},"409":{"description":"The loan has not been drawn, so it has no schedule yet"}}}},"/backoffice/loans/disbursements":{"post":{"tags":["lending","back-office"],"summary":"Propose drawing a loan","description":"Proposes; does not disburse. A second person approves, and approving freezes the schedule, records the day and - with `to_account` named and the bank's lending account configured (`PLATFORM_LENDING_ACCOUNT`) - pays the principal into it through the core under that approval, keyed by it. `to_account` must be one of the borrower's own accounts in the loan's currency, checked against the core when proposed and again when carried out; any other is refused with 400. Without it the approval records the day and its execution record says `money_moved: false`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["application_id","note"],"properties":{"application_id":{"type":"string","format":"uuid"},"note":{"type":"string","minLength":10,"description":"Trimmed, char count >= 10, else 400."},"to_account":{"type":"string","nullable":true,"description":"Optional; must be one of the borrower's core accounts in the loan's currency, else 400."}}},"example":{"application_id":"01a0d3b6-95d6-76fe-9497-a001c02eb20a","note":"draw into the borrower's account","to_account":"1"}}}},"responses":{"202":{"description":"Proposed. Nothing is drawn until a second person agrees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeLoanDisbursementProposal"}}}},"400":{"description":"No reason given, or an account that is not the borrower's"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/loans/repayments":{"post":{"tags":["lending","back-office"],"summary":"Propose recording a repayment received","description":"`received_on` is the day the money arrived, not today. An operator who could record a repayment alone could clear a customer's arrears alone, so this proposes. With `from_account` - one of the borrower's own accounts, checked against the core - approving it takes the amount from that account into the bank's lending account through the core. Without it, the money came from outside (a rail, a branch) and is recorded as received.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["application_id","amount_minor","received_on","note"],"properties":{"application_id":{"type":"string","format":"uuid"},"amount_minor":{"type":"integer","format":"int64","minimum":1,"description":"<= 0 is 400. The currency is the agreement's, never the request's."},"received_on":{"type":"string","format":"date","description":"The day the money arrived. After today (UTC) is 400."},"external_ref":{"type":"string","nullable":true},"note":{"type":"string","minLength":10,"description":"Trimmed, char count >= 10, else 400."},"from_account":{"type":"string","nullable":true,"description":"One of the borrower's own core accounts, in the loan's currency (checked against the core; any failure is 400). Absent/null = money from outside, recorded as received.\n"}}},"example":{"application_id":"01a0d3b6-95d6-76fe-9497-a001c02eb20a","amount_minor":350,"received_on":"2026-09-24","note":"first instalment from the borrower's account","from_account":"1"}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposed"}}}},"400":{"description":"Nothing to repay, a day that has not happened, or an account that is not the borrower's","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such loan application in this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The loan has not been drawn, so there is nothing to repay.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/loans/arrears":{"get":{"tags":["lending","back-office"],"summary":"Loans in arrears, worst first","description":"Ordered by days past due and not by amount. A customer three months behind on a small loan is a worse problem than one three days behind on a large one.\n","responses":{"200":{"description":"Late loans, longest overdue first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeArrearsQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/exchange/completions":{"post":{"tags":["exchange","back-office"],"summary":"Propose completing an exchange left half done","description":"For a quote whose money out moved and whose money in did not. Proposes the missing leg; a different person approves it. The executor first asks the core whether the leg exists after all — its reference starts with the quote's id and `:in` — and records it if so. Otherwise it moves the buy amount from the bank's account in the buy currency to the customer's buy account under that approval, keyed by the quote and the approval, never under the original leg's key, whose refusal the core may have cached. Only a half-done quote can be proposed; any other is 409.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quote_id","note"],"properties":{"quote_id":{"type":"string","format":"uuid"},"note":{"type":"string","minLength":10,"description":"Trimmed, char count >= 10, else 400."}}},"example":{"quote_id":"01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01","note":"bank USD account refunded, completing the customer's exchange"}}}},"responses":{"202":{"description":"Proposed. Nothing moves until a second person approves","content":{"application/json":{"example":{"approval_id":"01992d20-1a2b-7c3d-8e4f-5a6b7c8d9e02","state":"proposed"},"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposed"}}}},"400":{"description":"No reason given","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a quote in this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The quote is not half done — finished, never started, or refused — or a completion for it is already waiting for a second person\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/transfers":{"get":{"tags":["transfers","back-office"],"summary":"Transfers held for a person to decide about","description":"Ordered by how long the customer has waited, not by amount. Each row names the assessment that stopped it so an analyst reads the reasons rather than guessing.\n","responses":{"200":{"description":"Held transfers, longest waiting first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeRiskHeldTransferQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/transfers/{id}/resolution":{"post":{"tags":["transfers","back-office"],"summary":"Propose releasing or refusing a held transfer","description":"Releasing held funds is named by the platform’s rules as something no operator completes alone, so this proposes and a second person approves. A refusal must say why; the customer is entitled to the reason.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Proposed. The transfer stays held until a second person agrees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"decision is not release|refuse, or note shorter than 10 trimmed chars.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"The id is not among the (first 500) held transfers of this tenant; it may already have been resolved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decision","note"],"properties":{"decision":{"type":"string","enum":["release","refuse"],"description":"Anything else is 400."},"note":{"type":"string","minLength":10,"description":"Trimmed, char count >= 10, else 400. Stored trimmed as the approval subject's `why` (for a refusal it is the reason the customer is given).\n"}}}}}}}},"/backoffice/funding/{id}/credit":{"post":{"tags":["crypto","back-office"],"summary":"Propose crediting a funding that has settled","description":"The state the platform could not previously express: funding could be started and failed, and nothing recorded the money arriving, so a converted deposit sat unconverted for ever. Crediting a customer is money moving, so this proposes and a second person approves.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Proposed. The customer is not credited until a second person agrees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"credited_minor <= 0, or note shorter than 8 trimmed BYTES.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such funding attempt in this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The funding is not at a stage that can be credited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["credited_minor","note"],"properties":{"credited_minor":{"type":"integer","format":"int64","minimum":1,"description":"What the customer is credited, in their currency's minor units. <= 0 is 400."},"note":{"type":"string","minLength":8,"description":"Trimmed BYTE length (str::len) >= 8, else 400."}}}}}}}},"/accounts":{"get":{"tags":["accounts"],"summary":"Every account the caller holds","description":"Figures come from the accounting core, relayed. Where the core cannot answer, the balance field is absent rather than zero - a screen can recover from \"we could not reach your balance\" and cannot recover from being told there is nothing there.\n","responses":{"200":{"description":"Accounts, each with its provenance and a balance that may be absent","content":{"application/json":{"example":[{"id":"4","name":"OxaBank Account","kind":"savings","currency":"USD","balance_minor":113833,"provenance":"live","account_number":"000002000000","routing_number":"121145433"},{"id":"9","name":"OxaBank EUR Account","kind":"savings","currency":"EUR","balance_minor":0,"provenance":"live","iban":"GB45TCCL00000000000001","bic":"TCCLGB31"},{"id":"12","name":"Global Legal Entity Identifier Foundation","kind":"business","currency":"USD","balance_minor":400000,"provenance":"live","business":"Global Legal Entity Identifier Foundation","business_role":"payer"}],"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountView"}}}}}}}},"/accounts/usd":{"post":{"tags":["accounts"],"summary":"Open the caller's own USD account","description":"Spec 052. For a person the bank opened only as a profile acting for a company. One each: refused when the caller already holds a USD account of their own. Opened in the core on the customer's own confirmation of \"A new USD account\", 0.00 USD, as for a payment.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["method","authenticated_at","evidence_ref","signed_amount","signed_currency","signed_payee"],"properties":{"method":{"type":"string"},"authenticated_at":{"type":"string","format":"date-time"},"evidence_ref":{"type":"string"},"signed_amount":{"type":"string","example":"0.00"},"signed_currency":{"type":"string","example":"USD"},"signed_payee":{"type":"string","example":"A new USD account"},"signature":{"type":"string","nullable":true}}}}}},"responses":{"201":{"description":"Opened","content":{"application/json":{"example":{"account_id":"14","currency":"USD","money_moved":false},"schema":{"type":"object"}}}},"400":{"description":"Not an authentication method, or a confirmation of something else"},"409":{"description":"The bank has not opened the caller, or they already hold a USD account"},"503":{"description":"No core, or no USD product configured"}}}},"/accounts/euro":{"post":{"tags":["accounts"],"summary":"Open the caller's own euro account, with its IBAN, or carry on opening it","description":"Spec 032. One per customer. Opens a EUR account in the core and an account in the customer's name at the provider, both under the platform's own reference, so asking again resumes rather than repeats. Takes the customer's confirmation bound to \"0.00 EUR to A new EUR account\". The holder's name, email, phone and address go to the provider and are not kept by the platform.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EuroAccountOpenBody"},"example":{"first_name":"Ana","last_name":"García","email":"ana@example.com","phone":"+34 600 000 000","address":{"line_1":"Calle Mayor 1","city":"Madrid","postal_code":"28013","country_code":"ES"},"method":"device_biometric","authenticated_at":"2026-10-01T10:00:00Z","evidence_ref":"no-identity-provider:euro-account","signed_amount":"0.00","signed_currency":"EUR","signed_payee":"A new EUR account"}}}},"responses":{"200":{"description":"The account, `open` with its IBAN and BIC, or `opening` with why it stopped","content":{"application/json":{"example":{"id":"9","currency":"EUR","iban":"GB45TCCL00000000000001","bic":"TCCLGB31","account_number":null,"routing_number":null,"state":"open","failure":null},"schema":{"$ref":"#/components/schemas/EuroAccountView"}}}},"400":{"description":"A holder detail that will not do (the answer says which), not an authentication method, a confirmation of something else, or the provider refusing to open for them\n"},"409":{"description":"The caller has no account with the bank to open it beside"},"503":{"description":"Euro accounts are not offered here (no provider or no euro product configured), or the core or the provider did not answer; asking again carries on\n"}}}},"/accounts/us-details":{"post":{"tags":["accounts"],"summary":"Issue the caller's US account details for their dollar account, or show them","description":"Spec 037. Column issues an account number under the bank's own Column account, once per customer, under the platform's own reference, so asking again shows the same number. Takes the customer's confirmation bound to \"0.00 USD to Your US account details\". Column is sent no holder's details. Money that arrives by ACH or wire is proposed as a credit and credited on a second person's approval.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["method","authenticated_at","evidence_ref","signed_amount","signed_currency","signed_payee"],"properties":{"method":{"type":"string"},"authenticated_at":{"type":"string","format":"date-time"},"evidence_ref":{"type":"string"},"signed_amount":{"type":"string"},"signed_currency":{"type":"string"},"signed_payee":{"type":"string"},"signature":{"type":"string","description":"With method device_key, the device key's signature over oxabank-confirm/1, the kind, the id, the amount, the currency and the payee, one per line; base64, DER or r and s; the kind is us_details and the id -. With method passkey, the WebAuthn assertion as four base64url parts joined by dots: the credential id, the authenticator data, the client data JSON and the signature; its challenge is SHA-256 of that same message."}}},"example":{"method":"device_biometric","authenticated_at":"2026-10-01T10:00:00Z","evidence_ref":"no-identity-provider:us-details","signed_amount":"0.00","signed_currency":"USD","signed_payee":"Your US account details"}}}},"responses":{"200":{"description":"The details, `open` once issued, or `opening` with why they are not yet","content":{"application/json":{"example":{"id":"4","currency":"USD","iban":null,"bic":null,"account_number":"000002000001","routing_number":"121145433","state":"open","failure":null},"schema":{"$ref":"#/components/schemas/EuroAccountView"}}}},"400":{"description":"Not an authentication method, a confirmation of something else, or Column refusing"},"409":{"description":"The caller has no account with the bank, or no dollar account yet"},"503":{"description":"US account details are not offered here (no Column configured), or the core or Column did not answer; asking again carries on\n"}}}},"/accounts/{id}/days":{"get":{"tags":["accounts"],"summary":"What the caller's account held at the end of each closed day, newest first","description":"Spec 033. Each balance is the core's answer when the bank closed the day, kept with the day and the subject it belonged to; the last 90 closed days. Somebody else's account, or one with no closed day, answers an empty list. `closing_minor` is null where the core could not answer for the account at the close.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The closed days","content":{"application/json":{"example":{"days":[{"day":"2026-09-30","closing_minor":112833,"currency":"USD"}]},"schema":{"$ref":"#/components/schemas/ClosingBalanceList"}}}}}}},"/accounts/{id}/statement":{"get":{"tags":["accounts"],"summary":"One account's statement, newest first","description":"The account's own movements as the core booked them — never the general ledger. `amount_minor` is signed from this account's point of view. A transfer between two accounts the same customer holds appears on both, negative on one and positive on the other. A reversed movement, a hold and a hold's release are not listed.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/Offset"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"Movements, each with the day it was booked and the platform's reference where the platform made it","content":{"application/json":{"example":[{"id":"75","posted_on":"2026-09-29","amount_minor":-100,"currency":"USD","reference":"01a0ed3d-9514-7369-9754-6a0de877aa82","provenance":"live"},{"id":"70","posted_on":"2026-09-28","amount_minor":5000000,"currency":"USD","reference":null,"provenance":"live"}],"schema":{"type":"array","items":{"$ref":"#/components/schemas/StatementEntryView"}}}}},"404":{"description":"Not the caller's account. Never 403"}}}},"/accounts/{id}/statement.pdf":{"get":{"tags":["accounts"],"summary":"One month of an account, as a PDF to keep","description":"Spec 060. Any account the caller may act on, for one calendar month: its opening and closing balances, what came in and went out, and every movement with the balance after it. Worked out from the core's balance today and the movements since, rendered by Typst; nothing is kept, so the same month asked twice is the same document.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"month","in":"query","required":true,"schema":{"type":"string","pattern":"^[0-9]{4}-[0-9]{2}$"},"example":"2026-10"}],"responses":{"200":{"description":"The statement","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Not a month","or one not begun":null},"404":{"description":"Not an account the caller may act on"},"503":{"description":"The core could not be read"}}}},"/backoffice/cards":{"get":{"tags":["cards","back-office"],"summary":"Cards issued in this tenant","responses":{"200":{"description":"Cards, with state and expiry. Never a card number (Principle VIII)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CardView"}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/cards/limits":{"post":{"tags":["cards","back-office"],"summary":"Propose changing a card's spending limits","description":"Proposes; does not change. An operator who could raise a customer's limit alone is the control not existing, and raising a limit is exactly what somebody who has taken over an account wants done.\n","responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"reason shorter than 8 trimmed BYTES, or a resulting limit is negative.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such card in this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["card_id","reason"],"properties":{"card_id":{"type":"string","format":"uuid"},"per_transaction":{"type":"integer","format":"int64","minimum":0,"nullable":true,"description":"Absent = unchanged; null = no limit (allowed on this operator path); negative = 400."},"daily":{"type":"integer","format":"int64","minimum":0,"nullable":true,"description":"Absent = unchanged; null = no limit; negative = 400."},"monthly":{"type":"integer","format":"int64","minimum":0,"nullable":true,"description":"Absent = unchanged; null = no limit; negative = 400."},"reason":{"type":"string","minLength":8,"description":"Trimmed BYTE length >= 8, else 400. Stored UNtrimmed on the proposal."}}}}}}}},"/backoffice/cards/holds/expire":{"post":{"tags":["cards","back-office"],"summary":"Release holds nobody resolved","description":"Not optional. A capture or a release is a message and messages are lost; without this one dropped message holds a customer's money until somebody notices, and nobody notices.\n","responses":{"200":{"description":"What was released","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyHoldsExpired"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/fraud":{"get":{"tags":["fraud","back-office"],"summary":"Payments flagged and not yet looked at, oldest first","description":"Oldest rather than highest-scoring: a held payment is somebody standing still. The health block reports how many were decided with no answer from the provider, and a false-positive rate that is absent rather than zero when nothing has been reviewed.\n","responses":{"200":{"description":"Open assessments and the queue's health","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeFraudQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/fraud/{id}":{"get":{"tags":["fraud","back-office"],"summary":"One assessment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The assessment, including whether it can still be stopped","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeFraudAssessment"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such assessment in this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["fraud","back-office"],"summary":"Record what an analyst found","description":"A verdict on a payment that already settled records a loss; it does not reach into the past and stop it. The response says which of the two happened, because the word on the button and what it did are not the same thing.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The verdict and its consequence","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeFraudVerdictResult"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"Somebody else reviewed this first"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["verdict","note"],"properties":{"verdict":{"type":"string","enum":["legitimate","fraudulent"]},"note":{"type":"string","minLength":10,"description":"Trimmed, char count >= 10 (fraud::MINIMUM_NOTE; DB CHECK agrees), else 400."}}}}}}}},"/cards":{"get":{"tags":["cards"],"summary":"The caller's own cards","description":"State, expiry, limits, and what each card has spent and is holding today. Never a card number, a verification value or track data: none of the three is representable anywhere under `platform/` (Principle VIII), so this is not a field a client may ask for and be refused - it does not exist to ask for.\n","responses":{"200":{"description":"Cards, each with its state and the room left under its limits","content":{"application/json":{"example":[{"id":"8c2e5a10-9d33-4b72-8f01-6e7a9b0c1d2e","status":"active","status_detail":"Ready to use","last_four":"4417","network":"visa","expires_on":"2029-09-30","residency":"virtual","currency":"USD","actions":["freeze","cancel"],"limits":{"per_transaction":500000,"daily":1000000,"monthly":5000000,"remaining_today":955500,"remaining_this_month":4870000},"provenance":"live","business_id":"01a10d4f-03dd-70dd-9c93-e83b3cfa4e7c"}],"schema":{"type":"array","items":{"$ref":"#/components/schemas/CardView"}}}}}}},"post":{"tags":["cards"],"summary":"Get a card, for one of one's own accounts","description":"Issued by the card issuer for one of the caller's own core accounts, in its currency. The platform stores the issuer's token and the last four — never a card number (Principle VIII) — and the card starts `issued`, for the customer to activate. At most five cards not cancelled. With no issuer connected, 503 and nothing is created. A sandbox issuer's card is `provenance: synthetic`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_ref","form","cardholder_name"],"properties":{"account_ref":{"type":"string"},"form":{"type":"string","enum":["virtual","physical"]},"cardholder_name":{"type":"string"},"currency":{"type":"string","nullable":true,"description":"Defaults to the account's"}}},"example":{"account_ref":"4","form":"virtual","cardholder_name":"ANA RIVERA"}}}},"responses":{"201":{"description":"Issued","content":{"application/json":{"example":{"id":"01a0e8c0-2b58-7e09-894e-9720341ea93d","status":"issued","status_detail":"this card has not been activated yet","last_four":"0025","network":"visa","expires_on":"2029-09-30","residency":"delegated","currency":"USD","actions":["activate","cancel"],"limits":{"per_transaction":null,"daily":null,"monthly":null,"remaining_today":null,"remaining_this_month":null},"provenance":"synthetic"},"schema":{"$ref":"#/components/schemas/CardView"}}}},"400":{"description":"Not one of the caller's accounts","another currency":null,"or not a card form":null},"409":{"description":"Five cards already","or the issuer refused":null},"503":{"description":"No card issuer is connected","or it could not be reached":null}}}},"/cards/{id}/actions":{"post":{"tags":["cards"],"summary":"Activate, freeze, unfreeze or cancel one's own card","description":"The cardholder is the authoriser and no second person is involved (Principle II). Freezing only ever removes capability and is what somebody reaches for when their bag is gone; cancelling is terminal and also theirs, because it is their card. A transition that does not apply is refused saying so, never silently ignored.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["activate","freeze","unfreeze","cancel"]}}}}}},"responses":{"200":{"description":"The card as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardView"}}}},"400":{"description":"Not something that can be done to a card"},"404":{"description":"Not the caller's card. Never 403 - saying an id exists is saying an id exists"},"409":{"description":"The transition does not apply from the state the card is in"}}}},"/cards/{id}/limits":{"post":{"tags":["cards"],"summary":"Lower one's own spending limits","description":"Lowering only. Raising is an operator decision under maker-checker at `/backoffice/cards/limits`, because a limit the customer can raise is not a limit - and the customer whose phone has been taken is exactly the one it protects. An omitted field keeps its current value. Removing a limit is raising it to infinity and is refused as a raise.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardLimitsBody"}}}},"responses":{"200":{"description":"The card with its new limits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CardView"}}}},"400":{"description":"The change raises a limit, or the three are not consistent with one another"},"404":{"description":"Not the caller's card"}}}},"/loans":{"get":{"tags":["lending"],"summary":"The caller's own loan applications, at whatever stage each has reached","description":"`decision` is what the credit policy decided and stays so after a referral is decided by a person; that person's decision is `operator_decision`, null until then, and carries what was decided and why but never the members of staff who proposed and approved it. `decision.operator_note` is never sent here.\n","responses":{"200":{"description":"Applications, each with its stage and the reason where it was declined","content":{"application/json":{"example":[{"id":"b41d7e88-0c25-4f39-a6d0-5e8f7a2b3c14","state":"under_review","awaiting_us":true,"is_final":false,"amount_minor":4500000,"currency":"USD","term_months":24,"purpose":"A used car","decision":null,"operator_decision":null,"offer":null,"core_loan_ref":null,"created_at":"2026-09-18T08:30:00Z","your_moves":["withdraw"]},{"id":"01926f3a-8c21-7d3f-8e52-6b7c8d9e0f1a","state":"approved","awaiting_us":true,"is_final":false,"amount_minor":13000000,"currency":"USD","term_months":60,"purpose":null,"decision":{"outcome":"referred","reasons":[{"code":"more_than_a_year_of_income","detail":"more than a year of income"}],"provider":"zen","policy_version":"sha256:90c2377d6a57d781b19ce19d921a4927f0ca0ef3d35f021e744b232eb905e8b6","provenance":"synthetic","decided_at":"2026-09-28T12:00:00Z"},"operator_decision":{"decision":"approve","reason":"income verified against three payslips","decided_at":"2026-09-28T13:00:00Z"},"offer":null,"core_loan_ref":null,"created_at":"2026-09-28T12:00:00Z","your_moves":["withdraw"]}],"schema":{"type":"array","items":{"$ref":"#/components/schemas/LoanApplicationView"}}}}}}},"post":{"tags":["lending"],"summary":"Apply for a loan","description":"Submitted straight away rather than saved as a draft: a draft nobody submits is a state the app has to explain, and an app can hold an unsent form perfectly well by itself. The request is quoted before it is stored, so one that cannot be scheduled is refused with the reason rather than accepted and found unworkable later. Affordability is the platform's own check and runs before anything external is asked.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_minor","currency","term_months","monthly_income_minor"],"properties":{"amount_minor":{"type":"integer","format":"int64","minimum":1},"currency":{"type":"string"},"term_months":{"type":"integer","format":"int32","minimum":1,"maximum":480},"monthly_income_minor":{"type":"integer","format":"int64"},"monthly_obligations_minor":{"type":"integer","format":"int64","default":0},"purpose":{"type":"string","nullable":true},"credit_check":{"type":"object","nullable":true,"required":["consent","consent_version","subject"],"properties":{"consent":{"type":"boolean","description":"false is accepted and means no bureau call"},"consent_version":{"type":"string"},"subject":{"type":"object","required":["given_names","first_surname","second_surname","birth_date","tax_id","address"],"properties":{"given_names":{"type":"string"},"first_surname":{"type":"string"},"second_surname":{"type":"string"},"birth_date":{"type":"string","format":"date"},"tax_id":{"type":"string"},"address":{"type":"object","required":["street","neighbourhood","municipality","city","region","postcode"],"properties":{"street":{"type":"string"},"neighbourhood":{"type":"string"},"municipality":{"type":"string"},"city":{"type":"string"},"region":{"type":"string"},"postcode":{"type":"string"}}}}}}}}}}}},"responses":{"202":{"description":"Decided once, inside this request, by the credit decision provider: `approved` with the policy's figures and an offer priced from them (`indicative: false`, the principal capped at what was asked); `declined` with the policy's reasons; or `referred` to a person. Every way the provider can fail is a referral that names it — `no_decision_provider` (none configured), `decision_unavailable`, `decision_unusable`, `priced_offer_unaffordable` — never an approval or a decline. An approval moves no money: the loan is paid out only when an operator proposes the disbursement and a second person approves it. Every decision names its provider, the policy's version and its provenance (`synthetic` for the example policy).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoanApplicationView"},"examples":{"approved":{"summary":"Approved by the example policy, priced, nothing moved","value":{"id":"01926f3a-7b10-7c2e-9d41-5a6b7c8d9e0f","state":"approved","awaiting_us":true,"is_final":false,"amount_minor":10000000,"currency":"USD","term_months":24,"purpose":"A used car","decision":{"outcome":"approved","reasons":[{"code":"within_example_policy","detail":"within the example credit policy"}],"provider":"zen","policy_version":"sha256:90c2377d6a57d781b19ce19d921a4927f0ca0ef3d35f021e744b232eb905e8b6","provenance":"synthetic","figures":{"limit_minor":10000000,"rate_percent":"18.5","max_term_months":36},"decided_at":"2026-09-28T12:00:00Z","bureau":{"bureau":"Círculo de Crédito (sandbox-stub)","outcome":"scored","score":712,"score_name":"FICO Score","reasons":["D8","K0","F1"],"inquiry_ref":"930001","consent":{"given_at":"2026-09-28T12:00:00Z","version":"circulo-consulta-v1"}}},"operator_decision":null,"offer":{"principal_minor":10000000,"term_months":24,"instalment_minor":501661,"total_repayable_minor":12039841,"total_interest_minor":2039841,"annual_rate_percent":"18.5","indicative":false},"core_loan_ref":null,"created_at":"2026-09-28T12:00:00Z","your_moves":["withdraw"]}},"declined":{"summary":"Declined by the policy, with its reason","value":{"state":"declined","decision":{"outcome":"declined","reasons":[{"code":"obligations_too_high","detail":"existing obligations are more than 40% of monthly income"}],"provider":"zen","policy_version":"sha256:4f2a…","provenance":"synthetic","decided_at":"2026-09-28T12:00:00Z"},"your_moves":[],"awaiting_us":false,"is_final":true,"id":"01a0e1c2-0000-7000-8000-00000000d0c1","amount_minor":10000000,"currency":"USD","term_months":24,"purpose":"A used car","created_at":"2026-09-28T12:00:00.000Z","core_loan_ref":null,"offer":null}},"referred_no_provider":{"summary":"No provider configured — a person decides","value":{"state":"referred","decision":{"outcome":"referred","reasons":[{"code":"no_decision_provider","detail":"No credit decision provider is configured, so a person decides."}],"provider":"none","policy_version":null,"provenance":"unavailable","decided_at":"2026-09-28T12:00:00Z"},"your_moves":["withdraw"],"awaiting_us":true,"is_final":false,"id":"01a0e1c2-0000-7000-8000-000000000e01","amount_minor":10000000,"currency":"USD","term_months":24,"purpose":"A used car","created_at":"2026-09-28T12:00:00.000Z","core_loan_ref":null,"offer":null}},"referred_unusable":{"summary":"The policy answered something that is not a decision","value":{"state":"referred","decision":{"outcome":"referred","reasons":[{"code":"decision_unusable","detail":"We could not decide this automatically, so a person will look at it."}],"provider":"zen","policy_version":"sha256:9c1d…","provenance":"live","decided_at":"2026-09-28T12:00:00Z"},"your_moves":["withdraw"],"awaiting_us":true,"is_final":false,"id":"01a0e1c2-0000-7000-8000-000000000e02","amount_minor":10000000,"currency":"USD","term_months":24,"purpose":"A used car","created_at":"2026-09-28T12:00:00.000Z","core_loan_ref":null,"offer":null}}}}}},"400":{"description":"The request cannot be scheduled, and the answer says which part of it cannot"}}}},"/loans/{id}/agreement":{"get":{"tags":["lending"],"summary":"A person's loan agreement, to read and to sign","description":"Spec 075. Rendered and kept with its SHA-256 the first time an approved loan's is asked for, naming the account it is paid into (the person's own account in the loan's currency). 409 until the loan is approved, or when the person holds no account in its currency.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The agreement","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoanAgreementView"}}}},"404":{"description":"Not the caller's loan"},"409":{"description":"Not approved yet, or no account in the loan's currency"}}}},"/loans/{id}/agreement/sign":{"post":{"tags":["lending"],"summary":"Sign the loan agreement with the device's confirmation","description":"Spec 075. The confirmation binds the amount and the document's SHA-256 (`authorize`), checked as a payment's is: strong, fresh, the caller's. The loan becomes `accepted` (your_moves' `accept` is this), the signed copy is kept, and only now can it be paid out.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Signed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoanAgreementView"}}}},"400":{"description":"The confirmation does not bind this document","or is not strong or fresh":null},"404":{"description":"Not the caller's loan"},"409":{"description":"Signed already, or not an approved loan"}}}},"/loans/{id}/withdraw":{"post":{"tags":["lending"],"summary":"Withdraw one's own application","description":"Theirs to do at any point before the money moves. It is the customer's own decision about their own application, so no second person is involved (Principle II).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The application, now withdrawn","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoanApplicationView"}}}},"404":{"description":"Not the caller's application. Never 403"},"409":{"description":"The application is past the point where it can be withdrawn"}}}},"/loans/quote":{"post":{"tags":["lending"],"summary":"What would this cost?","description":"No application, no record and no credit footprint. A customer working out whether they can afford something has not applied for anything. Give an income and the answer includes whether it is affordable and the arithmetic behind that; omit it and the answer is a schedule and nothing more. The rate is indicative and says so (Principle V).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount_minor","currency","term_months"],"properties":{"amount_minor":{"type":"integer","format":"int64","minimum":1},"currency":{"type":"string"},"term_months":{"type":"integer","format":"int32","minimum":1,"maximum":480},"monthly_income_minor":{"type":"integer","format":"int64","nullable":true},"monthly_obligations_minor":{"type":"integer","format":"int64","nullable":true}}}}}},"responses":{"200":{"description":"The instalment, the total and, where an income was given, the affordability","content":{"application/json":{"example":{"instalment_minor":213480,"total_repayable_minor":5123520,"total_interest_minor":623520,"annual_rate_percent":"12.9","indicative":true,"instalments":[],"affordability":{"affordable":true,"detail":"The instalment is 28% of what is left after your other commitments"}},"schema":{"$ref":"#/components/schemas/LoanQuote"}}}},"400":{"description":"The request cannot be scheduled"}}}},"/backoffice/documents/{id}/pdf":{"get":{"tags":["lending","back-office"],"summary":"Any document in the tenant, as read and signed or the signed copy","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"signed","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"The PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such document","or not signed yet":null}}}},"/backoffice/credit-lines":{"get":{"tags":["lending","back-office"],"summary":"Every credit line the bank opened, and what is proposed for it","description":"Spec 059. Open, closed and charged-off lines, the newest first, each with the core's figures, how many days its oldest payment is late on the bank's day, its autopay, the proposal waiting on it and the last ones, and why it cannot be charged off now.\n","responses":{"200":{"description":"The lines","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeCreditLines"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/credit-lines/{id}/{action}":{"post":{"tags":["lending","back-office"],"summary":"Propose a line's new limit, closing it, or charging it off","description":"Spec 059. **This decides nothing.** One operator proposes with a reason (credit_line_limit, credit_line_closing, credit_line_charge_off); a second approves, and it is carried out against where the line stands then. One proposal waits on a line at a time. A new limit is at least 100.00 USD, on an open line; closing is of an open line; a charge-off needs the oldest payment PLATFORM_CHARGE_OFF_DAYS late, checked now and again when approved.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"action","in":"path","required":true,"schema":{"type":"string","enum":["limit","close","charge-off"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":8},"limit_minor":{"type":"integer","minimum":10000,"description":"For a new limit"}}}}}},"responses":{"201":{"description":"Proposed, awaiting a second person","content":{"application/json":{"schema":{"type":"object","required":["approval_id","state","subject"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]},"subject":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"No reason","or a limit under the least or the same":null},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such line or action"},"409":{"description":"Not open, not late enough, or a proposal already waits on it"},"503":{"description":"The core did not answer"}}}},"/backoffice/loans":{"get":{"tags":["lending","back-office"],"summary":"Applications waiting on the bank, oldest first","description":"Carries `waiting_longest_since` as its own field: it is the figure an SLA is measured against, and leaving the console to compute it from a list it might page through is how a queue quietly stops being measured.\n","responses":{"200":{"description":"The referral queue, its size, and how long the oldest has waited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeLoanQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/loans/moves":{"get":{"tags":["lending","back-office"],"summary":"A person's term loans still on the platform's schedule","description":"Spec 072, US6. Each drawn, open person's loan the core does not keep yet, with what it owes by the platform's schedule, how late it is, and what the last try to move it found. `ready` says whether a term loan product is configured in the core.\n","responses":{"200":{"description":"`ready` and `loans`","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["lending","back-office"],"summary":"Propose moving the loans into the core","description":"Spec 072, the platform’s rule. A credit officer proposes, naming the loans or all of them; a second person within their authority over what the loans owe together approves (`loan_move`). Each loan is replayed in the core — opened on its offer, paid out and repaid on its own days, both booked — and compared instalment by instalment, to the cent; equal, the core keeps it; not, it is taken back whole and stays as it was, with the difference. Once per loan.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"loan_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"No reason"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"Nothing to move, or a loan named that cannot be"},"503":{"description":"No term loan product in the core"}}}},"/backoffice/credit-lines/{id}/opening":{"post":{"tags":["lending","back-office"],"summary":"Propose opening a company's signed line","description":"Spec 072. Once the company's people signed its agreement and any guarantee asked is signed, a credit officer proposes; a second person within their authority over its limit opens it in the core (`credit_line_opening`), naming the guarantor there where the core knows them.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["note"],"properties":{"note":{"type":"string","minLength":10}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such company line"},"409":{"description":"Not signed, or opened already"}}}},"/backoffice/settings/lending":{"get":{"tags":["settings","back-office"],"summary":"How long a company's offer stays open for its signatures","description":"Spec 072, the platform’s rule. 30 days while the bank has set nothing.","responses":{"200":{"description":"`offer_days` and whether the bank `set` them","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["settings","back-office"],"summary":"Propose a new period for a company's offer","description":"Spec 072, the platform’s rule. A credit officer proposes; a supervisor approves (`lending_setting`). Offers decided after it take it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["offer_days","reason"],"properties":{"offer_days":{"type":"integer","minimum":1,"maximum":365},"reason":{"type":"string","minLength":8}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"A period out of range, or no reason"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"The same change already waits"}}}},"/backoffice/loans/decisions":{"post":{"tags":["lending","back-office"],"summary":"Propose approving or declining a referred application","description":"**This decides nothing.** It raises a proposal a different person approves (Principle II). The referral queue is exactly where a single-signature approval would be abused: it is the pile of applications an automated decision already declined to approve. A reason is required and a token one is refused, because an adverse-action notice has to be produced from the reasons actually given.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["application_id","decision","reason"],"properties":{"application_id":{"type":"string","format":"uuid"},"decision":{"type":"string","enum":["approve","decline"]},"reason":{"type":"string","minLength":8,"description":"Trimmed, byte length (str::len) >= 8, else 400."},"amount_minor":{"type":"integer","description":"Spec 072. With `rate_percent` and `term_months`, the terms an approval is offered at, priced from the first of next month; required to approve a company's referral, which has no priced offer. All three or none.\n"},"rate_percent":{"type":"string","example":"16.5"},"term_months":{"type":"integer","minimum":1,"maximum":120},"guarantee_required":{"type":"boolean","description":"Spec 072, the platform’s rule. A guarantee asked of one of the company's people."},"guarantee_minor":{"type":"integer","description":"For this amount, at most the loan's; the whole debt when absent."}}}}}},"responses":{"202":{"description":"Proposed. Nothing is decided until a second person agrees; when they do, the application moves and the decision is written as its `operator_decision`, leaving the policy's `decision` as it was\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeLoanProposal"}}}},"400":{"description":"Not a decision, or a reason too thin to produce a notice from"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/onboarding":{"get":{"tags":["onboarding"],"summary":"Where the caller is up to","description":"The state is derived from what is outstanding and today's date, never stored, so evidence that expires becomes outstanding again with no job running. Checks the bank performs on its own are not listed as steps for the applicant.\n","responses":{"200":{"description":"What is outstanding, what has expired, and what happens next","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingJourney"}}}}}},"post":{"tags":["onboarding"],"summary":"Begin, or say plainly that we cannot","description":"Eligibility is checked **before** anything is asked. Refusing somebody after they have photographed a passport is worse than refusing them before, and it is the same refusal. A refusal is recorded with its reason so support can retrieve it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["country","age_years"],"properties":{"country":{"type":"string","description":"ISO-3166 alpha-2 (not validated)"},"age_years":{"type":"integer","format":"int32","minimum":0,"maximum":129},"expected_monthly_minor":{"type":"integer","format":"int64","minimum":0,"default":0},"own_account":{"type":"boolean","default":true,"description":"Spec 052. A dollar account of their own when the bank opens them, or (false) only a profile that acts for a company they were invited to; they may open one later with `POST /accounts/usd`\n"}}}}}},"responses":{"200":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingJourney"}}}},"201":{"description":"Started, with the first outstanding requirement","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingJourney"}}}},"400":{"description":"Not eligible, with the reason. Recorded, not merely returned"},"409":{"description":"A journey is already open for this caller"}}}},"/onboarding/steps/{kind}":{"post":{"tags":["onboarding"],"summary":"Submit one step of the journey","description":"Records the step as **submitted**, never as satisfied. Supplying a document and somebody checking it are two events and this route is only the first. A kind the bank performs itself is refused saying so - \"that one is ours to do, not yours\" - which is the platform’s rule enforced at the door rather than only in the list the applicant is shown.\n","parameters":[{"name":"kind","in":"path","required":true,"schema":{"type":"string","enum":["identity","address","tax_residency","source_of_funds","terms_accepted"]},"description":"`screening` parses but is refused: it is the bank's own check, not the customer's step. `identity` is refused too now: it is submitted with the document, at /onboarding/identity\n"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"provider_ref":{"type":"string","nullable":true,"description":"A reference held by whoever verified it. The platform stores no identity document and no document number\n"},"tax_residency":{"type":"array","description":"Spec 076. Required for `tax_residency`: each country the customer is tax resident in (ISO 3166-1 alpha-2, once each) with its tax identification number where they have one. The countries are kept to read; the numbers are sealed under the platform's key.\n","items":{"type":"object","required":["country"],"properties":{"country":{"type":"string","minLength":2,"maxLength":2},"tin":{"type":"string","nullable":true,"maxLength":40}}}}}}}}},"responses":{"200":{"description":"The journey, with this step now submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingJourney"}}}},"400":{"description":"Not a step of this journey, or one the bank performs itself"},"404":{"description":"No journey open for this caller"}}}},"/onboarding/identity":{"post":{"tags":["onboarding"],"summary":"Submit identity — the document's machine-readable zone and the face check's result","description":"Spec 034. The phone reads the zone on the device and sends only its lines; the photo never leaves the phone. The platform reads the lines again itself (ICAO 9303: every check digit and the composite), refuses one that does not read cleanly or has expired, and a face check that did not pass, each with the reason; flags a date of birth that disagrees with the age declared; keeps the details a person needs to decide sealed (the document number to its last four); and records identity as submitted. A person records the verdict.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mrz","liveness"],"properties":{"mrz":{"type":"array","items":{"type":"string"},"description":"Two lines of 44 (a passport) or three of 30 (an identity card)"},"liveness":{"type":"object","required":["passed"],"properties":{"passed":{"type":"boolean"},"reference":{"type":"string","nullable":true}}}}},"example":{"mrz":["P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<","L898902C36UTO7408122F2904157ZE184226B<<<<<18"],"liveness":{"passed":true,"reference":"face-turned-both-ways"}}}}},"responses":{"200":{"description":"The journey, with identity now submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingJourney"}}}},"400":{"description":"The zone did not read cleanly, the document has expired, or the face check did not pass; the answer says which"},"404":{"description":"No journey open for this caller"},"409":{"description":"Identity is not asked of this applicant, or is already satisfied"}}}},"/onboarding/screening":{"post":{"tags":["screening","onboarding"],"summary":"Screen the caller as part of their onboarding","description":"The response says `clear` or `in_review` and that is the whole of it. There is no field here a list name could occupy, which is the point: the customer is never told they matched a watchlist, or which one, by any means. An unreachable list source produces an error and never an empty result.\n","responses":{"200":{"description":"Clear, or in review. Nothing about why","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingScreeningResult"}}}},"404":{"description":"No journey open for this caller"}}}},"/business":{"get":{"tags":["business"],"summary":"The caller's applications for a business account","description":"Spec 039. Each with what the registry said and when, the mismatches with what was declared, how many owners are named, and its state (`draft`, `submitted`, `rejected`, `opened`). The screening's outcome and an operator's reason are not here (tipping-off). `memberships`: every opened company the caller acts for and their role, whoever applied. Spec 052: a company without an LEI carries `registration_number` and `registry_name`; `registry_source` says who described it (`gleif`, `sandbox`, `manual` for an operator's look-up by hand, or `none`) and `provenance` how far to believe it (`live`, `synthetic`, `unavailable`), with `manual_record` once an operator has looked.\n","responses":{"200":{"description":"Newest first","content":{"application/json":{"example":{"applications":[{"id":"01a0fe37-baed-720a-922b-d24c9981b6ad","legal_name":"Global LEI Foundation","country":"CH","lei":"506700GE1G29325QX363","registry":{"lei":"506700GE1G29325QX363","legal_name":"Global Legal Entity Identifier Foundation","entity_status":"ACTIVE","registration_status":"ISSUED","jurisdiction":"CH","registered_as":"CHE200595965","source":"gleif"},"registry_read_at":"2026-10-02T20:04:10Z","mismatches":[{"code":"legal_name","detail":"declared \"Global LEI Foundation\", the registry says \"Global Legal Entity Identifier Foundation\""}],"owners_named":2,"state":"submitted","account_id":null}]},"schema":{"type":"object"}}}}}},"post":{"tags":["business"],"summary":"Start an application for a company, looked up in the registry","description":"Spec 039. The caller is a customer the bank already knows. The LEI's ISO 17442 check digits are read first; the registry (`PLATFORM_REGISTRY`: GLEIF, or the sandbox's) is asked, and its record kept beside the declaration with the mismatches (`legal_name`, `country`, `entity_inactive`, `lei_not_issued`, or `registry_not_asked` where none is configured). Spec 052: a company without an LEI gives `registration_number`, `registry_name` and `registered_address` instead; no registry is asked (`registry_not_asked`), and an operator looks it up by hand (`POST /backoffice/business/{id}/lookup`) before it can be opened.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["legal_name","country"],"properties":{"legal_name":{"type":"string"},"country":{"type":"string","description":"ISO 3166 alpha-2"},"lei":{"type":"string","description":"The company's LEI; or the three that follow"},"registration_number":{"type":"string","description":"Its number in its registry","for a company without an LEI":null},"registry_name":{"type":"string","description":"The registry that issued it","like Companies House":null},"registered_address":{"type":"string"}}},"examples":{"lei":{"value":{"legal_name":"Oxa Sandbox Trading Ltd","country":"US","lei":"5299OXABSANDBOX00111"}},"no_lei":{"value":{"legal_name":"Oxa Corner Shop Ltd","country":"GB","registration_number":"SC123456","registry_name":"Companies House","registered_address":"1 High Street, Edinburgh EH1 1AA"}}}}}},"responses":{"201":{"description":"Started, in `draft`","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"No legal name, not a country, not an LEI, no entity with that LEI at the registry, or neither an LEI nor a whole registration"},"409":{"description":"The caller has no account with the bank, or the company already has a live application"},"503":{"description":"The registry could not be asked"}}}},"/business/{id}/owners":{"put":{"tags":["business"],"summary":"Name the beneficial owners and submit","description":"Spec 039. Each owner owns 25% or more or controls the company, is an adult, and the shares add up to at most 100%; the caller declares they may act for the company. The company and every owner are screened (an alert goes to the screening queue) and the owners are sealed to the application. Refused once the bank has decided.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owners","authorised"],"properties":{"authorised":{"type":"boolean"},"owners":{"type":"array","items":{"type":"object","required":["full_name","date_of_birth","nationality","ownership_percent"],"properties":{"full_name":{"type":"string"},"date_of_birth":{"type":"string","format":"date"},"nationality":{"type":"string"},"ownership_percent":{"type":"integer","minimum":0,"maximum":100},"controls":{"type":"boolean"}}}}}},"example":{"authorised":true,"owners":[{"full_name":"Ana Rivera","date_of_birth":"1980-05-01","nationality":"US","ownership_percent":100}]}}}},"responses":{"200":{"description":"Submitted","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Not authorised, or an owner that will not do (the answer says which)"},"404":{"description":"Not the caller's application"},"409":{"description":"The bank has decided on it"}}}},"/business/{id}/loans":{"get":{"tags":["business"],"summary":"The company's loans and its credit line","description":"Spec 072. Every person of the company sees what it asked for, the policy's answer in its own words, the offer and until when it can be signed, and what the platform read of the company with where each figure came from. Never what an operator wrote.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"`your_role`, `loans` (term loans, newest first) and `line`","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"The caller does not act for that company"}}},"post":{"tags":["business"],"summary":"Ask for a term loan or a credit line for the company","description":"Spec 072, the platform’s rule–the platform’s rule. The company's admins only. The bank's credit policy decides on what the company declares — its turnover over twelve months and what it pays other lenders each month — and what the platform reads of it: its age from its registry's record, its days with the bank, and what came into its accounts here over 90 days (`unavailable` when the core does not answer, never zero). A policy with no rules for a company refers it (`policy_not_for_companies`). An approval, the policy's or a credit officer's on a referral, may ask for a guarantee: `decision.guarantee`, its amount and never the officer's reason. A term loan's offer, and an offered line, can be signed for the bank's offer period, 30 days by default. One application waiting per company; one line.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["product","amount_minor","term_months","account_id","turnover_12m_minor","obligations_monthly_minor"],"properties":{"product":{"type":"string","enum":["term","line"]},"amount_minor":{"type":"integer","minimum":10000},"term_months":{"type":"integer","minimum":1,"maximum":120},"purpose":{"type":"string"},"account_id":{"type":"string"},"turnover_12m_minor":{"type":"integer","minimum":0},"obligations_monthly_minor":{"type":"integer","minimum":0}}},"example":{"product":"term","amount_minor":2000000,"term_months":12,"purpose":"stock","account_id":"88","turnover_12m_minor":60000000,"obligations_monthly_minor":0}}}},"responses":{"201":{"description":"Asked and decided","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"An amount, a term or an account that will not do (the answer says which)"},"403":{"description":"Not one of the company's admins"},"404":{"description":"The caller does not act for that company, or the account is not the company's"},"409":{"description":"The company's account is not open, or it already has a loan waiting or a line"}}}},"/business/{id}/loans/{loan}":{"get":{"tags":["business"],"summary":"One of the company's term loans","description":"Spec 072. As in the list.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The loan","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"The caller does not act for that company, or it has no such loan"}}}},"/business/{id}/loans/{loan}/withdraw":{"post":{"tags":["business"],"summary":"Withdraw what the company asked for, before the money moves","description":"Spec 072. The company's admins; a term loan or an offered or signed line.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Withdrawn","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not one of the company's admins"},"404":{"description":"The caller does not act for that company, or it has no such loan"},"409":{"description":"It can no longer be withdrawn"}}}},"/business/{id}/loans/{loan}/agreement":{"get":{"tags":["business"],"summary":"The company's loan agreement, rendered and kept the first time it is asked for","description":"Spec 072, the platform’s rule. Any of the company's people. The agreement by its SHA-256, what a signature binds (`authorize`), who has signed (`signatures`, by position), who is still needed (`needs`: `first`, `second`, or none) and why a second is (`second_because`: `threshold` or `per_payment`), the guarantee asked and where it stands, and the last day it can be signed.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The agreement","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"The caller does not act for that company, or it has no such loan"},"409":{"description":"There is no offer to sign"}}}},"/business/{id}/loans/{loan}/agreement/sign":{"post":{"tags":["business"],"summary":"Sign the company's loan agreement","description":"Spec 072, the platform’s rule. An admin signs first; at or above the company's threshold, or above that admin's own limit per payment, a second admin or approver — never the first — signs too. Each is that person's own confirmation over the agreement's amount and SHA-256, as a payment's. Once signed whole, and any guarantee asked for is signed, the offer is accepted and waits for the bank to pay it out. Refused after the offer's last day.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Signed; the agreement as it now stands","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The confirmation does not bind this agreement, or is not strong or fresh"},"403":{"description":"Not an admin (first), or not an admin or approver (second)"},"404":{"description":"The caller does not act for that company, or it has no such loan"},"409":{"description":"Signed already by the caller, signed whole, expired, or not to be signed"}}}},"/business/{id}/loans/{loan}/guarantor":{"post":{"tags":["business"],"summary":"Name the guarantor among the company's people","description":"Spec 072, the platform’s rule. An admin, when the policy or a credit officer asked for a guarantee. The person is one the bank has checked and opened; a person still being checked, or turned down, is said only to be unable to guarantee. One named or signed at a time.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["member"],"properties":{"member":{"type":"string","format":"uuid"}}}}}},"responses":{"201":{"description":"Named","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not one of the company's admins"},"404":{"description":"The caller does not act for that company, or no such person or loan"},"409":{"description":"No guarantee is asked, one is named already, or that person cannot guarantee"}}}},"/business/{id}/loans/{loan}/payments":{"post":{"tags":["business"],"summary":"Repay the company's term loan, or draw on or repay its line","description":"Spec 072, US3 and US4, the platform’s rule, the platform’s rule. A payer or an admin asks; nothing moves until they confirm it. A repayment is at most what paying off today takes; a draw at most what is available of the line. `authorize` is what the confirmation binds.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","amount_minor"],"properties":{"kind":{"type":"string","enum":["draw","repayment"]},"amount_minor":{"type":"integer","minimum":1},"account_id":{"type":"string"}}}}}},"responses":{"201":{"description":"Asked, waiting for its payer's confirmation","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not a payer or an admin of the company"},"404":{"description":"The caller does not act for that company, or no such loan or account"},"409":{"description":"More than it owes or than is available, or not paid out or open"}}}},"/business/{id}/loans/{loan}/payments/{payment}/confirm":{"post":{"tags":["business"],"summary":"Confirm a repayment or draw","description":"Spec 072. Its payer's own confirmation, as a payment's. Within their limits and below the company's threshold it goes; across one it waits for a second signature and says which rule (`cosign_reason`). A draw goes only if it still fits under the limit, under a lock on the line.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"payment","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Done, or waiting for a second signature","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The confirmation does not bind it, or is not strong or fresh"},"404":{"description":"Not the caller's"},"409":{"description":"Not waiting for them, no longer fits, or the core refused it"}}}},"/business/{id}/loans/{loan}/payments/{payment}/cosign":{"post":{"tags":["business"],"summary":"Give the second signature","description":"Spec 072. An admin or approver of the company, never its payer; it goes under their authority.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"payment","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Done","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not an admin or approver"},"409":{"description":"Its own payer, not waiting, signed already, or no longer fits"}}}},"/business/{id}/loans/{loan}/payments/{payment}/decline":{"post":{"tags":["business"],"summary":"Decline one waiting for a second signature","description":"Spec 072. An admin or approver, never its payer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"loan","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"payment","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Declined","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not an admin or approver, or its own payer"},"409":{"description":"Not waiting for a second signature"}}}},"/guarantees":{"get":{"tags":["business"],"summary":"What the caller guarantees or is asked to guarantee","description":"Spec 072, the platform’s rule. Theirs to read even after they leave the company.","responses":{"200":{"description":"The guarantees, newest first","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}}}},"/guarantees/{id}":{"get":{"tags":["business"],"summary":"One guarantee, with its pages","description":"Spec 072. Rendered and kept the first time its guarantor asks.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The guarantee","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not the caller's"}}}},"/guarantees/{id}/sign":{"post":{"tags":["business"],"summary":"Sign a guarantee","description":"Spec 072, the platform’s rule. The guarantor's own confirmation over the amount guaranteed and the guarantee's SHA-256. The company's loan is accepted if that was all it waited for.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Signed","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The confirmation does not bind this guarantee, or is not strong or fresh"},"404":{"description":"Not the caller's"},"409":{"description":"Not waiting for their signature"}}}},"/guarantees/{id}/decline":{"post":{"tags":["business"],"summary":"Decline a guarantee","description":"Spec 072. The company's admin may name another.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Declined","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not the caller's"},"409":{"description":"Not waiting for their answer"}}}},"/business/join":{"post":{"tags":["business"],"summary":"Join a company with the one-time code an admin handed over","description":"Spec 041. A code is twelve characters, read without dashes or case, kept by the platform only as its SHA-256, used once, within seven days. Spec 052: a person the bank does not know yet joins too, as `being_checked`: the code is spent at once, and they see and do nothing of the company until the bank has opened them (`joined`), or turned them down (`could_not_join`, never why).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string"}}},"example":{"code":"ZAWM-E9EQ-UDZV"}}}},"responses":{"200":{"description":"A member now, with this role","content":{"application/json":{"example":{"business_id":"01a0fe37-baed-720a-922b-d24c9981b6ad","name":"Global Legal Entity Identifier Foundation","role":"payer","state":"joined"},"schema":{"type":"object"}}}},"400":{"description":"No such code, used, or expired (all the same)"},"409":{"description":"The caller already acts for the company"}}}},"/business/{id}/people":{"get":{"tags":["business"],"summary":"The company's people, its second-signature threshold and the payments waiting for one","description":"Spec 041. For anyone who acts for the company. Each person by the name the core holds for them, their role and whether it is the caller; the threshold in minor units and a raise or removal waiting for a second person; the payments waiting for a second signature, with what the second signatory confirms (`authorize`). Spec 052: each person's `state`, `joined`, `being_checked` (invited, the bank still checking them; removing them withdraws the invitation) or `could_not_join` (no reason is given).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The company's people","content":{"application/json":{"example":{"business_id":"01a0fe37-baed-720a-922b-d24c9981b6ad","your_role":"admin","people":[{"id":"01a10169-0ae0-7072-b919-91121a836dda","name":"Ana Rivera","role":"payer","you":false,"added_at":"2026-10-03T10:57:00Z","state":"joined"}],"cosign_above_minor":2000,"pending_change":null,"awaiting_cosignature":[{"id":"01a10169-1000-7000-8000-000000000001","amount_minor":3000,"currency":"USD","payee":"Anyone","reference":"big one","authorized_at":"2026-10-03T10:58:00Z","authorize":{"amount":"30.00","currency":"USD","payee":"Anyone"}}]},"schema":{"type":"object"}}}},"404":{"description":"The caller does not act for that company"}}}},"/business/{id}/invitations":{"post":{"tags":["business"],"summary":"A one-time code for a person to join the company with, shown once","description":"Spec 041. Admins only.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role"],"properties":{"role":{"type":"string","enum":["admin","payer","approver","viewer"]}}}}}},"responses":{"201":{"description":"The code, shown this once","content":{"application/json":{"example":{"code":"ZAWM-E9EQ-UDZV","role":"payer","expires_at":"2026-10-10T10:57:00Z"},"schema":{"type":"object"}}}},"400":{"description":"Not a role"},"403":{"description":"Not an admin of the company"},"404":{"description":"The caller does not act for that company"}}}},"/business/{id}/people/{member}":{"put":{"tags":["business"],"summary":"Change a person's role","description":"Spec 041. Admins only; the company keeps at least one admin.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"member","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role"],"properties":{"role":{"type":"string","enum":["admin","payer","approver","viewer"]}}}}}},"responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not an admin"},"409":{"description":"No such person, or the company would have no admin"}}},"delete":{"tags":["business"],"summary":"Remove a person","description":"Spec 041. Admins only; the last admin stays.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"member","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Removed"},"403":{"description":"Not an admin"},"409":{"description":"No such person, or the last admin"}}}},"/business/{id}/threshold":{"put":{"tags":["business"],"summary":"Set the amount from which a payment from the company's account needs a second signature","description":"Spec 041. Admins only. Setting one or lowering it takes effect at once; raising or removing it (`null`) waits for a second admin or approver to confirm. Spec 052: a threshold is one currency's (`currency`, dollars when not said).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["above_minor"],"properties":{"above_minor":{"type":"integer","nullable":true,"minimum":1},"currency":{"type":"string","default":"USD"}}},"example":{"above_minor":100000,"currency":"USD"}}}},"responses":{"200":{"description":"`in_force`, `waits_for_a_second_person` or `unchanged`","content":{"application/json":{"example":{"effect":"waits_for_a_second_person"},"schema":{"type":"object"}}}},"400":{"description":"Not more than nothing"},"403":{"description":"Not an admin"}}}},"/business/{id}/threshold/confirm":{"post":{"tags":["business"],"summary":"Confirm a raise or removal of the threshold another person asked for","description":"Spec 041. An admin or an approver, not whoever asked for it. Spec 052, by currency.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","default":"USD"}}}}}},"responses":{"200":{"description":"In force now","content":{"application/json":{"example":{"currency":"USD","cosign_above_minor":100000},"schema":{"type":"object"}}}},"403":{"description":"Not an admin or an approver"},"409":{"description":"Nothing waits, or the caller asked for it"}}}},"/business/{id}/accounts/euro":{"post":{"tags":["business","accounts"],"summary":"Open the company's euro account, or carry on opening it","description":"Spec 052. Admins only. As `POST /accounts/euro`, with the company as the account's holder at the provider and the admin as its contact, confirmed by the admin; one per company. Its IBAN is shown with the company's accounts to the company's people.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EuroAccountOpenBody"}}}},"responses":{"200":{"description":"The account as it now is","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EuroAccountView"}}}},"403":{"description":"Not an admin"},"404":{"description":"No such opened company the caller acts for"},"503":{"description":"Euro accounts are not offered here"}}}},"/business/{id}/cards":{"get":{"tags":["business","cards"],"summary":"The company's cards, every holder's","description":"Spec 052. Admins only. Each card with its holder's membership and the card's own view.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The company's cards","content":{"application/json":{"example":{"business_id":"01a10d4f-03dd-70dd-9c93-e83b3cfa4e7c","cards":[{"holder":"01a10d4f-0000-7000-8000-000000000002","yours":false,"card":{"id":"01a10d60-0000-7000-8000-000000000001","status":"active","last_four":"4242","currency":"USD","business_id":"01a10d4f-03dd-70dd-9c93-e83b3cfa4e7c"}}]},"schema":{"type":"object"}}}},"403":{"description":"Not an admin"}}},"post":{"tags":["business","cards"],"summary":"Issue a card on the company's account to one of its people who pays","description":"Spec 052. Admins only, for an admin or a payer, on one of the company's accounts, virtual. Issued through the card issuer as any card; its payments keep to its holder's limits and the company's threshold, and are declined above them. Removing the holder, or a role that no longer pays, cancels it at the issuer. An admin of the company freezes, unfreezes or cancels it with `POST /cards/{id}/actions`; activating it is its holder's.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["holder","account_id","cardholder_name"],"properties":{"holder":{"type":"string","format":"uuid","description":"The person's membership id"},"account_id":{"type":"string","description":"One of the company's accounts"},"cardholder_name":{"type":"string","maxLength":26},"form":{"type":"string","enum":["virtual"],"default":"virtual"}}}}}},"responses":{"201":{"description":"Issued","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Not virtual, no name, or not the company's account"},"403":{"description":"Not an admin"},"409":{"description":"The holder neither pays nor administers, or holds as many cards as allowed"},"503":{"description":"No card issuer is connected"}}}},"/business/{id}/people/{member}/limits":{"get":{"tags":["business"],"summary":"What one person may pay alone, and what they have paid alone today","description":"Spec 052. The person themselves, or an admin of the company. By currency: the limit per payment and per day (`null`, none), a loosening waiting for a second person, and what they paid alone from the company in the bank's day (`spent_today_minor`), a payment waiting for a second signature not counted until it is signed.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"member","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The person's limits","content":{"application/json":{"example":{"member":"01a10cd8-0000-7000-8000-000000000002","limits":[{"currency":"USD","per_payment_minor":50000,"per_day_minor":100000,"pending_change":null,"spent_today_minor":6000,"left_today_minor":94000}]},"schema":{"type":"object"}}}},"404":{"description":"Not of the company, or another person's to anyone but an admin"}}},"put":{"tags":["business"],"summary":"Set what one person may pay alone","description":"Spec 052. Admins only, for an admin or a payer. Each limit is a ceiling: lower, or one where there was none, is in force at once; higher, or none, waits for a second admin or approver. Above either limit a payment waits for a second signature, as above the company's threshold, and says which.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"member","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","default":"USD"},"per_payment_minor":{"type":"integer","nullable":true,"minimum":1},"per_day_minor":{"type":"integer","nullable":true,"minimum":1}}},"example":{"currency":"USD","per_payment_minor":50000,"per_day_minor":100000}}}},"responses":{"200":{"description":"Each limit's effect: `in_force`, `waits_for_a_second_person` or `unchanged`","content":{"application/json":{"example":{"per_payment":"in_force","per_day":"waits_for_a_second_person","limits":{"currency":"USD","per_payment_minor":50000,"per_day_minor":100000}},"schema":{"type":"object"}}}},"400":{"description":"Not more than nothing, or not a currency"},"403":{"description":"Not an admin"},"409":{"description":"The person neither pays nor administers"}}}},"/business/{id}/people/{member}/limits/confirm":{"post":{"tags":["business"],"summary":"Confirm a looser limit another person asked for","description":"Spec 052. An admin or an approver, not whoever asked for it.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"member","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","default":"USD"}}}}}},"responses":{"200":{"description":"In force now","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not an admin or an approver"},"409":{"description":"Nothing waits, or the caller asked for it"}}}},"/transfers/{id}/cosign":{"post":{"tags":["transfers"],"summary":"Give a company's payment its second signature","description":"Spec 041. By an admin or approver of the company it is paid from, never its payer, with their own confirmation bound to the same amount and payee as `/confirm`'s. It moves under that authority.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Signed, and as far as it went","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferIntentView"}}}},"400":{"description":"The confirmation is of something else, or not an authentication method"},"403":{"description":"The caller's role does not give a second signature"},"404":{"description":"Not a payment of a company the caller acts for"},"409":{"description":"Not waiting for a second signature, or the caller made it"}}}},"/transfers/{id}/decline":{"post":{"tags":["transfers"],"summary":"Decline a company's payment's second signature","description":"Spec 041. As `/cosign`; the payment is refused and does not move.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Refused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferIntentView"}}}},"403":{"description":"The caller's role does not give a second signature"},"404":{"description":"Not a payment of a company the caller acts for"},"409":{"description":"Not waiting for a second signature, or the caller made it"}}}},"/backoffice/business":{"get":{"tags":["back-office","business"],"summary":"Every business application, for the operator","description":"Spec 039. The registry's record and the mismatches, the owners opened, the screening for the company and each owner, the state, and `blocked`: why it cannot be opened now (the entity not active, the LEI not issued, an open screening alert, a confirmed match, no owner).\n","responses":{"200":{"description":"Undecided first","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/business/{id}/credit":{"get":{"tags":["lending","back-office"],"summary":"A company's checks at the business credit bureau, newest first","description":"Spec 074. With what the bank's record suggests searching with — the legal name, the registered address read into street, city, state and zip as well as free text allows, and the registration number where it is an EIN — and whether a business bureau is configured.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The checks","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeBusinessCredit"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such company"}}}},"/backoffice/business/{id}/credit/search":{"post":{"tags":["lending","back-office"],"summary":"Find the company in the business credit bureau's files","description":"Spec 074. Experian's Business Information search: a name (≤40), a city and a two-letter state are required; the street, five-digit zip, EIN and phone sharpen the match. No match is an empty list. Nothing is kept.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessBureauSubject"}}}},"responses":{"200":{"description":"The matches, surest first","content":{"application/json":{"schema":{"type":"object","required":["matches"],"properties":{"matches":{"type":"array","items":{"$ref":"#/components/schemas/BusinessBureauMatch"}}}}}}},"400":{"description":"The bureau refused the request (its words in detail)"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such company"},"503":{"description":"No business bureau configured, or the bureau could not be asked"}}}},"/backoffice/business/{id}/credit/checks":{"post":{"tags":["lending","back-office"],"summary":"Score the match picked, and keep the answer on the company","description":"Spec 074. Experian's Intelliscore Plus (paying) and Financial Stability Risk (lasting), each with its risk class, percentile, recommended credit limit and factors in the bureau's words. Kept with who asked and the bureau's reference; a company's loan is decided with its latest check of the last 30 days.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["bureau_id"],"properties":{"bureau_id":{"type":"string","description":"The bureau's id for the company (Experian's BIN, nine digits)"}}}}}},"responses":{"201":{"description":"The check, kept","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessCreditCheck"}}}},"400":{"description":"The bureau refused the request (its words in detail)"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such company"},"503":{"description":"No business bureau configured, or the bureau could not be asked"}}}},"/backoffice/business/{id}/open":{"post":{"tags":["back-office","business"],"summary":"Propose opening it, for a second operator","description":"Spec 039. A `business_opening` approval; once a second operator approves it, the core opens the company as an entity client under its registered name and a dollar account, and the applicant sees it among their accounts. Checked again when it is carried out.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"Proposed","content":{"application/json":{"example":{"approval_id":"01a0fe38-6f08-72ba-9d79-202f95fabb4d","state":"opening"},"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such application in this tenant"},"409":{"description":"Not submitted, already proposed, or blocked, and why"}}}},"/backoffice/business/{id}/reject":{"post":{"tags":["back-office","business"],"summary":"Turn it down, with why","description":"Spec 039. The reason is the bank's record; the applicant reads only that it was not opened.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":8}}}}}},"responses":{"200":{"description":"Rejected","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"No reason"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"Already opening or opened"}}}},"/backoffice/business/{id}/lookup":{"post":{"tags":["back-office","business"],"summary":"Record what the registry said, read by hand, for a company without an LEI","description":"Spec 052. The operator looked the company up at its registry and records what they read: the name it holds, its status (`active`, `inactive`, `not_found`), its registration date and address, and where they looked. Kept with who and when; the mismatches are worked out again from it. Opening is refused until it says `active`, and the operator who recorded it cannot approve the opening.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status","source"],"properties":{"legal_name":{"type":"string","description":"As the registry holds it; may be empty when not found"},"status":{"type":"string","enum":["active","inactive","not_found"]},"registered_on":{"type":"string","format":"date"},"address":{"type":"string"},"source":{"type":"string","description":"Where they looked","the registry or its page":null}}},"example":{"legal_name":"OXA CORNER SHOP LIMITED","status":"active","registered_on":"2019-04-02","address":"1 High Street, Edinburgh EH1 1AA","source":"https://find-and-update.company-information.service.gov.uk/company/SC123456"}}}},"responses":{"200":{"description":"Recorded","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"No source, no name for a company that was found, or a registration date in the future"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such application in the tenant"},"409":{"description":"The company has an LEI, or the bank has decided"}}}},"/backoffice/incoming":{"get":{"tags":["back-office"],"summary":"Money that arrived through a customer's named account, newest first","description":"Spec 038. Each with its sender, what became of its credit (`credit_state`, `credited`), and of a return (`return_state`, `return_reason`, `returned`). `returnable` is true where its credit was rejected, it was not credited, no return is proposed, and its rail can be sent back here (an ACH credit or an international wire through Column).\n","responses":{"200":{"description":"The last hundred","content":{"application/json":{"example":{"incoming":[{"provider":"column","notice_id":"evnt_ach_1","rail":"ach","received_at":"2026-10-01T23:21:00Z","amount_minor":2550,"currency":"USD","sender_name":"ACME PAYROLL","sender_account":null,"sender_reference":"SALARY","credit_state":"rejected","credited":false,"return_state":null,"return_reason":null,"returned":false,"returnable":true}]},"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/incoming/return":{"post":{"tags":["back-office"],"summary":"Propose sending back money that arrived and was not credited, for a second operator","description":"Spec 038. Proposes an `incoming_return`; once a second operator approves it the provider is asked to return it, once (ACH `R03`, `R02`, `R23` or `R16`; an international wire's `return_reason`; a domestic wire reversed with Column's reversal `reason`, the platform’s specification). Refused while its credit is undecided or approved, once it is credited or a return is proposed, for a euro credit, and for an ACH return past the second banking day after it arrived (except `refused_by_customer`). Money with a return proposed is never credited.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["provider","notice_id","reason"],"properties":{"provider":{"type":"string"},"notice_id":{"type":"string"},"reason":{"type":"string","enum":["no_such_account","account_closed","refused_by_customer","compliance"]}}},"example":{"provider":"column","notice_id":"evnt_ach_1","reason":"account_closed"}}}},"responses":{"201":{"description":"Proposed; it waits for a second operator under /approvals","content":{"application/json":{"example":{"approval_id":"01a0f9c5-0000-7000-8000-000000000003","reason":"account_closed"},"schema":{"type":"object"}}}},"400":{"description":"Not a reason"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No money arrived under that notice in this tenant"},"409":{"description":"Not returnable now, and why"}}}},"/backoffice/onboarding":{"get":{"tags":["onboarding","back-office"],"summary":"Everybody with something outstanding, oldest first","responses":{"200":{"description":"The queue, oldest wait first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeOnboardingQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/onboarding/verdicts":{"post":{"tags":["onboarding","back-office"],"summary":"Record what a check found","description":"Acts directly rather than proposing, and that is the considered exception to Principle II: a verdict is writing down what a check found, not granting anybody anything. **Opening the account is the granting step**, it is elsewhere, and that is where maker-checker belongs.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["onboarding_id","kind","satisfied"],"properties":{"onboarding_id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/BackofficeOnboardingRequirementKind"},"satisfied":{"type":"boolean"},"failure_code":{"type":"string","nullable":true,"description":"Effectively required when satisfied is false: the failure (code AND detail) is only stored when BOTH are present; with detail but no code, the requirement is marked failed with a NULL failure_detail.\n"},"failure_detail":{"type":"string","nullable":true,"minLength":8,"description":"Required when satisfied is false; trimmed byte length >= 8, else 400."}}}}}},"responses":{"200":{"description":"The journey as the verdict leaves it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeOnboardingView"}}}},"400":{"description":"Not a requirement of this journey"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such journey in this tenant"}}}},"/backoffice/screening":{"get":{"tags":["screening","back-office"],"summary":"Alerts waiting for a person, strongest match first","responses":{"200":{"description":"Open alerts, with the list and the entry version each was raised against","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeRiskScreeningQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/screening/adjudications":{"post":{"tags":["screening","back-office"],"summary":"Propose an adjudication","description":"**This changes nothing.** It raises a proposal a different person approves. Clearing an alert is what lets a customer through, and an officer who could clear their own alert alone is the control not existing. The `revision` binds the decision to the version of the entry the officer actually saw, so a clearance lapses when the publisher changes it and returns for review carrying the earlier decision.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key","revision","verdict","note"],"properties":{"key":{"type":"string","description":"`subject|list|entry_id` of an OPEN alert, else 404."},"revision":{"type":"string","description":"The entry revision the officer saw. Must equal the alert's current revision, else 400."},"verdict":{"type":"string","enum":["cleared","confirmed"],"description":"Anything else is 400."},"note":{"type":"string","minLength":10,"description":"Trimmed, char count >= 10 (screening::MINIMUM_NOTE), else 400. Stored trimmed."},"proposal_id":{"type":"string","format":"uuid","nullable":true,"description":"The automated proposal the officer had in front of them, kept on the approval so the file shows what was suggested next to what was decided. Must be the tenant's (else 404) and about this key and revision (else 409).\n"}}}}}},"responses":{"202":{"description":"Proposed. The alert stays open until a second person agrees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"Not a verdict, or a note too thin to stand as a reason","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"The key is not among the (first 500) open alerts; it may already have been decided. Or the named proposal does not exist for this tenant.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The named proposal is about a different alert or an earlier revision of the entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/screening/alert":{"get":{"tags":["screening","back-office"],"summary":"One alert, with everything ever decided about it","parameters":[{"name":"key","in":"query","required":true,"schema":{"type":"string"},"description":"`subject|list|entry_id`. A query parameter rather than a path segment because the key contains the separator\n"}],"responses":{"200":{"description":"The alert and its decision history","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeRiskScreeningAlertDetail"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/screening/settings":{"get":{"tags":["screening","back-office"],"summary":"Whether a provider's service screens beside the bank's own lists","description":"Spec 073. `service_on` (false while the bank has set nothing), `provider` (the deployment's, or null), `provider_configured`, `min_decided` (1000) and `min_confirmed` (50) for training a model, `set_by`, `set_at`, and whether a change already waits (`change_waiting`).\n","responses":{"200":{"description":"The settings in force","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["screening","back-office"],"summary":"Propose turning the provider's service on or off, or the training minimums","description":"Spec 073, the platform’s rule. A compliance officer proposes; another approves (`screening_setting`). Turning the service on sends every subject screened from then on to the provider, beside the bank's own lists and never instead of them.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["service_on","min_decided","min_confirmed"],"properties":{"service_on":{"type":"boolean"},"min_decided":{"type":"integer","minimum":1},"min_confirmed":{"type":"integer","minimum":1}}}}}},"responses":{"202":{"description":"Proposed; nothing changes until another compliance officer approves","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The minimum confirmed matches must be more than zero and no more than the minimum decided.\n"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"No screening service is configured for this bank; or the same change already waits.\n"}}}},"/backoffice/screening/runs":{"get":{"tags":["screening","back-office"],"summary":"The subjects whose latest run waits on the provider's service, or on a missing list","description":"Spec 073, the platform’s rule, and the platform’s specification, FR-7705a. A subject here is told `in_review`: the provider could not answer, or a declared list has no version in use, and \"we could not check\" is never a clear result. The platform screens each again once the provider answers or the list loads; two compliance officers may close one on the lists that did load instead. Ask for exactly one of `service_state=unavailable` and `lists_state=waiting`.\n","parameters":[{"name":"service_state","in":"query","required":false,"schema":{"type":"string","enum":["unavailable"]}},{"name":"lists_state","in":"query","required":false,"schema":{"type":"string","enum":["waiting"]}}],"responses":{"200":{"description":"`{count, runs: [{id, subject_reference, screened_at, outcome, service_provider, service_error, missing_lists, lists_state}]}`, oldest first\n","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Neither filter, or both"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/alerts":{"get":{"tags":["alerts","back-office"],"summary":"The alerts of the caller's roles","description":"Spec 077, the platform’s rule, the platform’s rule. Only the roles the caller holds; an auditor sees every role's, read only. Ordered by weight, then severity, then the latest occurrence. AlertsWork.\n","parameters":[{"name":"state","in":"query","required":false,"schema":{"type":"string","enum":["open","acknowledged","resolved","cleared"]}},{"name":"severity","in":"query","required":false,"schema":{"type":"string","enum":["info","low","medium","high","critical"]}},{"name":"role","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The alerts, at most 500","content":{"application/json":{"schema":{"type":"object","required":["count","alerts"],"properties":{"count":{"type":"integer","minimum":0},"alerts":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeAlert"}}}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/alerts/{id}":{"get":{"tags":["alerts","back-office"],"summary":"One alert, its event, its occurrences and its deliveries","description":"Spec 077. The event shows only the fields the rule catalogue declares. Another role's alert is \"No such alert.\", the same as one that does not exist.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The alert","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeAlertDetail"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such alert."}}}},"/backoffice/alerts/{id}/acknowledge":{"post":{"tags":["alerts","back-office"],"summary":"Acknowledge an alert","description":"Spec 077, the platform’s rule. One person, audited (`alert.acknowledged`).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The alert as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeAlert"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such alert."},"409":{"description":"This alert is already resolved; or this alert was cleared by an event."}}}},"/backoffice/alerts/{id}/resolve":{"post":{"tags":["alerts","back-office"],"summary":"Resolve an alert with a note","description":"Spec 077, the platform’s rule. One person, a note of at least 10 characters, audited (`alert.resolved`).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["note"],"properties":{"note":{"type":"string","minLength":10}}}}}},"responses":{"200":{"description":"The alert as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeAlert"}}}},"400":{"description":"A resolution needs a note of at least 10 characters."},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such alert."},"409":{"description":"This alert is already resolved; or this alert was cleared by an event."}}}},"/backoffice/alerts/rules":{"get":{"tags":["alerts","back-office"],"summary":"The alert rules in force, and the role mailboxes","description":"Spec 077, the platform’s rule, the platform’s rule. Each rule in force with its version, state, and what it has done (`fired`, `not_evaluated`, `cleared`), and whether a change waits. With `format=yaml`, the rules in force in the catalogue's own format, as a document.\n","parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["yaml"]}}],"responses":{"200":{"description":"The rules and mailboxes; or, with format=yaml, application/yaml","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeAlertRules"}},"application/yaml":{"schema":{"type":"string"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["alerts","back-office"],"summary":"Propose adding, changing, switching off or on, or removing a rule","description":"Spec 077, the platform’s rule, the platform’s rule. Checked here, and again when carried out; proposed by a person of the rule's role or a supervisor; carried out when another person of that role, or a supervisor, approves (`alert_rule`). AlertRulesManage.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["change","rule","reason"],"properties":{"change":{"type":"string","enum":["add","change","off","on","remove"]},"rule":{"$ref":"#/components/schemas/AlertRule"},"reason":{"type":"string","minLength":10}}}}}},"responses":{"202":{"description":"Proposed; nothing changes until another person of the rule's role, or a supervisor, approves","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"The platform's sentence: an event it does not emit, a field the event does not carry, a comparison that does not suit the field, no such role, a message placeholder that is not the event's, grouping by customer on an event that names none, a rule that would raise and clear on the same event.\n"},"403":{"description":"Your role does not allow this."},"409":{"description":"The same change already waits for approval."}}}},"/backoffice/alerts/rules/{rule_id}/versions":{"get":{"tags":["alerts","back-office"],"summary":"Every version of one rule","parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The versions, oldest first, each with the approval that made it","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"There is no rule with that id."}}}},"/backoffice/alerts/mailboxes":{"post":{"tags":["alerts","back-office"],"summary":"Propose where a role's alerts are emailed","description":"Spec 077, research R10. A team's mailbox, never a person's address; under the same approval as a rule (`alert_rule`). AlertRulesManage.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role","address","reason"],"properties":{"role":{"type":"string"},"address":{"type":"string"},"reason":{"type":"string","minLength":10}}}}}},"responses":{"202":{"description":"Proposed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyProposedNoted"}}}},"400":{"description":"That is not an email address; or there is no role named so."},"403":{"description":"Your role does not allow this."},"409":{"description":"The same change already waits for approval."}}}},"/backoffice/screening/lists":{"get":{"tags":["screening","back-office"],"summary":"Every declared list and how it stands","description":"Spec 077. The JSON lists and the OFAC sets, read or not: `state` is `current`, `partial` (an optional file the manifest named is missing), `stale` (the last read failed; the previous version is in use) or `failed` (no version in use: every run waits, FR-7705a). RiskReview; an auditor reads.\n","responses":{"200":{"description":"The lists, by name","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeScreeningList"}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"503":{"description":"The watchlists are not loaded"}}}},"/backoffice/screening/runs/{run_id}/local-only":{"post":{"tags":["screening","back-office"],"summary":"Propose closing a run that waits on the provider, on the bank's own lists alone","description":"Spec 073, US2. A compliance officer proposes with a reason; another approves (`screening_local_only`). The run becomes `closed_local_only` with the outcome its local matches come to now, and onboarding is settled as a fresh clear run would settle it.\n","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":10}}}}}},"responses":{"202":{"description":"Proposed; nothing changes until another compliance officer approves","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Say why, in at least 10 characters."},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such run"},"409":{"description":"This run is not waiting for anything (the provider or a missing list); or the same change already waits."}}}},"/backoffice/screening/models":{"get":{"tags":["screening","back-office"],"summary":"Every screening model the bank has trained, newest first","description":"Spec 073. Each model without its file: `id`, `version`, `sha256`, `state` (`candidate`, `refused`, `promoted`, `withdrawn`, `superseded`), `trained_by`, `trained_at`, the period of decisions it learnt from, `counts` (decided, confirmed, cleared, synthetic, the bank's share of the weight), `heldout` (the recent decisions it is tested on), `report`, `answering_version`, `trained_on`, `weights`, `proposals_answered` (how many of the bank's proposals it made) and its promotion and withdrawal approvals. Beside the list, `answering` (`{version, provenance, id}`: the bank's promoted model, `live`; or `screening-lr-1`, `synthetic`; null with a `note` when no AI plane is configured and the rules answer) and `decided_since_answering_trained`.\n","responses":{"200":{"description":"`{answering, note, decided_since_answering_trained, models: [...]}`","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["screening","back-office"],"summary":"Train a candidate on the bank's decided alerts","description":"Spec 073, US3. Acts directly, as running a check does: a candidate answers nothing until two compliance officers promote it. The bank's decisions leave for the AI plane as nine numbers and a verdict each, blended there with synthetic alerts, the bank's carrying two thirds of the weight. The most recent decisions, at least 20 of them confirmed, are held out to test it.\n","responses":{"201":{"description":"The candidate, as the list shows it","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"Not enough decided alerts to train on (with the counts held and needed), or not enough recent confirmed matches to test a model on; or the plane refused what it was sent.\n"},"503":{"description":"The AI plane is not configured, or did not answer; nothing was trained."}}}},"/backoffice/screening/models/{id}":{"get":{"tags":["screening","back-office"],"summary":"One screening model","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The model, as the list shows it","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such model"}}}},"/backoffice/screening/models/{id}/promotion":{"post":{"tags":["screening","back-office"],"summary":"Propose making a candidate the model that answers the bank's alerts","description":"Spec 073, US4. A compliance officer proposes; another approves (`screening_model_promotion`). Only a candidate whose harness report is promotable, and whose file is the one pinned, may be proposed, and both are checked again when it is carried out. The model answering before is superseded and answers again if this one is withdrawn.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Proposed; nothing changes until another compliance officer approves","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such model"},"409":{"description":"Only a candidate can be promoted; this model did not pass the harness (and why); its file is not the one pinned; or the same proposal already waits.\n"}}}},"/backoffice/screening/models/{id}/withdrawal":{"post":{"tags":["screening","back-office"],"summary":"Propose withdrawing the model that answers","description":"Spec 073, US4. A compliance officer proposes with a reason; another approves (`screening_model_withdrawal`). The model it replaced answers again, or the one in the AI plane's image when there was none.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","minLength":10}}}}}},"responses":{"202":{"description":"Proposed; nothing changes until another compliance officer approves","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Say why, in at least 10 characters."},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such model"},"409":{"description":"Only the answering model can be withdrawn; or the same proposal already waits."}}}},"/backoffice/screening/rescreen":{"post":{"tags":["screening","back-office"],"summary":"Screen somebody again, by hand","description":"Acts directly rather than proposing, the same exception the onboarding verdict route makes: running a check is not granting anybody anything. What comes out of it needs two people; running it does not. The recurring job does the same work unattended.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["subject_reference","full_name"],"properties":{"subject_reference":{"type":"string","description":"Not validated; becomes the prefix of every match key."},"full_name":{"type":"string","pattern":"\\S","description":"Must contain a non-whitespace character, else 400."},"also_known_as":{"type":"array","default":[],"items":{"type":"string"}},"date_of_birth":{"type":"string","format":"date","nullable":true},"country":{"type":"string","nullable":true},"organisation":{"type":"boolean","default":false,"description":"true screens the subject as an organisation, else a person."}}}}}},"responses":{"200":{"description":"Screened, with the list versions used and the moment it ran","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeRiskScreeningRun"}}}},"400":{"description":"Not enough of a subject to screen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"503":{"description":"Watchlists not loaded, no watchlists configured, or the list source failed. Never a clear result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/payees":{"get":{"tags":["transfers"],"summary":"The caller's payees","description":"Each carries whether they have ever been paid before, because that is what the first-payment warning at approval is derived from.\n","responses":{"200":{"description":"Payees, each marked first payment or not","content":{"application/json":{"example":{"payees":[{"id":"c7a3b201-5e46-4d18-b9c2-7f0a1b2c3d4e","display_name":"Ana Rivera","account_ref":"012180001234567895","first_payment":true,"destination":"spei"}]},"schema":{"$ref":"#/components/schemas/PayeeList"}}}}}},"post":{"tags":["transfers"],"summary":"Add a payee","responses":{"201":{"description":"The payee, marked as never paid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeView"}}}},"400":{"description":"A name or account missing, or an 18-digit account that is not a valid CLABE — a wrong check digit or a bank SPEI does not reach — refused now, while the customer is looking at the number, with the reason. The same for a US bank account (`aba:<routing>:<account>:<checking|savings>`, the platform’s specification) whose routing number is not nine digits or fails the ABA check digit, whose account is not 4 to 17 digits, or whose type is neither. A shape the platform cannot route is kept and answered `destination: unknown`\n"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeNewBody"}}}}}},"/transfers":{"get":{"tags":["transfers"],"summary":"The caller's own transfers, newest first","responses":{"200":{"description":"Transfers, each carrying `money_moved`. It is `true` only on a `posted` transfer - the accounting core confirmed the movement and `core_ref` names it. A transfer to an account the core holds is posted after authorisation. `posting` means the core was asked and has not been heard back from; it is not moved and not refused, and the field does not guess. A payee at another Mexican bank — `destination: spei`, a CLABE — is `sent` once the core has debited the customer to the bank's settlement account and the rail accepted the order (`core_ref` and `rail_ref` both named, `money_moved: true`), `settled` when the rail reports delivery, and `returned` when the rail sent it back and a second person approved the credit back. `destination: unknown` is a reference the platform cannot route yet: it stays `authorized` with `money_moved: false`\n","content":{"application/json":{"example":{"transfers":[{"id":"f0e9d8c7-b6a5-4321-9876-543210fedcba","state":"authorized","from_account":"3f1b9d20-7c44-4a1e-9e0e-2b5d8c1a4f77","payee":{"id":"c7a3b201-5e46-4d18-b9c2-7f0a1b2c3d4e","display_name":"Ana Rivera"},"amount_minor":40000,"currency":"MXN","reference":"Dinner","created_at":"2026-09-20T19:11:04Z","authorize":{"amount":"400.00","currency":"MXN","payee":"Ana Rivera","expires_in_minutes":10},"warnings":[{"code":"first_payment","detail":"You have not paid Ana Rivera before"}],"authorized_at":"2026-09-20T19:11:31Z","auth_method":"device_biometric","refused_reason":null,"core_ref":null,"rail_ref":null,"destination":"sepa","money_moved":false,"goes_out_on":"2026-09-21","after_cutoff":true,"cutoff":"14:00","holiday":null,"instant":true,"cosign_reason":null},{"id":"0a1b2c3d-4e5f-4061-8a7b-9c0d1e2f3a4b","state":"posted","from_account":"1","payee":{"id":"d8b4c312-6f57-4e29-8ad3-801b2c3d4e5f","display_name":"Savings"},"amount_minor":100,"currency":"MXN","reference":"Rent share","created_at":"2026-09-24T09:02:10Z","authorize":{"amount":"1.00","currency":"MXN","payee":"Savings","expires_in_minutes":10},"warnings":[],"authorized_at":"2026-09-24T09:02:40Z","auth_method":"device_biometric","refused_reason":null,"core_ref":"57","rail_ref":null,"destination":"core","money_moved":true,"goes_out_on":null,"after_cutoff":null,"cutoff":null,"holiday":null,"instant":false,"cosign_reason":null}]},"schema":{"$ref":"#/components/schemas/TransferList"}}}}}},"post":{"tags":["transfers"],"summary":"Say what you want to do. Nothing happens yet","description":"Creates an **intent** the customer is shown. What executes later is derived from the stored intent and never from anything sent with the approval. The intent expires, and it outlives an authentication on purpose, so a customer is never asked to authenticate again for a payment the platform has already forgotten. A first payment to a payee never paid before comes back as a warning and is not a refusal.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferNewBody"}}}},"responses":{"201":{"description":"The intent as the customer must be shown it, with any warnings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferIntentView"}}}},"400":{"description":"Not something that can be paid"},"404":{"description":"Not the caller's payee or account. Never 403"}}}},"/transfers/{id}/confirm":{"post":{"tags":["transfers"],"summary":"Authorise it","description":"The customer is the authoriser (Principle II). The authorisation is refused unless it is all three of: two or more categories of authentication factor, given inside the freshness window, and bound to the exact amount and payee being executed - and the refusal says which of the three failed. The signed fields are compared against the **stored intent**; they are not the source of what executes. After authorisation the platform asks the accounting core to move the money, with the intent's id as the reference and idempotency key, so asking twice moves it once. The response says what the core did: `posted` with `money_moved: true`; `refused` if the core said no (the money did not move); `authorized` with `money_moved: false` if the payee is not an account the core holds; or `posting` if the core could not be heard back from, which is resolved by looking the movement up, never by moving it again.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Authorised, and `money_moved` says whether the core moved it. A held transfer says a person will decide and nothing is posted; risk scoring being unavailable never turns this into a refusal\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferIntentView"}}}},"400":{"description":"Not an authentication method, a single factor where two categories are required, or an authentication outside its window. The answer says which\n"},"404":{"description":"Not the caller's transfer"},"409":{"description":"Already confirmed, or the intent has expired and the details must be checked again. Confirming twice settles once and the second attempt is told so\n"}}}},"/funding":{"get":{"tags":["crypto"],"summary":"The caller's own fundings","responses":{"200":{"description":"Fundings and the stage each has reached","content":{"application/json":{"example":[{"id":"a1b2c3d4-e5f6-4708-9a0b-1c2d3e4f5061","stage":"awaiting_deposit","detail":"Waiting for your transfer to arrive","pending":true,"final":false,"network":"polygon","token":"USDC","token_decimals":6,"deposit_address":null,"deposit_memo":null,"address_provenance":"unavailable","amount_observed":null,"confirmations":0,"confirmations_required":128,"tx_hash":null,"settles_in":"MXN","fee_minor":null,"credited_minor":null,"failure_detail":null,"created_at":"2026-09-21T10:00:00Z"}],"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundingView"}}}}}}},"post":{"tags":["crypto"],"summary":"Start a funding","description":"Spec 062: where a ramp is bound for the network and token (PLATFORM_RAMP=evm), the address is the node's — one per customer and network, never another customer's — and the funding is watched from the block it opened at: seen, confirmed, converted at the ramp's rate less its fee, and its credit proposed for a second person, who moves the dollars in the core. Without one, the attempt has **no address**, honestly absent (Principle V): a deposit address is usable or absent, and one that cannot receive funds is never presented as one that can.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["network","token"],"properties":{"network":{"type":"string","description":"Any string; not validated against a list"},"token":{"type":"string"},"token_decimals":{"type":"integer","format":"int32","minimum":0,"maximum":24,"default":6},"settles_in":{"type":"string","default":"USD"},"to_account":{"type":"string","description":"Spec 062: the customer's own account the dollars go to; their first in settles_in when absent"}}}}}},"responses":{"202":{"description":"The attempt, with provenance `unavailable` where an address would be. Selling is not offered at all: there is no off-ramp\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FundingStarted"}}}},"400":{"description":"Not a network or token this tenant funds in"}}}},"/funding/networks":{"get":{"tags":["crypto"],"summary":"The chains and tokens the bank's ramp takes","description":"Spec 062. Empty when no ramp is bound.","responses":{"200":{"description":"The networks","content":{"application/json":{"schema":{"type":"object","required":["networks"],"properties":{"networks":{"type":"array","items":{"type":"object","required":["id","label","token","token_decimals","confirmations_required","provenance"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"token":{"type":"string"},"token_decimals":{"type":"integer"},"confirmations_required":{"type":"integer"},"provenance":{"type":"string","enum":["live","synthetic"]}}}}}},"example":{"networks":[{"id":"anvil","label":"anvil (OxaBank sandbox chain)","token":"USDC","token_decimals":6,"confirmations_required":3,"provenance":"synthetic"}]}}}}}}},"/funding/conversion":{"post":{"tags":["crypto"],"summary":"What would this deposit come to?","description":"No funding, no address and no record. A customer working out whether it is worth it has not started anything. The rate carries its provenance like every other figure.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount","token"],"properties":{"amount":{"type":"string","pattern":"^\\s*\\+?[0-9]+\\s*$","description":"A whole number in the token's smallest unit (1000000 = 1 USDC at 6 decimals), parsed as i128 after trimming. Must be > 0. NOT a decimal.\n"},"token":{"type":"string"},"token_decimals":{"type":"integer","format":"int32","minimum":0,"maximum":24,"default":6},"settles_in":{"type":"string","default":"USD"},"settlement_decimals":{"type":"integer","format":"int32","minimum":0,"maximum":24,"default":2}}}}}},"responses":{"200":{"description":"What it would come to, with where the rate came from","content":{"application/json":{"example":{"gross_minor":1000000,"fee_minor":8500,"net_minor":991500,"fee_exceeded_deposit":false,"settles_in":"MXN","peg":"1 USDC = 1 USD","indicative":true,"rate":{"numerator":"1834","denominator":"100"}},"schema":{"$ref":"#/components/schemas/FundingConversion"}}}},"400":{"description":"Not a convertible pair, or an amount that is not a decimal"}}}},"/backoffice/funding":{"get":{"tags":["crypto","back-office"],"summary":"Fundings still moving, oldest first","responses":{"200":{"description":"The open queue","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeFundingQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/funding/{id}/fail":{"post":{"tags":["crypto","back-office"],"summary":"Mark a stuck funding failed","description":"An operator's, and it acts directly rather than proposing. Unlike a limit change or a credit decision it **removes** a claim rather than granting one, and the money is untouched either way; making it wait for a second person would leave a customer with no answer while two people coordinate about a record. What an operator explicitly cannot do here is credit a funding - that moves money and lives at `/backoffice/funding/{id}/credit` under maker-checker.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["detail"],"properties":{"detail":{"type":"string","minLength":8,"description":"Trimmed, byte length >= 8, else 400. Stored trimmed as failure_detail."}}}}}},"responses":{"200":{"description":"Marked failed, with the detail recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FundingView"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such funding in this tenant"},"409":{"description":"The funding is not at a stage that can be failed"}}}},"/investments":{"get":{"tags":["investments"],"summary":"What the caller holds, how far it has drifted, and what a rebalance would do","description":"Every figure states where it came from. Tax lots, dividends and corporate actions are not accounted for and the response does not pretend otherwise.\n","responses":{"200":{"description":"Holdings, drift against the model, and the provenance of each figure","content":{"application/json":{"example":{"id":"d5e6f708-1a2b-4c3d-8e9f-0a1b2c3d4e5f","model":"Balanced","risk_profile":"balanced","assessed_on":"2026-06-01","suitability":{"permits":true,"detail":"The model matches the profile on file","verdict":"suitable"},"total_minor":87200,"positions":[{"instrument":"WORLD-EQ","quantity":"12.4000","quantity_scale":4,"price_minor":4100}],"drift":[{"instrument":"WORLD-EQ","target_bps":6000,"actual_bps":5830,"drift_bps":-170,"target_minor":52320,"actual_minor":50836}],"proposed_rebalance":{"within_band":true,"orders":[],"cash_after_minor":36364},"provenance":"unavailable"},"schema":{"$ref":"#/components/schemas/InvestmentAccountView"}}}}}},"post":{"tags":["investments"],"summary":"Open an investment account on a model","description":"Suitability is checked against the stated risk profile before the account opens. The model is the platform's; the execution is a broker's and the cash leg is the accounting core's.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["model","risk_profile"],"properties":{"model":{"type":"string"},"risk_profile":{"type":"string"}}}}}},"responses":{"201":{"description":"The account, on the model it was opened against"},"400":{"description":"The model is not suitable for the stated profile, and the answer says why"},"409":{"description":"An account already exists for this caller on this model"}}}},"/investments/contributions":{"get":{"tags":["investments"],"summary":"Money the caller added to investing, newest first","responses":{"200":{"description":"The contributions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestmentContributions"}}}},"404":{"description":"No investment account"}}},"post":{"tags":["investments"],"summary":"Add money to investing from one of the caller's dollar accounts","description":"Spec 076. On the customer's device confirmation, bound to the amount and to `InvestmentAccountView.contributions.payee`; a device key signs over the kind `investment_contribution` and the investment account's id. The core moves the money from their account to the bank's investing cash account under the reference `investment:<contribution id>`, which is what their statement shows. 503 while the bank takes no money into investing.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/TransferConfirmBody"},{"type":"object","required":["from_account","amount_minor"],"properties":{"from_account":{"type":"string","description":"One of the caller's own USD accounts"},"amount_minor":{"type":"integer","format":"int64","minimum":1}}}]}}}},"responses":{"201":{"description":"Moved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestmentContribution"}}}},"400":{"description":"Not the caller's dollar account, nothing to add, or the confirmation does not bind it"},"404":{"description":"No investment account"},"503":{"description":"The bank takes no money into investing yet, or the core did not move it"}}}},"/investments/fills":{"get":{"tags":["investments"],"summary":"What the broker bought and sold for the caller, and at what","description":"Spec 076. Each filled order: instrument, side, quantity (scaled as positions are), value and when. The positions' `cost_minor` is these fills at average cost. Empty, with `broker_bound: false`, until a broker adapter is bound and fills an order.\n","responses":{"200":{"description":"The fills","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvestmentFills"}}}},"404":{"description":"No investment account"}}}},"/investments/models":{"get":{"tags":["investments"],"summary":"What is on offer","responses":{"200":{"description":"Models, each with the profile it suits","content":{"application/json":{"example":{"models":[{"name":"Balanced","risk_profile":"balanced","drift_band_bps":500,"targets":[]}]},"schema":{"$ref":"#/components/schemas/InvestmentModelList"}}}}}}},"/backoffice/investments":{"get":{"tags":["investments","back-office"],"summary":"Investment accounts in this tenant","responses":{"200":{"description":"Accounts, with drift against their models","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyInvestmentQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/investments/rebalances":{"post":{"tags":["investments","back-office"],"summary":"Propose a rebalance","description":"**This places no orders.** A different person approves it (Principle II). An operator who could rebalance an account alone could churn it, and churning is the oldest way to take a client's money legally - the control here is not bureaucracy, it is the actual protection.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_id","reason"],"properties":{"account_id":{"type":"string","format":"uuid"},"reason":{"type":"string","minLength":8,"description":"Trimmed BYTE length >= 8, else 400. Stored untrimmed on the proposal."}}}}}},"responses":{"202":{"description":"Proposed. No order is placed until a second person agrees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyRebalanceProposed"}}}},"400":{"description":"reason shorter than 8 trimmed BYTES.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such account in this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The account is on a model no longer offered; the planner refused; or the account is inside its drift band (\"trading it would be churn\").\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/exchange":{"get":{"tags":["exchange"],"summary":"Recent exchange quotes, for somebody answering a customer about one","description":"The same figures the customer saw, and the core's reference for each leg. An exchange is authorised by the customer, so there is nothing here to approve — with one exception: a quote `posting` with `out_core_ref` and no `in_core_ref` is half done, the customer's money having left and the other currency not arrived, and it is completed through `POST /backoffice/exchange/completions`.\nThe subject is included because an operator is answering a named customer; every other customer-facing response in this platform omits it.\n","responses":{"200":{"description":"Quotes across customers, newest first","content":{"application/json":{"example":{"quotes":[{"id":"01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01","subject":"customer-1","sell_minor":100000,"sell_currency":"USD","buy_minor":86800,"buy_currency":"EUR","mid":"0.87237","rate":"0.86800815","spread_bps":50,"spread_minor":437,"rate_source":"European Central Bank reference rate","rate_as_of":"2026-09-18","state":"authorized","created_at":"2026-09-24T10:02:00Z","money_moved":false}]},"schema":{"$ref":"#/components/schemas/BackofficeMoneyExchangeQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/notifications":{"get":{"tags":["notifications"],"summary":"What the bank did on the caller's behalf, newest first","description":"Built from the domain events the platform recorded, filtered by subject **in the query**. An event a customer is not entitled to see never reaches this endpoint: the notifications module answers with nothing for it, and nothing is what this returns. Filtering in the client is not filtering, and neither is stripping fields at the edge — one forgotten field there is a customer told they matched a watchlist.\nEach notice carries a `target`, so the app opens the thing it is about rather than parsing a sentence to guess.\n","responses":{"200":{"description":"Notices, newest first","content":{"application/json":{"example":{"notices":[{"id":"01992d20-1a2b-7c3d-8e4f-5a6b7c8d9e02","title":"Payment authorised","line":"You approved it and we accepted it.","at":"2026-09-24T10:14:00Z","tone":"positive","target":{"kind":"transfer","id":"01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01"}},{"id":"01992d1f-9f8e-7d6c-8b5a-4c3d2e1f0a99","title":"We opened your case","line":"We will tell you as it moves.","at":"2026-09-24T09:12:00Z","tone":null,"target":{"kind":"dispute","id":"01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b11"}}]},"schema":{"$ref":"#/components/schemas/NoticeList"}}}}}}},"/backoffice/notifications":{"get":{"tags":["notifications"],"summary":"What a named customer was told, for somebody answering them about it","description":"The same notices the customer sees and no more. An operator asking \"what did we tell them?\" needs the customer's view rather than a richer one — a back office that saw more than the customer would answer questions the customer never asked.\n","parameters":[{"name":"subject","in":"query","required":true,"schema":{"type":"string"},"description":"A customer's token subject (the outbox `subject` column). Matched exactly; unknown subjects give notices []."}],"responses":{"200":{"description":"That customer's notices, newest first","content":{"application/json":{"example":{"subject":"customer-1","notices":[{"id":"01992d20-1a2b-7c3d-8e4f-5a6b7c8d9e02","title":"Payment authorised","line":"You approved it and we accepted it.","at":"2026-09-24T10:14:00Z","tone":"positive","target":{"kind":"transfer","id":"01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01"}}]},"schema":{"$ref":"#/components/schemas/BackofficeNoticeList"}}}},"400":{"description":"`subject` missing (axum Query rejection, plain text), or several tenant memberships (Problem)."},"403":{"$ref":"#/components/responses/OperatorsOnly"},"500":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"No database configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/exchange/quotes":{"get":{"tags":["exchange"],"summary":"The caller's exchange quotes, newest first","description":"Each one carries the rate it was quoted at, frozen as the two integers the source published it as. Re-deriving a rate at read time would mean the record of what somebody agreed to changing after they agreed to it.\n","responses":{"200":{"description":"Quotes with the rate applied, the spread as money, and what was credited","content":{"application/json":{"example":{"quotes":[{"id":"01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01","from_account":"acct-usd-1","to_account":"acct-eur-1","sell_minor":100000,"sell_currency":"USD","buy_minor":86800,"buy_currency":"EUR","mid":"0.87237","rate":"0.86800815","spread_bps":50,"spread_minor":437,"remainder":89880000,"remainder_denominator":1000000000,"rate_source":"European Central Bank reference rate","rate_as_of":"2026-09-18","state":"authorized","refused_reason":null,"created_at":"2026-09-24T10:02:00Z","authorize":{"amount":"1000.00","currency":"USD","payee":"Your EUR account"},"out_core_ref":null,"in_core_ref":null,"money_moved":false}]},"schema":{"$ref":"#/components/schemas/ExchangeQuoteList"}}}}}},"post":{"tags":["exchange"],"summary":"Quote a conversion between two of the caller's own accounts","description":"Nothing is converted. The quote is stored and what a confirmation executes is this row, so the figures a customer agreed to are the figures that run.\nBoth rates are returned. `mid` is what the source published and `rate` is what the customer gets; `spread_minor` is the difference in money, which is the only form in which the two are comparable. A margin folded into a single quoted rate is a fee charged without being named.\n`503` when no rate provider is configured, which is a supported way to run (Principle IX). The platform never substitutes a rate of its own.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExchangeQuoteBody"},"example":{"from_account":"acct-usd-1","to_account":"acct-eur-1","sell_minor":100000}}}},"responses":{"201":{"description":"A quote, stored, awaiting the customer's confirmation","content":{"application/json":{"example":{"id":"01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01","from_account":"acct-usd-1","to_account":"acct-eur-1","sell_minor":100000,"sell_currency":"USD","buy_minor":86800,"buy_currency":"EUR","mid":"0.87237","rate":"0.86800815","spread_bps":50,"spread_minor":437,"remainder":89880000,"remainder_denominator":1000000000,"rate_source":"European Central Bank reference rate","rate_as_of":"2026-09-18","state":"awaiting_confirmation","refused_reason":null,"created_at":"2026-09-24T10:02:00Z","authorize":{"amount":"1000.00","currency":"USD","payee":"Your EUR account"},"out_core_ref":null,"in_core_ref":null,"money_moved":false},"schema":{"$ref":"#/components/schemas/ExchangeQuoteView"}}}},"409":{"description":"The rate is too old to quote on, and the refusal says how old. A customer told only that we cannot quote has been told they cannot and not why.\n","content":{"application/json":{"example":{"type":"about:blank","title":"Conflict","status":409,"detail":"this rate was published 17 days ago and we will not quote on one older than 4"}}}},"503":{"description":"No rate provider is configured, so the platform cannot quote and says so."}}}},"/exchange/quotes/{id}/cosign":{"post":{"tags":["exchange","business"],"summary":"Give a company's exchange its second signature","description":"Spec 052. An exchange from a company's account crossing its threshold or its maker's limits, in the sell currency, waits after its maker's confirmation (`awaiting_cosignature`, with `cosign_reason`). An admin or approver of the company, never its maker, confirms the same figures with their own confirmation; the rate's freshness is checked again, and a stale one expires (409) rather than moves. It moves under the signatory's authority.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"}}}},"responses":{"200":{"description":"Signed; the quote as it now is","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not an admin or approver of the company"},"404":{"description":"Not a company's exchange the caller acts for"},"409":{"description":"Not waiting, made by the caller, or its rate now too old (expired)"}}}},"/exchange/quotes/{id}/decline":{"post":{"tags":["exchange","business"],"summary":"Decline a company's exchange waiting for a second signature","description":"Spec 052. An admin or approver of the company, never its maker. Nothing moves; the quote is `refused`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Declined","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Not an admin or approver of the company"},"409":{"description":"Not waiting, or made by the caller"}}}},"/exchange/quotes/{id}/confirm":{"post":{"tags":["exchange"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"summary":"Confirm a stored quote","description":"The request carries the customer's authority and nothing the platform reads for the figures. Every number comes from the stored quote; the signed fields are compared with the quote's `authorize` object — the amount leaving, its currency and \"Your <buy> account\" — and a mismatch, a single factor or a stale authentication is refused saying which, as for a transfer.\nThe core does not convert, so a confirmed quote moves in two legs within one currency each, through the bank's own account in each currency: `out` from the customer's sell account, then `in` to their buy account. `posted` with `money_moved: true` has both; `posting` with only `out_core_ref` is half done — the customer's money has left and the other currency has not arrived — and is shown as that; `refused` means the first leg was refused and nothing moved; `authorized` with `money_moved: false` means the bank holds no account in one of the two currencies.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransferConfirmBody"},"example":{"method":"device_biometric","authenticated_at":"2026-09-24T10:02:30Z","evidence_ref":"no-identity-provider:01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01","signed_amount":"1000.00","signed_currency":"USD","signed_payee":"Your EUR account"}}}},"responses":{"200":{"description":"The quote, settled, and whether both legs moved","content":{"application/json":{"example":{"id":"01992d10-7a2b-7c3d-8e4f-5a6b7c8d9e01","from_account":"acct-usd-1","to_account":"acct-eur-1","sell_minor":100000,"sell_currency":"USD","buy_minor":86800,"buy_currency":"EUR","mid":"0.87237","rate":"0.86800815","spread_bps":50,"spread_minor":437,"remainder":89880000,"remainder_denominator":1000000000,"rate_source":"European Central Bank reference rate","rate_as_of":"2026-09-18","state":"posted","refused_reason":null,"created_at":"2026-09-24T10:02:00Z","authorize":{"amount":"1000.00","currency":"USD","payee":"Your EUR account"},"out_core_ref":"11","in_core_ref":"12","money_moved":true},"schema":{"$ref":"#/components/schemas/ExchangeQuoteView"}}}},"400":{"description":"Not an authentication method, a single factor, an authentication outside its window, or signed figures that are not this quote's. The answer says which\n"},"409":{"description":"Already settled, or too old to act on. One quote, one answer."}}}},"/disputes":{"get":{"tags":["disputes"],"summary":"The caller's disputes, newest first, and the reasons a new one can have","description":"Every sentence a customer reads is composed by the disputes module and sent here; the app renders it and composes none. `provisional_credit_proposed` says proposed because it is: a provisional credit is an approval a second person has not necessarily signed yet.\n","responses":{"200":{"description":"Cases with their stage, what happens next and the moves the customer may make","content":{"application/json":{"example":{"disputes":[{"id":"01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b11","payment_kind":"transfer","payment_ref":"01992c70-1a2b-7c3d-8e4f-5a6b7c8d9e0f","amount_minor":12980,"currency":"USD","reason":"not_received","reason_prompt":"I paid and it never arrived","stage":"evidence_required","next_step":"We still need: What you ordered, and what arrived.","deadline":null,"outstanding":[{"kind":"order_detail","ask":"What you ordered, and what arrived"}],"provisional_credit_proposed":false,"outcome":null,"raised_at":"2026-09-24T09:12:00Z","your_moves":["supply_evidence","withdraw"]}],"reasons":[{"reason":"not_received","prompt":"I paid and it never arrived","within_days":120,"needs":[{"kind":"order_detail","ask":"What you ordered, and what arrived"},{"kind":"merchant_contacted","ask":"That you asked the merchant first"}],"provisional_credit":false}]},"schema":{"$ref":"#/components/schemas/DisputeList"}}}}}},"post":{"tags":["disputes"],"summary":"Dispute one of the caller's payments","description":"The payment is looked up in the platform's own records and scoped to the caller: a card settlement on one of their cards, or a transfer of theirs that was authorised. Its amount and date come from there and never from this request. A reason outside the closed list, or raised after its reason's limit, is refused with the limit named. One payment has at most one open case: a second attempt answers 409 and carries the case that already exists.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["payment_kind","payment_ref","reason"],"properties":{"payment_kind":{"type":"string","enum":["card","transfer"]},"payment_ref":{"type":"string","description":"A card authorisation id","or a transfer id":null},"reason":{"type":"string","enum":["not_recognised","not_received","not_as_described","cancelled","duplicate","wrong_amount"]}}}}}},"responses":{"201":{"description":"The case, and for the reasons that earn one, a provisional credit proposed","content":{"application/json":{"example":{"id":"01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b10","payment_kind":"card","payment_ref":"auth-7Q2K9","amount_minor":685,"currency":"USD","reason":"not_recognised","reason_prompt":"I do not recognise this payment","stage":"raised","next_step":"We have what we need. We will come back to you as this moves.","deadline":null,"outstanding":[],"provisional_credit_proposed":true,"outcome":null,"raised_at":"2026-09-24T09:12:00Z","your_moves":["withdraw"]},"schema":{"$ref":"#/components/schemas/DisputeCaseView"}}}},"400":{"description":"Not a reason on the list, or raised after its limit, which the detail names"},"404":{"description":"Not one of the caller's payments. Never 403"},"409":{"description":"A case is already open on this payment, and the body carries it","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeAlreadyOpen"}}}}}}},"/disputes/{id}/evidence":{"post":{"tags":["disputes"],"summary":"Supply something the case asked for","description":"Only a kind the case's reason asks for is accepted. Supplying the last thing outstanding moves the case on in the same transaction, so a customer is never asked again for what they gave.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","reference"],"properties":{"kind":{"type":"string","enum":["order_detail","cancellation_proof","agreed_amount","merchant_contacted"]},"reference":{"type":"string","minLength":1,"maxLength":2000,"pattern":"\\S","description":"Trimmed before checking; must be non-blank and at most 2000 characters after trimming"}}}}}},"responses":{"200":{"description":"The case as it now stands","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeCaseView"}}}},"400":{"description":"The case does not ask for that"},"404":{"description":"Not the caller's case"},"409":{"description":"The case is not asking for evidence at the moment"}}}},"/disputes/{id}/withdraw":{"post":{"tags":["disputes"],"summary":"Take a case back","description":"Allowed from any open stage. Where money went back provisionally, its reversal is proposed in the same transaction, because withdrawing says the claim no longer stands.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The case, withdrawn","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisputeCaseView"}}}},"404":{"description":"Not the caller's case"},"409":{"description":"The case is already closed"}}}},"/backoffice/disputes":{"get":{"tags":["disputes","back-office"],"summary":"Open cases, oldest first, each with the moves the module allows","description":"The moves are the disputes module's table, so this queue cannot offer a move the platform then refuses. Evidence is shown as the customer wrote it.\n","responses":{"200":{"description":"The queue","content":{"application/json":{"example":{"count":1,"disputes":[{"id":"01992c7e-5b1a-7f00-8a3e-4f0c9d2e1b10","customer":"ana","payment_kind":"card","payment_ref":"auth-7Q2K9","amount_minor":685,"currency":"USD","reason":"not_recognised","stage":"raised","next_step":"We have what we need. We will come back to you as this moves.","evidence":[],"provisional_approval_id":"01992c7e-5b1b-7c00-9b11-2d3e4f5a6b7c","settlement_approval_id":null,"moves":["send_to_merchant","decide"]}]},"schema":{"$ref":"#/components/schemas/BackofficeMoneyDisputeQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/disputes/{id}/moves":{"post":{"tags":["disputes","back-office"],"summary":"Send a case to the merchant, or decide it","description":"Deciding is the operator's act and is recorded directly. What the decision does to money is not: an upheld case's refund, or a declined case's reversal of a provisional credit, is an approval of kind `dispute_refund` raised in the same transaction as the decision, which a different person approves. Its executor reads the amount from the case, refuses an approval the case did not record, and books money returned to the customer as money in, never as the payment seam's refund, which takes money out. Where the bank's funding account is configured (`PLATFORM_DISPUTE_FUNDING_ACCOUNT`), the money moves between it and the account the disputed payment left from, and the execution record carries `money_moved: true` and the core's `core_ref`; otherwise it is a ledger entry and says `money_moved: false`. A posted transfer can be disputed as well as an authorised one.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["move"],"properties":{"move":{"type":"string","enum":["send_to_merchant","decide"],"description":"Unknown label is 400. The customer labels supply_evidence and withdraw parse but are refused with 409.\n"},"outcome":{"type":"string","enum":["upheld","declined"],"description":"Required when move is decide (else 400); ignored for send_to_merchant."}}}}}},"responses":{"200":{"description":"The case as it now stands, with any approval it raised","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyDisputeCase"}}}},"400":{"description":"Not a move, or a decision without an outcome","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a case in this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The module does not allow that move from this stage, or somebody moved it first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/support":{"get":{"tags":["support"],"summary":"The caller's own conversations","responses":{"200":{"description":"Conversations, each showing whose turn it is","content":{"application/json":{"example":[{"id":"9f8e7d6c-5b4a-4938-2716-0a5b4c3d2e1f","topic":"A payment I do not recognise","turn":"waiting_on_us","waiting_on_us":true,"resolved":false,"created_at":"2026-09-22T09:41:00Z","messages":[{"author":"customer","channel":"written","body":"There is a payment on my card I did not make","at":"2026-09-22T09:41:00Z","from_assistant":false}],"callback":{"state":"requested","attempts":0,"still_owed":true,"topic":"A payment I do not recognise","requested_at":"2026-09-22T09:44:00Z"},"callback_note":"we call you on the number we already hold; we will never ask you to give one out"}],"schema":{"type":"array","items":{"$ref":"#/components/schemas/SupportConversation"}}}}}}},"post":{"tags":["support"],"summary":"Write in","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"topic":{"type":"string","nullable":true},"message":{"type":"string","nullable":true}}}}}},"responses":{"201":{"description":"The conversation, now waiting on the bank","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportConversation"}}}}}}},"/support/{id}/callback":{"post":{"tags":["support"],"summary":"Ask us to call","description":"Written first, and voice is a callback the bank makes. A call is written back into the same thread so an agent never makes the customer explain it twice.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"topic":{"type":"string","nullable":true,"description":"Refused (400) if it looks like card data"}}}}}},"responses":{"200":{"description":"A callback is owed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportConversation"}}}},"404":{"description":"Not the caller's conversation"}}},"delete":{"tags":["support"],"summary":"Never mind, no call needed","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The callback is no longer owed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportConversation"}}}},"404":{"description":"Not the caller's conversation"}}}},"/support/{id}/messages":{"post":{"tags":["support"],"summary":"Say something","description":"Reopens a resolved thread, because a reply is a continuation. A message carrying what looks like card data is refused outright: no card number reaches platform storage or logs by any door, and customers paste them (Principle VIII).\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"pattern":"\\S","description":"Must be non-blank; stored trimmed; refused (400) if it looks like card data"}}}}}},"responses":{"200":{"description":"The conversation, now waiting on the bank","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportConversation"}}}},"400":{"description":"Empty, or carrying something that looks like card data"},"404":{"description":"Not the caller's conversation"}}}},"/backoffice/support":{"get":{"tags":["support","back-office"],"summary":"The queue, by who has been waiting longest on us","description":"Whose turn it is is derived from the messages rather than stored, so the filtering happens in the handler. A `WHERE turn = 'us'` would need a stored turn, which is the thing this avoids. `calls_owed` is carried separately because a callback owed is a different debt from a reply owed.\n","responses":{"200":{"description":"Conversations waiting on the bank, longest first, and the calls owed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeSupportQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/support/{id}/draft":{"post":{"tags":["support","back-office"],"summary":"A reply for the operator to edit and send, from the assistant","description":"Spec 061. From the packet the assistant handed over with the conversation and its transcript: the model's draft where PLATFORM_ASSISTANT_MODEL_URL names one, the rules' where it does not. **This sends nothing**: the operator edits it and replies as themselves.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The draft, and what wrote it","content":{"application/json":{"schema":{"type":"object","required":["draft","by"],"properties":{"draft":{"type":"string"},"by":{"type":"string","description":"rules, or model <name>"}}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such conversation"}}}},"/assistant":{"post":{"tags":["support"],"summary":"Start a conversation with OxaBank's assistant","description":"Spec 061. Voiager's machine and policy table, on the customer's own accounts and cards: it reads, freezes a card once the customer says yes, offers a statement, and hands everything else in its table to a person as a support conversation with the packet and the transcript. Marked as an assistant, never disguised.\n","responses":{"201":{"description":"The assistant's opening line","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantConversation"}}}}}}},"/assistant/{id}":{"get":{"tags":["support"],"summary":"A conversation with the assistant, the caller's own","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The conversation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantConversation"}}}},"404":{"description":"Not the caller's"}}}},"/assistant/{id}/turns":{"post":{"tags":["support"],"summary":"Say something to the assistant","description":"Spec 061. Read by the rules, or where they find nothing by the optional model as one word from a closed list. An act (freezing a card) happens only after a yes to that exact question, through the same route the app uses, as the customer. Nothing card-shaped is kept.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":1000}}}}}},"responses":{"200":{"description":"The conversation, with this turn's reply","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantConversation"},"example":{"id":"0199d000-0000-7000-8000-0000000000f1","phase":"confirm","finished":false,"answered_by":"rules","support_id":null,"may":["Read your balances, your recent activity and last month's statement"],"never":["Move money"],"messages":[{"who":"assistant","text":"Hi Anna, I'm OxaBank's assistant. What can I do for you?","at":"2026-10-05T21:00:00Z","from_assistant":true},{"who":"customer","text":"I lost my card","at":"2026-10-05T21:00:05Z","from_assistant":false},{"who":"assistant","text":"I can freeze your card ending 4242 now, so nothing more goes through on it. You can unfreeze it later in Cards. Shall I?","at":"2026-10-05T21:00:05Z","from_assistant":true}],"reply":{"lines":["I can freeze your card ending 4242 now, so nothing more goes through on it. You can unfreeze it later in Cards. Shall I?"],"spoken":["I can freeze your card ending in four two four two now, so nothing more goes through on it. You can unfreeze it later in Cards. Shall I?"],"offers":[],"suggestions":["Yes, please","No, thanks"],"finished":false,"handed_over":false},"voice":{"voice":"af_heart","speed":0.95,"acknowledgements":["Sure, one moment.","Okay, let me check."],"not_heard":"Sorry, I didn't catch that. Could you say it again?"},"created_at":"2026-10-05T21:00:00Z"}}}},"400":{"description":"Empty","too long":null,"or card-shaped":null},"404":{"description":"Not the caller's"}}}},"/backoffice/support/calls":{"post":{"tags":["support","back-office"],"summary":"Move a callback along","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"routes/support.rs `CallProgressBody`.","required":["conversation_id","outcome"],"properties":{"conversation_id":{"type":"string","format":"uuid"},"outcome":{"$ref":"#/components/schemas/BackofficeCallbackOutcome"},"summary":{"type":"string","nullable":true,"description":"What was said, written into the thread as the agent's voice message. Required for outcome completed: at least 8 bytes after trimming, and no card number (400). Ignored for scheduled and no_answer. Stored trimmed.\n"}}}}}},"responses":{"200":{"description":"The conversation, with the call's outcome written into the thread","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportConversation"}}}},"400":{"description":"Problem. `outcome` is not scheduled|no_answer|completed; outcome completed with a summary under 8 bytes after trimming (or none); a completed summary that looks like a card number; or several tenant memberships. Malformed JSON is also 400 (axum, plain text).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such conversation in this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"The callback's current state does not allow the move, e.g. scheduling one that is completed/abandoned/already scheduled, no_answer on one merely requested, completing one that is completed/abandoned. Problem, detail \"a callback that is <state> cannot be <action>\".\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"415":{"description":"Content-Type is not application/json (axum, plain text)."},"422":{"description":"Missing conversation_id/outcome, conversation_id not a UUID, or wrong JSON types (axum, plain text)."},"500":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"No database configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/support/replies":{"post":{"tags":["support","back-office"],"summary":"Reply to a customer","description":"`from_assistant` is **told, not detected**. An assistant's reply does not discharge the bank's turn, and a queue that counted it would look healthy while nobody was being helped - the most common way support metrics are gamed. A conversation is answered only when a person sends one. Agents paste card numbers too, so a reply carrying what looks like card data is refused here as well.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["conversation_id","body"],"properties":{"conversation_id":{"type":"string","format":"uuid"},"body":{"type":"string","minLength":1,"description":"Must be non-empty after trim; refused (400) if it looks like card data. Stored trimmed."},"from_assistant":{"type":"boolean","default":false},"channel":{"type":"string","nullable":true,"enum":["chat","voice",null],"default":"chat","description":"Server accepts any string; anything other than \"voice\" is stored as \"chat\"."}}}}}},"responses":{"200":{"description":"The conversation. Whose turn it is now depends on who actually replied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportConversation"}}}},"400":{"description":"Empty, or carrying something that looks like card data"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such conversation in this tenant"}}}},"/backoffice/support/resolve":{"post":{"tags":["support","back-office"],"summary":"Mark a conversation resolved, or reopen it","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["conversation_id"],"properties":{"conversation_id":{"type":"string","format":"uuid"},"resolved":{"type":"boolean","default":true}}}}}},"responses":{"200":{"description":"The conversation. A customer's next message reopens it regardless","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportConversation"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such conversation in this tenant"}}}},"/backoffice/days/{day}":{"get":{"tags":["back-office","reconciliation"],"summary":"What closing a business day would meet, and how its close went","description":"Spec 033. The day is a calendar day in the bank's zone (`PLATFORM_BANK_TIMEZONE`, UTC by default). Lists the payments still `posting` that were authorised in it, each bound rail's cut-off on it and whether it has passed, the reconciliation runs that covered it with their exceptions, the core jobs a close runs, and the close: `open`, `proposed`, `closed` (with the core's report job by job and how many balances were kept) or `failed` (with why). Spec 035: each rail's `holiday` on the day by its calendar (`PLATFORM_CALENDARS`, `PLATFORM_HOLIDAYS`), or null; and `close.proposed_by` is `schedule` where the platform proposed the day itself (`PLATFORM_DAY_CLOSE_AT`), its proposal carrying the day's checks. Spec 049: `business_date`, the core's open business day where it keeps one (null where it books on the calendar), and `not_the_open_day`, why this day cannot be closed now (days are closed in order, each the core's open one), or null; a closed day's `close.report` holds its `transactions` (the day's movements in the core for every account a balance was kept for, `by_kind` with kind, rail, currency, count, in_minor and out_minor, and the `exceptions` the platform did not make or cannot name), what was `carried_over` (transfers, exchanges half done, pulls, money arriving, approvals that move money, card holds) and its `loans` (how many, in arrears and by how much, the interest the day accrued, the newly overdue).\n","parameters":[{"name":"day","in":"path","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"The day's checks and its close","content":{"application/json":{"example":{"day":"2026-09-30","zone":"UTC","over":true,"posting":[],"cutoffs":[{"rail":"sepa","cutoff":"14:00","zone":"Europe/London","at":"2026-09-30T13:00:00Z","passed":true,"holiday":null}],"reconciliation":[{"run_id":"01a0f7b1-0000-7000-8000-000000000001","account_ref":"5","ran_by":"schedule","clean":4,"exceptions":0}],"jobs":["SA_PINT","ACC_AATR","LA_ARAG","LA_OPEN","LA_UNPA","ACC_RBAL","TBL_UDET","LA_ECOB"],"business_date":"2026-09-30","not_the_open_day":null,"close":{"state":"open"}},"schema":{"type":"object"}}}},"400":{"description":"Not a day written YYYY-MM-DD"},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/days/{day}/close":{"post":{"tags":["back-office"],"summary":"Propose closing a business day, for a second operator to approve","description":"Spec 033. Proposes a `day_close` approval. Once a second operator approves it, the core's close of business runs (each job reported), every customer account's balance at that moment is kept with the day, and the day is closed — or `failed` with why, after which it may be proposed again. A day not yet over in the bank's zone, or already proposed or closed, is refused. Spec 049: where the core keeps a business date, so is a day that is not its open one; closing advances it by a day before the jobs run (Loan COB among them), and the close keeps the day's transactions, what carried over, and every loan's position.\n","parameters":[{"name":"day","in":"path","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"201":{"description":"Proposed; it waits for a second operator under /approvals","content":{"application/json":{"example":{"day":"2026-09-30","approval_id":"01a0f7b1-0000-7000-8000-000000000002","close":{"state":"proposed"}},"schema":{"type":"object"}}}},"400":{"description":"Not a day written YYYY-MM-DD"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"The day is not over yet, or is already proposed or closed"}}}},"/backoffice/statements":{"get":{"tags":["reconciliation","back-office"],"summary":"The bank statements the platform holds","description":"The tenant's statements, the most recently ingested first: each one's heading, the file it came from and how many lines it held. Under the back office, so only the bank's operators reach it; the contract had it at `/statements`, where a customer's session would have been all that was asked. Amounts are decimal strings.\n","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"The statements, newest first","content":{"application/json":{"example":{"statements":[{"id":"01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e20","account_ref":"USD-OPERATING","statement_ref":"STMT-2026-09-29","opening_balance":"125000.0000","closing_balance":"126250.0000","currency":"USD","from":"2026-09-29","to":"2026-09-29","source":"camt053-2026-09-29.xml","ingested_at":"2026-09-30T06:10:00Z","entry_count":3}]},"schema":{"type":"object","required":["statements"],"properties":{"statements":{"type":"array","items":{"$ref":"#/components/schemas/BankStatement"}}}}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}},"post":{"tags":["reconciliation","back-office"],"summary":"Hand the platform a statement the bank sent","description":"The bank's side of reconciliation. The body is the file as the bank delivered it — CAMT.053 today — and it is parsed by `BankStatementAdapter`, never by a parser of the route's own. Each statement in it is stored once per account and statement reference: the same file again is `already_held` and stores nothing. Every line keeps the bank's status, and reconciliation reads a line that is not booked as provisional rather than as a break. `reconciles` says whether the booked lines add up from the opening to the closing balance; a file that does not is stored and flagged, because it is what the bank said.\n","parameters":[{"name":"source","in":"query","required":true,"schema":{"type":"string","minLength":1},"description":"The file's name; trimmed, and blank after trimming is 400. Stored as source_file on every statement."}],"requestBody":{"required":true,"description":"The statement file's bytes, as the bank delivered it. The handler takes axum::body::Bytes and never reads Content-Type, so ANY content type is accepted (application/xml, text/xml, application/octet-stream, or none); what matters is that the bytes are UTF-8 CAMT.053 XML (the only BankStatementAdapter wired in state.rs is platform_adapter_camt053::Camt053). Limit 2 MiB.\n","content":{"application/xml":{"schema":{"type":"string","format":"binary"}},"text/xml":{"schema":{"type":"string","format":"binary"}},"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"What each statement in the file came to","content":{"application/json":{"example":{"source":"camt053-2026-09-15.xml","statements":[{"statement_ref":"STMT-2026-258","account_ref":"DE89370400440532013000","currency":"EUR","period_from":"2026-09-15","period_to":"2026-09-15","outcome":"stored","statement_id":"01a0d5d0-1a2b-7c3d-8e4f-5a6b7c8d9e03","entries":3,"pending_entries":1,"reconciles":true}]},"schema":{"$ref":"#/components/schemas/BackofficeStatementUpload"}}}},"400":{"description":"Not a statement this platform reads; the parser's reason is given and nothing is stored"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"413":{"description":"Body over axum's default 2 MiB limit (no DefaultBodyLimit is configured). Plain text."},"500":{"description":"A store failure part way through a multi-statement file; statements before it stay stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"No database configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/reconciliation/runs":{"get":{"tags":["reconciliation","back-office"],"summary":"Recent runs, newest first","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int64","default":50,"minimum":1,"maximum":200},"description":"Clamped to 1..200 rather than refused. The contract has no default; the code's is 50."}],"responses":{"200":{"description":"Runs, each with what every source offered and what disagreed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationRunList"}}}},"400":{"description":"`limit` is not an integer (axum Query rejection, plain text), or several tenant memberships (Problem)."},"403":{"$ref":"#/components/responses/OperatorsOnly"},"500":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"post":{"tags":["reconciliation","back-office"],"summary":"Reconcile one account over a period","description":"Acts directly rather than proposing: running a comparison changes nothing, and what comes out of it is where a person and a second signature belong. A period in which one source offered nothing while the others offered something is **refused as a run** rather than reported as breaks, because that is a missing statement far more often than a quiet day. A run that could not read one source completely says so, and does not present unread records as disagreements.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationRunRequest"}}}},"responses":{"200":{"description":"The run, with each disagreement classified rather than merely counted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationRun"}}}},"400":{"description":"Not a period that can be reconciled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"409":{"description":"A source offered nothing while the others offered something. Refused as a run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"No database, no core adapter, or the core failed while paging","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/reconciliation/runs/{id}":{"get":{"tags":["reconciliation","back-office"],"summary":"One run and its breaks","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The run, the counts each source offered, and every break with its kind","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationRun"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such run in this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Documented from the code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/backoffice/reconciliation/exceptions":{"get":{"tags":["reconciliation","back-office"],"summary":"Breaks still open, newest run first","description":"A disagreement that will resolve itself with time stays visible while it is unresolved: the books do not balance until it does.\n","responses":{"200":{"description":"Open breaks, each saying what acting on it would actually do","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationQueue"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"}}}},"/backoffice/reconciliation/resolve":{"post":{"tags":["reconciliation","back-office"],"summary":"Close a break, or propose the entry that will close it","description":"`explained` and `written_off` act directly: they are an operator writing down what they found, the same exception the onboarding verdict route makes and for the same reason. `adjusting_entry` does not - it posts money to the ledger, so it raises a proposal, and the break **stays open until a second person approves it and the entry actually lands**. Only a payment the engine and the bank both saw and the bank never booked is resolvable by posting; every other kind is refused that route.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationResolveRequest"}}}},"responses":{"200":{"description":"Closed, for the two resolutions an operator may complete alone","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationResolved"}}}},"202":{"description":"Proposed. The break stays open until the entry is actually posted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationAdjustmentProposed"}}}},"400":{"description":"Not a resolution, a break this resolution does not apply to, or a token note","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"No such break in this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"MISSING FROM THE CONTRACT. Break already closed; an entry already proposed for it; the break has no record/amount to post from; or a concurrent resolve/attach won.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/sessions":{"get":{"tags":["sessions"],"summary":"Where the caller is signed in","description":"The caller's live sessions, most recently used first, filtered by tenant and subject in the query. `current` is true on exactly one, the session this request came from. `last_used_at` is right to within five minutes. The device identifier is never returned.\n","responses":{"200":{"description":"The caller's live sessions","content":{"application/json":{"example":{"sessions":[{"id":"01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01","name":null,"model":"Google Pixel 8","os":"Android 14","app_version":"1.0.0","began_at":"2026-09-25T09:00:00Z","last_used_at":"2026-09-25T09:14:00Z","expires_at":"2026-09-26T09:00:00Z","current":true},{"id":"01992d7a-5e6f-7a8b-9c0d-1e2f3a4b5c02","name":"Work laptop","model":"Chrome","os":"Windows","app_version":"1.0.0","began_at":"2026-09-24T18:30:00Z","last_used_at":"2026-09-24T19:05:00Z","expires_at":"2026-09-25T18:30:00Z","current":false}]},"schema":{"type":"object","required":["sessions"],"properties":{"sessions":{"type":"array","items":{"$ref":"#/components/schemas/SessionView"}}}}}}},"401":{"description":"`signed-out`: no live session holds this token"},"503":{"description":"No store"}}},"post":{"tags":["sessions"],"summary":"Begin this token's session on this device","description":"The one request a token makes before its session exists, so it is not refused for having none, and it verifies its own token. Beginning ends the live session this device had for this customer, as `replaced`. What the device says it is gets recorded for a person to recognise and is trusted for nothing. The device identifier is never returned, here or anywhere: it is the device's, and nobody else needs it. A token whose session has ended begins nothing and is answered `signed-out`, the same whether its session ended, expired or was replaced. Spec 043: a person the provider verified who is a member of no tenant first joins `PLATFORM_SIGNUP_TENANT` as a customer, once, audited. A token carrying the provider's session (`sid`) of a live session from this device continues it: 200, the same session, the new token holding it and the old one no longer. A provider session whose platform session was signed out or ended by its person or an operator begins nothing and is answered `signed-out`; its person signs in again.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["device_id"],"properties":{"device_id":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,64}$"},"model":{"type":"string","maxLength":64,"description":"Trimmed. No control character"},"os":{"type":"string","maxLength":64,"description":"Trimmed. No control character"},"app_version":{"type":"string","maxLength":64,"description":"Trimmed. No control character"}}},"example":{"device_id":"9f2c4e1ab7d0435e8c6f1a2b3c4d5e6f","model":"Google Pixel 8","os":"Android 14","app_version":"1.0.0"}}}},"responses":{"200":{"description":"This token's session already exists and is live. The same body, so that a retried begin is not a failure\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionView"}}}},"201":{"description":"A session begun","content":{"application/json":{"example":{"id":"01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01","name":null,"model":"Google Pixel 8","os":"Android 14","app_version":"1.0.0","began_at":"2026-09-25T09:00:00Z","last_used_at":"2026-09-25T09:00:00Z","expires_at":"2026-09-26T09:00:00Z","current":true},"schema":{"$ref":"#/components/schemas/SessionView"}}}},"400":{"description":"A description outside the bounds above; the detail says which. Or a subject in several tenants that named none, as on every route\n"},"401":{"description":"The token is not valid; or `signed-out`, because this token's session has ended. One answer whatever the reason, because telling a caller which would tell somebody holding a copied token what happened to its owner\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"},"example":{"type":"https://oxabank.dev/problems/signed-out","title":"Signed out","status":401,"detail":"You are signed out on this device. Sign in again.","correlation_id":"01992e41-8b2c-7d3e-9f4a-5b6c7d8e9f01"}}}},"403":{"description":"The subject belongs to no tenant"},"409":{"description":"Another sign-in from this device won a race with this one. Beginning again succeeds"},"503":{"description":"No store, or the identity provider cannot be reached"}}}},"/sessions/{id}/end":{"post":{"tags":["sessions"],"summary":"End one of the caller's sessions","description":"Ending the session this request came from is signing out, and answers `signed_out`; this response is delivered, and the token is refused from its next request. Ending another of the caller's sessions answers `ended_by_customer`. One that has already ended answers 200 with how it ended, so that a retry after a lost response is not a failure.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"How the session ended","content":{"application/json":{"example":{"id":"01992d7a-5e6f-7a8b-9c0d-1e2f3a4b5c02","ended_at":"2026-09-25T09:15:00Z","ending":"ended_by_customer"},"schema":{"$ref":"#/components/schemas/SessionEndedView"}}}},"401":{"description":"`signed-out`: the session this request came from has already ended"},"404":{"description":"Not one of the caller's sessions. Somebody else's and one that never existed get the same answer\n"}}}},"/sessions/{id}/name":{"post":{"tags":["sessions"],"summary":"Name a device, through one of its sessions","description":"The name is the device's, reached through one of its sessions, because the device identifier is never returned. The session has to be one of the caller's and live. Trimmed, then at most 64 characters, none of them a control character, in any script; an empty name removes it. Every device of the customer's shows it, and the device keeps it when it signs in again. Every change appends `session.named` to the audit, saying whether a name was set or removed and never what it was; saving the name a device already has changes nothing and appends nothing. The platform keeps the name sealed and opens it only to answer this customer. No operator reads it.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Trimmed, then at most 64 characters, no control character. Empty removes the name"}}},"example":{"name":"Work laptop"}}}},"responses":{"200":{"description":"The session as the list shows it, with the name it now has","content":{"application/json":{"example":{"id":"01992d7a-5e6f-7a8b-9c0d-1e2f3a4b5c02","name":"Work laptop","model":"Chrome","os":"Windows","app_version":"1.0.0","began_at":"2026-09-24T18:30:00Z","last_used_at":"2026-09-24T19:05:00Z","expires_at":"2026-09-25T18:30:00Z","current":false},"schema":{"$ref":"#/components/schemas/SessionView"}}}},"400":{"description":"Longer than 64 characters, or a control character in it; the detail says which"},"401":{"description":"`signed-out`: no live session holds this token"},"404":{"description":"Not one of the caller's live sessions. Somebody else's, one that ended and one that never existed get the same answer\n"},"503":{"description":"No store"}}}},"/backoffice/sessions":{"get":{"tags":["sessions","backoffice"],"summary":"A customer's live sessions, for an operator","description":"Operators only: anybody else is refused with 403 before a row is read, and the body is the forbidden problem, saying nothing about any customer. The sessions are by what each device says it is, without `current`, since the operator is on none of them, and without `name`, which is the customer's alone. A subject with no live sessions, or unknown to the tenant, is 200 with an empty list: telling the two apart would answer \"is this person a customer here?\".\n","parameters":[{"name":"subject","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The customer's live sessions","content":{"application/json":{"example":{"subject":"customer-1","sessions":[{"id":"01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01","model":"Google Pixel 8","os":"Android 14","app_version":"1.0.0","began_at":"2026-09-25T09:00:00Z","last_used_at":"2026-09-25T09:14:00Z","expires_at":"2026-09-26T09:00:00Z"}]},"schema":{"type":"object","required":["subject","sessions"],"properties":{"subject":{"type":"string"},"sessions":{"type":"array","items":{"type":"object","required":["id","model","os","app_version","began_at","last_used_at","expires_at"],"properties":{"id":{"type":"string","format":"uuid"},"model":{"type":"string"},"os":{"type":"string"},"app_version":{"type":"string"},"began_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"}}}}}}}}},"400":{"description":"No `subject`"},"403":{"$ref":"#/components/responses/OperatorsOnly"},"503":{"description":"No store"}}}},"/backoffice/sessions/{id}/end":{"post":{"tags":["sessions","backoffice"],"summary":"End one of a customer's sessions, for them","description":"One operator, no second person. The audit names the operator as the actor and the customer in its detail. An already ended session answers 200 with how it ended; that session's token is refused from its next request.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"How the session ended","content":{"application/json":{"example":{"id":"01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e01","ended_at":"2026-09-25T09:20:00Z","ending":"ended_by_operator"},"schema":{"$ref":"#/components/schemas/SessionEndedView"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a session in the operator's tenant"}}}},"/sessions/end-others":{"post":{"tags":["sessions"],"summary":"Sign out everywhere else","description":"Ends every live session of the caller's but the one this request came from, each as `ended_by_customer` and each with its own audit entry naming the session that asked. A session whose token had already lapsed is written down as `expired` at its expiry and is not counted. Nothing else to end answers `ended: 0`.\n","responses":{"200":{"description":"How many sessions this ended","content":{"application/json":{"example":{"ended":2},"schema":{"type":"object","required":["ended"],"properties":{"ended":{"type":"integer","minimum":0}}}}}},"401":{"description":"`signed-out`: no live session holds this token"},"503":{"description":"No store"}}}},"/backoffice/loans/{id}/repayments/{repayment_id}/reverse":{"post":{"tags":["lending","back-office"],"summary":"Propose reversing a repayment","description":"A mistaken repayment is corrected by a **reversing row**, never by editing or deleting one: an arrears position that can be changed retroactively is not evidence of anything. Both rows survive - the original stays, marked reversed by the row pointing at it, and the reversal names its author. Proposes rather than acts, because reversing a repayment puts a customer back into arrears, and that is a notice, a fee and possibly a credit file entry. The reversing row carries the **target's** `received_on` and no `external_ref` of its own.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"repayment_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["note"],"properties":{"note":{"type":"string","minLength":10,"description":"Trimmed, char count >= 10, else 400."}}}}}},"responses":{"202":{"description":"Proposed. Nothing is reversed until a second person agrees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BackofficeMoneyReversalProposed"}}}},"400":{"description":"No reason given, or one too thin to stand as a record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/OperatorsOnly"},"404":{"description":"Not a repayment of this loan in this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"409":{"description":"Already reversed, or the row named is itself a reversal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}},"components":{"securitySchemes":{"oidc":{"type":"openIdConnect","openIdConnectUrl":"https://id.your-bank.example/.well-known/openid-configuration"}},"parameters":{"PathId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"Offset":{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Reserved. The platform does not read it yet; repeating a write repeats it."}},"responses":{"OperatorsOnly":{"description":"Not an operator of the tenant: a member who is not marked one, or somebody who belongs to no tenant. Decided before the route reads anything; the same body on every operator route whatever was asked, with nothing of any customer, card, approval, entry or event in it.\n","content":{"application/json":{"example":{"type":"https://oxabank.dev/problems/forbidden","title":"Forbidden","status":403,"detail":"This is for the bank's operators.","correlation_id":"01992e40-3c1d-7a2b-9e4f-1a2b3c4d5e99"},"schema":{"$ref":"#/components/schemas/Problem"}}}}},"schemas":{"Provenance":{"type":"string","enum":["live","synthetic","unavailable"],"description":"Required on every verification, screening or decision result. `unavailable` means the capability does not exist yet and is never replaced by an invented outcome.\n"},"Problem":{"type":"object","description":"RFC 9457 problem detail. Carries no internal or vendor detail.","properties":{"type":{"type":"string"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"correlation_id":{"type":"string","format":"uuid"}}},"Tenant":{"type":"object","required":["id","slug","status"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"display_name":{"type":"string"},"status":{"type":"string","enum":["active","suspended"]}}},"ApprovalState":{"type":"string","enum":["proposed","approved","rejected","executed","failed"]},"NewApproval":{"type":"object","required":["subject_type","subject"],"properties":{"subject_type":{"type":"string","example":"journal_entry"},"subject":{"type":"object"},"proposal_id":{"type":"string","format":"uuid","nullable":true}}},"Approval":{"description":"Corrected: the server has no richer approval view. Every approvals route returns ApprovalView; the fields tenant_id, subject_type, proposed_at, decided_at, rejection_reason and proposal_id are never sent. Kept as an alias so existing $refs (POST /approvals 201, POST /rules 201) stay valid.\n","allOf":[{"$ref":"#/components/schemas/ApprovalView"}]},"AuditEntry":{"type":"object","description":"routes/audit.rs `AuditView`. No tenant_id and no detail are returned.","required":["id","correlation_id","actor","action","subject_type","subject_id","occurred_at","hash"],"properties":{"id":{"type":"integer","format":"int64","description":"Chain position; ascending"},"correlation_id":{"type":"string","format":"uuid"},"actor":{"type":"string"},"action":{"type":"string"},"subject_type":{"type":"string"},"subject_id":{"type":"string"},"occurred_at":{"type":"string","format":"date-time"},"hash":{"type":"string","pattern":"^[0-9a-f]{64}$","description":"SHA-256 chain hash, 64 lowercase hex characters"}}},"ChainVerification":{"type":"object","description":"routes/audit.rs `ChainVerification`. All four keys are always present.","required":["intact","entries_checked","first_divergence_at","reason"],"properties":{"intact":{"type":"boolean"},"entries_checked":{"type":"integer","minimum":0,"description":"When intact, the chain length. When broken, the zero-based INDEX of the divergent entry (i.e. how many entries verified before it), not the chain length.\n"},"first_divergence_at":{"type":"integer","format":"int64","nullable":true,"description":"The AuditEntry.id of the first divergent entry. Null when intact."},"reason":{"nullable":true,"description":"Null when intact.","allOf":[{"$ref":"#/components/schemas/AuditVerifyReason"}]}}},"Rule":{"type":"object","properties":{"id":{"type":"string"},"version":{"type":"integer"},"scope":{"type":"string"},"condition":{"type":"object"},"action":{"type":"object"},"author":{"type":"string"},"approved_by":{"type":"string","nullable":true},"effective_from":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["draft","active","superseded"]}}},"Proposal":{"type":"object","description":"platform-api/src/routes/proposals.rs `view`. A suggestion, never an instruction.","required":["id","subject_type","subject_ref","classification","explanation","suggested_action","confidence","evidence","provenance","provider","provider_version","prompt_hash","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"subject_type":{"type":"string","description":"What was judged — `screening_match`"},"subject_ref":{"type":"string","description":"Which one — for a screening match, `key@revision`"},"classification":{"type":"string","description":"For a screening match, `likely_false_positive`, `likely_true_match` or `needs_review`"},"explanation":{"type":"string"},"suggested_action":{"type":"object","nullable":true,"description":"For a screening match, `{verdict: cleared|confirmed}`; null when the proposer suggests nothing"},"confidence":{"type":"number","minimum":0,"maximum":1},"evidence":{"type":"array","items":{"type":"object","required":["code","detail"],"properties":{"code":{"type":"string"},"detail":{"type":"string"}}}},"provenance":{"$ref":"#/components/schemas/Provenance"},"provider":{"type":"string","description":"`rule-based` for the deterministic proposer"},"provider_version":{"type":"string"},"prompt_hash":{"type":"string","description":"SHA-256 of the canonical enquiry"},"created_at":{"type":"string","format":"date-time"}}},"BankStatement":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"account_ref":{"type":"string"},"statement_ref":{"type":"string"},"opening_balance":{"type":"string"},"closing_balance":{"type":"string"},"currency":{"type":"string"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"entry_count":{"type":"integer"},"source":{"type":"string","description":"The file the statement arrived in"},"ingested_at":{"type":"string","format":"date-time"}}},"ReplayRequest":{"type":"object","description":"routes/events.rs `ReplayBody`. `from` is serde-untagged: a JSON string is read as a name (only \"earliest\" and \"latest\" are accepted; any other string is 400), a JSON non-negative integer as a position (u64; a negative or fractional number fails deserialisation, 422). `topic` outside the enum is 400.\n","required":["topic","from"],"properties":{"topic":{"$ref":"#/components/schemas/BackofficeReplayTopic"},"from":{"oneOf":[{"type":"string","enum":["earliest","latest"]},{"type":"integer","format":"int64","minimum":0}]},"limit":{"type":"integer","format":"int64","minimum":0,"default":1000,"nullable":true,"description":"How many of the tenant's published rows (all topics) to look at in this call, not how many to send. Absent or null means 1000; the value is clamped to 1..10000.\n"}}},"ApprovalView":{"type":"object","description":"What every approvals route actually returns (routes/approvals.rs `ApprovalView`): GET /approvals items, the 201 of POST /approvals and the 200 of POST /approvals/{id}/decision. Deliberately narrow: no tenant_id, proposed_at, decided_at, rejection_reason or proposal_id. `kind` is the subject_type, so a screen knows whose role decides it.\n","required":["id","kind","state","proposed_by","decided_by","correlation_id","subject"],"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"What it is: ApprovalKind's label (teller_movement, loan_decision, role_change…)"},"state":{"$ref":"#/components/schemas/ApprovalState"},"proposed_by":{"type":"string","description":"The proposer's verified token subject"},"decided_by":{"type":"string","nullable":true,"description":"The decider's verified token subject. Null while `proposed`."},"correlation_id":{"type":"string","format":"uuid"},"subject":{"description":"The proposal's subject exactly as proposed (a serde_json::Value stored as jsonb). Any JSON value; in practice an object. Its shape depends on the approval kind.\n"}}},"AuditVerifyReason":{"type":"string","description":"The Rust Debug name of `platform_audit::verify::Divergence`, i.e. PascalCase, not snake_case. ContentAltered - the stored hash no longer matches the entry (edited). LinkBroken - prev_hash does not match the previous entry (inserted, removed or reordered). MissingGenesis - the chain's first entry has a prev_hash.\n","enum":["ContentAltered","LinkBroken","MissingGenesis"]},"ReconciliationSide":{"type":"string","enum":["engine","core","bank"]},"ReconciliationExceptionKind":{"type":"string","description":"`format!(\"{:?}\", kind).to_lowercase()` of recon::ExceptionKind - Debug name lowercased with NO underscores (partialrefund, missingincore, missingatbank), not the serde snake_case form. Only `missingincore` can be resolved with `adjusting_entry`.\n","enum":["timing","fee","partialrefund","duplicate","missingincore","missingatbank","unknown"]},"ReconciliationResolution":{"type":"string","description":"CHECK constraint in the schema. `adjusting_entry` is only ever written when the adjusting approval executes (reconciliation_store::close_for_approval), never by POST.../resolve.\n","enum":["explained","written_off","adjusting_entry"]},"ReconciliationRecord":{"type":"object","description":"routes/reconciliation.rs `record_json`, one side's record as the run compared it.","required":["side","id","amount","currency","date","references","provisional"],"properties":{"side":{"$ref":"#/components/schemas/ReconciliationSide"},"id":{"type":"string","description":"The source's own identifier"},"amount":{"type":"string","description":"rust_decimal::Decimal as a string, always positive (e.g. \"250.00\")","example":"250.00"},"currency":{"type":"string","example":"USD","description":"May be \"\" for a core entry with no lines"},"date":{"type":"string","format":"date"},"references":{"type":"array","description":"Vec<(String, String)> serialised as an array of 2-element [name, value] arrays, e.g. [[\"correlation_id\",\"...\"],[\"end_to_end_id\",\"...\"]].\n","items":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"string"}}},"provisional":{"type":"boolean"}}},"ReconciliationMatch":{"type":"object","required":["matched_on","engine","core","bank"],"properties":{"matched_on":{"type":"string","description":"The keys that matched, joined with \", then \", or \"nothing\""},"engine":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ReconciliationRecord"}]},"core":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ReconciliationRecord"}]},"bank":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ReconciliationRecord"}]}}},"ReconciliationExceptionRecords":{"type":"object","description":"routes/reconciliation.rs `exception_payload`, stored as jsonb at run time.","required":["matched","unmatched"],"properties":{"matched":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ReconciliationMatch"}]},"unmatched":{"type":"array","items":{"$ref":"#/components/schemas/ReconciliationRecord"}}}},"ReconciliationException":{"type":"object","description":"routes/reconciliation.rs `exception_view`. One break.","required":["run_id","seq","kind","sides","detail","delta","currency","records","resolution","resolved_by","resolved_at","resolved_note","approval_id"],"properties":{"run_id":{"type":"string","format":"uuid"},"seq":{"type":"integer","format":"int32"},"kind":{"$ref":"#/components/schemas/ReconciliationExceptionKind"},"sides":{"type":"array","items":{"$ref":"#/components/schemas/ReconciliationSide"}},"detail":{"type":"string"},"delta":{"type":"string","nullable":true,"description":"Decimal as a string; null when a side is missing entirely"},"currency":{"type":"string","nullable":true},"records":{"$ref":"#/components/schemas/ReconciliationExceptionRecords"},"resolution":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ReconciliationResolution"}]},"resolved_by":{"type":"string","nullable":true},"resolved_at":{"type":"string","format":"date-time","nullable":true},"resolved_note":{"type":"string","nullable":true},"approval_id":{"type":"string","format":"uuid","nullable":true,"description":"Set with a null resolution means an adjusting entry is proposed and awaiting a second person"}}},"ReconciliationRun":{"type":"object","description":"routes/reconciliation.rs `run_view`. `exceptions` is populated by POST /runs and GET /runs/{id}; GET /runs always sends it as an empty array. The stored `rules` are never returned.\n","required":["id","account_ref","period","ran_at","ran_by","counts","truncated","exceptions"],"properties":{"id":{"type":"string","format":"uuid"},"account_ref":{"type":"string"},"period":{"type":"object","required":["from","to"],"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}},"ran_at":{"type":"string","format":"date-time"},"ran_by":{"type":"string","description":"The operator's token subject, or \"reconciliation-schedule\" for a scheduled run"},"counts":{"type":"object","required":["engine","core","bank","clean","exceptions"],"properties":{"engine":{"type":"integer","format":"int32"},"core":{"type":"integer","format":"int32"},"bank":{"type":"integer","format":"int32"},"clean":{"type":"integer","format":"int32"},"exceptions":{"type":"integer","format":"int32"}}},"truncated":{"type":"boolean","description":"True when the core's ledger hit the 50-page x 200-entry read ceiling"},"exceptions":{"type":"array","items":{"$ref":"#/components/schemas/ReconciliationException"}}}},"ReconciliationRunList":{"type":"object","description":"GET /backoffice/reconciliation/runs (not in this group's op list; included because the contract has no schema for it).","required":["count","runs"],"properties":{"count":{"type":"integer"},"runs":{"type":"array","items":{"$ref":"#/components/schemas/ReconciliationRun"}}}},"ReconciliationQueue":{"type":"object","description":"GET /backoffice/reconciliation/exceptions. Open breaks only (resolution IS NULL), newest run first then seq, at most 200.","required":["count","timing","awaiting_approval","exceptions"],"properties":{"count":{"type":"integer","description":"Number of exceptions in this response (capped at 200), not a total"},"timing":{"type":"integer","description":"How many of them are kind=timing"},"awaiting_approval":{"type":"integer","description":"How many have an approval_id"},"exceptions":{"type":"array","items":{"$ref":"#/components/schemas/ReconciliationException"}}}},"ReconciliationRunRequest":{"type":"object","required":["account_ref","from","to"],"properties":{"account_ref":{"type":"string"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date","description":"Must not be before `from`; (to - from) may be at most 62 days, else 400."}}},"ReconciliationResolveRequest":{"type":"object","required":["run_id","seq","resolution","note"],"properties":{"run_id":{"type":"string","format":"uuid"},"seq":{"type":"integer","format":"int32"},"resolution":{"type":"string","enum":["explained","written_off","adjusting_entry"]},"note":{"type":"string","minLength":10,"description":"At least 10 characters AFTER trimming (counted as Unicode chars), else 400. Checked before anything else, including before `resolution` is validated. Stored trimmed.\n"}}},"ReconciliationResolved":{"type":"object","description":"200 of POST /backoffice/reconciliation/resolve (explained | written_off).","required":["run_id","seq","resolution"],"properties":{"run_id":{"type":"string","format":"uuid"},"seq":{"type":"integer","format":"int32"},"resolution":{"type":"string","enum":["explained","written_off"]}}},"ReconciliationAdjustmentProposed":{"type":"object","description":"202 of POST /backoffice/reconciliation/resolve (adjusting_entry).","required":["approval_id","state","note"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]},"note":{"type":"string","description":"Fixed text: \"the break stays open until the entry is posted. Nothing has moved yet.\""}}},"BackofficeOnboardingQueue":{"type":"object","description":"GET /backoffice/onboarding. Onboardings in the tenant with at least one requirement not `satisfied` and no ineligible_reason, oldest first, at most 200.\n","required":["count","waiting_longest_since","onboardings"],"properties":{"count":{"type":"integer","minimum":0},"waiting_longest_since":{"type":"string","format":"date-time","nullable":true,"description":"created_at of the oldest onboarding in the list; null when the queue is empty."},"onboardings":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeOnboardingView"}}}},"BackofficeOnboardingView":{"type":"object","description":"routes/onboarding.rs `view()` (json!), non-ineligible branch. This is the SAME function GET /v1/onboarding, POST /v1/onboarding and POST /v1/onboarding/steps/{kind} return. The back office never sees the ineligible branch (the queue filters ineligible_reason IS NULL, and a verdict on an ineligible onboarding 404s because it has no requirement rows), so here `final` is always present and `state` is never `ineligible`. If the customer group defines an OnboardingView, reconcile to it (it must make `final` optional and add `ineligible`).\n","required":["id","state","final","detail","next_step","requirements"],"properties":{"identity_evidence":{"type":"object","nullable":true,"description":"Spec 034. Present in the back office's queue only: what the applicant's document said, as the platform read its machine-readable zone again, for the person deciding identity. No image is held; the number is its last four. `details` is null if the seal does not open.\n","required":["document_type","details","checks_held","liveness_passed","mismatches","recorded_at"],"properties":{"document_type":{"type":"string","enum":["passport","identity_card"]},"details":{"type":"object","nullable":true,"properties":{"document_code":{"type":"string"},"issuing_state":{"type":"string"},"number_last_four":{"type":"string"},"surname":{"type":"string"},"given_names":{"type":"string"},"nationality":{"type":"string"},"birth":{"type":"string","format":"date"},"sex":{"type":"string","nullable":true},"expiry":{"type":"string","format":"date"},"age_on_document":{"type":"integer"},"declared_age":{"type":"integer"},"declared_country":{"type":"string"}}},"checks_held":{"type":"boolean","description":"Every check digit held; a zone that did not is never kept"},"liveness_passed":{"type":"boolean"},"mismatches":{"type":"array","items":{"type":"string","enum":["birth"]}},"recorded_at":{"type":"string","format":"date-time"}}},"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["awaiting_customer","awaiting_us","ready","refused"]},"final":{"type":"boolean"},"detail":{"type":"string","description":"Prose. \"N thing(s) left for you\" | \"nothing for you to do; we are checking N thing(s)\" | \"everything checks out\" | \"<kind>: <detail>;...\" for refused.\n"},"next_step":{"type":"object","nullable":true,"required":["kind","prompt"],"properties":{"kind":{"$ref":"#/components/schemas/BackofficeOnboardingRequirementKind"},"prompt":{"type":"string"}}},"requirements":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeOnboardingRequirement"}}}},"BackofficeOnboardingRequirement":{"type":"object","required":["kind","status","blocking","expires_on","lapsed","failure_detail","provider_ref"],"properties":{"kind":{"$ref":"#/components/schemas/BackofficeOnboardingRequirementKind"},"status":{"type":"string","enum":["outstanding","submitted","satisfied","failed"],"description":"Raw column value; no DB CHECK, but the code only ever writes these four."},"blocking":{"type":"boolean"},"expires_on":{"type":"string","format":"date","nullable":true},"lapsed":{"type":"boolean","description":"true when status is satisfied and today > expires_on."},"failure_detail":{"type":"string","nullable":true},"provider_ref":{"type":"string","nullable":true}}},"BackofficeOnboardingRequirementKind":{"type":"string","enum":["identity","address","screening","tax_residency","source_of_funds","terms_accepted"]},"BackofficeLoanQueue":{"type":"object","description":"GET /backoffice/loans. Applications in state submitted, referred or approved, oldest first, at most 200.\n","required":["count","waiting_longest_since","applications"],"properties":{"count":{"type":"integer","minimum":0},"waiting_longest_since":{"type":"string","format":"date-time","nullable":true,"description":"created_at of the oldest application listed; null when empty."},"applications":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeLoanApplicationView"}}}},"BackofficeLoanApplicationView":{"description":"lending.rs view(): the application whole. `decision` keeps any `operator_note`, and `operator_decision` names who proposed and who approved.\n","allOf":[{"$ref":"#/components/schemas/LoanApplicationView"},{"type":"object","properties":{"operator_decision":{"allOf":[{"$ref":"#/components/schemas/LoanOperatorDecision"}],"nullable":true}}}]},"BackofficeLoanProposal":{"type":"object","description":"202 body of POST /backoffice/loans/decisions.","required":["approval_id","state","note"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]},"note":{"type":"string"}}},"BackofficeLoanDisbursementProposal":{"type":"object","description":"202 body of POST /backoffice/loans/disbursements.","required":["approval_id","state","money_moved","note"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]},"money_moved":{"type":"boolean","enum":[false],"description":"Always false at proposal time. Whether approval will move money is only in `note`."},"note":{"type":"string"}}},"BackofficeArrearsQueue":{"type":"object","description":"GET /backoffice/loans/arrears. Drawn loans (up to 500 scanned, by disbursed_on) whose derived position has arrears_minor > 0, sorted by days_past_due descending.\n","required":["count","loans"],"properties":{"count":{"type":"integer","minimum":0},"loans":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeArrearsLoan"}}}},"BackofficeArrearsLoan":{"type":"object","required":["id","customer","currency","arrears_minor","days_past_due"],"properties":{"id":{"type":"string","format":"uuid","description":"The loan application id."},"customer":{"type":"string","description":"The borrower's OIDC subject."},"currency":{"type":"string"},"arrears_minor":{"type":"integer","format":"int64","minimum":1},"days_past_due":{"type":"integer","format":"int64","minimum":0}}},"BackofficeSupportQueue":{"type":"object","description":"GET /backoffice/support. Scans the oldest 200 unresolved conversations; `conversations` holds those whose derived turn is `us`, `calls_owed` those whose callback is still owed. One conversation can appear in both lists.\n","required":["waiting_on_us","oldest_since","conversations","calls_owed"],"properties":{"waiting_on_us":{"type":"integer","minimum":0,"description":"Length of `conversations` (an integer count, not a boolean)."},"oldest_since":{"type":"string","format":"date-time","nullable":true,"description":"created_at of the oldest UNRESOLVED conversation scanned, whether or not it is waiting on us; null when there are none.\n"},"conversations":{"type":"array","items":{"$ref":"#/components/schemas/SupportConversation"}},"calls_owed":{"type":"array","items":{"$ref":"#/components/schemas/SupportConversation"}}}},"BackofficeFraudQueue":{"type":"object","description":"GET /backoffice/fraud. Assessments with verdict IS NULL and outcome in (allow_and_review, hold), oldest decided_at first, at most 200.\n","required":["count","oldest","health","assessments"],"properties":{"count":{"type":"integer","minimum":0},"oldest":{"type":"string","format":"date-time","nullable":true,"description":"decided_at of the oldest listed assessment; null when empty."},"health":{"$ref":"#/components/schemas/BackofficeFraudHealth"},"assessments":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeFraudAssessment"}}}},"BackofficeFraudHealth":{"type":"object","required":["assessed","decided_without_an_answer","reviewed","fraudulent","false_positive_rate"],"properties":{"assessed":{"type":"integer","format":"int64","minimum":0},"decided_without_an_answer":{"type":"integer","format":"int64","minimum":0},"reviewed":{"type":"integer","format":"int64","minimum":0},"fraudulent":{"type":"integer","format":"int64","minimum":0},"false_positive_rate":{"type":"number","format":"double","minimum":0,"maximum":1,"nullable":true,"description":"(reviewed - fraudulent) / reviewed; null (key present) when nothing has been reviewed."}}},"BackofficeFraudAssessment":{"type":"object","description":"routes/fraud.rs `view(&StoredAssessment)`. Also the 200 body of GET /backoffice/fraud/{id} (not in this group's list, but the same function). In the queue, outcome is only allow_and_review|hold and every review field is null.\n","required":["id","correlation_id","action","subject_ref","amount_minor","currency","score","band","answered","outcome","reasons","decided_at","can_still_be_stopped","verdict","consequence","reviewed_by","reviewed_at","review_note"],"properties":{"id":{"type":"string","format":"uuid"},"correlation_id":{"type":"string","format":"uuid"},"action":{"type":"string","enum":["card_authorization","transfer","payout","refund"],"description":"fraud::Action labels; the column has no CHECK."},"subject_ref":{"type":"string"},"amount_minor":{"type":"integer","format":"int64"},"currency":{"type":"string"},"score":{"type":"number","format":"double","minimum":0,"maximum":1,"nullable":true},"band":{"type":"string","nullable":true,"enum":["allow","review","block",null]},"answered":{"type":"boolean"},"outcome":{"type":"string","enum":["allow","allow_and_review","hold","block"]},"reasons":{"type":"array","items":{"type":"object","required":["code","detail"],"properties":{"code":{"type":"string"},"detail":{"type":"string"}}}},"decided_at":{"type":"string","format":"date-time"},"can_still_be_stopped":{"type":"boolean","description":"outcome == hold"},"verdict":{"type":"string","nullable":true,"enum":["legitimate","fraudulent",null]},"consequence":{"type":"string","nullable":true,"enum":["release","refuse","nothing","record_loss",null]},"reviewed_by":{"type":"string","nullable":true},"reviewed_at":{"type":"string","format":"date-time","nullable":true},"review_note":{"type":"string","nullable":true}}},"BackofficeFraudVerdictResult":{"type":"object","description":"200 body of POST /backoffice/fraud/{id}.","required":["id","verdict","consequence","note"],"properties":{"id":{"type":"string","format":"uuid"},"verdict":{"type":"string","enum":["legitimate","fraudulent"]},"consequence":{"type":"string","enum":["release","refuse","nothing","record_loss"],"description":"hold+legitimate=release, hold+fraudulent=refuse, allow_and_review+fraudulent=record_loss, allow_and_review+legitimate=nothing.\n"},"note":{"type":"string","description":"One fixed sentence per consequence (prose, not an enum to match on)."}}},"BackofficeFundingQueue":{"type":"object","description":"GET /backoffice/funding. Fundings in stage awaiting_deposit, confirming or converting, oldest first, at most 200.\n","required":["count","oldest_since","funding"],"properties":{"count":{"type":"integer","minimum":0},"oldest_since":{"type":"string","format":"date-time","nullable":true,"description":"created_at of the oldest listed funding; null when empty."},"funding":{"type":"array","description":"Key is singular `funding`, not `fundings`.","items":{"$ref":"#/components/schemas/FundingView"}}}},"AccountView":{"type":"object","description":"routes/accounts.rs `AccountView`. Figures relayed from the core.","required":["id","name","kind","currency","balance_minor","provenance"],"properties":{"id":{"type":"string","description":"The core's account id (e.g. \"4\"); not a uuid in general"},"name":{"type":"string","description":"Never empty"},"kind":{"type":"string","description":"Free-form, the core's word (core adapter writes \"savings\")"},"currency":{"type":"string"},"balance_minor":{"type":"integer","format":"int64","nullable":true,"description":"Signed minor units. `null` (key present) where the core could not answer, or the balance is not exactly representable in minor units. Never absent.\n"},"provenance":{"type":"string","enum":["live"]},"iban":{"type":"string","description":"A euro account the customer opened. Absent for every other account"},"bic":{"type":"string","description":"Its bank's BIC, beside the IBAN"},"account_number":{"type":"string","description":"A dollar account's US account number, once Column issued it. Absent for every other account\n"},"routing_number":{"type":"string","description":"Its routing number, beside the account number"},"business":{"type":"string","description":"The company a business account belongs to, which the caller acts for (specs 039, 041). Absent for the caller's own accounts\n"},"business_role":{"type":"string","enum":["admin","payer","approver","viewer"],"description":"What the caller does for that company; absent likewise"}}},"ClosingBalanceList":{"type":"object","description":"routes/accounts.rs `closing_balances`","required":["days"],"properties":{"days":{"type":"array","items":{"type":"object","required":["day","closing_minor","currency"],"properties":{"day":{"type":"string","format":"date"},"closing_minor":{"type":"integer","format":"int64","nullable":true},"currency":{"type":"string"}}}}}},"EuroAccountOpenBody":{"type":"object","description":"routes/named_accounts.rs `OpenBody`","required":["first_name","last_name","email","phone","address","method","authenticated_at","evidence_ref","signed_amount","signed_currency","signed_payee"],"properties":{"first_name":{"type":"string","maxLength":100},"last_name":{"type":"string","maxLength":100},"email":{"type":"string","maxLength":254},"phone":{"type":"string","description":"6 to 15 digits, with spaces, +, - and brackets"},"address":{"type":"object","required":["line_1","city","country_code"],"properties":{"line_1":{"type":"string","maxLength":70},"line_2":{"type":"string","nullable":true,"maxLength":70},"city":{"type":"string","maxLength":70},"state":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true,"maxLength":16},"country_code":{"type":"string","pattern":"^[A-Za-z]{2}$","description":"ISO 3166-1 alpha-2"}}},"method":{"type":"string","enum":["passkey","device_biometric","device_key","app_pin","sms_otp","email_otp"]},"authenticated_at":{"type":"string","format":"date-time"},"evidence_ref":{"type":"string","minLength":1},"signed_amount":{"type":"string","description":"\"0.00\""},"signed_currency":{"type":"string","description":"\"EUR\""},"signed_payee":{"type":"string","description":"\"A new EUR account\""},"signature":{"type":"string","description":"With method device_key, the device key's signature over oxabank-confirm/1, the kind, the id, the amount, the currency and the payee, one per line; base64, DER or r and s; the kind is euro_account and the id -. With method passkey, the WebAuthn assertion as four base64url parts joined by dots: the credential id, the authenticator data, the client data JSON and the signature; its challenge is SHA-256 of that same message."}}},"FxWire":{"type":"object","description":"routes/fx_wires.rs `view`, the platform’s specification","required":["id","state","said","from_account","payee","sell_minor","sell_currency","buy_minor","buy_currency","rate","spread_bps","spread_minor","rate_source","rate_as_of","reference","authorize","money_moved","return_reason","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["awaiting_authorization","refused","posting","sent","settled","returned","refunded"]},"said":{"type":"string"},"from_account":{"type":"string"},"payee":{"type":"object","required":["id","display_name"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":"string"}}},"sell_minor":{"type":"integer"},"sell_currency":{"type":"string"},"buy_minor":{"type":"integer"},"buy_currency":{"type":"string"},"rate":{"type":"string","description":"What a dollar buys","after the spread":null},"spread_bps":{"type":"integer"},"spread_minor":{"type":"integer","description":"The spread as money","in the payee's currency":null},"rate_source":{"type":"string"},"rate_as_of":{"type":"string","format":"date"},"reference":{"type":"string","nullable":true},"authorize":{"type":"object","required":["amount","currency","payee","expires_in_minutes"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"},"expires_in_minutes":{"type":"integer"}}},"money_moved":{"type":"boolean"},"return_reason":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"LinkedAccount":{"type":"object","description":"routes/pulls.rs `link_view`, the platform’s specification. Never the whole account number.","required":["id","holder_name","routing_number","last4","account_type","label","verified","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"holder_name":{"type":"string"},"routing_number":{"type":"string"},"last4":{"type":"string"},"account_type":{"type":"string","enum":["checking","savings"]},"label":{"type":"string","description":"From <holder> ••<last4>, as the customer signs it"},"verified":{"type":"boolean","description":"Spec 054: the customer said its two small deposits"},"verification":{"description":"The latest verification, in the list only","nullable":true,"allOf":[{"$ref":"#/components/schemas/MicroDeposit"}]},"created_at":{"type":"string","format":"date-time"}}},"MicroDeposit":{"type":"object","description":"routes/micro_deposits.rs `view`, the platform’s specification. Never the amounts.","required":["id","linked_account_id","state","said","tries_left","authorize","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"linked_account_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["awaiting_authorization","refused","posting","sent","verified","failed","returned"]},"said":{"type":"string","description":"Where it is","in the platform's words":null},"tries_left":{"type":"integer","nullable":true,"description":"While sent"},"authorize":{"type":"object","nullable":true,"required":["amount","currency","payee","expires_in_minutes"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"},"expires_in_minutes":{"type":"integer"}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CreditLine":{"type":"object","description":"routes/credit_lines.rs `view`, the platform’s specification. The figures are the core's, or null while it is not open or not answering.","required":["id","state","said","currency","asked_minor","limit_minor","drawn_minor","available_minor","payoff_minor","overdue_minor","next_due","days_past_due","closed_by","closed_at","annual_rate","term_months","account_id","provenance","movements","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed","open","declined","refused","closed","charged_off"],"description":"Spec 059: closed and charged_off are never drawn; what they owe is still paid back"},"said":{"type":"string","description":"Where it is","in the platform's words":null},"currency":{"type":"string"},"asked_minor":{"type":"integer"},"limit_minor":{"type":"integer"},"drawn_minor":{"type":"integer","nullable":true},"available_minor":{"type":"integer","nullable":true},"payoff_minor":{"type":"integer","nullable":true,"description":"What paying it all off today takes"},"overdue_minor":{"type":"integer","nullable":true},"next_due":{"type":"object","nullable":true,"required":["on","amount_minor"],"properties":{"on":{"type":"string","format":"date"},"amount_minor":{"type":"integer"}}},"days_past_due":{"type":"integer","nullable":true,"description":"Spec 059: the bank's day less the oldest unpaid instalment's day once that day has passed, else 0; read from the schedule, not the core's overdue figure; null without the core's figures"},"closed_by":{"type":"string","nullable":true,"enum":["customer","bank",null]},"closed_at":{"type":"string","format":"date-time","nullable":true},"annual_rate":{"type":"string","description":"A year's rate, \"24\" for 24%"},"term_months":{"type":"integer"},"account_id":{"type":"string"},"provenance":{"type":"string","enum":["live","platform"]},"movements":{"type":"array","items":{"$ref":"#/components/schemas/LineMovement"}},"agreement":{"description":"Spec 060: the line's agreement once asked for, signed or not; a line is drawn only once it is signed","nullable":true,"allOf":[{"$ref":"#/components/schemas/LineAgreement"}]},"autopay":{"description":"Spec 058: the line's autopay, asked for, on or the latest stopped; open and closed lines only","nullable":true,"allOf":[{"$ref":"#/components/schemas/LineAutopay"}]},"created_at":{"type":"string","format":"date-time"}}},"LineAutopay":{"type":"object","description":"routes/line_autopay.rs `view`, the platform’s specification","required":["id","state","said","from_account","cap_minor","authorize","runs","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["awaiting_authorization","active","stopped"]},"said":{"type":"string"},"from_account":{"type":"string"},"cap_minor":{"type":"integer","description":"The most taken on a due day"},"authorize":{"type":"object","nullable":true,"required":["amount","currency","payee","expires_in_minutes"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"},"expires_in_minutes":{"type":"integer"}}},"runs":{"type":"array","items":{"type":"object","required":["due_on","amount_minor","state","said"],"properties":{"due_on":{"type":"string","format":"date"},"amount_minor":{"type":"integer"},"state":{"type":"string","enum":["asked","done","refused"]},"said":{"type":"string"}}}},"created_at":{"type":"string","format":"date-time"}}},"LineMovement":{"type":"object","description":"routes/credit_lines.rs `movement_view`, the platform’s specification","required":["id","kind","amount_minor","currency","account_id","state","said","authorize","money_moved","auth_method","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["draw","repayment"]},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"account_id":{"type":"string"},"state":{"type":"string","enum":["awaiting_authorization","refused","done"]},"said":{"type":"string"},"authorize":{"type":"object","nullable":true,"required":["amount","currency","payee","expires_in_minutes"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"},"expires_in_minutes":{"type":"integer"}}},"money_moved":{"type":"boolean"},"auth_method":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"LineAgreement":{"type":"object","description":"routes/documents.rs `agreement_view`, the platform’s specification","required":["id","state","sha256","signed_at","auth_method","authorize","pdf","signed_pdf","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["unsigned","signed"]},"sha256":{"type":"string","description":"The SHA-256 of the PDF as read and signed"},"signed_at":{"type":"string","format":"date-time","nullable":true},"auth_method":{"type":"string","nullable":true},"authorize":{"type":"object","nullable":true,"description":"While unsigned, what the customer confirms","required":["amount","currency","payee"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"}}},"pdf":{"type":"string","description":"Where the PDF as read is"},"signed_pdf":{"type":"string","nullable":true,"description":"Where the signed copy is","once signed":null},"created_at":{"type":"string","format":"date-time"}}},"LineChange":{"type":"object","description":"routes/credit_lines.rs `change_view`, the platform’s specification. Where it stands is its approval's.","required":["approval_id","kind","from_minor","to_minor","reason","proposed_by","state","decided_by","created_at"],"properties":{"approval_id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["limit","close","charge_off"]},"from_minor":{"type":"integer","description":"The limit when it was proposed"},"to_minor":{"type":"integer","nullable":true,"description":"A new limit's"},"reason":{"type":"string"},"proposed_by":{"type":"string"},"state":{"type":"string","enum":["proposed","approved","rejected","executed","failed"]},"decided_by":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"BackofficeCreditLine":{"description":"routes/credit_lines.rs `backoffice_list`, the platform’s specification","allOf":[{"$ref":"#/components/schemas/CreditLine"},{"type":"object","required":["customer","closed_reason","autopay_state","waiting","changes","charge_off_refusal"],"properties":{"customer":{"type":"string","description":"The customer's subject"},"closed_reason":{"type":"string","nullable":true},"autopay_state":{"type":"string","nullable":true,"enum":["awaiting_authorization","active","stopped",null]},"waiting":{"nullable":true,"description":"The one proposal waiting on the line","allOf":[{"$ref":"#/components/schemas/LineChange"}]},"changes":{"type":"array","items":{"$ref":"#/components/schemas/LineChange"}},"charge_off_refusal":{"type":"string","nullable":true,"description":"Why it cannot be charged off now","or null when it can":null}}}]},"BackofficeCreditLines":{"type":"object","required":["lines","today","charge_off_days"],"properties":{"lines":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeCreditLine"}},"today":{"type":"string","format":"date","description":"The bank's day the lateness is measured on"},"charge_off_days":{"type":"integer","description":"PLATFORM_CHARGE_OFF_DAYS, 180 by default"}}},"RecurringPull":{"type":"object","description":"routes/recurring_pulls.rs `view`, the platform’s specification","required":["id","state","said","linked_account_id","from","to_account","amount_minor","currency","every","first_on","next_on","taken","mandate","authorize","auth_method","created_at","stopped_at"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["awaiting_authorization","refused","active","stopped"]},"said":{"type":"string","description":"Where it is","in the platform's words":null},"linked_account_id":{"type":"string","format":"uuid"},"from":{"type":"string","nullable":true},"to_account":{"type":"string"},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"every":{"type":"string","enum":["week","month"]},"first_on":{"type":"string","format":"date"},"next_on":{"type":"string","format":"date","nullable":true,"description":"While active"},"taken":{"type":"integer","description":"How many due days have been started"},"mandate":{"type":"string","description":"The recurring authorisation the customer agrees to"},"authorize":{"type":"object","nullable":true,"required":["amount","currency","payee","expires_in_minutes"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"},"expires_in_minutes":{"type":"integer"}}},"auth_method":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"stopped_at":{"type":"string","format":"date-time","nullable":true}}},"Pull":{"type":"object","description":"routes/pulls.rs `pull_view`, the platform’s specification","required":["id","state","said","linked_account_id","from","to_account","amount_minor","currency","mandate","authorize","auth_method","money_moved","return_reason","created_at","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["awaiting_authorization","refused","sent","settled","credited","returned","reversed"]},"said":{"type":"string","description":"Where it is","in the platform's words":null},"linked_account_id":{"type":"string","format":"uuid"},"from":{"type":"string","nullable":true},"to_account":{"type":"string"},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"mandate":{"type":"string","description":"The authorisation text the customer agrees to"},"authorize":{"type":"object","nullable":true,"required":["amount","currency","payee","expires_in_minutes"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"},"expires_in_minutes":{"type":"integer"}}},"auth_method":{"type":"string","nullable":true},"money_moved":{"type":"boolean"},"return_reason":{"type":"string","nullable":true},"recurring_id":{"type":"string","format":"uuid","nullable":true,"description":"Spec 055: the standing authorisation it was started under"},"due_on":{"type":"string","format":"date","nullable":true,"description":"The day it was due"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Passkey":{"type":"object","description":"routes/passkeys.rs `view`, the platform’s specification","required":["id","name","credential_id","registered_at","last_used_at"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"credential_id":{"type":"string","description":"base64url","as the browser's allowCredentials takes it":null},"registered_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time","nullable":true}}},"EuroAccountView":{"type":"object","description":"routes/named_accounts.rs `view`: a euro account and its IBAN, or a dollar account's US account details\n","required":["id","currency","iban","bic","account_number","routing_number","state","failure"],"properties":{"id":{"type":"string","nullable":true,"description":"The core's account id","once opened":null},"currency":{"type":"string","enum":["EUR","USD"]},"iban":{"type":"string","nullable":true},"bic":{"type":"string","nullable":true},"account_number":{"type":"string","nullable":true},"routing_number":{"type":"string","nullable":true},"state":{"type":"string","enum":["open","opening"]},"failure":{"type":"string","nullable":true,"description":"Why the last attempt stopped"}}},"StatementEntryView":{"type":"object","description":"routes/accounts.rs `EntryView`.","required":["id","posted_on","amount_minor","currency","reference","provenance"],"properties":{"id":{"type":"string","description":"The core's id for the movement"},"posted_on":{"type":"string","format":"date","description":"The day the core booked it. The core records no time, so none is given"},"amount_minor":{"type":"integer","format":"int64","description":"Signed from this account's point of view; negative is money out"},"currency":{"type":"string","description":"Always the account's currency"},"reference":{"type":"string","nullable":true,"description":"The platform's reference the movement was made under — a transfer's or an approval's id, sometimes prefixed (`transfer-release:`). Null for a movement the platform did not make. The core holds no correlation id, so none is claimed\n"},"counterparty":{"type":"string","nullable":true,"description":"Spec 076. Who the money went to or came from, where the platform made the movement: the payee of a payment out (or between the caller's own accounts), the payer of one in by their name with the bank. Null is \"not known here\", never a name made up\n"},"provenance":{"type":"string","enum":["live"]}}},"CardLimitsView":{"type":"object","description":"routes/cards.rs `LimitsView`. Minor units; `null` means no limit.","required":["per_transaction","daily","monthly","remaining_today","remaining_this_month"],"properties":{"per_transaction":{"type":"integer","format":"int64","nullable":true,"minimum":0},"daily":{"type":"integer","format":"int64","nullable":true,"minimum":0},"monthly":{"type":"integer","format":"int64","nullable":true,"minimum":0},"remaining_today":{"type":"integer","format":"int64","nullable":true,"minimum":0,"description":"null exactly when `daily` is null; holds and settled spend included, floored at 0"},"remaining_this_month":{"type":"integer","format":"int64","nullable":true,"minimum":0,"description":"null exactly when `monthly` is null"}}},"CardView":{"type":"object","description":"routes/cards.rs `CardView0` (the module's own `CardView` is not what is served).","required":["id","status","status_detail","last_four","network","expires_on","residency","currency","actions","limits","provenance","form"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","description":"Derived for today; `expired` is never stored, only derived from `expires_on`.","enum":["requested","issued","active","frozen","expired","cancelled"]},"status_detail":{"type":"string","description":"CardStatus::why_declined(), one fixed sentence per status.","enum":["this card has not been produced yet","this card has not been activated yet","this card is active","this card is frozen","this card has expired","this card has been cancelled"]},"last_four":{"type":"string","pattern":"^[0-9]{4}$"},"network":{"type":"string","description":"Issuer's word, e.g. \"visa\""},"expires_on":{"type":"string","format":"date","description":"Last good day"},"residency":{"type":"string","enum":["delegated","vaulted"]},"currency":{"type":"string"},"actions":{"type":"array","description":"Legal actions now, in stable order (issued: activate,cancel; active: freeze,cancel; frozen: unfreeze,cancel; expired: cancel; requested/cancelled: none)","items":{"$ref":"#/components/schemas/CardAction"}},"limits":{"$ref":"#/components/schemas/CardLimitsView"},"provenance":{"type":"string","enum":["live","synthetic"]},"business_id":{"type":"string","format":"uuid","description":"The company whose account it spends from, for a company card; absent on a customer's own\n"},"form":{"type":"string","enum":["virtual","physical"],"description":"Spec 076. As it was issued; a card issued before is virtual"}}},"CardAction":{"type":"string","enum":["activate","freeze","unfreeze","cancel"]},"CardLimitsBody":{"type":"object","description":"routes/cards.rs `LimitsBody` (double Option). A key that is absent leaves that limit unchanged; a key sent as `null` removes the limit, which is refused (409) as a raise whenever a limit existed. Negative values are 400.\n","properties":{"per_transaction":{"type":"integer","format":"int64","nullable":true,"minimum":0},"daily":{"type":"integer","format":"int64","nullable":true,"minimum":0},"monthly":{"type":"integer","format":"int64","nullable":true,"minimum":0}}},"PayeeView":{"type":"object","description":"routes/transfers.rs `payee_view` (json!).","required":["id","display_name","account_ref","destination","first_payment"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":"string"},"account_ref":{"type":"string"},"destination":{"type":"string","enum":["core","spei","ach","swift","sepa","unknown"],"description":"core = digits only, shorter than 18; spei = a valid CLABE; ach = a US bank account, written aba:<routing>:<account>:<checking|savings>; swift = an account abroad, written swift:<BIC>:<IBAN or account>, with an address; sepa = a valid IBAN of a SEPA country given alone, paid in euros from the customer's euro account; unknown = anything else\n"},"first_payment":{"type":"boolean","description":"True while no transfer to them has been authorized"},"address":{"type":"object","nullable":true,"description":"The account holder's address, for a payee abroad; null for every other","required":["line_1","city","country_code"],"properties":{"line_1":{"type":"string","maxLength":70},"line_2":{"type":"string","nullable":true},"city":{"type":"string","maxLength":70},"state":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true},"country_code":{"type":"string","pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2"}}}}},"PayeeList":{"type":"object","required":["payees"],"properties":{"payees":{"type":"array","description":"Ordered by display_name","items":{"$ref":"#/components/schemas/PayeeView"}}}},"PayeeNewBody":{"type":"object","description":"routes/transfers.rs `NewPayeeBody`.","required":["display_name","account_ref"],"properties":{"display_name":{"type":"string","minLength":1,"pattern":"\\S","description":"Refused if blank after trim"},"account_ref":{"type":"string","minLength":1,"pattern":"\\S","description":"Refused if blank after trim. If it is exactly 18 digits (whitespace ignored) it must be a valid CLABE (check digit and a SPEI bank code) or it is 400; any other shape is kept and classified `core` or `unknown`.\n"},"address":{"type":"object","description":"Required for a swift:<BIC>:<account> reference and refused on any other","required":["line_1","city","country_code"],"properties":{"line_1":{"type":"string","maxLength":70},"line_2":{"type":"string","nullable":true},"city":{"type":"string","maxLength":70},"state":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true},"country_code":{"type":"string","pattern":"^[A-Z]{2}$","description":"ISO 3166-1 alpha-2"}}}}},"TransferPayeeRef":{"type":"object","required":["id","display_name"],"properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":"string","description":"The payee's name as stored on the intent"}}},"TransferAuthorizeBinding":{"type":"object","description":"Exactly what must be displayed and signed; the confirm's signed_* fields must equal these.","required":["amount","currency","payee","expires_in_minutes"],"properties":{"amount":{"type":"string","description":"Decimal major units with the currency's exponent, e.g. \"400.00\""},"currency":{"type":"string"},"payee":{"type":"string","description":"The payee display name"},"expires_in_minutes":{"type":"integer","enum":[10],"description":"Constant INTENT_LIFETIME_MINUTES"}}},"TransferWarning":{"type":"object","required":["code","detail"],"properties":{"code":{"type":"string","enum":["first_payment"]},"detail":{"type":"string"}}},"TransferIntentView":{"type":"object","description":"routes/transfers.rs `intent_view` (json!). Every key is always present.","required":["id","state","from_account","payee","amount_minor","currency","reference","created_at","authorize","warnings","authorized_at","auth_method","refused_reason","core_ref","destination","money_moved","rail_ref","instant","goes_out_on","after_cutoff","cutoff","holiday","business_id","cosigned"],"properties":{"business_id":{"type":"string","format":"uuid","nullable":true,"description":"The company it is paid from, for a payment from a business account"},"cosigned":{"type":"boolean","description":"A second person of the company signed it; its signatory's identity is the company's record, not in this view\n"},"cosign_reason":{"type":"string","nullable":true,"enum":["threshold","per_payment","per_day",null],"description":"Why it waits, or waited, for a second signature: the company's threshold, or the payer's own limit per payment or for the day\n"},"instant":{"type":"boolean","description":"Spec 050. True for a euro payment to an IBAN up to €100,000, which the FX provider sends as SEPA Instant — in seconds, at any hour — where the payee's bank takes instant payments; where it does not, the payment goes as SEPA by the cut-off and calendar the other fields say\n"},"goes_out_on":{"type":"string","format":"date","nullable":true,"description":"The day the rail takes it, by the rail's cut-off in its own zone: that day before the cut-off on Monday to Friday, else the next of those. `null` within OxaBank or on a rail with no cut-off configured\n"},"after_cutoff":{"type":"boolean","nullable":true,"description":"True when it goes out later than the day it was authorised (or prepared)"},"cutoff":{"type":"string","nullable":true,"description":"The rail's cut-off as the customer is told it, \"16:30\", in the rail's zone"},"holiday":{"type":"object","nullable":true,"description":"The first day the rail's calendar keeps that held it over, and its name. `null` where none did, within OxaBank, or on a rail with no cut-off\n","required":["day","name"],"properties":{"day":{"type":"string","format":"date"},"name":{"type":"string","example":"Thanksgiving Day"}}},"id":{"type":"string","format":"uuid"},"state":{"type":"string","description":"DB check constraint transfer_intents_state_check. `awaiting_cosignature`: a company's payment at or above its threshold, or above one of its payer's limits, confirmed by its payer and waiting for a second person of the company\n","enum":["awaiting_authorization","awaiting_cosignature","authorized","held","refused","expired","posting","posted","sent","settled","returned"]},"from_account":{"type":"string"},"payee":{"$ref":"#/components/schemas/TransferPayeeRef"},"amount_minor":{"type":"integer","format":"int64","minimum":1},"currency":{"type":"string"},"reference":{"type":"string","nullable":true,"maxLength":35},"created_at":{"type":"string","format":"date-time"},"authorize":{"$ref":"#/components/schemas/TransferAuthorizeBinding"},"warnings":{"type":"array","description":"Populated only on the POST /transfers response. GET /transfers and confirm always send [].","items":{"$ref":"#/components/schemas/TransferWarning"}},"authorized_at":{"type":"string","format":"date-time","nullable":true},"auth_method":{"type":"string","nullable":true,"enum":["passkey","device_biometric","device_key","app_pin","sms_otp","email_otp"]},"refused_reason":{"type":"string","nullable":true},"core_ref":{"type":"string","nullable":true},"destination":{"type":"string","enum":["core","spei","ach","swift","sepa","unknown"]},"money_moved":{"type":"boolean","description":"true iff state is posted, sent or settled"},"rail_ref":{"type":"string","nullable":true}}},"TransferList":{"type":"object","required":["transfers"],"properties":{"transfers":{"type":"array","description":"At most 50","items":{"$ref":"#/components/schemas/TransferIntentView"}}}},"TransferNewBody":{"type":"object","description":"routes/transfers.rs `NewTransferBody` plus the checks in modules/transfers validate().","required":["from_account","payee_id","amount_minor","currency"],"properties":{"from_account":{"type":"string","description":"One of the caller's core account ids; otherwise 404"},"payee_id":{"type":"string","format":"uuid","description":"One of the caller's payees; otherwise 404"},"amount_minor":{"type":"integer","format":"int64","minimum":1},"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"Must equal the from_account's currency (400 otherwise)"},"reference":{"type":"string","nullable":true,"maxLength":35,"description":"Counted in characters"}}},"TransferConfirmBody":{"type":"object","description":"routes/transfers.rs `ConfirmBody`; also the body of POST /exchange/quotes/{id}/confirm (same Rust type). The five methods all parse; sms_otp and email_otp are then refused 400 as one factor. authenticated_at must be within 300s in the past (future or older is 400). evidence_ref must be non-blank. signed_* must equal the stored `authorize` object.\n","required":["method","authenticated_at","evidence_ref","signed_amount","signed_currency","signed_payee"],"properties":{"method":{"type":"string","enum":["passkey","device_biometric","device_key","app_pin","sms_otp","email_otp"]},"authenticated_at":{"type":"string","format":"date-time"},"evidence_ref":{"type":"string","minLength":1,"pattern":"\\S"},"signed_amount":{"type":"string"},"signed_currency":{"type":"string"},"signed_payee":{"type":"string"},"signature":{"type":"string","description":"With method device_key, the device key's signature over oxabank-confirm/1, the kind, the id, the amount, the currency and the payee, one per line; base64, DER or r and s. The kind is transfer, cosign or exchange, and the id the transfer's or the quote's. With method passkey, the WebAuthn assertion as four base64url parts joined by dots: the credential id, the authenticator data, the client data JSON and the signature; its challenge is SHA-256 of that same message. Without PLATFORM_CONFIRMATION=device_key the other methods are taken as before; with it, only device_key and passkey."}}},"ExchangeAuthorizeBinding":{"type":"object","description":"What the customer signs. No expires_in_minutes here, unlike a transfer.","required":["amount","currency","payee"],"properties":{"amount":{"type":"string","description":"sell_minor as decimal major units, e.g. \"1000.00\""},"currency":{"type":"string","description":"sell_currency"},"payee":{"type":"string","description":"Always \"Your <buy_currency> account\""}}},"ExchangeQuoteView":{"type":"object","description":"routes/exchange.rs `view` (json!). Every key is always present.","required":["id","from_account","to_account","sell_minor","sell_currency","buy_minor","buy_currency","mid","rate","spread_bps","spread_minor","remainder","remainder_denominator","rate_source","rate_as_of","state","refused_reason","created_at","authorize","out_core_ref","in_core_ref","money_moved"],"properties":{"id":{"type":"string","format":"uuid"},"from_account":{"type":"string"},"to_account":{"type":"string"},"sell_minor":{"type":"integer","format":"int64","minimum":1},"sell_currency":{"type":"string"},"buy_minor":{"type":"integer","format":"int64","minimum":1},"buy_currency":{"type":"string"},"mid":{"type":"string","description":"rust_decimal string of rate_numerator/rate_denominator"},"rate":{"type":"string","description":"mid * (10000 - spread_bps) / 10000, rust_decimal string"},"spread_bps":{"type":"integer","format":"int64","minimum":0,"maximum":9999},"spread_minor":{"type":"integer","format":"int64","minimum":0},"remainder":{"type":"integer","format":"int64","minimum":0},"remainder_denominator":{"type":"integer","format":"int64","minimum":1},"rate_source":{"type":"string"},"rate_as_of":{"type":"string","format":"date"},"state":{"type":"string","description":"DB check constraint exchange_quotes_state_check. `awaiting_cosignature`: a company's exchange crossing its threshold or its maker's limits, confirmed by its maker and waiting for a second person of the company\n","enum":["awaiting_confirmation","awaiting_cosignature","authorized","refused","expired","posting","posted"]},"refused_reason":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"authorize":{"$ref":"#/components/schemas/ExchangeAuthorizeBinding"},"out_core_ref":{"type":"string","nullable":true},"in_core_ref":{"type":"string","nullable":true,"description":"Never set without out_core_ref"},"money_moved":{"type":"boolean","description":"true iff state is posted"},"business_id":{"type":"string","format":"uuid","nullable":true,"description":"The company whose accounts it is between"},"cosign_reason":{"type":"string","nullable":true,"enum":["threshold","per_payment","per_day",null]},"cosigned":{"type":"boolean","description":"A second person of the company signed it"}}},"ExchangeQuoteList":{"type":"object","required":["quotes"],"properties":{"quotes":{"type":"array","description":"At most 50, newest first","items":{"$ref":"#/components/schemas/ExchangeQuoteView"}}}},"ExchangeQuoteBody":{"type":"object","description":"routes/exchange.rs `QuoteBody`.","required":["from_account","to_account","sell_minor"],"properties":{"from_account":{"type":"string","description":"Caller's core account; otherwise 404"},"to_account":{"type":"string","description":"Caller's core account, in a different currency; otherwise 404 / 400"},"sell_minor":{"type":"integer","format":"int64","minimum":1,"description":"<= 0 is 400 (Refused::NoAmount)"}}},"SessionView":{"type":"object","description":"routes/sessions.rs `SessionView`. Never carries device_id.","required":["id","name","model","os","app_version","began_at","last_used_at","expires_at","current"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","nullable":true,"description":"Always null today (view() hard-codes None)"},"model":{"type":"string","maxLength":64,"description":"May be \"\""},"os":{"type":"string","maxLength":64,"description":"May be \"\""},"app_version":{"type":"string","maxLength":64,"description":"May be \"\""},"began_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"current":{"type":"boolean","description":"Always true on POST /sessions"}}},"SessionEnding":{"type":"string","description":"DB check constraint on sessions.ending","enum":["signed_out","ended_by_customer","ended_by_operator","replaced","expired"]},"SessionEndedView":{"type":"object","description":"routes/sessions.rs `EndedView`. Both fields are Option in Rust and so nullable on the wire, but on a 200 the session has ended, and the DB constraint makes ended_at and ending non-null together.\n","required":["id","ended_at","ending"],"properties":{"id":{"type":"string","format":"uuid"},"ended_at":{"type":"string","format":"date-time","nullable":true},"ending":{"allOf":[{"$ref":"#/components/schemas/SessionEnding"}],"nullable":true}}},"NoticeTarget":{"type":"object","description":"modules/notifications `Target`, serde tag = \"kind\". `id` is present only for card, transfer, dispute, exchange, account and loan; `onboarding` and `nothing` carry `kind` alone. `id` is the id string from the event payload, and \"\" if the payload lacked it.\n","required":["kind"],"properties":{"kind":{"type":"string","enum":["onboarding","card","transfer","dispute","exchange","account","loan","nothing"]},"id":{"type":"string"}}},"NoticeView":{"type":"object","description":"routes/notifications.rs `notice_of` (json!). Every key is always present.","required":["id","title","line","at","tone","target"],"properties":{"id":{"type":"string","format":"uuid","description":"The stored event row's id"},"title":{"type":"string"},"line":{"type":"string"},"at":{"type":"string","format":"date-time","description":"The event's occurred_at"},"tone":{"type":"string","nullable":true,"enum":["positive","attention"]},"target":{"$ref":"#/components/schemas/NoticeTarget"}}},"NoticeList":{"type":"object","required":["notices"],"properties":{"notices":{"type":"array","description":"Newest first; built from at most 100 events, so there may be fewer than 100 notices","items":{"$ref":"#/components/schemas/NoticeView"}}}},"LoanInstalment":{"type":"object","description":"One row of a schedule (platform_module_lending::Instalment).","required":["number","due_on","principal_minor","interest_minor","total_minor","balance_after_minor"],"properties":{"number":{"type":"integer","format":"int32","minimum":1},"due_on":{"type":"string","format":"date"},"principal_minor":{"type":"integer","format":"int64"},"interest_minor":{"type":"integer","format":"int64"},"total_minor":{"type":"integer","format":"int64","description":"principal + interest"},"balance_after_minor":{"type":"integer","format":"int64"}}},"LoanAffordabilityVerdict":{"type":"object","description":"platform_module_lending::AffordabilityVerdict, internally tagged by `verdict`. Which of the other fields are present depends on the tag: `affordable` has ratio_percent and left_over_minor; `too_much` has ratio_percent, allowed_percent and left_over_minor; `more_than_is_left` has left_over_minor and short_by_minor; `no_income` has none.\n","required":["verdict"],"properties":{"verdict":{"type":"string","enum":["affordable","too_much","more_than_is_left","no_income"]},"ratio_percent":{"type":"integer","format":"int64"},"allowed_percent":{"type":"integer","format":"int64"},"left_over_minor":{"type":"integer","format":"int64"},"short_by_minor":{"type":"integer","format":"int64"}}},"LoanAffordability":{"type":"object","required":["affordable","detail","verdict"],"properties":{"affordable":{"type":"boolean"},"detail":{"type":"string","description":"The module's sentence, e.g. \"this takes 28% of your monthly income, leaving 3,500.00\""},"verdict":{"$ref":"#/components/schemas/LoanAffordabilityVerdict"}}},"LoanQuote":{"type":"object","description":"POST /loans/quote. Always indicative (house rate 12.9%).","required":["instalment_minor","total_repayable_minor","total_interest_minor","annual_rate_percent","indicative","instalments"],"properties":{"instalment_minor":{"type":"integer","format":"int64"},"total_repayable_minor":{"type":"integer","format":"int64"},"total_interest_minor":{"type":"integer","format":"int64"},"annual_rate_percent":{"type":"string","description":"Exact decimal as a string, e.g. \"12.9\""},"indicative":{"type":"boolean","enum":[true],"description":"Always true on this route"},"instalments":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/LoanInstalment"}},"affordability":{"allOf":[{"$ref":"#/components/schemas/LoanAffordability"}],"description":"Present only when monthly_income_minor was sent (and not null). Absent otherwise (skip_serializing_if), never null.\n"}}},"LoanOffer":{"type":"object","description":"The application's `offer`. Written at apply as the indicative quote (`indicative: true`) and replaced by the provider's priced offer on an approval (`indicative: false`). Rows written before the platform’s specification carry only instalment_minor, total_repayable_minor, total_interest_minor, annual_rate_percent and indicative, hence the shorter `required`.\n","required":["instalment_minor","total_repayable_minor","total_interest_minor","annual_rate_percent","indicative"],"properties":{"principal_minor":{"type":"integer","format":"int64","description":"Capped at what was asked on a priced offer"},"term_months":{"type":"integer","format":"int32"},"instalment_minor":{"type":"integer","format":"int64"},"total_repayable_minor":{"type":"integer","format":"int64"},"total_interest_minor":{"type":"integer","format":"int64"},"annual_rate_percent":{"type":"string","description":"Exact decimal as a string"},"instalments":{"type":"array","items":{"$ref":"#/components/schemas/LoanInstalment"}},"indicative":{"type":"boolean"}}},"LoanDecisionReason":{"type":"object","required":["code","detail"],"properties":{"code":{"type":"string","description":"Provider codes pass through as-is. The platform's own are no_decision_provider, decision_unavailable, decision_unusable and priced_offer_unaffordable. For decision_unavailable / decision_unusable the customer always reads the bank's sentence (\"We could not decide this automatically, so a person will look at it.\").\n"},"detail":{"type":"string"}}},"LoanDecisionFigures":{"type":"object","description":"Approved only.","required":["limit_minor","rate_percent","max_term_months"],"properties":{"limit_minor":{"type":"integer","format":"int64"},"rate_percent":{"type":"string","description":"Exact decimal as a string"},"max_term_months":{"type":"integer","format":"int32"}}},"LoanBureauRecord":{"type":"object","description":"What is kept of a credit-bureau check (never the person). Present only when the applicant consented and a bureau is configured.","required":["bureau","outcome","score","score_name","reasons","inquiry_ref","consent"],"properties":{"bureau":{"type":"string","description":"Bureau and environment, e.g. \"Círculo de Crédito (sandbox-stub)\""},"outcome":{"type":"string","enum":["scored","no_file","unavailable"]},"score":{"type":"integer","format":"int32","nullable":true},"score_name":{"type":"string","nullable":true},"reasons":{"type":"array","items":{"type":"string"}},"inquiry_ref":{"type":"string","nullable":true},"consent":{"type":"object","required":["given_at","version"],"properties":{"given_at":{"type":"string","format":"date-time"},"version":{"type":"string","example":"circulo-consulta-v1"}}}}},"LoanEngineDecision":{"type":"object","description":"The decision step's record, as the customer receives it: `operator_note` is removed by customer_view. Rows decided before the platform’s specification carry only outcome, reasons and provenance (\"platform\"), which is why provider, policy_version and decided_at are not in `required`; every row written by the current code has all three.\n","required":["outcome","reasons","provenance"],"properties":{"outcome":{"type":"string","enum":["approved","declined","referred"]},"reasons":{"type":"array","items":{"$ref":"#/components/schemas/LoanDecisionReason"}},"provider":{"type":"string","description":"`none` when no provider is configured, else the provider's name (`zen`, `in-memory`, a vendor)"},"policy_version":{"type":"string","nullable":true,"description":"Key always present on current rows; null for provider `none`"},"provenance":{"type":"string","enum":["live","synthetic","unavailable","platform"],"description":"`platform` appears only on pre-spec-013 rows"},"decided_at":{"type":"string","format":"date-time"},"figures":{"allOf":[{"$ref":"#/components/schemas/LoanDecisionFigures"}],"description":"Present only when outcome is approved"},"bureau":{"allOf":[{"$ref":"#/components/schemas/LoanBureauRecord"}],"description":"Present only when the applicant consented and a bureau is configured. Absent, never null"},"explanation":{"allOf":[{"$ref":"#/components/schemas/LoanDecisionExplanation"}],"description":"Spec 075. Present on a person's decisions from the platform’s specification on; absent on older ones"},"operator_note":{"type":"string","nullable":true,"description":"Back office only (GET /backoffice/loans): what the policy said for the operator. customer_view() removes it, so a customer response never carries it."}}},"LoanOperatorDecision":{"type":"object","description":"Back office only (GET /backoffice/loans): what a person decided on a referral, written as the application's `operator_decision` when the approvals executor carries out an approved POST /backoffice/loans/decisions. Kept beside `decision`, never over it: the policy's record survives the referral. Names the maker and the checker, which is why the customer is sent `LoanCustomerOperatorDecision` instead.\n","required":["decision","reason","decided_at","decided_by","proposed_by"],"properties":{"decision":{"type":"string","enum":["approve","decline"]},"reason":{"type":"string","description":"The proposer's reason, from which an adverse-action notice is produced"},"decided_at":{"type":"string","format":"date-time","description":"When the approval was carried out. Absent on rows a person decided before the schema, which moved them here as they were\n"},"decided_by":{"type":"string","nullable":true,"description":"Subject of the approving checker"},"proposed_by":{"type":"string","description":"Subject of the proposing operator"}}},"LoanCustomerOperatorDecision":{"type":"object","description":"What customer_view() sends of a person's decision: what was decided and why, never who. Built from a list of these three keys, so a key added to the stored record is not sent by default.\n","required":["decision","reason"],"properties":{"decision":{"type":"string","enum":["approve","decline"]},"reason":{"type":"string"},"decided_at":{"type":"string","format":"date-time","description":"Absent on rows a person decided before the schema"}}},"LoanDecision":{"description":"The decision step's record. It stays the policy's after a person decides a referral — that decision is `operator_decision` — so a referral a person approved still reads `outcome: referred` here.\n","allOf":[{"$ref":"#/components/schemas/LoanEngineDecision"}]},"LoanDecisionExplanation":{"type":"object","description":"Spec 075. Why a person's application was decided as it was, kept on the decision: how the provider weighed it (FinLend's explainable factor model — a risk score, lower is better, its grade and every factor), the policy that read the score, the figures of the person's own statement, their latest screening (back office only: removed from the customer's copy, which must never say whether a list matched), and whether a bureau was asked, on what consent.\n","required":["assessment","policy","statement","bureau"],"properties":{"assessment":{"type":"object","nullable":true,"description":"Null when the provider does not explain itself, or there is no provider","required":["score","grade","band","factors"],"properties":{"score":{"type":"integer","minimum":3,"maximum":97,"description":"Lower is better"},"grade":{"type":"string","enum":["A","B","C","D","E"]},"band":{"type":"string"},"factors":{"type":"array","items":{"type":"object","required":["name","detail","points"],"properties":{"name":{"type":"string","description":"The factor with its figure, e.g. \"Debt to income 14%\""},"detail":{"type":"string","description":"What it measures"},"points":{"type":"integer","description":"Added to the risk score (negative: taken away); the score is 50 plus every factor, held to 3–97"}}}}}},"policy":{"type":"object","required":["provider","version","model","thresholds"],"properties":{"provider":{"type":"string","nullable":true},"version":{"type":"string","nullable":true},"provenance":{"type":"string","nullable":true,"enum":["live","synthetic","unavailable",null]},"model":{"type":"string","nullable":true,"description":"e.g. \"FinLend factor model for a person, v1\""},"thresholds":{"type":"object","nullable":true,"required":["approve_below","refer_below"],"properties":{"approve_below":{"type":"integer","description":"40: a lower score is approved"},"refer_below":{"type":"integer","description":"70: a lower score not approved is referred; at or above, declined"}}}}},"statement":{"type":"object","required":["read","currency","instalment_minor"],"properties":{"read":{"type":"boolean","description":"False when the statement could not be read: every figure null, never zero"},"currency":{"type":"string"},"from":{"type":"string","format":"date","nullable":true},"to":{"type":"string","format":"date","nullable":true},"money_in_minor":{"type":"integer","format":"int64","nullable":true,"description":"Money between the person's own accounts left out"},"money_out_minor":{"type":"integer","format":"int64","nullable":true},"left_monthly_minor":{"type":"integer","format":"int64","nullable":true,"description":"In less out, a month's worth, before the new instalment"},"instalment_minor":{"type":"integer","format":"int64","description":"The new instalment","at the quoted price":null},"left_after_instalment_minor":{"type":"integer","format":"int64","nullable":true},"history_days":{"type":"integer","nullable":true,"description":"How far back the person's history goes"}}},"screening":{"type":"object","description":"Back office only; absent from the customer's copy","required":["screened"],"properties":{"screened":{"type":"boolean"},"screened_at":{"type":"string","format":"date-time"},"outcome":{"type":"string","enum":["clear","pending","blocked"]},"names_checked":{"type":"integer"},"lists":{"type":"array","items":{"type":"object","required":["list"],"properties":{"list":{"type":"string"},"version":{"type":"string","nullable":true}}}},"matched":{"type":"boolean"},"open_matches":{"type":"integer"},"provider":{"type":"string","nullable":true}}},"bureau":{"type":"object","required":["asked"],"properties":{"asked":{"type":"boolean"},"bureau":{"type":"string"},"outcome":{"type":"string","enum":["scored","no_file","unavailable"]},"consent":{"type":"object","required":["version","given_at"],"properties":{"version":{"type":"string"},"given_at":{"type":"string","format":"date-time"}}}}}}},"LoanAgreementView":{"type":"object","description":"loan_agreements.rs `view`, the platform’s specification. A person's loan agreement: the terms of the offer, its schedule, the account it is paid into and repaid from, and the document with what to confirm to sign it. The loan is `accepted` once signed, and only then paid out.\n","required":["loan_id","state","signed","terms","schedule","agreement"],"properties":{"loan_id":{"type":"string","format":"uuid"},"state":{"type":"string"},"signed":{"type":"boolean"},"signed_at":{"type":"string","format":"date-time","nullable":true},"account_id":{"type":"string","nullable":true},"terms":{"type":"object","required":["currency"],"properties":{"amount_minor":{"type":"integer","format":"int64","nullable":true},"currency":{"type":"string"},"annual_rate_percent":{"type":"string","nullable":true},"term_months":{"type":"integer","nullable":true},"instalment_minor":{"type":"integer","format":"int64","nullable":true},"total_repayable_minor":{"type":"integer","format":"int64","nullable":true},"total_interest_minor":{"type":"integer","format":"int64","nullable":true}}},"schedule":{"type":"array","items":{"type":"object","required":["number","due_on","total_minor","principal_minor","interest_minor"],"properties":{"number":{"type":"integer"},"due_on":{"type":"string","format":"date"},"total_minor":{"type":"integer","format":"int64"},"principal_minor":{"type":"integer","format":"int64"},"interest_minor":{"type":"integer","format":"int64"},"balance_after_minor":{"type":"integer","format":"int64"}}}},"agreement":{"type":"object","nullable":true,"required":["id","state","sha256","pdf"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["unsigned","signed"]},"sha256":{"type":"string"},"signed_at":{"type":"string","format":"date-time","nullable":true},"auth_method":{"type":"string","nullable":true},"authorize":{"type":"object","nullable":true,"description":"What the customer's device confirms to sign — the amount and the document's SHA-256","required":["amount","currency","payee"],"properties":{"amount":{"type":"string"},"currency":{"type":"string"},"payee":{"type":"string"}}},"pdf":{"type":"string"},"signed_pdf":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}}}},"MyLoans":{"type":"object","description":"lending.rs `my_loans`. Each of the caller's loans, drawn or not; a drawn one with its position, derived at the moment of asking.","required":["loans"],"properties":{"loans":{"type":"array","items":{"type":"object","required":["id","state","currency","amount_minor","drawn"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string"},"currency":{"type":"string"},"amount_minor":{"type":"integer","format":"int64","description":"What was lent: the offer's principal, or what was asked"},"term_months":{"type":"integer","nullable":true},"drawn":{"type":"boolean"},"disbursed_on":{"type":"string","format":"date","nullable":true},"held_by":{"type":"string","enum":["core","platform"]},"position":{"type":"object","required":["as_of","received_minor","due_by_now_minor","arrears_minor","days_past_due","in_arrears","settled","provenance","instalments"],"properties":{"as_of":{"type":"string","format":"date"},"received_minor":{"type":"integer","format":"int64"},"due_by_now_minor":{"type":"integer","format":"int64"},"arrears_minor":{"type":"integer","format":"int64"},"days_past_due":{"type":"integer"},"in_arrears":{"type":"boolean"},"settled":{"type":"boolean"},"provenance":{"type":"string","enum":["derived"]},"instalments":{"type":"array","items":{"type":"object","required":["number","due_on","due_minor","paid_minor","outstanding_minor"],"properties":{"number":{"type":"integer"},"due_on":{"type":"string","format":"date"},"due_minor":{"type":"integer","format":"int64"},"paid_minor":{"type":"integer","format":"int64"},"outstanding_minor":{"type":"integer","format":"int64"},"cleared_on":{"type":"string","format":"date","nullable":true},"days_past_due":{"type":"integer"}}}}}}}}}}},"LoanApplicationView":{"type":"object","description":"lending.rs customer_view(). Returned by GET /loans (items), POST /loans and POST /loans/{id}/withdraw.","required":["id","state","awaiting_us","is_final","amount_minor","currency","term_months","purpose","decision","operator_decision","offer","core_loan_ref","created_at","your_moves"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["draft","submitted","referred","approved","accepted","declined","withdrawn","disbursed","expired","written_off"]},"awaiting_us":{"type":"boolean","description":"true for submitted, referred, approved, accepted"},"is_final":{"type":"boolean","description":"true for declined, withdrawn, expired, written_off — not disbursed: a drawn loan is still being repaid"},"amount_minor":{"type":"integer","format":"int64","description":"What was asked (requested_minor)","not the offer's principal":null},"currency":{"type":"string"},"term_months":{"type":"integer","format":"int32"},"purpose":{"type":"string","nullable":true},"decision":{"allOf":[{"$ref":"#/components/schemas/LoanDecision"}],"nullable":true},"operator_decision":{"allOf":[{"$ref":"#/components/schemas/LoanCustomerOperatorDecision"}],"nullable":true,"description":"Null until a person decides a referral and a second person approves it"},"offer":{"allOf":[{"$ref":"#/components/schemas/LoanOffer"}],"nullable":true},"core_loan_ref":{"type":"string","nullable":true},"business_id":{"type":"string","format":"uuid","nullable":true,"description":"Spec 072. The company that borrows; absent or null for a person."},"declared_turnover_minor":{"type":"integer","nullable":true,"description":"Spec 072. What the company declared it turned over in twelve months."},"read_figures":{"type":"object","nullable":true,"description":"Spec 072. What the bank read of the company, each figure with its provenance."},"offer_expires_on":{"type":"string","format":"date","nullable":true,"description":"Spec 072. The last day a company's offer can be signed."},"account_id":{"type":"string","nullable":true,"description":"Spec 075. The account it is paid into and repaid from, named on its agreement"},"created_at":{"type":"string","format":"date-time"},"your_moves":{"type":"array","description":"draft: [submit, withdraw]; submitted/referred: [withdraw]; approved: [withdraw, accept] — accept is signing the agreement (POST /loans/{id}/agreement/sign); accepted: [withdraw]; final states: []","items":{"type":"string","enum":["submit","withdraw","accept"]}}}},"OnboardingNextStep":{"type":"object","required":["kind","prompt"],"properties":{"kind":{"type":"string","enum":["identity","address","tax_residency","source_of_funds","terms_accepted"]},"prompt":{"type":"string","description":"The module's sentence, e.g. \"read and accept the terms\""}}},"OnboardingRequirementView":{"type":"object","required":["kind","status","blocking","expires_on","lapsed","failure_detail","provider_ref"],"properties":{"kind":{"type":"string","enum":["identity","address","screening","tax_residency","source_of_funds","terms_accepted"],"description":"`screening` is listed here (it is the bank's own check) but never offered as next_step"},"status":{"type":"string","enum":["outstanding","submitted","satisfied","failed"]},"blocking":{"type":"boolean"},"expires_on":{"type":"string","format":"date","nullable":true},"lapsed":{"type":"boolean","description":"status is satisfied and expires_on is before today"},"failure_detail":{"type":"string","nullable":true},"provider_ref":{"type":"string","nullable":true}}},"OnboardingJourney":{"type":"object","description":"onboarding.rs view(). Two shapes share this schema. Normal: state is awaiting_customer, awaiting_us, ready or refused and `final` is present. Ineligible (eligibility refused at start): state is `ineligible`, detail is the recorded reason, next_step is null, requirements is [] and `final` is ABSENT.\n","required":["id","state","detail","next_step","requirements"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["awaiting_customer","awaiting_us","ready","refused","ineligible"]},"final":{"type":"boolean","description":"true for ready and refused. Absent when state is ineligible"},"detail":{"type":"string","description":"awaiting_customer: \"N thing(s) left for you\"; awaiting_us: \"nothing for you to do; we are checking N thing(s)\"; ready: \"everything checks out\"; refused: \"kind: detail;...\" joined; ineligible: the eligibility reason.\n"},"next_step":{"allOf":[{"$ref":"#/components/schemas/OnboardingNextStep"}],"nullable":true,"description":"Non-null only when state is awaiting_customer"},"requirements":{"type":"array","items":{"$ref":"#/components/schemas/OnboardingRequirementView"}}}},"OnboardingScreeningResult":{"type":"object","required":["status","screened_at"],"properties":{"status":{"type":"string","enum":["clear","in_review"]},"screened_at":{"type":"string","format":"date-time"}}},"FundingView":{"type":"object","description":"crypto.rs view(). GET /funding items; POST /funding adds `note` (FundingStarted). Spec 076, by decision: the platform holds no tokens and keeps no crypto account — a funding is credited in dollars to the customer's account, one leg, under `crypto-credit:<funding id>`. There is no holdings view and no average cost; a client must not pair statement legs into one, and must not treat an account as crypto by its kind. Holdings come with a custodian adapter.\n","required":["id","stage","detail","pending","final","network","token","token_decimals","deposit_address","deposit_memo","address_provenance","amount_observed","confirmations","confirmations_required","tx_hash","settles_in","fee_minor","credited_minor","failure_detail","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"stage":{"type":"string","enum":["awaiting_deposit","confirming","converting","credited","failed"]},"detail":{"type":"string","enum":["waiting for your deposit","confirming on the network","converting to your account currency","credited to your account","this could not be completed, and somebody is looking at it"],"description":"Fixed per stage, in the order of the stage enum"},"pending":{"type":"boolean","description":"awaiting_deposit, confirming, converting"},"final":{"type":"boolean","description":"credited, failed"},"network":{"type":"string"},"token":{"type":"string","description":"Token symbol"},"token_decimals":{"type":"integer","format":"int32"},"deposit_address":{"type":"string","nullable":true,"description":"The ramp's node address, the customer's own; null when no ramp is bound or none is free"},"deposit_memo":{"type":"string","nullable":true},"address_provenance":{"type":"string","enum":["live","synthetic","unavailable"],"description":"unavailable iff deposit_address is null; synthetic on the bank's sandbox chain"},"amount_observed":{"type":"string","nullable":true,"description":"i128 in the token's smallest unit, as a decimal-digit string"},"confirmations":{"type":"integer","format":"int32","minimum":0},"confirmations_required":{"type":"integer","format":"int32","description":"ethereum 12, polygon 128, arbitrum 20, any other network 256"},"tx_hash":{"type":"string","nullable":true},"settles_in":{"type":"string"},"fee_minor":{"type":"integer","format":"int64","minimum":0,"description":"Never null (column NOT NULL DEFAULT 0)"},"credited_minor":{"type":"integer","format":"int64","nullable":true},"failure_detail":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"FundingStarted":{"allOf":[{"$ref":"#/components/schemas/FundingView"},{"type":"object","required":["note"],"properties":{"note":{"type":"string","description":"Fixed text: no ramp provider is configured, so there is no deposit address..."}}}]},"FundingConversionRate":{"type":"object","required":["numerator","denominator"],"properties":{"numerator":{"type":"string","description":"i128 as a decimal-digit string"},"denominator":{"type":"string","description":"i128 as a decimal-digit string"}}},"FundingConversion":{"type":"object","description":"POST /funding/conversion. House rate 9993/10000 and fee 50 minor until a provider quotes.","required":["gross_minor","fee_minor","net_minor","fee_exceeded_deposit","settles_in","peg","rate","indicative"],"properties":{"gross_minor":{"type":"integer","format":"int64"},"fee_minor":{"type":"integer","format":"int64"},"net_minor":{"type":"integer","format":"int64","minimum":0},"fee_exceeded_deposit":{"type":"boolean"},"settles_in":{"type":"string"},"peg":{"type":"string","pattern":"^-?[0-9]+\\.[0-9]{4}$","description":"The ratio at four places, e.g. \"0.9993\""},"rate":{"$ref":"#/components/schemas/FundingConversionRate"},"indicative":{"type":"boolean","enum":[true],"description":"Always true today"}}},"InvestmentRiskProfile":{"type":"string","enum":["cautious","balanced","adventurous"]},"InvestmentTarget":{"type":"object","required":["instrument","weight_bps"],"properties":{"instrument":{"type":"string"},"weight_bps":{"type":"integer","format":"int64"}}},"InvestmentModel":{"type":"object","required":["name","risk_profile","targets","drift_band_bps"],"properties":{"name":{"type":"string","enum":["cautious","balanced","adventurous"]},"risk_profile":{"$ref":"#/components/schemas/InvestmentRiskProfile"},"targets":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/InvestmentTarget"}},"drift_band_bps":{"type":"integer","format":"int64"}}},"InvestmentModelList":{"type":"object","required":["models"],"properties":{"models":{"type":"array","items":{"$ref":"#/components/schemas/InvestmentModel"}}}},"InvestmentSuitabilityVerdict":{"type":"object","description":"platform_module_investments::SuitabilityVerdict, internally tagged by `verdict`. `too_risky` has assessed and required; `more_cautious_than_assessed` has assessed and model; `stale` has days_ago and valid_for_days; `suitable` has none.\n","required":["verdict"],"properties":{"verdict":{"type":"string","enum":["suitable","too_risky","more_cautious_than_assessed","stale"]},"assessed":{"$ref":"#/components/schemas/InvestmentRiskProfile"},"required":{"$ref":"#/components/schemas/InvestmentRiskProfile"},"model":{"$ref":"#/components/schemas/InvestmentRiskProfile"},"days_ago":{"type":"integer","format":"int32","minimum":0},"valid_for_days":{"type":"integer","format":"int32","minimum":0}}},"InvestmentSuitability":{"type":"object","required":["permits","detail","verdict"],"properties":{"permits":{"type":"boolean"},"detail":{"type":"string"},"verdict":{"$ref":"#/components/schemas/InvestmentSuitabilityVerdict"}}},"InvestmentPosition":{"type":"object","required":["instrument","quantity","quantity_scale","price_minor"],"properties":{"instrument":{"type":"string"},"quantity":{"type":"string","description":"The raw scaled i128 as digits (e.g. \"124000\" with quantity_scale 4 means 12.4000). Not a decimal string"},"quantity_scale":{"type":"integer","format":"int32"},"price_minor":{"type":"integer","format":"int64"},"cost_minor":{"type":"integer","format":"int64","nullable":true,"description":"Spec 076. What the holding cost, from the broker's fills at average cost (GET /investments/fills); null where nothing was filled. Gain is the position's value less this. Tax lots are not accounted for\n"}}},"InvestmentDrift":{"type":"object","required":["instrument","target_bps","actual_bps","drift_bps","target_minor","actual_minor"],"properties":{"instrument":{"type":"string"},"target_bps":{"type":"integer","format":"int64"},"actual_bps":{"type":"integer","format":"int64"},"drift_bps":{"type":"integer","format":"int64","description":"actual_bps - target_bps"},"target_minor":{"type":"integer","format":"int64"},"actual_minor":{"type":"integer","format":"int64"}}},"InvestmentOrder":{"type":"object","required":["instrument","side","amount_minor"],"properties":{"instrument":{"type":"string"},"side":{"type":"string","enum":["buy","sell"]},"amount_minor":{"type":"integer","format":"int64","description":"By value, not quantity"}}},"InvestmentRebalancePlan":{"type":"object","required":["within_band","orders","cash_after_minor"],"properties":{"within_band":{"type":"boolean"},"orders":{"type":"array","items":{"$ref":"#/components/schemas/InvestmentOrder"}},"cash_after_minor":{"type":"integer","format":"int64"}}},"InvestmentContribution":{"type":"object","required":["id","from_account","amount_minor","currency","reference","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"from_account":{"type":"string"},"amount_minor":{"type":"integer","format":"int64"},"currency":{"type":"string"},"reference":{"type":"string","description":"`investment:<id>`, as the statement shows it"},"created_at":{"type":"string","format":"date-time"}}},"InvestmentContributions":{"type":"object","required":["contributions"],"properties":{"contributions":{"type":"array","items":{"$ref":"#/components/schemas/InvestmentContribution"}}}},"InvestmentFills":{"type":"object","required":["fills","broker_bound"],"properties":{"broker_bound":{"type":"boolean"},"fills":{"type":"array","items":{"type":"object","required":["order_id","instrument","side","quantity","quantity_scale","value_minor","filled_at"],"properties":{"order_id":{"type":"string","format":"uuid"},"instrument":{"type":"string"},"side":{"type":"string","enum":["buy","sell"]},"quantity":{"type":"string","description":"Scaled digits","as positions'":null},"quantity_scale":{"type":"integer"},"value_minor":{"type":"integer","format":"int64"},"filled_at":{"type":"string","format":"date-time"}}}}}},"InvestmentAccountView":{"type":"object","description":"investments.rs account_view(). GET /investments (and POST /investments 201, not in this group).","required":["id","model","risk_profile","assessed_on","suitability","total_minor","positions","drift","proposed_rebalance","provenance","contributions"],"properties":{"contributions":{"type":"object","description":"Spec 076. Adding money to investing (POST /investments/contributions)","required":["open","currency","payee"],"properties":{"open":{"type":"boolean","description":"Whether the bank takes money into investing yet"},"currency":{"type":"string"},"payee":{"type":"string","description":"What the device confirms as the payee","beside the amount":null}}},"id":{"type":"string","format":"uuid"},"model":{"type":"string","enum":["cautious","balanced","adventurous"]},"risk_profile":{"type":"string","description":"Stored text; written only as cautious|balanced|adventurous"},"assessed_on":{"type":"string","format":"date"},"suitability":{"allOf":[{"$ref":"#/components/schemas/InvestmentSuitability"}],"nullable":true,"description":"Null only if the stored risk_profile is not one of the three"},"total_minor":{"type":"integer","format":"int64"},"positions":{"type":"array","items":{"$ref":"#/components/schemas/InvestmentPosition"}},"drift":{"type":"array","items":{"$ref":"#/components/schemas/InvestmentDrift"}},"proposed_rebalance":{"$ref":"#/components/schemas/InvestmentRebalancePlan"},"provenance":{"type":"string","enum":["unavailable"],"description":"Hard-coded: there is no broker"}}},"DisputeEvidenceAsk":{"type":"object","required":["kind","ask"],"properties":{"kind":{"type":"string","enum":["order_detail","cancellation_proof","agreed_amount","merchant_contacted"]},"ask":{"type":"string"}}},"DisputeCaseView":{"type":"object","description":"disputes.rs customer_view() for a row this build can read.","required":["id","payment_kind","payment_ref","amount_minor","currency","reason","reason_prompt","stage","next_step","deadline","outstanding","provisional_credit_proposed","outcome","raised_at","your_moves"],"properties":{"id":{"type":"string","format":"uuid"},"payment_kind":{"type":"string","enum":["card","transfer"]},"payment_ref":{"type":"string","description":"Card authorisation id, or transfer intent id (uuid)"},"amount_minor":{"type":"integer","format":"int64"},"currency":{"type":"string"},"reason":{"type":"string","enum":["not_recognised","not_received","not_as_described","cancelled","duplicate","wrong_amount"]},"reason_prompt":{"type":"string"},"stage":{"type":"string","enum":["raised","evidence_required","with_the_merchant","decided","withdrawn"]},"next_step":{"type":"string"},"deadline":{"type":"string","format":"date-time","nullable":true,"description":"raised_at + 30 days, only while with_the_merchant"},"outstanding":{"type":"array","items":{"$ref":"#/components/schemas/DisputeEvidenceAsk"}},"provisional_credit_proposed":{"type":"boolean"},"outcome":{"type":"string","nullable":true,"enum":["upheld","declined",null]},"raised_at":{"type":"string","format":"date-time"},"your_moves":{"type":"array","description":"raised/with_the_merchant: [withdraw]; evidence_required: [supply_evidence, withdraw]; decided/withdrawn: []","items":{"type":"string","enum":["supply_evidence","withdraw"]}}}},"DisputeUnreadableView":{"type":"object","description":"What customer_view() returns for a stored row whose reason or stage this build does not know.","required":["id","stage","unreadable"],"properties":{"id":{"type":"string","format":"uuid"},"stage":{"type":"string","description":"The stored stage text","unparsed":null},"unreadable":{"type":"boolean","enum":[true]}}},"DisputeView":{"oneOf":[{"$ref":"#/components/schemas/DisputeCaseView"},{"$ref":"#/components/schemas/DisputeUnreadableView"}]},"DisputeReasonOption":{"type":"object","required":["reason","prompt","within_days","needs","provisional_credit"],"properties":{"reason":{"type":"string","enum":["not_recognised","not_received","not_as_described","cancelled","duplicate","wrong_amount"]},"prompt":{"type":"string"},"within_days":{"type":"integer","format":"int64","description":"not_recognised 540, all others 120"},"needs":{"type":"array","items":{"$ref":"#/components/schemas/DisputeEvidenceAsk"}},"provisional_credit":{"type":"boolean","description":"true for not_recognised and duplicate"}}},"DisputeList":{"type":"object","required":["disputes","reasons"],"properties":{"disputes":{"type":"array","items":{"$ref":"#/components/schemas/DisputeView"}},"reasons":{"type":"array","items":{"$ref":"#/components/schemas/DisputeReasonOption"}}}},"DisputeAlreadyOpen":{"type":"object","description":"POST /disputes 409 body. NOT a Problem document.","required":["detail","dispute"],"properties":{"detail":{"type":"string","description":"\"You already have a case open on this payment, raised on <d Month>. <next_step>\""},"dispute":{"$ref":"#/components/schemas/DisputeView"}}},"SupportMessage":{"type":"object","required":["author","channel","body","at","from_assistant"],"properties":{"author":{"type":"string","enum":["customer","agent","assistant","system"]},"channel":{"type":"string","enum":["chat","voice"]},"body":{"type":"string"},"at":{"type":"string","format":"date-time"},"from_assistant":{"type":"boolean","description":"author == assistant"}}},"SupportCallback":{"type":"object","required":["state","attempts","still_owed","topic","requested_at"],"properties":{"state":{"type":"string","enum":["requested","scheduled","missed","completed","abandoned"]},"attempts":{"type":"integer","format":"int32","minimum":0},"still_owed":{"type":"boolean","description":"requested, scheduled, missed"},"topic":{"type":"string","nullable":true},"requested_at":{"type":"string","format":"date-time"}}},"AssistantHandover":{"type":"object","description":"platform-module-assistant `Handover`, the platform’s specification","required":["reason","kind","intent","checks","actions","turns"],"properties":{"reason":{"type":"string"},"kind":{"type":"string"},"intent":{"type":"string","nullable":true},"checks":{"type":"array","items":{"type":"string"}},"actions":{"type":"array","items":{"type":"string"}},"turns":{"type":"integer"}}},"AssistantConversation":{"type":"object","description":"routes/assistant.rs `view`, the platform’s specification","required":["id","phase","finished","answered_by","support_id","may","never","messages","reply","voice","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"phase":{"type":"string","enum":["open","confirm","done"]},"finished":{"type":"boolean"},"answered_by":{"type":"string","description":"What read the last turn: rules, or model <name>"},"support_id":{"type":"string","format":"uuid","nullable":true,"description":"The support conversation it was handed to"},"may":{"type":"array","items":{"type":"string"}},"never":{"type":"array","items":{"type":"string"}},"messages":{"type":"array","items":{"type":"object","required":["who","text","at","from_assistant"],"properties":{"who":{"type":"string","enum":["customer","assistant"]},"text":{"type":"string"},"at":{"type":"string","format":"date-time"},"from_assistant":{"type":"boolean"}}}},"reply":{"type":"object","nullable":true,"required":["lines","spoken","offers","suggestions","finished","handed_over"],"properties":{"lines":{"type":"array","items":{"type":"string"}},"spoken":{"type":"array","items":{"type":"string"},"description":"Spec 063: the same lines as a person says them (amounts, dates and numbers in words, nothing unreadable), one for each line. What the apps speak."},"offers":{"type":"array","items":{"type":"object","required":["label","href"],"properties":{"label":{"type":"string"},"href":{"type":"string","description":"A platform path (a PDF) or an app screen (/disputes)"}}}},"suggestions":{"type":"array","items":{"type":"string"}},"finished":{"type":"boolean"},"handed_over":{"type":"boolean"}}},"voice":{"type":"object","description":"Spec 063: how the apps speak, on the device; no audio is sent to the platform","required":["voice","speed","acknowledgements","not_heard"],"properties":{"voice":{"type":"string","description":"The on-device voice's name (Kokoro's)"},"speed":{"type":"number"},"acknowledgements":{"type":"array","items":{"type":"string"},"description":"Said the moment the customer stops speaking, while the answer is made"},"not_heard":{"type":"string","description":"Said when nothing could be made of what was heard"}}},"created_at":{"type":"string","format":"date-time"}}},"SupportConversation":{"type":"object","description":"support.rs view(). Every customer support route returns this (GET as a bare array).","required":["id","topic","turn","waiting_on_us","resolved","created_at","messages","callback","callback_note"],"properties":{"id":{"type":"string","format":"uuid"},"topic":{"type":"string","nullable":true},"turn":{"type":"string","enum":["us","customer","resolved"]},"waiting_on_us":{"type":"boolean","description":"turn == us"},"resolved":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/SupportMessage"}},"callback":{"allOf":[{"$ref":"#/components/schemas/SupportCallback"}],"nullable":true},"handover":{"description":"Spec 061, the back office's queue only: what the assistant handed over with it","nullable":true,"allOf":[{"$ref":"#/components/schemas/AssistantHandover"}]},"callback_note":{"type":"string","enum":["we call you on the number we already hold; we will never ask you to give one out"]}}},"BackofficeDeadLetterList":{"type":"object","description":"GET /backoffice/events/dead-letters (routes/events.rs `dead_letters`). The tenant's outbox rows with status 'dead_lettered', oldest first (ORDER BY created_at, id), at most `limit` (default 100, clamped to 1..500).\n","required":["dead_letters"],"properties":{"dead_letters":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeDeadLetter"}}}},"BackofficeDeadLetter":{"type":"object","description":"platform_store::unit_of_work::DeadLetter, written key by key by the handler.","required":["id","type","attempts","last_error","created_at"],"properties":{"id":{"type":"string","format":"uuid","description":"The outbox row's id (also the event id). This is the id POST.../{id}/requeue takes."},"type":{"type":"string","nullable":true,"description":"`event->>'type'` of the stored row, e.g. \"transfer.posted\". Null when the row's JSON has no string `type` (a row the relay could not decode is dead-lettered at once).\n"},"attempts":{"type":"integer","format":"int32","minimum":0},"last_error":{"type":"string","nullable":true,"description":"The relay's last failure text; free prose, not an enum."},"created_at":{"type":"string","format":"date-time","description":"When the outbox row was written, not when it was dead-lettered."}}},"BackofficeDeadLetterRequeued":{"type":"object","description":"POST /backoffice/events/dead-letters/{id}/requeue. A fixed shape: the path id echoed and the literal state \"pending\" (the row's status, attempts reset to 0, last_error NULL).\n","required":["id","state"],"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["pending"]}}},"BackofficeReplayResult":{"type":"object","description":"202 of POST /events/replay: platform_events::replay::Replayed plus the topic echoed. Positions count the tenant's PUBLISHED outbox rows (status 'published'), all topics together, in outbox order. `sent` counts only events whose topic is `topic`; rows that are not decodable events are skipped but still advance the position.\n","required":["topic","sent","from","next"],"properties":{"topic":{"$ref":"#/components/schemas/BackofficeReplayTopic"},"sent":{"type":"integer","format":"int64","minimum":0},"from":{"type":"integer","format":"int64","minimum":0,"description":"Where this call started: 0 for \"earliest\", the tenant's published count at the time for \"latest\", or the number sent.\n"},"next":{"type":"integer","format":"int64","minimum":0,"description":"The `from` to send on the following call. Equals `from` when nothing was looked at."}}},"BackofficeReplayInterrupted":{"type":"object","description":"503 of POST /events/replay when the bus stopped answering part way (ReplayError::Bus). NOT a Problem: a plain object with no `from` and no type/title/status/correlation_id. `sent` events went out; resume with from = `next`.\n","required":["topic","sent","next","detail"],"properties":{"topic":{"$ref":"#/components/schemas/BackofficeReplayTopic"},"sent":{"type":"integer","format":"int64","minimum":0},"next":{"type":"integer","format":"int64","minimum":0},"detail":{"type":"string","description":"\"the bus stopped answering; resume from <next>: <reason>\""}}},"BackofficeReplayTopic":{"type":"string","description":"platform_events::kafka::TOPICS. The handler refuses any other value with 400 (a Problem naming the list), so a request can only succeed with one of these.\n","enum":["platform.payments","platform.core","platform.approvals","platform.audit","platform.dlq"]},"BackofficeNoticeList":{"type":"object","description":"GET /backoffice/notifications (routes/notifications.rs `whose`). The same `notices_for` the customer's GET /notifications uses, run for `subject` instead of the caller, plus the subject echoed. Differs from NoticeList only by the `subject` key. Built from the subject's newest 100 outbox rows (any status, ORDER BY id DESC); rows the notifications module has no notice for are dropped, so there may be fewer than 100. An unknown subject is not an error: it is 200 with notices: [].\n","required":["subject","notices"],"properties":{"subject":{"type":"string","description":"The query parameter echoed exactly as sent (not trimmed, not validated)."},"notices":{"type":"array","items":{"$ref":"#/components/schemas/NoticeView"}}}},"BackofficeCallbackOutcome":{"type":"string","description":"What happened to a call. `scheduled`: requested|missed -> scheduled. `no_answer`: scheduled|missed -> missed (attempts+1), or -> abandoned when attempts reaches 3 (MAX_ATTEMPTS). `completed`: requested|scheduled|missed -> completed. Any other value is 400; a move the current state does not allow is 409.\n","enum":["scheduled","no_answer","completed"]},"BackofficeStatementUpload":{"type":"object","description":"201 of POST /backoffice/statements (routes/reconciliation.rs `ingest_statement`). 201 even when every statement was already held and nothing was stored.\n","required":["source","statements"],"properties":{"source":{"type":"string","description":"The `source` query parameter, trimmed."},"statements":{"type":"array","minItems":1,"description":"One per <Stmt> in the file, in file order. Never empty (an empty parse is 400).","items":{"$ref":"#/components/schemas/BackofficeStatementUploadResult"}}}},"BackofficeStatementUploadResult":{"type":"object","description":"routes/reconciliation.rs `store_statement` view (json!). Counts and balances-check are computed from the file as uploaded, including when outcome is already_held.\n","required":["statement_ref","account_ref","currency","period_from","period_to","outcome","statement_id","entries","pending_entries","reconciles"],"properties":{"statement_ref":{"type":"string","description":"The bank's statement id (<Stmt><Id>)."},"account_ref":{"type":"string","description":"The account as the file names it (IBAN or other id)."},"currency":{"type":"string","description":"ISO 4217 code from the file, e.g. USD."},"period_from":{"type":"string","format":"date"},"period_to":{"type":"string","format":"date"},"outcome":{"type":"string","enum":["stored","already_held"],"description":"already_held when a statement with the same (account_ref, statement_ref) exists in the tenant; nothing is stored and statement_id is the held one's.\n"},"statement_id":{"type":"string","format":"uuid"},"entries":{"type":"integer","minimum":0,"description":"Lines in the statement (usize)."},"pending_entries":{"type":"integer","minimum":0,"description":"Lines whose bank status is not booked (pending AND other statuses both count)."},"reconciles":{"type":"boolean","description":"Whether the BOOKED lines account for the movement from opening to closing balance. false is stored anyway and only flagged here.\n"}}},"BackofficeProducts":{"type":"object","description":"routes/settings.rs `products`, the platform’s specification","required":["products"],"properties":{"products":{"type":"array","items":{"type":"object","required":["id","kind","name","short_name","currency","active","accounting"],"properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["savings","loan"]},"name":{"type":"string"},"short_name":{"type":"string"},"description":{"type":"string","nullable":true},"currency":{"type":"string"},"annual_rate":{"type":"string","nullable":true,"description":"The nominal yearly rate in per cent"},"active":{"type":"boolean"},"accounting":{"type":"string"},"principal_minor":{"type":"integer","format":"int64","nullable":true},"min_principal_minor":{"type":"integer","format":"int64","nullable":true},"max_principal_minor":{"type":"integer","format":"int64","nullable":true},"repayments":{"type":"integer","nullable":true},"min_repayments":{"type":"integer","nullable":true},"max_repayments":{"type":"integer","nullable":true},"repayment_every":{"type":"string","nullable":true},"overdraft":{"type":"boolean","nullable":true},"min_balance_minor":{"type":"integer","format":"int64","nullable":true}}}}}},"BackofficeCalendars":{"type":"object","description":"routes/settings.rs `calendars`, the platform’s specification","required":["year","rails","rules","note"],"properties":{"year":{"type":"integer"},"note":{"type":"string"},"rules":{"type":"array","description":"What each set of rules keeps in the year, whether a rail follows it or not","items":{"type":"object","required":["rules","holidays"],"properties":{"rules":{"type":"string","enum":["fed","target2"]},"holidays":{"type":"array","items":{"type":"object","required":["date","name"],"properties":{"date":{"type":"string","format":"date"},"name":{"type":"string"}}}}}}},"rails":{"type":"array","items":{"type":"object","required":["rail","rules","holidays"],"properties":{"rail":{"type":"string"},"rules":{"type":"string","enum":["none","fed","target2"]},"holidays":{"type":"array","items":{"type":"object","required":["date","name","contract"],"properties":{"date":{"type":"string","format":"date"},"name":{"type":"string"},"contract":{"type":"boolean","description":"A day the bank's contract with the rail adds"}}}}}}}}},"BackofficeBranches":{"type":"object","description":"routes/settings.rs `branches`, the platform’s specification","required":["branches"],"properties":{"branches":{"type":"array","items":{"type":"object","required":["id","code","name","city","vault_account","currency","closed","staff","customers"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"city":{"type":"string"},"vault_account":{"type":"string"},"currency":{"type":"string"},"closed":{"type":"boolean"},"staff":{"type":"integer"},"customers":{"type":"integer"}}}}}},"BackofficeAuthority":{"type":"object","description":"routes/settings.rs `authority`, the platform’s specification","required":["roles","most_minor","note"],"properties":{"most_minor":{"type":"integer","format":"int64"},"note":{"type":"string"},"roles":{"type":"array","items":{"type":"object","required":["role","title","decides"],"properties":{"role":{"type":"string"},"title":{"type":"string"},"posting":{"type":"object","nullable":true,"required":["default_minor","limit_minor"],"properties":{"default_minor":{"type":"integer","format":"int64"},"limit_minor":{"type":"integer","format":"int64"},"set":{"$ref":"#/components/schemas/BackofficeAuthoritySet"}}},"decides":{"type":"array","items":{"type":"object","required":["domain","limit_minor"],"properties":{"domain":{"type":"string"},"limit_minor":{"type":"integer","format":"int64","nullable":true},"default_minor":{"type":"integer","format":"int64","nullable":true},"set":{"$ref":"#/components/schemas/BackofficeAuthoritySet"}}}}}}}}},"BackofficeAuthoritySet":{"type":"object","nullable":true,"required":["by","at","approval_id"],"properties":{"by":{"type":"string"},"at":{"type":"string","format":"date-time"},"approval_id":{"type":"string","format":"uuid"}}},"BusinessBureauSubject":{"type":"object","description":"platform_ports::business_bureau_port::BusinessSubject, the platform’s specification","required":["legal_name","city","region"],"properties":{"legal_name":{"type":"string"},"street":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","description":"The two-letter state"},"postcode":{"type":"string","nullable":true},"tax_id":{"type":"string","nullable":true,"description":"The EIN"},"phone":{"type":"string","nullable":true}}},"BusinessBureauMatch":{"type":"object","required":["bureau_id","name","address"],"properties":{"bureau_id":{"type":"string"},"name":{"type":"string"},"address":{"type":"string"},"reliability":{"type":"integer","nullable":true,"description":"How sure the bureau is, in hundredths (9640 is 96.40)"},"tradelines":{"type":"integer","nullable":true}}},"BusinessBureauScore":{"type":"object","required":["model","score","factors"],"properties":{"model":{"type":"string"},"score":{"type":"integer"},"percentile":{"type":"integer","nullable":true},"risk_class":{"type":"integer","nullable":true,"description":"1 low … 5 high"},"risk":{"type":"string","nullable":true},"recommended_limit_minor":{"type":"integer","format":"int64","nullable":true},"factors":{"type":"array","items":{"type":"object","required":["code","text"],"properties":{"code":{"type":"string"},"text":{"type":"string"}}}}}},"BusinessBureauAnswer":{"type":"object","description":"platform_ports::business_bureau_port::BusinessAnswer: scored or no_file","required":["outcome","bureau","bureau_id"],"properties":{"outcome":{"type":"string","enum":["scored","no_file"]},"bureau":{"type":"string"},"bureau_id":{"type":"string"},"business_name":{"type":"string"},"payment":{"allOf":[{"$ref":"#/components/schemas/BusinessBureauScore"}],"nullable":true},"stability":{"allOf":[{"$ref":"#/components/schemas/BusinessBureauScore"}],"nullable":true},"inquiry_ref":{"type":"string","nullable":true},"reasons":{"type":"array","items":{"type":"string"}}}},"BusinessCreditCheck":{"type":"object","description":"routes/business_credit.rs `check_view`, the platform’s specification","required":["id","bureau","bureau_id","business_name","outcome","answer","asked_by","asked_at"],"properties":{"id":{"type":"string","format":"uuid"},"bureau":{"type":"string"},"bureau_id":{"type":"string"},"business_name":{"type":"string"},"outcome":{"type":"string","enum":["scored","no_file"]},"answer":{"$ref":"#/components/schemas/BusinessBureauAnswer"},"inquiry_ref":{"type":"string","nullable":true},"asked_by":{"type":"string"},"asked_at":{"type":"string","format":"date-time"}}},"BackofficeBusinessCredit":{"type":"object","description":"routes/business_credit.rs `credit`, the platform’s specification","required":["business","bureau_configured","suggested","checks","stands_days"],"properties":{"business":{"type":"object","required":["id","legal_name"],"properties":{"id":{"type":"string","format":"uuid"},"legal_name":{"type":"string"},"registered_address":{"type":"string","nullable":true}}},"bureau_configured":{"type":"boolean"},"suggested":{"$ref":"#/components/schemas/BusinessBureauSubject"},"checks":{"type":"array","items":{"$ref":"#/components/schemas/BusinessCreditCheck"}},"stands_days":{"type":"integer"}}},"BackofficeTrialBalance":{"type":"object","description":"routes/books.rs `trial_balance`, the platform’s specification","required":["accounts","totals","entries_read","cut_short"],"properties":{"to":{"type":"string","format":"date","nullable":true},"accounts":{"type":"array","items":{"type":"object","required":["id","code","name","kind","manual_entries","amounts"],"properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","enum":["asset","liability","equity","income","expense","other"]},"manual_entries":{"type":"boolean"},"amounts":{"type":"array","items":{"type":"object","required":["currency","debited_minor","credited_minor","balance_minor"],"properties":{"currency":{"type":"string"},"debited_minor":{"type":"integer","format":"int64"},"credited_minor":{"type":"integer","format":"int64"},"balance_minor":{"type":"integer","format":"int64"}}}}}}},"totals":{"type":"array","items":{"type":"object","required":["currency","debited_minor","credited_minor","agrees"],"properties":{"currency":{"type":"string"},"debited_minor":{"type":"integer","format":"int64"},"credited_minor":{"type":"integer","format":"int64"},"agrees":{"type":"boolean"}}}},"entries_read":{"type":"integer"},"cut_short":{"type":"boolean","description":"More than 10,000 entries; the rest were not read"}}},"BackofficeJournal":{"type":"object","description":"routes/books.rs `journal`, the platform’s specification","required":["entries","cut_short"],"properties":{"entries":{"type":"array","items":{"type":"object","required":["id","posted_at","lines"],"properties":{"id":{"type":"string"},"posted_at":{"type":"string","format":"date-time"},"correlation_id":{"type":"string","nullable":true},"lines":{"type":"array","items":{"type":"object","required":["gl","direction","currency"],"properties":{"gl":{"type":"string"},"direction":{"type":"string","enum":["debit","credit"]},"amount_minor":{"type":"integer","format":"int64","nullable":true},"currency":{"type":"string"}}}}}}},"cut_short":{"type":"boolean"}}},"BackofficeClosures":{"type":"object","description":"routes/books.rs `closures`, the platform’s specification","required":["closures"],"properties":{"closures":{"type":"array","items":{"type":"object","required":["id","closing_date"],"properties":{"id":{"type":"string"},"closing_date":{"type":"string","format":"date"},"comments":{"type":"string","nullable":true}}}}}},"BackofficeOverview":{"type":"object","description":"routes/books.rs `overview`, the platform’s specification","required":["customers","tills","approvals_waiting","tasks"],"properties":{"customers":{"type":"object","required":["total","by_state","by_branch"],"properties":{"total":{"type":"integer"},"by_state":{"type":"object","additionalProperties":{"type":"integer"}},"by_branch":{"type":"array","items":{"type":"object","required":["branch","customers","deposits"],"properties":{"branch":{"type":"string"},"customers":{"type":"integer"},"deposits":{"type":"object","description":"Minor units by currency","additionalProperties":{"type":"integer","format":"int64"}}}}}}},"tills":{"type":"array","items":{"type":"object","required":["branch","name","open_tills","cash_minor","closed_over_short_minor"],"properties":{"branch":{"type":"string"},"name":{"type":"string"},"open_tills":{"type":"integer"},"cash_minor":{"type":"integer","format":"int64"},"closed_over_short_minor":{"type":"integer","format":"int64"}}}},"approvals_waiting":{"type":"object","required":["total","by_kind"],"properties":{"total":{"type":"integer"},"by_kind":{"type":"object","additionalProperties":{"type":"integer"}}}},"tasks":{"type":"object","required":["open","overdue"],"properties":{"open":{"type":"integer"},"overdue":{"type":"integer"}}}}},"BackofficePortfolio":{"type":"object","description":"routes/portfolio.rs `portfolio`, the platform’s specification","required":["loans","stages","at_risk","credit_lines"],"properties":{"loans":{"type":"array","items":{"type":"object","required":["id","customer","stage","requested_minor","currency","term_months","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"customer":{"type":"string"},"stage":{"$ref":"#/components/schemas/LoanStage"},"requested_minor":{"type":"integer"},"currency":{"type":"string"},"term_months":{"type":"integer"},"purpose":{"type":"string","nullable":true},"owed_minor":{"type":"integer","nullable":true},"arrears_minor":{"type":"integer","nullable":true},"days_past_due":{"type":"integer","nullable":true},"created_at":{"type":"string","format":"date-time"}}}},"stages":{"type":"array","items":{"type":"object","required":["stage","count"],"properties":{"stage":{"$ref":"#/components/schemas/LoanStage"},"count":{"type":"integer"}}}},"at_risk":{"type":"object","required":["outstanding_minor","par1_minor","par30_minor","par90_minor"],"properties":{"outstanding_minor":{"type":"integer"},"par1_minor":{"type":"integer"},"par30_minor":{"type":"integer"},"par90_minor":{"type":"integer"},"par1_bps":{"type":"integer","nullable":true},"par30_bps":{"type":"integer","nullable":true},"par90_bps":{"type":"integer","nullable":true}}},"credit_lines":{"type":"object","properties":{"states":{"type":"object","additionalProperties":{"type":"integer"}},"open_limits_minor":{"type":"integer"}}}}},"LoanStage":{"type":"string","enum":["partial_application","pending_approval","approved","rejected","withdrawn","active","active_in_arrears","closed_obligations_met","written_off"]},"BackofficeLoan":{"type":"object","description":"routes/portfolio.rs `loan`, the platform’s specification","required":["id","customer","stage","state","requested_minor","currency","term_months","repayments","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"customer":{"type":"string"},"stage":{"$ref":"#/components/schemas/LoanStage"},"state":{"type":"string"},"requested_minor":{"type":"integer"},"currency":{"type":"string"},"term_months":{"type":"integer"},"purpose":{"type":"string","nullable":true},"declared_income_minor":{"type":"integer"},"declared_obligations_minor":{"type":"integer"},"decision":{"type":"object","nullable":true,"additionalProperties":true},"operator_decision":{"type":"object","nullable":true,"additionalProperties":true},"offer":{"type":"object","nullable":true,"additionalProperties":true},"disbursed_on":{"type":"string","format":"date","nullable":true},"instalments":{"type":"array","nullable":true,"items":{"type":"object","properties":{"number":{"type":"integer"},"due_on":{"type":"string","format":"date"},"due_minor":{"type":"integer"},"paid_minor":{"type":"integer"},"cleared_on":{"type":"string","format":"date","nullable":true}}}},"position":{"type":"object","nullable":true,"properties":{"owed_minor":{"type":"integer"},"received_minor":{"type":"integer"},"arrears_minor":{"type":"integer"},"days_past_due":{"type":"integer"},"settled":{"type":"boolean"}}},"repayments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"amount_minor":{"type":"integer"},"received_on":{"type":"string","format":"date"},"recorded_by":{"type":"string"},"reversed":{"type":"boolean"},"reversal_of":{"type":"string","format":"uuid","nullable":true}}}},"written_off":{"type":"object","nullable":true,"properties":{"owed_minor":{"type":"integer"},"reason":{"type":"string"},"at":{"type":"string","format":"date-time"}}},"created_at":{"type":"string","format":"date-time"}}},"BackofficeTask":{"type":"object","required":["id","title","assignee","state","created_by","created_at"],"properties":{"id":{"type":"string","format":"uuid"},"customer":{"type":"string","nullable":true},"title":{"type":"string"},"detail":{"type":"string","nullable":true},"due_on":{"type":"string","format":"date","nullable":true},"assignee":{"type":"string"},"state":{"type":"string","enum":["open","done"]},"created_by":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"done_by":{"type":"string","nullable":true},"done_at":{"type":"string","format":"date-time","nullable":true}}},"BackofficeBranch":{"type":"object","required":["id","code","name","city","currency"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"},"city":{"type":"string"},"currency":{"type":"string"}}},"BackofficeTill":{"type":"object","description":"routes/tills.rs `till_view`, the platform’s specification","required":["id","code","branch_id","teller","currency","state","opened_by","opened_at","opening_minor","expected_minor"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"branch_id":{"type":"string","format":"uuid"},"teller":{"type":"string"},"currency":{"type":"string"},"state":{"type":"string","enum":["open","closed"]},"opened_by":{"type":"string"},"opened_at":{"type":"string","format":"date-time"},"opening_minor":{"type":"integer"},"expected_minor":{"type":"integer","nullable":true,"description":"What the drawer should hold"},"closed_by":{"type":"string","nullable":true},"closed_at":{"type":"string","format":"date-time","nullable":true},"counted_minor":{"type":"integer","nullable":true},"difference_minor":{"type":"integer","nullable":true,"description":"Counted less expected: positive over, negative short"},"close_note":{"type":"string","nullable":true},"entries":{"type":"array","items":{"type":"object","required":["id","kind","amount_minor","authority","by","at"],"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["cash_in","cash_out","buy","sell"]},"amount_minor":{"type":"integer","description":"What the drawer gained"},"customer":{"type":"string","nullable":true},"account":{"type":"string","nullable":true},"authority":{"type":"string","enum":["teller","approval","vault"]},"approval_id":{"type":"string","format":"uuid","nullable":true},"core_ref":{"type":"string","nullable":true},"by":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}},"BackofficeTills":{"type":"object","required":["branch_id","tills","tellers"],"properties":{"branch_id":{"type":"string","format":"uuid"},"tills":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeTill"}},"tellers":{"type":"array","items":{"type":"object","required":["subject","label","has_open_till"],"properties":{"subject":{"type":"string"},"label":{"type":"string","nullable":true},"has_open_till":{"type":"boolean"}}}}}},"BackofficeRole":{"type":"object","required":["role","title","description","permissions","branch_scoped","posting_limit_minor","decides"],"properties":{"role":{"type":"string","enum":["teller","head_teller","branch_manager","service_agent","operations","credit_officer","credit_committee","compliance","accountant","supervisor","auditor","iam_admin"]},"title":{"type":"string"},"description":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"}},"branch_scoped":{"type":"boolean","description":"Sees its branch's customers only"},"posting_limit_minor":{"type":"integer","description":"Cash a teller posts on their own; 0 for a role that does not"},"decides":{"type":"array","description":"The authority matrix","items":{"type":"object","required":["domain","limit_minor"],"properties":{"domain":{"type":"string","enum":["money","lending","risk","onboarding","cards","teller","books","accounts","clients","settings","alerts","iam"]},"limit_minor":{"type":"integer","nullable":true,"description":"Null is without limit"}}}}}},"BackofficeMe":{"type":"object","description":"routes/iam.rs `me`, the platform’s specification","required":["subject","name","roles","permissions","posting_limit_minor","branches","all_branches"],"properties":{"subject":{"type":"string"},"name":{"type":"string","nullable":true},"roles":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeRole"}},"posting_limit_minor":{"type":"integer"},"branches":{"type":"array","items":{"type":"object","required":["id","code","name"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"}}}},"all_branches":{"type":"boolean","description":"Head office — sees every branch"},"permissions":{"type":"array","items":{"type":"string"},"description":"signin, customers.read, teller.act, tills.own, tills.manage, tills.close, accounts.manage, clients.manage, notes.write, tasks.work, cards.freeze, cards.manage, support.handle, sessions.end, onboarding.review, lending.manage, risk.review, money.ops, operations.run, approvals.read, approvals.decide, audit.read, iam.manage, alerts.work, alerts.rules.manage, read.all"}},"example":{"subject":"staff-teller","name":"Tomás Herrera","roles":[{"role":"teller","title":"Teller","description":"Serves customers at the desk","permissions":["signin","customers.read","teller.act","tills.own","cards.freeze","approvals.read"],"branch_scoped":true,"posting_limit_minor":1000000,"decides":[]}],"permissions":["signin","customers.read","teller.act","cards.freeze","approvals.read"],"posting_limit_minor":1000000,"branches":[{"id":"0199d000-0000-7000-8000-0000000000b1","code":"MEX-01","name":"Reforma"}],"all_branches":false}},"BackofficeIamStaff":{"type":"object","description":"routes/iam.rs `staff`, the platform’s specification","required":["staff","roles","waiting","branches"],"properties":{"staff":{"type":"array","items":{"type":"object","required":["subject","label","roles","branches"],"properties":{"subject":{"type":"string"},"label":{"type":"string","nullable":true},"roles":{"type":"array","items":{"type":"string"}},"branches":{"type":"array","items":{"type":"string"},"description":"Branch codes they work"}}}},"roles":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeRole"}},"branches":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"}}}},"waiting":{"type":"array","items":{"type":"object","properties":{"approval_id":{"type":"string","format":"uuid"},"proposed_by":{"type":"string"},"subject":{"type":"string"},"role":{"type":"string"},"grant":{"type":"boolean"},"reason":{"type":"string"}}}}}},"BackofficeCustomers":{"type":"object","description":"routes/customers.rs `find`, the platform’s specification","required":["customers","term"],"properties":{"term":{"type":"string"},"customers":{"type":"array","items":{"type":"object","required":["subject","name","client_id","matched_by"],"properties":{"subject":{"type":"string"},"name":{"type":"string","nullable":true},"client_id":{"type":"string","nullable":true,"description":"The core's client number"},"matched_by":{"type":"string","enum":["name","subject","client","account","card","all"]}}}}}},"BackofficeCustomer":{"type":"object","description":"routes/customers.rs `profile`, the platform’s specification","required":["subject","name","client_id","onboarding","identity","screening","accounts","credit_line","loans","cards","transfers","crypto","disputes","support","sessions","businesses","activity","teller","unavailable"],"properties":{"subject":{"type":"string"},"name":{"type":"string","nullable":true},"client_id":{"type":"string","nullable":true},"onboarding":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"declared_country":{"type":"string"},"declared_age_years":{"type":"integer"},"expected_monthly_minor":{"type":"integer"},"ineligible_reason":{"type":"string","nullable":true},"own_account":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}},"identity":{"type":"object","nullable":true,"description":"What the document said, unsealed for an operator","properties":{"document_type":{"type":"string"},"liveness_passed":{"type":"boolean"},"mismatches":{"type":"array","items":{"type":"string"}},"recorded_at":{"type":"string","format":"date-time"},"details":{"type":"object","nullable":true,"additionalProperties":true}}},"screening":{"type":"array","items":{"type":"object","properties":{"match_key":{"type":"string"},"verdict":{"type":"string"},"decided_by":{"type":"string"},"decided_at":{"type":"string","format":"date-time"},"note":{"type":"string"}}}},"accounts":{"type":"array","items":{"type":"object","required":["id","name","kind","currency","balance_minor"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string"},"currency":{"type":"string"},"balance_minor":{"type":"integer","nullable":true},"business":{"type":"string","nullable":true,"description":"The company it is held for"},"standing":{"type":"object","nullable":true,"description":"Spec 068: where it stands beyond its balance, as the core keeps it","properties":{"state":{"type":"string","enum":["active","closed","submitted","approved","other"]},"blocked":{"type":"boolean"},"debits_blocked":{"type":"boolean"},"credits_blocked":{"type":"boolean"},"dormant":{"type":"boolean"},"currency":{"type":"string"},"balance_minor":{"type":"integer","nullable":true},"available_minor":{"type":"integer","nullable":true},"on_hold_minor":{"type":"integer"},"holds":{"type":"array","items":{"type":"object","properties":{"hold_ref":{"type":"string"},"amount_minor":{"type":"integer"},"reason":{"type":"string","nullable":true},"on":{"type":"string","format":"date","nullable":true}}}}}}}}},"credit_line":{"type":"object","nullable":true,"properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string"},"limit_minor":{"type":"integer"},"asked_minor":{"type":"integer"},"currency":{"type":"string"},"annual_rate":{"type":"string"},"term_months":{"type":"integer"},"account_id":{"type":"string"},"refused_reason":{"type":"string","nullable":true},"closed_reason":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"loans":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string"},"requested_minor":{"type":"integer"},"currency":{"type":"string"},"term_months":{"type":"integer"},"purpose":{"type":"string","nullable":true},"core_loan_ref":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}}},"cards":{"type":"array","items":{"type":"object","required":["id","last_four","network","status","expires_on","currency","limits"],"properties":{"id":{"type":"string","format":"uuid"},"last_four":{"type":"string"},"network":{"type":"string"},"status":{"type":"string","enum":["requested","issued","active","frozen","cancelled","expired"]},"expires_on":{"type":"string","format":"date"},"currency":{"type":"string"},"account_ref":{"type":"string"},"limits":{"type":"object","properties":{"per_transaction":{"type":"integer","nullable":true},"daily":{"type":"integer","nullable":true},"monthly":{"type":"integer","nullable":true}}},"business_id":{"type":"string","format":"uuid","nullable":true}}}},"transfers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"from_account":{"type":"string"},"payee":{"type":"string"},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"state":{"type":"string"},"refused_reason":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}}}},"crypto":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"network":{"type":"string"},"token":{"type":"string"},"stage":{"type":"string"},"credited_minor":{"type":"integer","nullable":true},"settles_in":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}},"disputes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"amount_minor":{"type":"integer"},"currency":{"type":"string"},"reason":{"type":"string"},"stage":{"type":"string"},"outcome":{"type":"string","nullable":true},"raised_at":{"type":"string","format":"date-time"}}}},"support":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"topic":{"type":"string","nullable":true},"resolved":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}}},"sessions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"device":{"type":"string"},"os":{"type":"string"},"app_version":{"type":"string"},"began_at":{"type":"string","format":"date-time"},"last_used_at":{"type":"string","format":"date-time"}}}},"businesses":{"type":"array","items":{"type":"object","properties":{"business_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"role":{"type":"string"}}}},"activity":{"type":"array","description":"Approvals whose proposal names this customer, newest first","items":{"type":"object","required":["approval_id","kind","state","proposed_by","proposed_at"],"properties":{"approval_id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"state":{"type":"string"},"proposed_by":{"type":"string"},"decided_by":{"type":"string","nullable":true},"proposed_at":{"type":"string","format":"date-time"},"decided_at":{"type":"string","format":"date-time","nullable":true},"reason":{"type":"string","nullable":true}}}},"state":{"type":"object","nullable":true,"description":"Spec 068: active, inactive, exited or blacklisted, and why","properties":{"state":{"type":"string","enum":["active","inactive","exited","blacklisted"]},"reason":{"type":"string","nullable":true},"changed_at":{"type":"string","format":"date-time","nullable":true}}},"notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"author":{"type":"string"},"body":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}},"tasks":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeTask"}},"branch":{"type":"object","nullable":true,"description":"Their home branch","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"name":{"type":"string"}}},"teller":{"type":"object","required":["takes_cash"],"properties":{"takes_cash":{"type":"boolean","description":"Whether the bank has a till account for cash"}}},"unavailable":{"type":"array","items":{"type":"string"},"description":"The parts that could not be read this time"}}},"BackofficeCustomerMovements":{"type":"object","description":"routes/customers.rs `movements`, the platform’s specification","required":["account","movements"],"properties":{"account":{"type":"object","required":["id","name","currency","balance_minor"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"currency":{"type":"string"},"balance_minor":{"type":"integer","nullable":true}}},"movements":{"type":"array","items":{"type":"object","required":["id","posted_on","amount_minor","currency"],"properties":{"id":{"type":"string"},"posted_on":{"type":"string","format":"date"},"amount_minor":{"type":"integer","description":"Negative is money out of the account"},"currency":{"type":"string"},"reference":{"type":"string","nullable":true}}}}}},"BackofficeMoneyProposed":{"type":"object","description":"202 body of the proposing routes that return only the approval id: POST /backoffice/loans/repayments (lending::propose_repayment) and POST /backoffice/exchange/completions (exchange::propose_completion). Two separate json! literals of the same shape. Nothing has happened yet; the approval is `proposed` and a different person decides it at POST /approvals/{id}/decision.\n","required":["approval_id","state"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]}}},"BackofficeMoneyProposedNoted":{"type":"object","description":"202 body of POST /backoffice/screening/adjudications, /backoffice/transfers/{id}/resolution, /backoffice/funding/{id}/credit and /backoffice/cards/limits. Four separate json! literals of the same shape; only the fixed `note` sentence differs per route.\n","required":["approval_id","state","note"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]},"note":{"type":"string","description":"One fixed sentence per route saying nothing has changed yet (prose, not an enum)."}}},"BackofficeMoneyReversalProposed":{"type":"object","description":"202 body of POST /backoffice/loans/{id}/repayments/{repayment_id}/reverse.","required":["approval_id","state","reversal_of"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]},"reversal_of":{"type":"string","format":"uuid","description":"The repayment row the reversal will point at (the path's repayment_id)."}}},"BackofficeMoneyRebalanceProposed":{"type":"object","description":"202 body of POST /backoffice/investments/rebalances.","required":["approval_id","state","orders","note"],"properties":{"approval_id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["proposed"]},"orders":{"type":"integer","minimum":1,"description":"A COUNT (planned.orders.len()), not the orders themselves. The orders are on the approval's subject. At least 1 in practice, since a plan within its band is refused (409).\n"},"note":{"type":"string","description":"Fixed sentence (\"no orders have been placed; a different person has to agree\")."}}},"BackofficeRiskScreeningListKind":{"type":"string","enum":["sanctions","pep","adverse_media","internal_deny_list","law_enforcement"],"description":"screening::ListKind::label(). A source kind the route does not recognise is stored as sanctions."},"BackofficeRiskScreeningBand":{"type":"string","enum":["weak","possible","strong"],"description":"screening::MatchBand::label()."},"BackofficeRiskScreeningReason":{"type":"object","description":"screening::MatchReason. Codes the module writes: name_similarity, dob_exact, yob_same, dob_conflict, dob_conflict_discounted, dob_unknown, country_same, identifier_exact (listed for reading; kept a string because the list lives in modules/screening/src/matching.rs).\n","required":["code","detail"],"properties":{"code":{"type":"string"},"detail":{"type":"string"}}},"BackofficeRiskScreeningAlert":{"type":"object","description":"routes/screening.rs `alert_view(&StoredMatch)`. An open alert: a queued match on the subject's latest run with no confirmation and no clearance against its current revision.\n","required":["key","list","kind","entry_id","revision","matched_name","score","band","identifier_match","previously_cleared","reasons"],"properties":{"key":{"type":"string","description":"`subject|list|entry_id`"},"list":{"type":"string"},"kind":{"$ref":"#/components/schemas/BackofficeRiskScreeningListKind"},"entry_id":{"type":"string"},"revision":{"type":"string","description":"The entry revision compared; an adjudication must send exactly this."},"matched_name":{"type":"string"},"score":{"type":"number","format":"double","minimum":0,"maximum":1},"band":{"$ref":"#/components/schemas/BackofficeRiskScreeningBand"},"identifier_match":{"type":"boolean"},"previously_cleared":{"type":"string","nullable":true,"description":"The older revision a clearance was given against, when a clearance lapsed; else null."},"reasons":{"type":"array","description":"The stored jsonb, written by the route as [{code, detail}]. Never empty.","items":{"$ref":"#/components/schemas/BackofficeRiskScreeningReason"}},"source":{"type":"string","enum":["lists","service"],"description":"Spec 073. `lists` for the bank's own, matched locally; `service` for a provider's hit, whose reasons are code `provider` (the provider's words) or `provider_unexplained`.\n"},"provider":{"type":"string","nullable":true,"description":"The provider that reported a `service` hit; null for the bank's own lists."},"provider_reference":{"type":"string","nullable":true,"description":"The provider's own reference for the answer, for raising a query with it."},"source_ref":{"type":"string","nullable":true,"description":"Spec 077. The list's own reference for the entry, OFAC's `ent_num`; null where the list has none."}}},"BackofficeAlert":{"type":"object","description":"Spec 077. One alert, as the queue lists it.","required":["id","code","rule_version","severity","message","role","weight","subject","customer","group_key","state","occurrences","first_at","last_at","provenance","delivery"],"properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string","description":"The rule's id"},"rule_version":{"type":"integer","minimum":1},"severity":{"type":"string","enum":["info","low","medium","high","critical"]},"message":{"type":"string","description":"The rule's message with the event's declared fields"},"role":{"type":"string"},"weight":{"type":"integer","minimum":1},"subject":{"type":"object","required":["kind","ref","href"],"properties":{"kind":{"type":"string","description":"transfer, screening_run, fraud_assessment, loan, dispute"},"ref":{"type":"string"},"href":{"type":"string","nullable":true,"description":"The back office page of the object","where it has one":null}}},"customer":{"type":"string","nullable":true,"description":"The customer","for a rule grouped by customer":null},"group_key":{"type":"string"},"state":{"type":"string","enum":["open","acknowledged","resolved","cleared"]},"occurrences":{"type":"integer","minimum":1},"first_at":{"type":"string","format":"date-time"},"last_at":{"type":"string","format":"date-time"},"provenance":{"type":"string","enum":["live","synthetic","unavailable"]},"delivery":{"type":"string","enum":["none","pending","sent","undelivered","unavailable"]}}},"BackofficeAlertDetail":{"allOf":[{"$ref":"#/components/schemas/BackofficeAlert"},{"type":"object","required":["event","occurrences_at","deliveries","acknowledged_by","acknowledged_at","resolved_by","resolved_at","resolution_note","cleared_by_event"],"properties":{"event":{"type":"object","nullable":true,"required":["id","kind","occurred_at","fields"],"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"occurred_at":{"type":"string","format":"date-time","nullable":true},"fields":{"type":"object","additionalProperties":true,"description":"Only the catalogue's declared fields"}}},"occurrences_at":{"type":"array","items":{"type":"object","required":["event_id","at","object"],"properties":{"event_id":{"type":"string","format":"uuid"},"at":{"type":"string","format":"date-time"},"object":{"type":"object","required":["kind","ref","href"],"properties":{"kind":{"type":"string"},"ref":{"type":"string"},"href":{"type":"string","nullable":true}}}}}},"deliveries":{"type":"array","items":{"type":"object","required":["recipient","outcome","reason","tries"],"properties":{"recipient":{"type":"string"},"outcome":{"type":"string","enum":["pending","sent","failed","unavailable"]},"reason":{"type":"string","nullable":true},"tries":{"type":"integer","minimum":0}}}},"acknowledged_by":{"type":"string","nullable":true},"acknowledged_at":{"type":"string","format":"date-time","nullable":true},"resolved_by":{"type":"string","nullable":true},"resolved_at":{"type":"string","format":"date-time","nullable":true},"resolution_note":{"type":"string","nullable":true},"cleared_by_event":{"type":"string","format":"uuid","nullable":true},"raised_at":{"type":"string","format":"date-time","description":"When the evaluator raised it; SC-7704 measures the gap from the event"}}}]},"AlertCondition":{"type":"object","required":["event","field","op","threshold"],"properties":{"event":{"type":"string"},"field":{"type":"string"},"op":{"type":"string","enum":["<","<=",">",">=","=","!=","in"]},"threshold":{"description":"A number, a word, a list for in, or {amount_minor, currency} for money"}}},"AlertRule":{"type":"object","description":"Spec 077, research R5. One rule, in the catalogue's own shape.","required":["id","name","event","field","op","threshold","severity","role","message","cooldown_minutes","weight","group_by"],"properties":{"id":{"type":"string","description":"Lower-case words joined by hyphens"},"name":{"type":"string"},"event":{"type":"string"},"field":{"type":"string"},"op":{"type":"string","enum":["<","<=",">",">=","=","!=","in"]},"threshold":{"description":"A number, a word, a list for in, or {amount_minor, currency} for money"},"severity":{"type":"string","enum":["info","low","medium","high","critical"]},"role":{"type":"string"},"message":{"type":"string"},"cooldown_minutes":{"type":"integer","minimum":0,"maximum":10080},"weight":{"type":"integer","minimum":1,"maximum":10},"group_by":{"type":"string","enum":["customer","object"]},"email_from":{"type":"string","nullable":true,"enum":["info","low","medium","high","critical"]},"clears_on":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"}],"nullable":true}}},"BackofficeAlertRules":{"type":"object","required":["rules","mailboxes"],"properties":{"rules":{"type":"array","items":{"type":"object","required":["rule","version","state","fired","not_evaluated","cleared","change_waiting","created_by","created_at"],"properties":{"rule":{"$ref":"#/components/schemas/AlertRule"},"version":{"type":"integer","minimum":1},"state":{"type":"string","enum":["active","off"]},"fired":{"type":"integer","minimum":0},"not_evaluated":{"type":"integer","minimum":0},"cleared":{"type":"integer","minimum":0},"change_waiting":{"type":"boolean"},"created_by":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}},"mailboxes":{"type":"array","items":{"type":"object","required":["role","address"],"properties":{"role":{"type":"string"},"address":{"type":"string"}}}}}},"BackofficeScreeningList":{"type":"object","description":"Spec 077. One declared list, as screening_lists' load report gives it.","required":["list","kind","format","version","provenance","entries","aliases","state","skipped_rows","skipped_total","unmapped_countries","missing_files","error","read_at"],"properties":{"list":{"type":"string"},"kind":{"type":"string"},"format":{"type":"string","enum":["json","ofac_csv"]},"version":{"type":"string","nullable":true,"description":"Null while no version is in use"},"provenance":{"type":"string","nullable":true,"enum":["live","synthetic","unavailable"]},"entries":{"type":"integer","minimum":0},"aliases":{"type":"integer","minimum":0},"state":{"type":"string","enum":["current","partial","stale","failed"]},"skipped_rows":{"type":"array","items":{"type":"string"},"description":"file:line of the first 20 rows skipped"},"skipped_total":{"type":"integer","minimum":0},"unmapped_countries":{"type":"array","items":{"type":"string"}},"missing_files":{"type":"array","items":{"type":"string"}},"error":{"type":"string","nullable":true,"description":"Why the last read failed"},"read_at":{"type":"string","format":"date-time","nullable":true}}},"BackofficeRiskScreeningQueue":{"type":"object","description":"GET /backoffice/screening. Open alerts, score descending then key, at most 200.","required":["count","alerts"],"properties":{"count":{"type":"integer","minimum":0},"alerts":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeRiskScreeningAlert"}}}},"BackofficeRiskScreeningDecision":{"type":"object","description":"One row of screening_decisions, as GET /backoffice/screening/alert lists it.","required":["verdict","entry_revision","decided_by","decided_at","note"],"properties":{"verdict":{"type":"string","enum":["cleared","confirmed"]},"entry_revision":{"type":"string"},"decided_by":{"type":"string"},"decided_at":{"type":"string","format":"date-time"},"note":{"type":"string"}}},"BackofficeRiskScreeningAlertDetail":{"type":"object","description":"GET /backoffice/screening/alert?key=. ALWAYS 200: an unknown or already-decided key answers {key, alert: null, history: [...]} (history may be empty); the handler never returns 404.\n","required":["key","alert","history"],"properties":{"key":{"type":"string","description":"The query key echoed back."},"alert":{"allOf":[{"$ref":"#/components/schemas/BackofficeRiskScreeningAlert"}],"nullable":true,"description":"The alert while it is open; null once decided, or when the key never existed. Looked up in the first 500 open alerts only.\n"},"history":{"type":"array","description":"Every decision ever recorded against this key, oldest decided_at first.","items":{"$ref":"#/components/schemas/BackofficeRiskScreeningDecision"}},"run":{"type":"object","nullable":true,"description":"Spec 073. The run that raised the open alert: `id`, `screened_at`, `list_versions` (the bank's own), `service_state` (`not_asked`, `answered`, `unavailable`, `closed_local_only`), `service_provider`, and `service_list_versions`, null when the provider did not say which lists it used. Spec 077 adds `narrowing` (`narrowed`, `full`), `narrowing_reason` (`too_common`, `too_short`, `no_strong_token`, or null), `missing_lists` and `lists_state` (`complete`, `waiting`, `closed_loaded_only`). Null when there is no open alert.\n"},"proposal":{"allOf":[{"$ref":"#/components/schemas/Proposal"}],"nullable":true,"description":"What automated judgement proposes about the open alert — the rule-based proposer until an AI plane exists. Asked once per alert and entry revision, and recorded before it is shown; null when there is no open alert or the proposal could not be recorded. Never a decision: the officer's adjudication still goes through a second person. Spec 073 adds `model_version` (null for the rules) and `trained_on`: `synthetic` for the image's model, or `{bank_decisions, synthetic, synthetic_weight_share}` for the bank's own.\n"}}},"BackofficeRiskScreeningMatch":{"type":"object","description":"One screening::Match as POST /backoffice/screening/rescreen renders it.","required":["key","list","kind","entry_id","revision","matched_name","score","band","identifier_match","previously_cleared","reasons"],"properties":{"key":{"type":"string","description":"`subject|list|entry_id`"},"list":{"type":"string"},"kind":{"$ref":"#/components/schemas/BackofficeRiskScreeningListKind"},"entry_id":{"type":"string"},"revision":{"type":"string"},"matched_name":{"type":"string"},"score":{"type":"number","format":"double","minimum":0,"maximum":1},"band":{"$ref":"#/components/schemas/BackofficeRiskScreeningBand"},"identifier_match":{"type":"boolean"},"previously_cleared":{"type":"string","nullable":true},"reasons":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeRiskScreeningReason"}}}},"BackofficeRiskScreeningRun":{"type":"object","description":"200 body of POST /backoffice/screening/rescreen: the screening::Run just filed. Includes matches that were neither queued nor suppressed (everything at or above the recording threshold), strongest first.\n","required":["subject_reference","screened_at","outcome","requires_enhanced_diligence","list_versions","queued","suppressed","matches"],"properties":{"subject_reference":{"type":"string"},"screened_at":{"type":"string","format":"date-time"},"outcome":{"type":"string","enum":["clear","pending","blocked"],"description":"screening::RunOutcome::label()."},"requires_enhanced_diligence":{"type":"boolean"},"list_versions":{"type":"array","description":"Vec<(String, String)> serialised by serde as 2-element arrays: [[list, version],...], NOT objects.\n","items":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"string"}}},"queued":{"type":"array","description":"Match keys a person has to look at (they become alerts).","items":{"type":"string"}},"suppressed":{"type":"array","description":"Match keys that would have queued but were already cleared against this revision.","items":{"type":"string"}},"matches":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeRiskScreeningMatch"}}}},"BackofficeRiskHeldTransfer":{"type":"object","required":["id","customer","amount_minor","currency","payee","held_since","assessment_id"],"properties":{"id":{"type":"string","format":"uuid"},"customer":{"type":"string","description":"The customer's token subject (transfer_intents.subject)."},"amount_minor":{"type":"integer","format":"int64"},"currency":{"type":"string"},"payee":{"type":"string","description":"The payee's display name (payee_display), not an object."},"held_since":{"type":"string","format":"date-time","nullable":true,"description":"authorized_at (Option in the store; set in practice for a held intent)."},"assessment_id":{"type":"string","format":"uuid","nullable":true,"description":"The fraud assessment that held it; GET /backoffice/fraud/{id} reads it."}}},"BackofficeRiskHeldTransferQueue":{"type":"object","description":"GET /backoffice/transfers. Intents in state `held`, ordered by authorized_at then created_at (longest waiting first), at most 200.\n","required":["count","waiting_longest_since","transfers"],"properties":{"count":{"type":"integer","minimum":0},"waiting_longest_since":{"type":"string","format":"date-time","nullable":true,"description":"authorized_at of the first row; null when the queue is empty."},"transfers":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeRiskHeldTransfer"}}}},"BackofficeMoneyHoldsExpired":{"type":"object","description":"200 body of POST /backoffice/cards/holds/expire. The card_holds rows of this tenant whose placed_on is more than 7 days before today are DELETED (not marked).\n","required":["released","released_minor","older_than_days"],"properties":{"released":{"type":"integer","format":"int64","minimum":0,"description":"Number of holds removed."},"released_minor":{"type":"integer","format":"int64","minimum":0,"description":"SUM(amount_minor) of the removed holds, 0 when none. Summed across all cards and currencies without regard to currency.\n"},"older_than_days":{"type":"integer","enum":[7],"description":"The fixed MAX_AGE_DAYS constant."}}},"BackofficeMoneyExchangeQuote":{"description":"One item of GET /backoffice/exchange: routes/exchange.rs `view(&StoredQuote)` (the SAME fn that builds ExchangeQuoteView for GET /exchange/quotes) with one extra key, `subject`.\n","allOf":[{"$ref":"#/components/schemas/ExchangeQuoteView"},{"type":"object","required":["subject"],"properties":{"subject":{"type":"string","description":"The customer's token subject; only the back office sees it."}}}]},"BackofficeMoneyExchangeQueue":{"type":"object","description":"GET /backoffice/exchange. Tenant-wide quotes, newest created_at first, at most 100.","required":["quotes"],"properties":{"quotes":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeMoneyExchangeQuote"}}}},"BackofficeMoneyInvestmentAccountError":{"type":"object","description":"What GET /backoffice/investments lists in place of an account account_view() cannot render (a retired model, or a drift/plan error), instead of omitting it.\n","required":["id","model","error"],"properties":{"id":{"type":"string","format":"uuid"},"model":{"type":"string","description":"The stored model_name as-is (NOT limited to cautious|balanced|adventurous here)."},"error":{"type":"string","description":"e.g. \"this account is on a model we no longer offer\", or the module's error text."}}},"BackofficeMoneyInvestmentQueue":{"type":"object","description":"GET /backoffice/investments. Tenant's accounts, newest created_at first, at most 200.","required":["count","accounts"],"properties":{"count":{"type":"integer","minimum":0,"description":"Equals accounts.length (errors included)."},"accounts":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/InvestmentAccountView"},{"$ref":"#/components/schemas/BackofficeMoneyInvestmentAccountError"}]}}}},"BackofficeMoneyDisputeEvidence":{"type":"object","description":"One disputes evidence row, as the customer wrote it.","required":["kind","reference","supplied_at"],"properties":{"kind":{"type":"string","enum":["order_detail","cancellation_proof","agreed_amount","merchant_contacted"],"description":"Only written after Evidence::from_label succeeds, so always one of these."},"reference":{"type":"string","description":"The customer's own text (trimmed, 1..2000 chars when supplied)."},"supplied_at":{"type":"string","format":"date-time"}}},"BackofficeMoneyDisputeCase":{"type":"object","description":"routes/disputes.rs `operator_view()` for a row this build can read: customer_view() (the Rust value behind DisputeCaseView) with `your_moves` REMOVED and customer, evidence, provisional_approval_id, settlement_approval_id and moves added. Not an allOf of DisputeCaseView because that schema requires `your_moves`, which this view never carries.\n","required":["id","payment_kind","payment_ref","amount_minor","currency","reason","reason_prompt","stage","next_step","deadline","outstanding","provisional_credit_proposed","outcome","raised_at","customer","evidence","provisional_approval_id","settlement_approval_id","moves"],"properties":{"id":{"type":"string","format":"uuid"},"payment_kind":{"type":"string","enum":["card","transfer"]},"payment_ref":{"type":"string","description":"Card authorisation id, or transfer intent id (uuid)."},"amount_minor":{"type":"integer","format":"int64"},"currency":{"type":"string"},"reason":{"type":"string","enum":["not_recognised","not_received","not_as_described","cancelled","duplicate","wrong_amount"]},"reason_prompt":{"type":"string"},"stage":{"type":"string","enum":["raised","evidence_required","with_the_merchant","decided","withdrawn"],"description":"In the queue only raised|evidence_required|with_the_merchant. After a `decide` move it is `decided`; after `send_to_merchant`, `with_the_merchant`.\n"},"next_step":{"type":"string"},"deadline":{"type":"string","format":"date-time","nullable":true,"description":"raised_at + 30 days, only while with_the_merchant."},"outstanding":{"type":"array","items":{"$ref":"#/components/schemas/DisputeEvidenceAsk"}},"provisional_credit_proposed":{"type":"boolean"},"outcome":{"type":"string","nullable":true,"enum":["upheld","declined",null]},"raised_at":{"type":"string","format":"date-time"},"customer":{"type":"string","description":"The customer's token subject."},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeMoneyDisputeEvidence"}},"provisional_approval_id":{"type":"string","format":"uuid","nullable":true,"description":"The `dispute_refund` approval for the provisional credit, when one was raised."},"settlement_approval_id":{"type":"string","format":"uuid","nullable":true,"description":"The `dispute_refund` approval raised by a decision (refund on upheld, reverse_provisional on declined); null when the decision moves no money or the case is undecided.\n"},"moves":{"type":"array","description":"moves_for(stage, Operator): raised -> [send_to_merchant, decide]; evidence_required and with_the_merchant -> [decide]; decided and withdrawn -> [].\n","items":{"type":"string","enum":["send_to_merchant","decide"]}}}},"BackofficeMoneyDisputeUnreadable":{"type":"object","description":"operator_view() for a stored row whose reason or stage this build does not know: customer_view()'s unreadable stub plus the operator keys. `moves` is [] when the stage itself is unknown, else the stage's operator moves.\n","required":["id","stage","unreadable","customer","evidence","provisional_approval_id","settlement_approval_id","moves"],"properties":{"id":{"type":"string","format":"uuid"},"stage":{"type":"string","description":"The stored stage text."},"unreadable":{"type":"boolean","enum":[true]},"customer":{"type":"string"},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/BackofficeMoneyDisputeEvidence"}},"provisional_approval_id":{"type":"string","format":"uuid","nullable":true},"settlement_approval_id":{"type":"string","format":"uuid","nullable":true},"moves":{"type":"array","items":{"type":"string","enum":["send_to_merchant","decide"]}}}},"BackofficeMoneyDisputeQueue":{"type":"object","description":"GET /backoffice/disputes. Cases with stage NOT IN (decided, withdrawn), oldest raised_at first, at most 200.\n","required":["count","disputes"],"properties":{"count":{"type":"integer","minimum":0},"disputes":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/BackofficeMoneyDisputeCase"},{"$ref":"#/components/schemas/BackofficeMoneyDisputeUnreadable"}]}}}}}}}