{
  "openapi": "3.1.0",
  "info": {
    "title": "Futa Merchant API",
    "version": "0.60.16",
    "description": "Local synthetic merchant payments and full/partial refunds with scoped keys, durable issuer outcomes, signed webhooks and staff approvals. No live rails or production release claim. Local merchant teams use verified MFA identities and durable least-privilege membership; no production enrollment or email delivery. Every authenticated /v1 response carries X-Request-Id, the server request ID its request log entry keeps. Every refusal answers the Error body with a catalogued code: a method no operation answers on a covered route 405 METHOD_NOT_ALLOWED (with Allow), a covered prefix without a route 404 ROUTE_NOT_FOUND, a missing parameter 400 INVALID_REQUEST, an unsupported request media type 415 UNSUPPORTED_MEDIA_TYPE, an Accept the route cannot produce 406 NOT_ACCEPTABLE, another refusal of the framework its own 4xx status (INVALID_REQUEST unless named here), a timed-out asynchronous answer 503 SERVICE_UNAVAILABLE, and only an unexpected failure 500 INTERNAL_ERROR (logged, never detailed). A POST body its operation does not bound otherwise is read within 16 KiB, chunked or not, and a longer one is refused before it is read with 413 REQUEST_TOO_LARGE; payment creation and cancellation, the phone searches and phone verification bound their own bodies and answer 400 INVALID_REQUEST past them. A payout file is read as it streams in, never held whole, within 16 MiB: 413 REQUEST_TOO_LARGE once its Content-Length or its 16,777,217th byte says it is longer. A client that went away is answered nothing. On /v1 a route no scope rule covers is 403 INSUFFICIENT_SCOPE. A request the servlet container answers itself (a firewall rejection, a failure before routing) carries only {timestamp, status, error, path}; there is no HTML error page. Payouts (0.59.0: the payout balance, single payouts, payout batches, name enquiries, thresholds, the low-balance alert, the payout webhooks and the key scopes payouts:read, payouts:write and balance:read) are served since 0.59.7 (the sandbox top-up since 0.59.8, the upload of a file since 0.59.14, a batch's report and result files since 0.59.15, the name enquiries since 0.59.16), relayed to switch-core closed and bounded. Payout and batch events are delivered since 0.59.17, as the payout-event and payout-batch-event webhooks state. The roles hold their payout permissions (TeamRole, 0.59.6) and a key may hold a payout scope. Invoices (0.60.0: invoice batches and their invoices through the API and the portal, a file's upload, mapping and validation, discarding, reminders, « Renvoyer », the results workbook and the QR sheet, the sandbox reminder run, the journal's INVOICES category and the key scopes invoices:read and invoices:write) are served before a send since 0.60.6 (a batch's creation, its file's upload, mapping and validation, its chunks, its discarding, the reads of batches and invoices, and the journal's INVOICES category), and the send since 0.60.8 (« Créer N factures » on /v1 and the portal, its link batch, its first messages, the reads after it and the batch sent journalled), and reminders since 0.60.12 (the schedules, links replaced before messages, held messages released, « Relancer les non payées » and « Renvoyer » on the portal, the sandbox reminder run on /v1, and their acts journalled), and the results workbook and the QR sheet since 0.60.14 (« Exporter XLSX » and « QR imprimables (PDF) » on the portal, each written by a job whose progress InvoiceFileJob gives). The roles hold their invoice permissions (TeamRole, 0.60.5) and an Owner or an Admin may give a key an invoice scope. No webhook event is added: each paid invoice's payment raises payment.approved."
  },
  "servers": [
    {
      "url": "https://api.sandbox.futa-app.com",
      "description": "Hosted sandbox, synthetic money only"
    },
    {
      "url": "http://127.0.0.1:18130",
      "description": "Loopback-only sandbox; production requires HTTPS."
    }
  ],
  "paths": {
    "/v1/payments": {
      "post": {
        "operationId": "createPayment",
        "summary": "Create a hosted or direct idempotent sandbox payment",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Retained at least 72 hours. Scope: authenticated merchant and create operation."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePayment"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment representation. Creation retries return the original representation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Current key lacks payments:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Requested branch is not owned by this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT for a different request/mode, a different embedding included; METHOD_UNAVAILABLE, PARTICIPANT_CAP_EXCEEDED, INTENT_EXPIRED, DIRECT_PAYMENT_NOT_ADMITTED or BRANCH_ARCHIVED for a rejected new direct admission. No new payment or reservation is retained on rejection. MERCHANT_LIMIT_EXCEEDED (above the merchant's per-payment limit; for a direct payment, its day or month in Kinshasa time) or PARTICIPANT_LIMIT_EXCEEDED (a direct payment above the participant's maximum per transaction).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Current key rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE or INVALID_CREATION_RESPONSE; outcome may be unknown. Retry the exact body and original key; never create a replacement key merely because the reply was lost.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:write. JSON body is limited to 16,384 UTF-8 bytes. Reject duplicate/unknown fields, coerced values, unpaired direct selectors, queries, caller X-Futa-* authority headers and encoded or chunked bodies. Hosted and direct requests share the merchant create idempotency namespace: changed terms or mode conflict. Retry ambiguous transport outcomes with the identical body/key. Replays return the original creation receipt, even after final outcome or expiry; GET the payment for current state. Each replay checks current credentials, scope and quota. Direct admission commits creation, reservation, event, workflow intent and actor audit atomically, or rolls back all effects. Provider authorization runs after commit; 201 is not approval. The prefix sandbox-run- (any case) is reserved for the developer console's sandbox runs: an Idempotency-Key or merchant_reference starting with it returns 400 INVALID_REQUEST."
      },
      "get": {
        "operationId": "listPayments",
        "summary": "List merchant payments",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "reference",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 1,
              "maximum": 9000000000000
            }
          },
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "USD",
                "CDF"
              ]
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "CREATED",
                "PENDING_APPROVAL",
                "UNCERTAIN",
                "APPROVED",
                "DECLINED",
                "EXPIRED"
              ]
            }
          },
          {
            "name": "method",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "simulator",
                "synthetic-bank",
                "unselected"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "Africa/Kinshasa",
                "Africa/Lubumbashi"
              ]
            }
          },
          {
            "name": "branch_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(unassigned|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"
            },
            "description": "Filter an immutable branch ID, including archived branches; unassigned selects payments without a branch. Omit for all merchant payments."
          }
        ],
        "responses": {
          "200": {
            "description": "Newest merchant-owned payments, up to 25 per page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Payment not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:read. Returns 25 newest payments, cursor-scoped to the authenticated merchant. A foreign or unknown cursor returns 404. No checkout capabilities or refund history are included. Tenant-scoped literal case-insensitive substring reference search and exact minor-unit amount, state, method, currency and half-open [from,to) creation-date filters. Paired bounds must be within 366 days. Cursor ownership is checked. Phone/account tokens are not searched or returned."
      }
    },
    "/v1/payments/{id}": {
      "get": {
        "operationId": "getPayment",
        "summary": "Read authoritative payment status",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The payment as it stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Payment"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Payment not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A conflict switch-core answered, relayed as it is; this read takes no key, so it is never a key conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/methods": {
      "get": {
        "operationId": "listMethods",
        "summary": "List sandbox payment methods",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "currency",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "USD",
                "CDF"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Simulated methods only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MethodList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid currency.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Suspended routes are omitted; an unavailable switch returns an error rather than a fabricated available method."
      }
    },
    "/v1/keys": {
      "get": {
        "operationId": "listKeys",
        "summary": "List key metadata without secrets",
        "tags": [
          "Keys"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Required merchant permission: keys:manage."
      },
      "post": {
        "operationId": "createKey",
        "summary": "Create a restricted test key",
        "tags": [
          "Keys"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedKey"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyRequest"
              }
            }
          }
        },
        "description": "Requested permissions must be a subset of the caller permissions, without keys:manage. A key never grants keys:manage: requesting it, or rotating a key that holds it, returns 403 SCOPE_ESCALATION. A payout scope (payouts:read, payouts:write, balance:read) is given only by an Owner's or an Admin's delegation: the sandbox bootstrap key and staff-provisioned keys hold none, so keys:manage alone never gives one, and a key gives one only when it holds it from such a delegation, the key it makes living by the same; otherwise 403 SCOPE_ESCALATION. Only a credential provisioned outside a key (the sandbox bootstrap key or staff maker-checker issuance) can manage keys here; people manage keys through the portal routes, which require a recent sign-in. Secret is revealed once; if the response is lost, list and revoke the unused key before retrying. Required merchant permission: keys:manage."
      }
    },
    "/v1/keys/{id}/rotate": {
      "post": {
        "operationId": "rotateKey",
        "summary": "Atomically replace and revoke a test key",
        "tags": [
          "Keys"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedKey"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "description": "Same permissions as predecessor; no privilege escalation. A key never grants keys:manage: requesting it, or rotating a key that holds it, returns 403 SCOPE_ESCALATION. A key holding a payout scope is rotated only by a caller that holds it too, the replacement living by the predecessor's delegation. Only a credential provisioned outside a key (the sandbox bootstrap key or staff maker-checker issuance) can manage keys here; people manage keys through the portal routes, which require a recent sign-in. Old key immediately invalid for subsequent requests. Repeating rotation returns 409. Save the replacement secret once. Required merchant permission: keys:manage."
      }
    },
    "/v1/keys/{id}/revoke": {
      "post": {
        "operationId": "revokeKey",
        "summary": "Revoke a merchant-owned key",
        "tags": [
          "Keys"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Revoked"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "description": "Repeated revocation succeeds without additional audit events. Required merchant permission: keys:manage."
      }
    },
    "/v1/webhooks/endpoint": {
      "get": {
        "operationId": "getWebhook",
        "summary": "Read merchant webhook configuration",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookConfiguration"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Required merchant permission: webhooks:read."
      },
      "post": {
        "operationId": "registerWebhook",
        "summary": "Register the merchant webhook endpoint",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedWebhook"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookRequest"
              }
            }
          }
        },
        "description": "One endpoint per test merchant, on one of the sandbox's loopback destinations (WebhookRequest.url); any other answers 400 INVALID_REQUEST. Existing endpoint returns 409. Signing secret is revealed once. Required merchant permission: webhooks:manage. A new endpoint is sent, by default, every payment event, refund.queued, refund.processing, refund.uncertain, refund.completed and refund.failed, and since 0.59.17 payout.completed, payout.failed and payout_batch.completed; any other event, obligation.collected and a payout's or a batch's approval and validation events included, is chosen through the endpoint's configuration, and an existing endpoint keeps what it chose."
      }
    },
    "/v1/webhooks/endpoint/rotate": {
      "post": {
        "operationId": "rotateWebhook",
        "summary": "Rotate merchant webhook signing secret",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssuedWebhook"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "The previous secret overlaps for one hour. Futa-Signature may contain multiple v1 signatures; accept any valid signature for the raw body. A subsequent rotation replaces the previous overlap. Required merchant permission: webhooks:manage."
      }
    },
    "/v1/webhooks/deliveries": {
      "get": {
        "operationId": "listDeliveries",
        "summary": "List merchant deliveries a page at a time",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request; INVALID_CURSOR for a cursor this list did not seal",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Newest first (creation or holding time, then event ID), a page at a time for as long as the deliveries table retains them (no retention period of its own), with each event held for this merchant in the last 30 days as a HELD entry that carries no body; next_cursor is null on the last page. Without cursor and limit the first page is the latest 100, as before. Required merchant permission: webhooks:read.",
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "description": "Opaque next_cursor from the previous page. It is sealed to this list and merchant: another merchant's, altered or invented cursor returns 400 INVALID_CURSOR.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{40,200}$"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "description": "Deliveries per page, newest first.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            }
          }
        ]
      }
    },
    "/v1/webhooks/deliveries/{id}/attempts": {
      "get": {
        "operationId": "listDeliveryAttempts",
        "summary": "Read attempts for a merchant-owned event",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryAttemptList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "description": "Every recorded attempt in order, with the time it was recorded and the HTTP status the destination answered. Status 0 is a transport error: no HTTP answer (connection refused, reset or timed out). No duration is recorded, so latency is not available. Required merchant permission: webhooks:read."
      }
    },
    "/v1/webhooks/deliveries/{id}/replay": {
      "post": {
        "operationId": "replayDelivery",
        "summary": "Replay an event without changing its identity",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Accepted"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            }
          }
        ],
        "description": "Idempotent per merchant and key. One new replay per event per minute. Reopens the 72-hour retry window and preserves delivery history. Receivers must deduplicate event IDs. Required merchant permission: webhooks:manage. The event must belong to the currently selected subscription or returns 409 EVENT_NOT_SUBSCRIBED. Explicit replay may reselect a previously filtered event. It retains the event ID."
      }
    },
    "/v1/payments/{id}/refunds": {
      "post": {
        "operationId": "createRefund",
        "summary": "Request a full or partial refund within 180 days",
        "tags": [
          "Refunds"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to merchant and refund operation, including original payment and payload. Retained indefinitely in sandbox. Exact retries replay the original response."
          }
        ],
        "responses": {
          "201": {
            "description": "Refund representation. Creation retries return the original accepted response; GET returns current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refund"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: malformed body, or an amount that is not a whole unit of its currency (whole francs in CDF).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Required refund permission missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found or belongs to another merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "REFUND_AMOUNT_EXCEEDED (more than remains refundable), PAYMENT_NOT_REFUNDABLE (the payment is not approved), REFUND_METHOD_UNSUPPORTED (the payment's method cannot be refunded yet), REFUND_WINDOW_EXPIRED (approved more than 180 days ago) or IDEMPOTENCY_CONFLICT (the key was used for another request); MERCHANT_LIMIT_EXCEEDED (past the day's refunds limit of the merchant's risk profile).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Merchant quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Switch unavailable; retry identical POST with same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires refunds:write. Refunds return to the original account only. No recipient field is accepted. Above the merchant's refund threshold for the currency, the refund is PENDING_APPROVAL until a person other than the requester approves it in the portal; a refund asked for with an API key that waits is approved by a person, never by a key or by whoever created or rotated that key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefundRequest"
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listPaymentRefunds",
        "summary": "List refunds linked to a merchant-owned payment",
        "tags": [
          "Refunds"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Use next_cursor from the preceding page. Each page contains at most 100 refunds, newest first."
          }
        ],
        "responses": {
          "200": {
            "description": "The payment's refunds, each with its current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefundList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or expired 180-day refund window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Required refund permission missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found or belongs to another merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A conflict switch-core answered, relayed as it is; this read takes no key, so it is never a key conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Merchant quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Switch unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires refunds:read. Refunds return to the original account only. No recipient field is accepted."
      }
    },
    "/v1/refunds/{id}": {
      "get": {
        "operationId": "getRefund",
        "summary": "Read the current merchant-owned refund status",
        "tags": [
          "Refunds"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The refund with its current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Refund"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or expired 180-day refund window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Required refund permission missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found or belongs to another merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A conflict switch-core answered, relayed as it is; this read takes no key, so it is never a key conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Merchant quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Switch unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires refunds:read. Refunds return to the original account only. No recipient field is accepted."
      }
    },
    "/v1/webhooks/endpoint/configuration": {
      "post": {
        "operationId": "configureWebhook",
        "summary": "Update the allowlisted destination and new-event subscriptions",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSettingsResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient permission",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, including foreign resources",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict or already consumed operation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Requires webhooks:manage. Configuration version must match current config_version; stale update returns 409 WEBHOOK_CHANGED. Same idempotency key and body returns original version, changed body returns 409 IDEMPOTENCY_CONFLICT. Selection applies at enqueue time; queued events retain admission decisions. Future attempts use the latest destination. Secret rotation is separate.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookSettingsRequest"
              }
            }
          }
        }
      }
    },
    "/v1/payments/summary": {
      "get": {
        "operationId": "getPaymentDashboard",
        "summary": "Read per-currency merchant payment dashboard",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "name": "reference",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 1,
              "maximum": 9000000000000
            }
          },
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "USD",
                "CDF"
              ]
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "CREATED",
                "PENDING_APPROVAL",
                "UNCERTAIN",
                "APPROVED",
                "DECLINED",
                "EXPIRED"
              ]
            }
          },
          {
            "name": "method",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "simulator",
                "synthetic-bank",
                "unselected"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "Africa/Kinshasa",
                "Africa/Lubumbashi"
              ]
            }
          },
          {
            "name": "branch_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(unassigned|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"
            },
            "description": "Filter an immutable branch ID, including archived branches; unassigned selects payments without a branch. Omit for all merchant payments."
          }
        ],
        "responses": {
          "200": {
            "description": "Per-currency metrics for the chosen period and current business day.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentDashboard"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payment-read permission required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Same tenant and payment-read authorization as history. Consistent read snapshot: chosen period and current business day in Africa/Kinshasa (default) or Africa/Lubumbashi. Missing period bounds default to now minus 30 days and now; maximum 366 days. Non-date filters also apply to today. Success rate excludes pending/uncertain/created states; approved gross volume includes subsequently refunded payments, never mixes currencies and is a string of minor units. This does not forecast settlement or expose payer identifiers."
      }
    },
    "/v1/payments/export": {
      "get": {
        "operationId": "exportPayments",
        "summary": "Download filtered merchant payments as CSV or XLSX",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "name": "reference",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64",
              "minimum": 1,
              "maximum": 9000000000000
            }
          },
          {
            "name": "currency",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "USD",
                "CDF"
              ]
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "CREATED",
                "PENDING_APPROVAL",
                "UNCERTAIN",
                "APPROVED",
                "DECLINED",
                "EXPIRED"
              ]
            }
          },
          {
            "name": "method",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "simulator",
                "synthetic-bank",
                "unselected"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "Africa/Kinshasa",
                "Africa/Lubumbashi"
              ]
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "csv",
                "xlsx"
              ],
              "default": "csv"
            }
          },
          {
            "name": "branch_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^(unassigned|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"
            },
            "description": "Filter an immutable branch ID, including archived branches; unassigned selects payments without a branch. Omit for all merchant payments."
          }
        ],
        "responses": {
          "200": {
            "description": "Complete filtered snapshot, at most 5,000 rows. No partial exports. Amounts are exact text in minor units.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payment-read permission required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "EXPORT_TOO_LARGE: narrow the filters. No file is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:read and the same tenant/live membership checks as history. Required half-open creation-date bounds span at most 366 days. Uses all applied history filters except cursor and one database query snapshot. Maximum 5,000 rows; 413 on overflow, never silent truncation. UTF-8 CSV with BOM and CRLF; dangerous formula prefixes are escaped with an apostrophe. XLSX has one Payments sheet with explicit text cells, no formulas or external links. Columns: id, reference, merchant_reference, amount_minor, currency, state, created_at (UTC), method. No payer identifiers or checkout capabilities. Private no-store response."
      }
    },
    "/v1/payment-links": {
      "get": {
        "operationId": "listPaymentLinks",
        "summary": "listPaymentLinks",
        "tags": [
          "PaymentLinks"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant-owned payment link result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkList"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed cursor or header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payment-read permission required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "LINK_NOT_FOUND: the cursor names no link of this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE, or INVALID_LINK_RESPONSE when the switch's answer is not the closed link shape; nothing of it is relayed. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:read. Local synthetic links: a link is its own record, each payer attempt a payment bound to its terms. Lists and details never carry the share URL. A link lasts 1 to 720 hours from its creation. Status: CANCELLED; PAID (a single-use link's approved attempt); UNCERTAIN or PENDING (an attempt under way); EXPIRED (past its expiry, with or without earlier payments); OPEN (a multiple-use link that has received payments and takes more); otherwise UNPAID. A declined attempt frees the link. Live merchant membership/version and shared quota apply."
      },
      "post": {
        "operationId": "createPaymentLink",
        "summary": "createPaymentLink",
        "tags": [
          "PaymentLinks"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant-owned payment link result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: terms the switch does not read (an unknown, missing or mistyped field, an amount, bound or expiry out of range), or a malformed header or idempotency key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payment-write permission required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "BRANCH_NOT_FOUND: the store named is not this merchant's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT: the key was used with another body; BRANCH_ARCHIVED: the store named is archived; MERCHANT_LIMIT_EXCEEDED: the fixed amount, an open link's minimum or its maximum is above the merchant's per-payment limit in force in the link's currency.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE, or INVALID_LINK_RESPONSE when the switch's answer is not the closed link shape; nothing of it is relayed. Retry with the same idempotency key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:write. Local synthetic links. Creating a link starts no payment; each payer attempt is a payment bound to the link's terms. Expiry is a duration of 1 to 720 hours from creation. A link no payer could pay, or one advertising more than the merchant may take (a fixed amount, an open link's minimum or its maximum above the merchant's per-payment limit in its currency), is refused with 409 MERCHANT_LIMIT_EXCEEDED, as a payment's creation is; an open link without a maximum is checked at each payment, and a limit lowered after creation is said to the payer when they pay. A single-use link has at most one attempt open, under way or approved; a declined attempt frees it. Lists and details never expose the share URL; sharing is a separate authorized call. Repeated creation under a key returns the same link with its current state; a changed payload conflicts. Live merchant membership/version and shared quota apply.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentLinkCreate"
              }
            }
          },
          "x-codegen-request-body-name": "createPaymentLink"
        },
        "x-codegen-request-body-name": "createPaymentLink"
      }
    },
    "/v1/payment-links/{id}": {
      "get": {
        "operationId": "getPaymentLink",
        "summary": "getPaymentLink",
        "tags": [
          "PaymentLinks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant-owned payment link result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed id or header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payment-read permission required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "LINK_NOT_FOUND: no such link in this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE, or INVALID_LINK_RESPONSE when the switch's answer is not the closed link shape; nothing of it is relayed. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:read. Local synthetic links: a link is its own record, each payer attempt a payment bound to its terms. Lists and details never carry the share URL. A link lasts 1 to 720 hours from its creation. Status: CANCELLED; PAID (a single-use link's approved attempt); UNCERTAIN or PENDING (an attempt under way); EXPIRED (past its expiry, with or without earlier payments); OPEN (a multiple-use link that has received payments and takes more); otherwise UNPAID. A declined attempt frees the link. Live merchant membership/version and shared quota apply. A link made for an invoice answers 404 here, as an unknown link; GET /v1/invoices/{id} reads it."
      }
    },
    "/v1/payment-links/{id}/share": {
      "get": {
        "operationId": "sharePaymentLink",
        "summary": "sharePaymentLink",
        "tags": [
          "PaymentLinks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant-owned payment link result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLinkShare"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed id or header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payment-write permission required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "LINK_NOT_FOUND: no such link in this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "LINK_NOT_SHAREABLE: the link is paid, cancelled or past its expiry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE, or INVALID_LINK_RESPONSE when the switch's answer is not the closed link shape; nothing of it is relayed. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:write. Reveals the link's share URL (`/p/{code}?token=…`), a bearer capability to pay through it, while the link is unpaid, open or has an attempt under way and is not past its expiry (409 LINK_NOT_SHAREABLE otherwise). Do not cache or log it. Live merchant membership/version and shared quota apply."
      }
    },
    "/v1/payment-links/{id}/cancel": {
      "post": {
        "operationId": "cancelPaymentLink",
        "summary": "cancelPaymentLink",
        "tags": [
          "PaymentLinks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9_-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant-owned payment link result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentLink"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed id, header or idempotency key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Payment-write permission required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "LINK_NOT_FOUND: no such link in this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "LINK_NOT_CANCELLABLE: the link is paid, expired or has an attempt under way; IDEMPOTENCY_CONFLICT: the key was used for another link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB, including chunked bodies.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE, or INVALID_LINK_RESPONSE when the switch's answer is not the closed link shape; nothing of it is relayed. Retry with the same idempotency key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:write. Cancels a link that is not paid, expired or under way (409 LINK_NOT_CANCELLABLE otherwise); its open attempts end and payers see it cancelled. Retries under the key return the link; a key used for another link conflicts. Never posts or reserves funds. A link made for an invoice answers 404 here and cannot be cancelled, as an unknown link."
      }
    },
    "/v1/settlements": {
      "get": {
        "operationId": "ListMerchantSettlements",
        "summary": "List merchant settlements",
        "tags": [
          "MerchantSettlements"
        ],
        "parameters": [
          {
            "name": "currency",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "USD",
                "CDF"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant-scoped settlement metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantSettlementList"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Settlement-read permission or live merchant membership denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Batch or cursor not found for this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Stale membership or STATEMENT_UNAVAILABLE for historical batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires settlements:read. Owner/Admin/Finance only in portal; live membership/version and quota apply. Merchant-only projection, including shared batches. Financial text is exact minor units. A frozen cumulative calculation at proposal shows gross - confirmed refunds - prior settlements - fees - unavailable = this batch net. Synthetic zero fees and destination only. States other than COMPLETED do not confirm simulated transfer. No staff identities, other merchants or sponsor instructions. Legacy batches lack a frozen breakdown; downloads return 409 STATEMENT_UNAVAILABLE. New v2 statements include only this merchant’s frozen verified_destination. Historical v1 statements return null. A proposal that a newer lot for the same merchant and currency replaced is never shown. Each lot carries its RG reference, its period (after the previous lot shown with an earlier cutoff, to its cutoff) with the period's collections by method and completed refunds, the transfer reference and Futa's completion time; the detail adds the period's lines as recorded and the follow-up. Lines describe the period, never which payments the lot paid. Files: the PDF adds the lines to the frozen breakdown; the CSV has one row per line with the lot's columns; 409 STATEMENT_TOO_LARGE above 10,000 lines."
      }
    },
    "/v1/settlements/{id}": {
      "get": {
        "operationId": "GetMerchantSettlement",
        "summary": "Get merchant settlement",
        "tags": [
          "MerchantSettlements"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant-scoped settlement metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantSettlement"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Settlement-read permission or live merchant membership denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Batch or cursor not found for this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Stale membership or STATEMENT_UNAVAILABLE for historical batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires settlements:read. Owner/Admin/Finance only in portal; live membership/version and quota apply. Merchant-only projection, including shared batches. Financial text is exact minor units. A frozen cumulative calculation at proposal shows gross - confirmed refunds - prior settlements - fees - unavailable = this batch net. Synthetic zero fees and destination only. States other than COMPLETED do not confirm simulated transfer. No staff identities, other merchants or sponsor instructions. Legacy batches lack a frozen breakdown; downloads return 409 STATEMENT_UNAVAILABLE. New v2 statements include only this merchant’s frozen verified_destination. Historical v1 statements return null. A proposal that a newer lot for the same merchant and currency replaced is never shown. Each lot carries its RG reference, its period (after the previous lot shown with an earlier cutoff, to its cutoff) with the period's collections by method and completed refunds, the transfer reference and Futa's completion time; the detail adds the period's lines as recorded and the follow-up. Lines describe the period, never which payments the lot paid. Files: the PDF adds the lines to the frozen breakdown; the CSV has one row per line with the lot's columns; 409 STATEMENT_TOO_LARGE above 10,000 lines."
      }
    },
    "/v1/settlements/{id}/statement": {
      "get": {
        "operationId": "DownloadMerchantSettlement",
        "summary": "Download merchant settlement",
        "tags": [
          "MerchantSettlements"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "pdf",
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Private merchant statement. CSV is one frozen breakdown row, not per-payment allocation. Current batch status is included.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Settlement-read permission or live merchant membership denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Batch or cursor not found for this merchant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Stale membership or STATEMENT_UNAVAILABLE for historical batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Shared merchant quota exhausted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Unavailable; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires settlements:read. Owner/Admin/Finance only in portal; live membership/version and quota apply. Merchant-only projection, including shared batches. Financial text is exact minor units. A frozen cumulative calculation at proposal shows gross - confirmed refunds - prior settlements - fees - unavailable = this batch net. Synthetic zero fees and destination only. States other than COMPLETED do not confirm simulated transfer. No staff identities, other merchants or sponsor instructions. Legacy batches lack a frozen breakdown; downloads return 409 STATEMENT_UNAVAILABLE. New v2 statements include only this merchant’s frozen verified_destination. Historical v1 statements return null. A proposal that a newer lot for the same merchant and currency replaced is never shown. Each lot carries its RG reference, its period (after the previous lot shown with an earlier cutoff, to its cutoff) with the period's collections by method and completed refunds, the transfer reference and Futa's completion time; the detail adds the period's lines as recorded and the follow-up. Lines describe the period, never which payments the lot paid. Files: the PDF adds the lines to the frozen breakdown; the CSV has one row per line with the lot's columns; 409 STATEMENT_TOO_LARGE above 10,000 lines."
      }
    },
    "/v1/payments/{id}/obligation": {
      "get": {
        "operationId": "getPaymentObligation",
        "summary": "Read the collection and ordered attempts of a payment obligation",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current family projection; passive read, never creates or closes a payment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentObligation"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request; no query, body or content encoding is allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "payments:read or current merchant membership required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Payment not owned by authenticated merchant, absent, or synthetic feature disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Membership version changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Merchant request quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Switch unavailable or invalid bounded upstream projection; no automatic retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{id}/receipt.pdf": {
      "get": {
        "operationId": "downloadPaymentReceipt",
        "summary": "Download a private PDF receipt for this exact collected payment",
        "tags": [
          "Payments"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "fr",
                "en"
              ],
              "default": "fr"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Private synthetic collected-payment receipt PDF",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request; no query, body or content encoding is allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "payments:read or current merchant membership required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Payment not owned by authenticated merchant, absent, or synthetic feature disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Requested attempt is not approved, or team membership version is stale",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Merchant request quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Switch unavailable or invalid bounded upstream projection; no automatic retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Requires payments:read and current tenant authority; team requests additionally require live MFA membership/version and quota. Default-off local synthetic receipt feature. This exact payment must be approved with immutable approval evidence, posting and retained collection. A declined original is never silently replaced by a child. The PDF identifies the collected payment and root obligation, original amount and approval time, with refunds as a separately dated snapshot. No checkout capabilities. GET accepts only an optional lang=fr or lang=en query; body and encoding are rejected. Private attachment; no retries or redirects are required."
      }
    },
    "/v1/payments/{id}/cancel": {
      "post": {
        "operationId": "cancelPayment",
        "tags": [
          "Payments"
        ],
        "summary": "Cancel an unsubmitted merchant payment",
        "description": "Requires payments:write. POST with no body or query. Merchant and actor come from current API-key authentication. Only unexpired, unsubmitted CREATED intents may transition; wallet/bank admission and cancellation serialize. Payments already cancelled through this route are returned without another transition. An intent becomes EXPIRED/PAYMENT_CANCELLED, a payment link's attempt included: its link stays payable and is cancelled only through the link. An attempt ended by its link's cancellation (EXPIRED/LINK_CANCELLED) or by the link (EXPIRED/ATTEMPT_EXPIRED, ATTEMPT_REPLACED when a payer's open amount changed) answers 409 PAYMENT_NOT_CANCELLABLE. A link created before 0.38.0 is its own payment: when the link was cancelled while that payment was still open, the payment keeps its LINK_CANCELLED projection and is returned as cancelled. Pending, uncertain, provider-admitted or other terminal payments cannot be cancelled. This never refunds or reverses money. Retain and retry the same ID and key after a timeout or lost reply; no automatic SDK retry. The cancellation receipt is durably retained in its own per-merchant namespace. Sandbox only; no live-provider cancellation authority.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Durable cancellation receipt. Same key returns the original response, including after restart.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelledPayment"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: empty body, canonical UUID, no query or caller authority headers; one valid Idempotency-Key required.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: authentication is checked on every request, including replay.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: payments:write required.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYMENT_NOT_FOUND: unknown or another merchant payment.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT or PAYMENT_NOT_CANCELLABLE: another original for this key, submitted, uncertain, terminal or expired intent.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: shared merchant quota exhausted.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE or INVALID_CANCELLATION_RESPONSE: outcome may be committed; retry the same original ID and key.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/{id}/detail": {
      "get": {
        "operationId": "readPaymentDetail",
        "summary": "Read the private synthetic payment evidence detail",
        "tags": [
          "MerchantPaymentDetails"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Private synthetic evidence projection",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPaymentDetail"
                }
              }
            }
          },
          "400": {
            "description": "Noncanonical path, duplicate authority headers, caller X-Futa authority, body/encoding or invalid query/cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "401": {
            "description": "Authentication required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "403": {
            "description": "payments:read or current merchant membership required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "404": {
            "description": "Payment absent or owned by another merchant, or cursor not owned by this payment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "429": {
            "description": "Merchant request quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Switch unavailable or invalid bounded upstream projection; no automatic retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          }
        },
        "description": "Requires payments:read and current tenant authority. Team access additionally requires verified password+OTP, live membership/version and quota. Passive synthetic evidence only. Decimal-string amounts are minor units; zero fees are synthetic policy. Net after refunds is not available settlement balance; no payment-level batch allocation exists. Missing payer identifiers remain absent. Closed bounded projection; no raw phone, provider tokens or capabilities. GET permits no body, encoding, caller authority headers or extra query parameters. No automatic retry. Detail and timeline are separate snapshots; current_sequence may advance between reads.",
        "security": [
          {
            "MerchantKey": []
          }
        ]
      }
    },
    "/v1/payments/{id}/detail/events": {
      "get": {
        "operationId": "readPaymentTimeline",
        "summary": "Read the private synthetic payment state timeline",
        "tags": [
          "MerchantPaymentDetails"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Canonical UUID of the last event from this exact payment; oldest sequence first, 25 events per page."
          }
        ],
        "responses": {
          "200": {
            "description": "Private synthetic evidence projection",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPaymentTimeline"
                }
              }
            }
          },
          "400": {
            "description": "Noncanonical path, duplicate authority headers, caller X-Futa authority, body/encoding or invalid query/cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "401": {
            "description": "Authentication required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "403": {
            "description": "payments:read or current merchant membership required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "404": {
            "description": "Payment absent or owned by another merchant, or cursor not owned by this payment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "429": {
            "description": "Merchant request quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Switch unavailable or invalid bounded upstream projection; no automatic retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            }
          }
        },
        "description": "Requires payments:read and current tenant authority. Team access additionally requires verified password+OTP, live membership/version and quota. Passive synthetic evidence only. Decimal-string amounts are minor units; zero fees are synthetic policy. Net after refunds is not available settlement balance; no payment-level batch allocation exists. Missing payer identifiers remain absent. Closed bounded projection; no raw phone, provider tokens or capabilities. GET permits no body, encoding, caller authority headers or extra query parameters. No automatic retry. Detail and timeline are separate snapshots; current_sequence may advance between reads.",
        "security": [
          {
            "MerchantKey": []
          }
        ]
      }
    },
    "/v1/payments/search/phone": {
      "post": {
        "operationId": "searchPaymentsByPhone",
        "summary": "Search merchant payments by exact private phone",
        "tags": [
          "MerchantPaymentPhone"
        ],
        "description": "Requires current payments:read scope and merchant quota; team routes require password+OTP, current membership and version. Passive POST: no command receipt or automatic retry. Canonical full phone is only in the bounded JSON body, never URL/query. Only enabled indexed direct/hosted synthetic wallet admissions are covered; historical and bank contacts are not reconstructed. Filters are conjunctive. Cursor must come from this exact merchant/phone/filter cohort. Rows carry integer minor-unit amounts; CDF is divisible by 100. An empty result is not proof of historical absence.",
        "security": [
          {
            "MerchantKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MerchantPaymentPhoneRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Up to 25 indexed-only payment rows; historical coverage incomplete.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPaymentPhonePage"
                }
              }
            }
          },
          "400": {
            "description": "Invalid bounded body, authority, transport, canonical phone, filters or date interval.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Current payments:read authority required.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Cursor is absent or outside the exact merchant/phone/filter cohort.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Merchant request quota exceeded.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Disabled/missing key material, unavailable switch or invalid bounded upstream response.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payments/search/phone/export": {
      "post": {
        "operationId": "exportPaymentsByPhone",
        "summary": "Download matching phone-filtered payments",
        "tags": [
          "MerchantPaymentPhone"
        ],
        "description": "Requires current payments:read scope and merchant quota; team routes require password+OTP, current membership and version. Passive POST: no command receipt or automatic retry. Canonical full phone is only in the bounded JSON body, never URL/query. Only enabled indexed direct/hosted synthetic wallet admissions are covered; historical and bank contacts are not reconstructed. Required from/to use inclusive/exclusive instants within 366 days. CSV is formula-protected UTF-8; XLSX uses explicit text cells. Read the fixed coverage headers on the raw response. Do not fall back to unfiltered exports.",
        "security": [
          {
            "MerchantKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MerchantPaymentPhoneExportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching indexed-only report, at most 5,000 rows; historical coverage incomplete.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              },
              "Referrer-Policy": {
                "schema": {
                  "type": "string",
                  "const": "no-referrer"
                }
              },
              "X-Content-Type-Options": {
                "schema": {
                  "type": "string",
                  "const": "nosniff"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "Fixed futa-payments-phone.csv or .xlsx attachment."
              },
              "X-Futa-Phone-Coverage": {
                "schema": {
                  "type": "string",
                  "const": "INDEXED_PHONE_ADMISSIONS"
                }
              },
              "X-Futa-Historical-Complete": {
                "schema": {
                  "type": "string",
                  "const": "false"
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Invalid bounded body, authority, transport, canonical phone, filters or date interval.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Current payments:read authority required.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "More than 5,000 matching payments; narrow the dates or filters. No partial report.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "Merchant request quota exceeded.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Disabled/missing key material, unavailable switch or invalid bounded upstream response.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "Includes no-store."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/balance": {
      "get": {
        "operationId": "getPayoutBalance",
        "summary": "The payout balance, per currency",
        "tags": [
          "Payouts"
        ],
        "parameters": [],
        "description": "Requires balance:read. Both currencies, never summed: available and reserved as switch-core's ledger sums them, each currency's low-balance alert and below_alert, and the funding reference PF-… a top-up carries.",
        "responses": {
          "200": {
            "description": "The payout balance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBalance"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks balance:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payouts": {
      "post": {
        "operationId": "createPayout",
        "summary": "Prepare and send a single payout",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant and the key: the same key with the same body answers what it created, whatever happened to it since, and another body answers 409 IDEMPOTENCY_CONFLICT. An unanswered request is sent again under its key with its body, never under a new key."
          }
        ],
        "description": "Requires payouts:write. One payout to a sandbox wallet or synthetic bank account, with its name enquiry. The payout is a submission: its amount and fee are reserved from the available balance in the transaction that records it, or it is refused with 409 INSUFFICIENT_PAYOUT_BALANCE and its shortfall; above the merchant's threshold for its currency it waits for a second person (PENDING_APPROVAL), its money reserved, for seven days at most, and nothing is sent before its approval. A payout a key prepared is approved only by a person who never held that key. Settled after the key is read: switch-core gives PAYOUTS_HELD, PAYOUT_ROUTE_UNAVAILABLE, NAME_ENQUIRY_NOT_FOUND, NAME_ENQUIRY_STALE, PAYOUT_DESTINATION_UNKNOWN, NAME_ACKNOWLEDGEMENT_REQUIRED, MERCHANT_LIMIT_EXCEEDED, INSUFFICIENT_PAYOUT_BALANCE only once it has read the key, so each ends an unanswered request; any other refusal may come before it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MerchantPayoutRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The payout as recorded: queued, or waiting for approval. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayout"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter; or, from switch-core, a participant it does not know or of another kind than the payout's, or acknowledge_unavailable_name set while the enquiry gave a name. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "NAME_ENQUIRY_NOT_FOUND: no such name enquiry for this merchant (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "409": {
            "description": "INSUFFICIENT_PAYOUT_BALANCE (with its shortfall); NAME_ENQUIRY_STALE (the enquiry is more than 24 hours old); NAME_ACKNOWLEDGEMENT_REQUIRED (the enquiry could not give the holder's name and the request does not acknowledge it); PAYOUT_DESTINATION_UNKNOWN; PAYOUT_ROUTE_UNAVAILABLE; MERCHANT_LIMIT_EXCEEDED (the profile's per-payout or per-day limit, synthetic-limits-v2); PAYOUTS_HELD (Futa holds the merchant's payouts); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                },
                "examples": {
                  "insufficientPayoutBalance": {
                    "value": {
                      "error": {
                        "code": "INSUFFICIENT_PAYOUT_BALANCE",
                        "message_en": "The available payout balance does not cover this amount and its fees: the shortfall is stated. Nothing was reserved.",
                        "message_fr": "Le solde de versement disponible ne couvre pas ce montant et ses frais : il manque le montant indiqué. Rien n'a été réservé.",
                        "shortfall": {
                          "amount": 4500,
                          "currency": "USD"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, so the outcome is unknown: send the same request again under the same key, never a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listPayouts",
        "summary": "Single payouts, newest first",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "next_cursor from the page before."
          }
        ],
        "description": "Requires payouts:read. At most 100 a page.",
        "responses": {
          "200": {
            "description": "A page of payouts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutPage"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_NOT_FOUND: the cursor is not one of this merchant's payouts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payouts/{id}": {
      "get": {
        "operationId": "getPayout",
        "summary": "One payout",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The payout's id."
          }
        ],
        "description": "Requires payouts:read. Its current state and figures.",
        "responses": {
          "200": {
            "description": "The payout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayout"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches": {
      "post": {
        "operationId": "createPayoutBatch",
        "summary": "Start a payout batch",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant and the key: the same key with the same body answers what it created, whatever happened to it since, and another body answers 409 IDEMPOTENCY_CONFLICT. An unanswered request is sent again under its key with its body, never under a new key."
          }
        ],
        "description": "Requires payouts:write. A batch in one currency, with the number of lines that will arrive (at most 100,000), sent afterwards in chunks of at most 1,000. Settled after the key is read: switch-core gives PAYOUT_BATCH_TOO_LARGE only once it has read the key, so each ends an unanswered request; any other refusal may come before it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutBatchCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The batch, UPLOADED. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "PAYOUT_BATCH_TOO_LARGE (the merchant's open batches, those not yet approved, rejected, expired or lapsed, are at their bound of 20, SYNTHETIC); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, so the outcome is unknown: send the same request again under the same key, never a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listPayoutBatches",
        "summary": "Payout batches, newest first",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "next_cursor from the page before."
          }
        ],
        "description": "Requires payouts:read. At most 100 a page.",
        "responses": {
          "200": {
            "description": "A page of batches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatchPage"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND: the cursor is not one of this merchant's batches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}": {
      "get": {
        "operationId": "getPayoutBatch",
        "summary": "One payout batch",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          }
        ],
        "description": "Requires payouts:read. Its state, counts, money and approval.",
        "responses": {
          "200": {
            "description": "The batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}/lines": {
      "post": {
        "operationId": "addPayoutBatchLines",
        "summary": "Send a chunk of a batch's lines",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Its form alone is checked: a chunk is known by the merchant, the key, the batch and the chunk's number, which keys its lines downstream. The same chunk sent again changes nothing, whatever its key. Other lines under the same number reach switch-core, which takes them only if it recorded no chunk of that number (one it refused recorded nothing, so corrected lines may follow under the same number) and answers 409 IDEMPOTENCY_CONFLICT otherwise."
          }
        ],
        "description": "Requires payouts:write. At most 1,000 lines and 512 KiB a call, each line numbered within the batch and in its currency, the amount exact integer minor units; the chunk's number keys it, from 1 to the number of lines the batch announced (switch-core's bound since merchant-payout-batch-lines-v2), so lines with longer texts go in smaller chunks and a batch still holds its 100,000 lines. The batch validates its lines once every announced line has arrived. Settled after the key is read: switch-core gives PAYOUT_BATCH_NOT_FOUND, PAYOUT_BATCH_TOO_LARGE, PAYOUT_BATCH_INVALID only once it has read the key, so each ends an unanswered request; any other refusal may come before it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutLineChunk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch after the chunk. A chunk sent again answers the batch as it stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter, or a chunk numbered beyond the lines its batch announced. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "409": {
            "description": "PAYOUT_BATCH_TOO_LARGE (the batch takes no more lines: they have all arrived, or this chunk's would pass the lines it announced); PAYOUT_BATCH_INVALID (a line in another currency, numbered beyond those announced or already received, or one more preparer or key holder than a batch keeps); IDEMPOTENCY_CONFLICT (switch-core recorded a chunk of that number with other lines).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 512 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, so the outcome is unknown: send the same request again under the same key, never a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listPayoutBatchLines",
        "summary": "A batch's lines, by number",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "next_cursor from the page before."
          },
          {
            "in": "query",
            "name": "state",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "VALID",
                "REVIEW",
                "EXCLUDED",
                "PENDING_APPROVAL",
                "QUEUED",
                "PROCESSING",
                "PAID",
                "FAILED",
                "UNCERTAIN",
                "REJECTED",
                "EXPIRED"
              ]
            },
            "description": "Only the lines in this state."
          }
        ],
        "description": "Requires payouts:read. At most 1,000 a page, masked, and only the lines the validation has checked: a line still arriving, or corrected and waiting to be checked again, is listed once it is.",
        "responses": {
          "200": {
            "description": "A page of lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatchLines"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's); PAYOUT_LINE_NOT_FOUND (the cursor is not a line of this batch).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}/lines/{line}/decision": {
      "post": {
        "operationId": "decidePayoutBatchLine",
        "summary": "Accept, keep, remove or correct a line to review",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "path",
            "name": "line",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The line's id."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant, the key and the command: the key covers one line, the version shown and the body, so reusing it for another answers 409 IDEMPOTENCY_CONFLICT. Exact retries replay the original answer."
          }
        ],
        "description": "Requires payouts:write. At the batch's version, while it is VALIDATED; a correction is judged again. Settled after the key is read: switch-core gives PAYOUT_LINE_NOT_FOUND, PAYOUT_BATCH_NOT_FOUND, PAYOUT_BATCH_NOT_VALIDATED, PAYOUT_BATCH_CHANGED, PAYOUT_BATCH_INVALID, PAYOUT_ROUTE_UNAVAILABLE only once it has read the key, so each ends an unanswered request; any other refusal may come before it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutLineDecisionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch after the decision, at its new version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND or PAYOUT_LINE_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "409": {
            "description": "PAYOUT_BATCH_NOT_VALIDATED (the batch is not VALIDATED: its lines are still arriving or being checked, a correction included, or it was submitted or lapsed); PAYOUT_BATCH_CHANGED (the batch's version is no longer the one sent); PAYOUT_BATCH_INVALID (ACCEPT or KEEP on a line that is not to review, a correction to another number or currency, or one more preparer or key holder than a batch keeps); PAYOUT_ROUTE_UNAVAILABLE (ACCEPT or KEEP on a ROUTE_PAUSED line: correct it once its route reopens, or remove it); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, so the outcome is unknown: send the same request again under the same key, never a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}/validation": {
      "get": {
        "operationId": "getPayoutBatchValidation",
        "summary": "A batch's validation: progress, counts and issue lines",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "next_cursor from the page before."
          }
        ],
        "description": "Requires payouts:read. Its real progress (lines checked over lines received), counts, what its valid lines would reserve, and its issue lines, at most 1,000 a page.",
        "responses": {
          "200": {
            "description": "The validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutValidationReport"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's); PAYOUT_LINE_NOT_FOUND (the cursor is not a line of this batch).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}/submit": {
      "post": {
        "operationId": "submitPayoutBatch",
        "summary": "Send a batch for approval",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant, the key and the command: the key covers one batch, the version shown and the body, so reusing it for another answers 409 IDEMPOTENCY_CONFLICT. Exact retries replay the original answer."
          }
        ],
        "description": "Requires payouts:write. « Envoyer à l'approbation »: a submission. The total of its valid lines, amounts and fees, is reserved from the available balance in the transaction that records it, or it is refused with 409 INSUFFICIENT_PAYOUT_BALANCE and its shortfall; the batch then waits for a second person (PENDING_APPROVAL), whatever its total, for seven days at most, and nothing is sent before its approval. Settled after the key is read: switch-core gives PAYOUT_BATCH_NOT_FOUND, PAYOUT_BATCH_NOT_VALIDATED, PAYOUT_BATCH_CHANGED, PAYOUT_BATCH_INVALID, PAYOUT_ROUTE_UNAVAILABLE, PAYOUTS_HELD, MERCHANT_LIMIT_EXCEEDED, INSUFFICIENT_PAYOUT_BALANCE only once it has read the key, so each ends an unanswered request; any other refusal may come before it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutVersionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch, PENDING_APPROVAL, its total reserved. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "409": {
            "description": "INSUFFICIENT_PAYOUT_BALANCE (with its shortfall); PAYOUT_BATCH_NOT_VALIDATED (not validated, or lines still to review); PAYOUT_BATCH_CHANGED (the batch's version is no longer the one sent); PAYOUT_BATCH_INVALID (no valid line, or one more preparer or key holder than a batch keeps); PAYOUT_ROUTE_UNAVAILABLE (a valid line's participant takes no payouts at the moment); MERCHANT_LIMIT_EXCEEDED (the profile's limits, synthetic-limits-v2); PAYOUTS_HELD (Futa holds the merchant's payouts); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                },
                "examples": {
                  "insufficientPayoutBalance": {
                    "value": {
                      "error": {
                        "code": "INSUFFICIENT_PAYOUT_BALANCE",
                        "message_en": "The available payout balance does not cover this amount and its fees: the shortfall is stated. Nothing was reserved.",
                        "message_fr": "Le solde de versement disponible ne couvre pas ce montant et ses frais : il manque le montant indiqué. Rien n'a été réservé.",
                        "shortfall": {
                          "amount": 4500,
                          "currency": "USD"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, so the outcome is unknown: send the same request again under the same key, never a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}/retry": {
      "post": {
        "operationId": "retryPayoutBatch",
        "summary": "Retry a finished batch's failed lines",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant, the key and the command: the key covers one batch, the version shown and the body, so reusing it for another answers 409 IDEMPOTENCY_CONFLICT. Exact retries replay the original answer."
          }
        ],
        "description": "Requires payouts:write. « Relancer »: a new batch of the lines the participant definitively refused, each linked to its original line and carrying its reference, checked again, then submitted, reserved and approved again as a new submission; the retry itself reserves nothing. An uncertain line is never retried. Settled after the key is read: switch-core gives PAYOUT_BATCH_NOT_FOUND, PAYOUT_BATCH_CHANGED, PAYOUT_LINE_NOT_RETRYABLE, PAYOUT_BATCH_TOO_LARGE only once it has read the key, so each ends an unanswered request; any other refusal may come before it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutVersionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new batch, retry_of the original, VALIDATING: its lines are checked again and nothing is reserved until it is submitted. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "409": {
            "description": "PAYOUT_LINE_NOT_RETRYABLE (no failed line without a retry that may still be sent); PAYOUT_BATCH_CHANGED (the batch's version is no longer the one sent); PAYOUT_BATCH_TOO_LARGE (the merchant's open batches are at their bound of 20, SYNTHETIC); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                },
                "examples": {
                  "lineNotRetryable": {
                    "value": {
                      "error": {
                        "code": "PAYOUT_LINE_NOT_RETRYABLE",
                        "message_en": "Only a failed line with no other retry that may be sent can be retried, never an uncertain one.",
                        "message_fr": "Seule une ligne échouée sans autre relance possible se relance, jamais une ligne en cours de vérification.",
                        "shortfall": null
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, so the outcome is unknown: send the same request again under the same key, never a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}/report": {
      "get": {
        "operationId": "getPayoutBatchReport",
        "summary": "The validation report as a futa-payout-batch-v1 file",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          }
        ],
        "description": "Requires payouts:read. The validation report, as the futa-payout-batch-v1 format writes it: written asynchronously from VALIDATED on, or once a batch never validated has LAPSED; 202 while it is being written, then the file. merchant-api copies switch-core's file into its object store, kept encrypted, through a bounded relay that takes it only as the template writes it (futa-payout-batch-v1-written.vectors.json), and answers it from there while the batch stays at the version it was written for; a batch at another version has its file asked for anew. RESULT_NOT_READY until the batch has reached the state the file describes.",
        "responses": {
          "200": {
            "description": "The file, UTF-8 with a byte-order mark, every cell quoted and formulas neutralised. Saved as Content-Disposition names it, after the batch's reference.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "attachment; filename=\"PB-XXXX-XXXX-report.csv\""
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "202": {
            "description": "The file is being written: ask again after Retry-After.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before asking again: 2, as switch-core's file job turns every two seconds.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutFileJob"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "409": {
            "description": "RESULT_NOT_READY (the batch has not reached the state the file describes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/payout-batches/{id}/result": {
      "get": {
        "operationId": "getPayoutBatchResult",
        "summary": "The result file as a futa-payout-batch-v1 file",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          }
        ],
        "description": "Requires payouts:read. The result file, as the futa-payout-batch-v1 format writes it: written asynchronously once the batch is COMPLETED, REJECTED, EXPIRED or LAPSED (a lapsed batch's files list only the lines it checked before it lapsed); 202 while it is being written, then the file. merchant-api copies switch-core's file into its object store, kept encrypted, through a bounded relay that takes it only as the template writes it (futa-payout-batch-v1-written.vectors.json), and answers it from there while the batch stays at the version it was written for; a batch at another version has its file asked for anew. RESULT_NOT_READY until the batch has reached the state the file describes.",
        "responses": {
          "200": {
            "description": "The file, UTF-8 with a byte-order mark, every cell quoted and formulas neutralised. Saved as Content-Disposition names it, after the batch's reference.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "attachment; filename=\"PB-XXXX-XXXX-result.csv\""
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "202": {
            "description": "The file is being written: ask again after Retry-After.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before asking again: 2, as switch-core's file job turns every two seconds.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutFileJob"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "PAYOUT_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "409": {
            "description": "RESULT_NOT_READY (the batch has not reached the state the file describes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/name-enquiries": {
      "post": {
        "operationId": "createNameEnquiry",
        "summary": "Check a destination's holder name",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant and the key: the same key with the same body answers what it answered, whatever happened to it since, and another body answers 409 IDEMPOTENCY_CONFLICT. An unanswered request is sent again under its key with its body, never under a new key."
          }
        ],
        "description": "Requires payouts:write. Rate-limited and audited: the participant's answer, masked as holder_name_masked shows it, and, when an expected_name is given, a verdict against it; a full name, or a name other than the one expected, is never shown. Kept 24 hours: a payout names its enquiry. Settled after the key is read: switch-core gives PAYOUT_ROUTE_UNAVAILABLE only once it has read the key, so each ends an unanswered request; any other refusal may come before it. Each enquiry switch-core answers is journalled once, with its participant and a keyed fingerprint of its destination, never the number nor a name.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutNameEnquiryRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The enquiry. Exact retries replay it and count once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutNameEnquiry"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "PAYOUT_ROUTE_UNAVAILABLE; IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED, the merchant's quota for the minute, as the Error body; or NAME_ENQUIRY_RATE_LIMITED, the merchant's name-enquiry budget (SYNTHETIC), as MerchantPayoutError: over a rolling hour, at most 5 enquiries for one destination, 60 by one person or key and 300 for the merchant; an exact retry under the same key counts once, and a batch's lines, name-checked by switch-core's validation, count against none of these. Retry-After is the seconds until every bound reached has room again: the latest of the times the oldest enquiry counted against each of them leaves its hour.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before asking again.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, so the outcome is unknown: send the same request again under the same key, never a new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/payout-top-ups": {
      "post": {
        "operationId": "createSandboxPayoutTopUp",
        "summary": "Fund the sandbox payout balance",
        "tags": [
          "Payouts"
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant and the key, and stored before the sponsor simulator is asked: an unanswered request sent again under its key with its body funds once; another body answers 409 IDEMPOTENCY_CONFLICT."
          }
        ],
        "description": "Requires payouts:write. Test keys only, synthetic money: asks the sponsor simulator for a credit notice carrying the merchant's funding reference PF-…. switch-core reads the notices in order and posts each once; the balance shows the money only then. No real funding, account or withdrawal exists. The top-up is kept under its key first, and the notice asked for under the top-up's own id with the same body every time.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutTopUpRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The notice the simulator wrote. Exact retries replay it and fund once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutTopUp"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the payout handler's and switch-core's are MerchantPayoutError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantPayoutError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks payouts:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "TOP_UP_UNAVAILABLE: the sponsor simulator did not answer, or answered other than the notice asked for, so the outcome is unknown: send the same request again under the same key, which funds at most once (TOP_UP_UNAVAILABLE also when the sandbox gives merchant-api no simulator key, nothing then being asked); or SWITCH_UNAVAILABLE: the merchant's funding reference could not be read from switch-core's balance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPayoutError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invoice-batches": {
      "post": {
        "operationId": "createInvoiceBatch",
        "summary": "Start an invoice batch",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant and the key: the same key with the same body answers what it created, whatever happened to it since, and another body answers 409 IDEMPOTENCY_CONFLICT. An unanswered request is sent again under its key with its body, never under a new key."
          }
        ],
        "description": "Requires invoices:write. A batch of invoices sent through the channels chosen (SMS, WHATSAPP, both, or none: an empty list), with its reminder policy (BEFORE_AND_ON_DUE_DATE « J−3 et jour d'échéance », WEEKLY « Chaque semaine », NONE « Aucune ») and the number of invoices that will arrive, at most 50,000, afterwards in chunks of at most 1,000. Nothing is sent and no link exists before the batch is sent.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceBatchCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The batch, RECEIVING. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter, or a reminder plan other than NONE with no channel. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "INVOICE_BATCH_LIMIT_REACHED (the merchant's open batches, those MAPPING, VALIDATING, RECEIVING or READY, are at their bound of 20, a SYNTHETIC choice); MESSAGES_NOT_CONFIGURED (a channel chosen outside the sandbox: no transport exists); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "operationId": "listInvoiceBatches",
        "summary": "List invoice batches",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "next_cursor from the page before."
          }
        ],
        "description": "Requires invoices:read. At most 100 a page, newest first, with their counts and totals from merchant-api's projection.",
        "responses": {
          "200": {
            "description": "A page of batches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceBatchPage"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_BATCH_NOT_FOUND: the cursor is not one of this merchant's batches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/invoice-batches/{id}": {
      "get": {
        "operationId": "getInvoiceBatch",
        "summary": "Read an invoice batch",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          }
        ],
        "description": "Requires invoices:read. The batch with its counts per state and its totals per currency, USD then CDF, never summed across currencies. Once it is sent, its paid counts and paid amounts per currency are read live from switch-core's link batch on each call, each invoice counted once by its newest link, while its counts per state follow the latest read of its invoices: when switch-core's paid figures differ from them, merchant-api reads the batch's links again in the background, retried without end, so they converge though no one pages its invoices; its UNPAID and EXPIRED counts are merchant-api's own, judged by each invoice's window, never switch-core's expired count, and its delivery counts come from channels' events as last received; no count is ever inferred.",
        "responses": {
          "200": {
            "description": "The batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invoice-batches/{id}/invoices": {
      "post": {
        "operationId": "addInvoiceBatchInvoices",
        "summary": "Add a chunk of invoices to a batch",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Its form alone is checked: a chunk is known by the merchant, the batch and the chunk's number. The same chunk sent again changes nothing and answers its receipt again, whatever its key; other invoices under a number already received answer 409 IDEMPOTENCY_CONFLICT."
          }
        ],
        "description": "Requires invoices:write. At most 1,000 invoices and 512 KiB a call, each numbered within the batch (line, from 1 to the invoices announced), the amount exact integer minor units with its currency, whole francs for CDF, the phone E.164. A malformed invoice refuses the whole chunk (400). Each well-formed one is ready or rejected with its problems, judged as a file's line is (futa-invoice-file-v1): a reference already held by an earlier line of the batch or by one of the merchant's invoices, which holds it while READY in a live batch (neither LAPSED nor DISCARDED) or with a live link (UNPAID, PENDING or UNCERTAIN), whatever the invoice's or its batch's state, case aside (INVOICE_REFERENCE_DUPLICATE); a due date outside the day after today to 90 days ahead in Kinshasa (INVOICE_DUE_DATE_OUT_OF_RANGE, SYNTHETIC); an amount above the merchant's per-payment limit in force, as switch-core answers it when the chunk arrives (MERCHANT_LIMIT_EXCEEDED); no phone while the batch has a channel (PHONE_MISSING). Before a reference is judged held by one of the merchant's invoices in a sent batch, those invoices' links are read again from switch-core, so an invoice paid or ended there frees its reference whatever was last read. When switch-core's limit, or those links, cannot be read, the chunk is kept nowhere and answers 503 SWITCH_UNAVAILABLE, never 500 nor read as « no limit »; the same chunk sent again is judged afresh. The batch is READY once every announced invoice has arrived. Texts follow the one text rule, so whatever is ready here switch-core and channels take as it is.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceChunk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The chunk's receipt. A chunk sent again answers the same receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceChunkReceipt"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter, a chunk numbered beyond the invoices its batch announced, or one numbering a line twice. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "409": {
            "description": "INVOICE_BATCH_TOO_LARGE (a line numbered beyond the invoices the batch announced, or received in another chunk); INVOICE_BATCH_CHANGED (the batch takes no more invoices: it was made from a file, its invoices have all arrived, or it was sent or has lapsed); IDEMPOTENCY_CONFLICT (another chunk of that number was received).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 512 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, while the merchant's per-payment limit or the links of the invoices holding the chunk's references were read; nothing was kept: send the same request again under the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listInvoiceBatchInvoices",
        "summary": "List a batch's invoices",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "query",
            "name": "cursor",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "next_cursor from the page before."
          },
          {
            "in": "query",
            "name": "state",
            "required": false,
            "schema": {
              "enum": [
                "READY",
                "REFUSED",
                "UNPAID",
                "VERIFYING",
                "PAID",
                "EXPIRED",
                "CANCELLED",
                "NOT_PAID",
                "UNDELIVERED"
              ]
            },
            "description": "One payment state, CANCELLED among them, or one of the board's groups: NOT_PAID (« Non payées »: UNPAID, VERIFYING and EXPIRED) or UNDELIVERED (« Non délivrées »: the invoices whose delivery is UNDELIVERED alone, a message that reached no one (INVALID_NUMBER or UNREACHABLE) or that channels never sent before the window closed (SEND_WINDOW_CLOSED); UNCONFIRMED and NOT_SENT are not counted there)."
          },
          {
            "in": "query",
            "name": "q",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "description": "The start of a reference, case aside, or a phone written in full (E.164, or ten digits starting with 0), found through its keyed hash. Never a name."
          }
        ],
        "description": "Requires invoices:read. At most 100 a page, in line order, each with its payment state and its delivery state apart (« Statut » and « Envoi »), from merchant-api's projection: payment as switch-core's links were last paged, never inferred, delivery from channels' events. A single invoice read (GET /v1/invoices/{id}) reads its payment live.",
        "responses": {
          "200": {
            "description": "A page of invoices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoicePage"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_BATCH_NOT_FOUND (also when it is another merchant's); INVOICE_NOT_FOUND (the cursor is not one of the batch's invoices).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/invoice-batches/{id}/send": {
      "post": {
        "operationId": "sendInvoiceBatch",
        "summary": "Send a batch: create its invoices' links and first messages",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant, the key and the command: the key covers one batch or invoice and the body, so reusing it for another answers 409 IDEMPOTENCY_CONFLICT. Exact retries replay the original answer."
          }
        ],
        "description": "Requires invoices:write. « Créer N factures », under the version the merchant read and in the language its body names (fr when none). The merchant's stored name is judged first by the text rule, at most 120 code points once trimmed: any stored name that fails it refuses the send 409 MERCHANT_NAME_INVALID before anything is made, so no send is one whose every message channels would refuse. Since 0.60.10 a name set through the portal's branding or the enrolment, or by staff creating a merchant, takes the rule with 120 code points; a name stored before then may fail it. switch-core judges the merchant's name it is sent by the same rule. Then each invoice is judged once, on the Kinshasa day of the send: one whose due date has left the range, or above the merchant's per-payment limit in force as switch-core answers it at the send, read before the send's transaction, reads REFUSED with its code; a send whose limit cannot be read answers 503 SWITCH_UNAVAILABLE, nothing made and the batch READY under its version. switch-core's line MERCHANT_LIMIT_EXCEEDED stays the authority for a limit lowered after that judgment. The send plan is fixed before the link batch is made: expected_lines, each invoice's position and each chunk's lines; every announced invoice is sent (invoice-batch-<id>-chunk-<n>, at most 1,000 a chunk), so the link batch always completes. When the judgment leaves no invoice, no link batch is made: the batch ends SENT with every invoice REFUSED and its refusal. merchant-api keeps at most 20 of a merchant's link batches in flight: a batch beyond them waits SENDING, in order, before its link batch is made, and switch-core's LINK_BATCH_LIMIT_REACHED is read as that wait, the recorded creation body replayed later; closed and complete link batches do not count, a failed send's counting until switch-core answers its close CLOSED. switch-core's answers map as the Invoices tag's table says for the send: a line MERCHANT_LIMIT_EXCEEDED or LINK_EXPIRY_OUT_OF_RANGE refuses that invoice; a whole request refused ends the batch SEND_FAILED, and merchant-api closes its link batch at switch-core, under invoice-batch-<id>-close, when one was created, a send whose creation was refused having none to close; the close always completes, its body never discarded: a 4xx is a defect, operations are alerted and the body is kept and replayed once the defect is fixed; transient answers are retried until answered. Then, for each channel chosen, its « Demande de paiement » under invoice-<id>-request, with the merchant's name, amount, link, reference and expiry_date formatted. Every body merchant-api sends to switch-core or to channels is recorded with its key before the first call and replayed byte for byte while its outcome is unknown: a link batch, its chunks, its close, a replacement and a message are never rebuilt from the clock. After a final 4xx the recorded body is discarded, save a close's, which is kept and replayed once the defect is fixed, and the next attempt at that number (n or k) is built from its own message; a body whose outcome is unknown is rebuilt only once channels answers MESSAGE_NOT_FOUND for its key or, for a replacement, once the invoice's links, read from switch-core, hold no link at its sequence, which is how switch-core reads its key as unused: switch-core has no route that reads a key. Transient answers (5xx, timeouts, connection failures) from switch-core or channels are retried without end, from 1 second apart, doubled each time, up to 5 minutes apart (SYNTHETIC), operations alerted after 15 minutes (SYNTHETIC): meanwhile the batch stays SENDING and the message keeps its state, and after a restart the recorded bodies are replayed. Reminder numbers n, resend numbers k and link numbers n are allocated in merchant-api's own transaction, never across a call, so a scheduled reminder and a person's « Relancer » build the same body and meet on one key. A link's end is the message window's, from the invoice (InvoiceLink.expires_at), and which window applies is judged when the message is actually sent. merchant-api calls channels only from 08:00 to 19:55 Kinshasa, its retries within that time (the five minutes before the window's 20:00 a SYNTHETIC margin), and sends at most 100,000 messages a merchant a Kinshasa day; others wait HELD in merchant-api (SEND_WINDOW, DAILY_CAP) and go at the next opening, in order, after payment is read again and the link replaced if needed; one whose invoice is no longer remindable then (paid, in verification, « Expirée », CANCELLED, in a batch that ended SEND_FAILED, or its replacement refused LINK_NOT_REPLACEABLE or LINK_EXPIRY_OUT_OF_RANGE) is never sent and ends NOT_SENT with NOT_REMINDABLE. A message not answered by 19:55 waits HELD with SEND_WINDOW, and at the next opening its key is read first from channels: a message found is adopted as the occurrence's outcome; MESSAGE_NOT_FOUND records a new body, with the new day's not_after, sent under the same key. Each request to channels carries not_after, that day's 20:00 Kinshasa, and the day the link ends as expiry_date. Every text is kept and sent as the text rule trims it. This answer comes first; sending resumes from the first body not answered, so a retry, a crash or a lost reply makes nothing twice, and no lock or connection is held across a call. No webhook is added: each invoice paid raises payment.approved.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceSendRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The batch, SENDING; its links and messages follow this answer. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "409": {
            "description": "INVOICE_BATCH_NOT_VALIDATED (its invoices have not all arrived, its mapping is not confirmed or still being checked, or no invoice is ready); INVOICE_BATCH_CHANGED (the batch's version is no longer the one sent, or it was sent, discarded or has lapsed); MESSAGES_NOT_CONFIGURED (a channel chosen outside the sandbox); MERCHANT_NAME_INVALID (the merchant's stored name fails the text rule); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract, while the merchant's per-payment limit was read; nothing was made and the batch stays READY under its version: send the same request again under the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invoices/{id}": {
      "get": {
        "operationId": "getInvoice",
        "summary": "Read an invoice",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The invoice's id."
          }
        ],
        "description": "Requires invoices:read. One invoice, with its links in order, the first and each replacement, and the payable one, if any: at most one at a time, fixed and single-use for the whole amount. Its payment state is read live from switch-core's links read by invoice on each call: PAID when any of its links was paid, VERIFYING while its newest link has a pending or uncertain attempt.",
        "responses": {
          "200": {
            "description": "The invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer, or answered outside its contract; the read changed nothing and can be sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/invoice-reminders/run": {
      "post": {
        "operationId": "runSandboxInvoiceReminders",
        "summary": "Run the reminder schedule as of an instant (sandbox only)",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant and the key: the same key with the same body answers what it created, whatever happened to it since, and another body answers 409 IDEMPOTENCY_CONFLICT. An unanswered request is sent again under its key with its body, never under a new key."
          }
        ],
        "description": "Requires invoices:write. Sandbox only and synthetic: runs the reminder schedule as of the instant given, for the caller's own batches only, so live checks need no waiting; only the sandbox serves it. For each invoice only the latest occurrence due at or before as_of that has not run is recorded under its key (invoice-<id>-reminder-<n>) and sent as the scheduler would: BEFORE_AND_ON_DUE_DATE at 09:00 Kinshasa on D−3 and D, WEEKLY at 09:00 every seven days from the first message until D, and for both one reminder on D+1 at 09:00 with a link ending D+8 09:00 (SYNTHETIC). The calling window and the daily cap are judged on as_of: a message outside 08:00 to 19:55 Kinshasa as of as_of, or beyond the cap, is recorded HELD, and a later run whose as_of falls inside the window releases it. Sandbox only: the run stops calling channels when the clock reaches 19:55 on as_of's Kinshasa day, its messages not sent by then held HELD with SEND_WINDOW, and a run whose as_of is within five minutes of 19:55 (19:50 or later) holds all its messages so, for a later run to release. A message the run sends carries not_after, 20:00 on as_of's Kinshasa day: as_of is never before the database's clock, so a message sent inside the window has a not_after still ahead. A batch that ended SEND_FAILED is never reminded. Before an invoice is reminded, its payment is read again from switch-core in the same run. An invoice whose after-due window ended unpaid reads EXPIRED and is reminded no more. Link ends come from the invoice, never from as_of or the clock; switch-core keeps each within its own clock plus 720 hours, so a run far ahead may get a link that ends sooner than its schedule.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceReminderRunRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What the run recorded. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceReminderRun"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter, or as_of outside the database's clock to 100 days after it. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "SWITCH_UNAVAILABLE: switch-core did not answer; the run stopped where it was: send the same request again under the same key, never a new one, and each occurrence already recorded is not sent twice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invoice-batches/{id}/discard": {
      "post": {
        "operationId": "discardInvoiceBatch",
        "summary": "« Annuler »: discard a batch not yet sent",
        "tags": [
          "Invoices"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The batch's id."
          },
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{8,128}$"
            },
            "description": "Scoped to the merchant, the key and the command: the key covers one batch or invoice and the body, so reusing it for another answers 409 IDEMPOTENCY_CONFLICT. Exact retries replay the original answer."
          }
        ],
        "description": "Requires invoices:write. « Annuler »: discards a batch not yet sent, under the version the merchant read; it becomes DISCARDED, frees its references and stops counting against the 20 open batches. Nothing of it was ever sent, so nothing is cancelled at switch-core or channels. Journaled (« Factures »).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceBatchVersionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The batch, DISCARDED. Exact retries replay it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceBatch"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_REQUEST: a malformed or incomplete body or parameter. The framework's own answer is the Error body; the invoice handler's is MerchantInvoiceError. A request carrying any X-Futa-* header, which only Futa's own services send, is refused so before anything is kept or relayed.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/MerchantInvoiceError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "INVALID_API_KEY: a missing, unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "INSUFFICIENT_SCOPE: the key lacks invoices:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_BATCH_NOT_FOUND (also when it is another merchant's).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "409": {
            "description": "INVOICE_BATCH_CHANGED (the batch's version is no longer the one sent, or the batch is being sent, was sent, failed to send, has lapsed or was already discarded); IDEMPOTENCY_CONFLICT.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantInvoiceError"
                }
              }
            }
          },
          "413": {
            "description": "REQUEST_TOO_LARGE: the body exceeds 16 KiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "429": {
            "description": "RATE_LIMITED: the merchant's quota for the minute is spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "webhooks": {
    "payment-event": {
      "post": {
        "operationId": "paymentEvent",
        "summary": "A payment changed state",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "description": "payment.created, payment.pending_approval, payment.uncertain, payment.approved, payment.declined and payment.expired, each sent unless the endpoint's event choice leaves it out. Map the order by data.id, the payment id POST /v1/payments answered, never by data.reference; deduplicate on id; apply an event only when its data.sequence is above the last applied; compare data.amount and data.currency with the order. Futa POSTs each event as application/json in UTF-8 to the merchant's one endpoint, signed in Futa-Signature. Any 2xx acknowledges it. Any other status, a redirect included (redirects are never followed), no answer within 3 seconds or a refused connection fails the attempt, retried after 2, 4, 8 … seconds, at most an hour apart, for 72 hours after it is queued; each attempt is signed anew, and a replay sends the same event again, for 72 hours more. Verify the signature on the raw body before parsing it, and answer 400 when it fails.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FutaSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged: the delivery is complete."
          },
          "default": {
            "description": "Not acknowledged: the attempt failed and is retried."
          }
        }
      }
    },
    "refund-event": {
      "post": {
        "operationId": "refundEvent",
        "summary": "A refund changed state",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "description": "refund.queued, refund.processing, refund.uncertain, refund.completed and refund.failed are sent unless the endpoint's event choice leaves them out; refund.pending_approval, refund.rejected and refund.expired only once chosen. A refund asked for in the portal is sent too, data.payment_id naming its payment; deduplicate on id and apply an event only when its data.sequence is above the last applied for that refund. Futa POSTs each event as application/json in UTF-8 to the merchant's one endpoint, signed in Futa-Signature. Any 2xx acknowledges it. Any other status, a redirect included (redirects are never followed), no answer within 3 seconds or a refused connection fails the attempt, retried after 2, 4, 8 … seconds, at most an hour apart, for 72 hours after it is queued; each attempt is signed anew, and a replay sends the same event again, for 72 hours more. Verify the signature on the raw body before parsing it, and answer 400 when it fails.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FutaSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefundEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged: the delivery is complete."
          },
          "default": {
            "description": "Not acknowledged: the attempt failed and is retried."
          }
        }
      }
    },
    "obligation-collection-event": {
      "post": {
        "operationId": "obligationCollectionEvent",
        "summary": "An obligation was collected",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "description": "obligation.collected, sent only once chosen: an obligation was collected by a replacement payment. Futa POSTs each event as application/json in UTF-8 to the merchant's one endpoint, signed in Futa-Signature. Any 2xx acknowledges it. Any other status, a redirect included (redirects are never followed), no answer within 3 seconds or a refused connection fails the attempt, retried after 2, 4, 8 … seconds, at most an hour apart, for 72 hours after it is queued; each attempt is signed anew, and a replay sends the same event again, for 72 hours more. Verify the signature on the raw body before parsing it, and answer 400 when it fails.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FutaSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ObligationCollectionEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged: the delivery is complete."
          },
          "default": {
            "description": "Not acknowledged: the attempt failed and is retried."
          }
        }
      }
    },
    "payout-event": {
      "post": {
        "operationId": "payoutEvent",
        "summary": "A payout reached a state",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "description": "payout.pending_approval, payout.completed, payout.failed, payout.rejected and payout.expired; deduplicate on id and apply an event only when its data.version is above the last applied for that payout. Futa POSTs each event as application/json in UTF-8 to the merchant's one endpoint, signed in Futa-Signature, and retries it as every event is retried (refund-event). Delivered since 0.59.17 to an endpoint that chose it; switch-core's queued, processing and uncertain states are never sent, nor held.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FutaSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged: the delivery is complete."
          },
          "default": {
            "description": "Not acknowledged: the attempt failed and is retried."
          }
        }
      }
    },
    "payout-batch-event": {
      "post": {
        "operationId": "payoutBatchEvent",
        "summary": "A payout batch was validated or completed",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "description": "payout_batch.validated and payout_batch.completed; deduplicate on id. Futa POSTs each event as application/json in UTF-8 to the merchant's one endpoint, signed in Futa-Signature, and retries it as every event is retried (refund-event). Delivered since 0.59.17 to an endpoint that chose it; switch-core's queued, processing and uncertain states are never sent, nor held.",
        "parameters": [
          {
            "$ref": "#/components/parameters/FutaSignature"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutBatchEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledged: the delivery is complete."
          },
          "default": {
            "description": "Not acknowledged: the attempt failed and is retried."
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "FutaSignature": {
        "name": "Futa-Signature",
        "in": "header",
        "required": true,
        "description": "t=<seconds since the Unix epoch when this attempt was signed>,v1=<signature>, with a second v1=<signature> during the hour after a secret's rotation: the current secret's first, the previous one's second. Each signature is the lower-case hexadecimal HMAC-SHA256 of t, a full stop and the raw request body, keyed with a signing secret (whsec_test_…), all in UTF-8. Refuse a t more than 300 seconds from your clock, then accept the delivery when any v1 matches, compared in constant time.",
        "schema": {
          "type": "string",
          "pattern": "^t=[0-9]+(,v1=[0-9a-f]{64}){1,2}$"
        }
      }
    },
    "schemas": {
      "CreatePayment": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "amount",
          "currency",
          "merchant_reference",
          "description",
          "return_url",
          "expires_at"
        ],
        "properties": {
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "ISO minor units (scale 2 for USD/CDF); CDF must be divisible by 100."
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "merchant_reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "return_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "pattern": "^https://",
            "description": "The merchant's page the payer goes back to. The hosted checkout shows it as a « Retour à … » link on the outcome pages that offer a way back to the shop, opened exactly as given, with nothing appended (no payment id, status or reference) and no referrer. It is never proof of payment: confirm the outcome by its webhook or GET /v1/payments/{id}. It is an absolute https URL with a host and no userinfo; any other address, http included, is refused with 400 INVALID_REQUEST."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "UTC instant after now and within 24 hours."
          },
          "branch_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Optional active branch owned by this merchant. Omit/null for unassigned. Attribution is immutable; archived branches allow exact existing-request replay only."
          },
          "method": {
            "type": "string",
            "enum": [
              "simulator"
            ],
            "description": "Direct mode only; must be paired with msisdn."
          },
          "embedding": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PaymentEmbedding"
              },
              {
                "type": "null"
              }
            ],
            "description": "Optional, one of the payment's creation terms. Null or absent keeps the hosted redirect: the checkout's pages are never framed. {mode: frame, origin} lets pages of that origin alone frame them; {mode: app} is for Futa's mobile SDKs. The Idempotency-Key binds it with the other terms: the same key with another embedding answers 409 IDEMPOTENCY_CONFLICT. Hosted creation only: with method and msisdn it is refused with 400 INVALID_REQUEST. Payment links take none, so their pages are never framed."
          },
          "msisdn": {
            "type": "string",
            "enum": [
              "+243810000001",
              "+243810000002",
              "+243810000003",
              "+243810000004",
              "+243810000005",
              "+243810000006"
            ],
            "description": "Synthetic scenario: suffix 001 success, 002 decline, 003 expiry, 004 uncertain, 005 late-success, 006 unavailable. Converted to a scenario token before persistence; not returned. Requires method=simulator."
          }
        },
        "description": "Omit method and msisdn for hosted creation (CREATED). Supply both for atomic sandbox direct admission (PENDING_APPROVAL). Never submit an issuer PIN. Only the documented synthetic simulator numbers are supported; no live phone prompt.",
        "dependentRequired": {
          "method": [
            "msisdn"
          ],
          "msisdn": [
            "method"
          ]
        },
        "dependentSchemas": {
          "method": {
            "properties": {
              "embedding": {
                "type": "null"
              }
            }
          }
        },
        "if": {
          "properties": {
            "currency": {
              "const": "CDF"
            }
          },
          "required": [
            "currency"
          ]
        },
        "then": {
          "properties": {
            "amount": {
              "multipleOf": 100
            }
          }
        }
      },
      "Payment": {
        "type": "object",
        "required": [
          "id",
          "reference",
          "amount",
          "currency",
          "state",
          "checkout_url",
          "sandbox",
          "version"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": "string"
          },
          "merchant_reference": {
            "type": "string"
          },
          "merchant_name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "format": "int64"
          },
          "payer_fee": {
            "type": "integer",
            "format": "int64"
          },
          "total": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "approval_deadline": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "Bearer capability for this checkout only; do not log or share outside the intended payer."
          },
          "sandbox": {
            "type": "boolean",
            "const": true
          },
          "method": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "integer"
          },
          "escalated": {
            "type": "boolean"
          },
          "refund_status": {
            "type": "string",
            "enum": [
              "NONE",
              "PARTIALLY_REFUNDED",
              "REFUNDED"
            ],
            "description": "Subsequent refund status; original payment authorization state remains unchanged."
          },
          "refunded_amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "refunds": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Refund"
            },
            "description": "Present only when the authenticated merchant key also has refunds:read. Aggregate refund status and refunded_amount remain available with payments:read."
          },
          "refunds_next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Present only with refunds:read; use this cursor with the refund history endpoint."
          },
          "refund_reserved_amount": {
            "type": "integer",
            "format": "int64",
            "description": "Amount reserved by queued, processing or uncertain refunds; available with payments:read."
          },
          "branch_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "branch_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "branch_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "embedding": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PaymentEmbedding"
              },
              {
                "type": "null"
              }
            ],
            "description": "The embedding the payment was created with, exactly as it was accepted; null for the hosted redirect. A creation answer recorded before the field existed and answered again omits it, which means null."
          }
        }
      },
      "PaymentEmbedding": {
        "description": "How the hosted checkout of a payment is presented besides a top-level page: framed by one origin, or in an app.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/PaymentEmbeddingFrame"
          },
          {
            "$ref": "#/components/schemas/PaymentEmbeddingApp"
          }
        ],
        "discriminator": {
          "propertyName": "mode",
          "mapping": {
            "frame": "#/components/schemas/PaymentEmbeddingFrame",
            "app": "#/components/schemas/PaymentEmbeddingApp"
          }
        }
      },
      "PaymentEmbeddingFrame": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "mode",
          "origin"
        ],
        "description": "The hosted checkout's pages for this payment may be framed by pages of this origin only.",
        "properties": {
          "mode": {
            "type": "string",
            "const": "frame"
          },
          "origin": {
            "type": "string",
            "maxLength": 255,
            "pattern": "^(?:https://(?!(?:[a-z0-9-]+\\.)*localhost(?::|$))(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)*(?![0-9]+(?::|$))(?!0x[0-9a-f]*(?::|$))[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?::(?!443$)(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?|http://(?:127\\.0\\.0\\.1|localhost):(?!80$)(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))$",
            "description": "The origin of the merchant page that frames the checkout, in its serialised form as window.location.origin gives it: https://, a host name in lower-case ASCII (an internationalised name in its xn-- form) and a port only when it is not 443, with no path (not even /), query, fragment or userinfo, at most 255 characters. The host name's last label is neither all digits nor 0x followed by hexadecimal digits, so no IP address in any spelling is taken over https, and neither is localhost nor a name under it. The sandbox alone also takes http://127.0.0.1:<port> and http://localhost:<port>, 127.0.0.1 being the one IP address taken, in four decimal octets. Origins are compared in this serialised form: another spelling of an origin (upper case, :443, a trailing /) is refused with 400 INVALID_REQUEST, never rewritten."
          }
        }
      },
      "PaymentEmbeddingApp": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "mode"
        ],
        "description": "The payment is opened in a mobile app's web view by Futa's mobile SDKs; its pages are never framed.",
        "properties": {
          "mode": {
            "type": "string",
            "const": "app"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message_en": {
                "type": "string"
              },
              "message_fr": {
                "type": "string"
              }
            }
          }
        }
      },
      "MethodList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "currency": {
                  "type": "string"
                },
                "sandbox": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "KeyRequest": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "payments:read",
                "payments:write",
                "keys:manage",
                "webhooks:read",
                "webhooks:manage",
                "refunds:read",
                "refunds:write",
                "settlements:read",
                "payouts:read",
                "payouts:write",
                "balance:read",
                "invoices:read",
                "invoices:write"
              ]
            },
            "uniqueItems": true,
            "minItems": 1,
            "description": "The key's scopes. payouts:read, payouts:write and balance:read open the /v1 payout routes: a key prepares payouts and reads them and the payout balance, and never approves or rejects one. Only an Owner or an Admin gives a key one of them (TeamRole); the sandbox bootstrap key and staff-provisioned keys never hold one. invoices:read and invoices:write open the /v1 invoice routes (0.60.0): invoices:write creates, sends and reminds, invoices:read reads. Only an Owner or an Admin gives a key one of them (TeamRole, 0.60.5), a Developer holding invoices:read only through such a key; the sandbox bootstrap key and staff-provisioned keys never hold one."
          }
        },
        "required": [
          "label",
          "permissions"
        ],
        "additionalProperties": false
      },
      "IssuedKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "secret": {
            "type": "string",
            "writeOnly": false,
            "description": "One-time high-entropy test credential. Save securely; never logged or returned by list operations."
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "payments:read",
                "payments:write",
                "keys:manage",
                "webhooks:read",
                "webhooks:manage",
                "refunds:read",
                "refunds:write",
                "settlements:read",
                "payouts:read",
                "payouts:write",
                "balance:read",
                "invoices:read",
                "invoices:write"
              ]
            },
            "uniqueItems": true,
            "minItems": 1,
            "description": "The key's scopes. payouts:read, payouts:write and balance:read open the /v1 payout routes: a key prepares payouts and reads them and the payout balance, and never approves or rejects one. Only an Owner or an Admin gives a key one of them (TeamRole); the sandbox bootstrap key and staff-provisioned keys never hold one. invoices:read and invoices:write open the /v1 invoice routes (0.60.0): invoices:write creates, sends and reminds, invoices:read reads. Only an Owner or an Admin gives a key one of them (TeamRole, 0.60.5), a Developer holding invoices:read only through such a key; the sandbox bootstrap key and staff-provisioned keys never hold one."
          },
          "environment": {
            "type": "string",
            "enum": [
              "test"
            ]
          }
        },
        "required": [
          "id",
          "secret",
          "permissions",
          "environment"
        ]
      },
      "KeyMetadata": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string"
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "payments:read",
                "payments:write",
                "keys:manage",
                "webhooks:read",
                "webhooks:manage",
                "refunds:read",
                "refunds:write",
                "settlements:read",
                "payouts:read",
                "payouts:write",
                "balance:read",
                "invoices:read",
                "invoices:write"
              ]
            },
            "uniqueItems": true,
            "minItems": 1,
            "description": "The key's scopes. payouts:read, payouts:write and balance:read open the /v1 payout routes: a key prepares payouts and reads them and the payout balance, and never approves or rejects one. Only an Owner or an Admin gives a key one of them (TeamRole); the sandbox bootstrap key and staff-provisioned keys never hold one. invoices:read and invoices:write open the /v1 invoice routes (0.60.0): invoices:write creates, sends and reminds, invoices:read reads. Only an Owner or an Admin gives a key one of them (TeamRole, 0.60.5), a Developer holding invoices:read only through such a key; the sandbox bootstrap key and staff-provisioned keys never hold one."
          },
          "environment": {
            "type": "string",
            "enum": [
              "test"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "predecessor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "issued_subject": {
            "type": [
              "string",
              "null"
            ]
          },
          "issued_membership_version": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          }
        },
        "required": [
          "id",
          "label",
          "permissions",
          "environment",
          "created_at",
          "revoked_at",
          "predecessor_id",
          "last_used_at"
        ]
      },
      "KeyList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/KeyMetadata"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "Revoked": {
        "type": "object",
        "properties": {
          "revoked": {
            "type": "boolean",
            "const": true
          }
        },
        "required": [
          "revoked"
        ]
      },
      "WebhookRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 1047,
            "pattern": "^http://127\\.0\\.0\\.1:(?:18160/sandbox/webhooks|1816[1-9](?=/[^?]{0,511}(?:\\?|$))(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})*)+(?:\\?(?=[^#]{1,512}$)(?:(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})|[/?])+)?)$",
            "description": "The merchant's one endpoint, which only the sandbox takes, on this machine's loopback: http://127.0.0.1 on a port from 18161 to 18169 with the receiver's own path, at most 512 characters from its first /, and an optional query of at most 512 characters after ?, in RFC 3986 characters (anything else percent-encoded), with no userinfo or fragment; or exactly http://127.0.0.1:18160/sandbox/webhooks, the channels service's own sink. The destinations taken before, http://127.0.0.1:18160, 18161 and 18162 with /sandbox/webhooks, stay valid. A delivery never follows a redirect: a 3xx answer is a failed attempt, retried. No other host is taken, https included: destinations beyond this machine are not offered."
          }
        },
        "required": [
          "url"
        ],
        "additionalProperties": false
      },
      "IssuedWebhook": {
        "type": "object",
        "properties": {
          "secret": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "secret",
          "version"
        ]
      },
      "WebhookConfiguration": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "secret_version": {
            "type": "integer"
          },
          "previous_valid_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "config_version": {
            "type": "integer",
            "format": "int64"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          }
        },
        "required": [
          "url",
          "secret_version",
          "previous_valid_until",
          "created_at",
          "updated_at",
          "config_version",
          "events"
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "attempts": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "next_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the next attempt is due; null for a HELD event, which is never sent."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the retry window closes; null for a HELD event."
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_status": {
            "type": [
              "integer",
              "null"
            ]
          },
          "event_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "The event's type; null for a held event whose type is not a plain name."
          },
          "selected": {
            "type": "boolean"
          },
          "configuration_version": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "state": {
            "type": "string",
            "enum": [
              "DELIVERED",
              "NOT_SELECTED",
              "EXPIRED",
              "PENDING",
              "RETRYING",
              "HELD"
            ],
            "description": "From the recorded facts, by the database clock: DELIVERED once a 2xx was recorded; NOT_SELECTED when the subscription filtered the event out; EXPIRED when the retry window closed undelivered; PENDING before any attempt; RETRYING after a failed attempt within the window. A replay reopens the window, so an EXPIRED or DELIVERED event returns to PENDING or RETRYING. HELD: never sent, because the event reached Futa in a form the contract does not state (or named this merchant before it existed here). Read the payment with GET /v1/payments/{id}, as payment_id names it; a held event is never replayed, and is kept for 30 days."
          },
          "payment_reference": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^FT-[0-9A-F]{32}$",
            "description": "The payment's reference carried by a payment event; null for other events."
          },
          "held_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the event was held; null for any other state."
          },
          "payment_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The payment the event is about: a payment event's data.id, a refund event's data.payment_id; null for other events."
          }
        },
        "required": [
          "event_id",
          "attempts",
          "created_at",
          "next_attempt_at",
          "expires_at",
          "delivered_at",
          "last_status",
          "event_type",
          "selected",
          "configuration_version",
          "state",
          "payment_reference",
          "held_at",
          "payment_id"
        ]
      },
      "WebhookDeliveryList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cursor for the next page, or null on the last page."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "DeliveryAttempt": {
        "type": "object",
        "properties": {
          "attempt": {
            "type": "integer"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the attempt was recorded."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status the destination answered; 0 is a transport error (no HTTP answer)."
          }
        },
        "required": [
          "attempt",
          "occurred_at",
          "status"
        ]
      },
      "DeliveryAttemptList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliveryAttempt"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "Accepted": {
        "type": "object",
        "properties": {
          "accepted": {
            "type": "boolean",
            "const": true
          }
        },
        "required": [
          "accepted"
        ]
      },
      "RefundRequest": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "Original payment currency in minor units. CDF requires whole francs (multiples of 100)."
          },
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          }
        },
        "required": [
          "amount",
          "reason"
        ],
        "additionalProperties": false
      },
      "Refund": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "payment_id": {
            "type": "string",
            "format": "uuid"
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "PENDING_APPROVAL",
              "QUEUED",
              "PROCESSING",
              "UNCERTAIN",
              "COMPLETED",
              "FAILED",
              "REJECTED",
              "EXPIRED"
            ],
            "description": "PENDING_APPROVAL: above the merchant's threshold, waiting for a person other than the requester to approve it in the portal; nothing is sent until then and the amount still counts against what remains refundable. REJECTED and EXPIRED (not approved within seven days) end it and free the amount."
          },
          "reason": {
            "type": "string"
          },
          "merchant_reason": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "issuer_reference": {
            "type": [
              "string",
              "null"
            ]
          },
          "sequence": {
            "type": "integer",
            "minimum": 0
          },
          "escalated": {
            "type": "boolean"
          },
          "sandbox": {
            "type": "boolean",
            "const": true
          },
          "approval": {
            "type": [
              "object",
              "null"
            ],
            "description": "Present when the refund needed approval.",
            "required": [
              "expires_at",
              "requested_by",
              "approved_by",
              "rejected_by",
              "decided_at"
            ],
            "properties": {
              "expires_at": {
                "type": "string",
                "format": "date-time"
              },
              "requested_by": {
                "type": "string",
                "description": "The team actor who asked: user:<subject> for a person, key:<id> for an API key."
              },
              "approved_by": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "rejected_by": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "decided_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        },
        "required": [
          "id",
          "payment_id",
          "amount",
          "currency",
          "state",
          "reason",
          "merchant_reason",
          "created_at",
          "issuer_reference",
          "sequence",
          "escalated",
          "sandbox"
        ]
      },
      "RefundList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Refund"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        },
        "required": [
          "data"
        ]
      },
      "PaymentSummary": {
        "type": "object",
        "required": [
          "id",
          "reference",
          "merchant_reference",
          "amount",
          "currency",
          "state",
          "created_at",
          "method"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": "string"
          },
          "merchant_reference": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "method": {
            "type": [
              "string",
              "null"
            ]
          },
          "branch_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "branch_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "branch_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "payer_masked_identifier": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+243•••••[0-9]{4}$",
            "description": "The payer's number as the payment's detail masks it, when the payer gave one; null otherwise."
          }
        }
      },
      "PaymentList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "maxItems": 25,
            "items": {
              "$ref": "#/components/schemas/PaymentSummary"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "PaymentEvent": {
        "title": "Payment event v1",
        "description": "A payment changed state: one event per state the payment reaches, its type payment. and the state in lower case. A notice, not the record: GET /v1/payments/{id} is authoritative.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "occurred_at",
          "schema_version",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event may arrive more than once (a retry or a replay): deduplicate on it."
          },
          "schema_version": {
            "const": 1
          },
          "type": {
            "type": "string",
            "pattern": "^payment\\.(created|pending_approval|uncertain|approved|declined|expired)$"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "merchant_id",
              "reference",
              "amount",
              "currency",
              "state",
              "sequence",
              "sandbox",
              "reason"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "The payment's id, as POST /v1/payments answered it: map the order by it, never by reference."
              },
              "merchant_id": {
                "type": "string"
              },
              "reference": {
                "type": "string",
                "description": "Futa's FT- reference for the payment, not the merchant's order reference."
              },
              "amount": {
                "type": "integer",
                "minimum": 1,
                "description": "ISO minor units (scale 2 for USD and CDF): compare it and currency with the order."
              },
              "currency": {
                "enum": [
                  "USD",
                  "CDF"
                ]
              },
              "state": {
                "enum": [
                  "CREATED",
                  "PENDING_APPROVAL",
                  "UNCERTAIN",
                  "APPROVED",
                  "DECLINED",
                  "EXPIRED"
                ]
              },
              "sequence": {
                "type": "integer",
                "minimum": 0,
                "description": "The payment's version at this state, rising with each change: apply an event only when its sequence is above the last one applied for that payment."
              },
              "sandbox": {
                "const": true
              },
              "reason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Futa's code for the state, or null (for example ISSUER_APPROVED, ISSUER_DECLINED)."
              }
            }
          }
        }
      },
      "RefundEvent": {
        "title": "Refund event v1",
        "description": "A refund changed state: one event per state the refund reaches, its type refund. and the state in lower case. Refunds asked for in the portal are sent too. A notice, not the record: GET /v1/refunds/{id} is authoritative.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "occurred_at",
          "schema_version",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event may arrive more than once (a retry or a replay): deduplicate on it."
          },
          "schema_version": {
            "const": 1
          },
          "type": {
            "type": "string",
            "pattern": "^refund\\.(pending_approval|queued|processing|uncertain|completed|failed|rejected|expired)$"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "payment_id",
              "merchant_id",
              "amount",
              "currency",
              "state",
              "reason",
              "merchant_reason",
              "created_at",
              "issuer_reference",
              "sequence",
              "escalated",
              "sandbox",
              "approval"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "The refund's id, as POST /v1/payments/{id}/refunds answered it."
              },
              "payment_id": {
                "type": "string",
                "format": "uuid",
                "description": "The refunded payment's id, which also names the payment of a refund asked for in the portal."
              },
              "merchant_id": {
                "type": "string"
              },
              "amount": {
                "type": "integer",
                "minimum": 1,
                "maximum": 9000000000000,
                "description": "ISO minor units in the payment's currency; CDF in whole francs."
              },
              "currency": {
                "enum": [
                  "USD",
                  "CDF"
                ]
              },
              "state": {
                "enum": [
                  "PENDING_APPROVAL",
                  "QUEUED",
                  "PROCESSING",
                  "UNCERTAIN",
                  "COMPLETED",
                  "FAILED",
                  "REJECTED",
                  "EXPIRED"
                ],
                "description": "PENDING_APPROVAL waits for a person other than the requester to approve it in the portal; QUEUED, PROCESSING and UNCERTAIN are still open; COMPLETED and FAILED are the issuer's outcome; REJECTED and EXPIRED (not approved in time) end it without sending anything."
              },
              "reason": {
                "type": "string",
                "description": "Futa's code for the state (for example AWAITING_FUNDS, ISSUER_COMPLETED, APPROVAL_REJECTED)."
              },
              "merchant_reason": {
                "type": "string",
                "description": "The reason given when the refund was asked for."
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "issuer_reference": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The issuer's reference for the refund, once it states one."
              },
              "sequence": {
                "type": "integer",
                "minimum": 0,
                "description": "The refund's version at this state, rising with each change: apply an event only when its sequence is above the last one applied for that refund."
              },
              "escalated": {
                "type": "boolean"
              },
              "sandbox": {
                "const": true
              },
              "approval": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "The approval the refund needed above the merchant's threshold, null otherwise.",
                "additionalProperties": false,
                "required": [
                  "expires_at",
                  "requested_by",
                  "approved_by",
                  "rejected_by",
                  "decided_at"
                ],
                "properties": {
                  "expires_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "requested_by": {
                    "type": "string",
                    "description": "The team actor who asked: user:<subject> for a person, key:<id> for an API key."
                  },
                  "approved_by": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "rejected_by": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "decided_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  }
                }
              }
            }
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "enum": [
          "payment.created",
          "payment.pending_approval",
          "payment.uncertain",
          "payment.approved",
          "payment.declined",
          "payment.expired",
          "refund.pending_approval",
          "refund.queued",
          "refund.processing",
          "refund.uncertain",
          "refund.completed",
          "refund.failed",
          "refund.expired",
          "refund.rejected",
          "obligation.collected",
          "payout.pending_approval",
          "payout.completed",
          "payout.failed",
          "payout.rejected",
          "payout.expired",
          "payout_batch.validated",
          "payout_batch.completed"
        ]
      },
      "WebhookSettingsRequest": {
        "type": "object",
        "required": [
          "url",
          "events",
          "version"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 1047,
            "pattern": "^http://127\\.0\\.0\\.1:(?:18160/sandbox/webhooks|1816[1-9](?=/[^?]{0,511}(?:\\?|$))(?:/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})*)+(?:\\?(?=[^#]{1,512}$)(?:(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})|[/?])+)?)$",
            "description": "The destination, as WebhookRequest.url takes it."
          },
          "events": {
            "type": "array",
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            },
            "description": "Empty selects no new events. Selection is captured at enqueue time; queued events retain their original decision."
          },
          "version": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          }
        }
      },
      "WebhookSettingsResult": {
        "type": "object",
        "required": [
          "version"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "PaymentMetrics": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "approved": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "terminal": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "declined": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "expired": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "in_progress": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Payments created or awaiting the payer's approval; uncertain payments are counted separately."
          },
          "uncertain": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "escalated": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "approved_volume": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Exact sum in minor units, represented as a decimal string to avoid JavaScript precision loss. Gross approved volume; refunds are not deducted."
          },
          "success_rate_bps": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 10000,
            "description": "Rounded approved / payments the payer submitted that ended (approved, declined or expired after submission), in basis points; intents that expired unpaid are not failures. Null for zero denominator. `terminal` is unchanged."
          }
        },
        "required": [
          "count",
          "approved",
          "terminal",
          "declined",
          "expired",
          "in_progress",
          "uncertain",
          "escalated",
          "currency",
          "approved_volume",
          "success_rate_bps"
        ]
      },
      "PaymentMethodMetrics": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "approved": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "terminal": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "declined": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "expired": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "in_progress": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Payments created or awaiting the payer's approval; uncertain payments are counted separately."
          },
          "uncertain": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "escalated": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "approved_volume": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Exact sum in minor units, represented as a decimal string to avoid JavaScript precision loss. Gross approved volume; refunds are not deducted."
          },
          "method": {
            "type": "string"
          }
        },
        "required": [
          "count",
          "approved",
          "terminal",
          "declined",
          "expired",
          "in_progress",
          "uncertain",
          "escalated",
          "currency",
          "approved_volume",
          "method"
        ]
      },
      "PaymentPeriod": {
        "type": "object",
        "required": [
          "from",
          "to",
          "currencies",
          "methods"
        ],
        "properties": {
          "from": {
            "type": "string",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "format": "date-time"
          },
          "currencies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentMetrics"
            }
          },
          "methods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentMethodMetrics"
            }
          }
        }
      },
      "PaymentDashboard": {
        "type": "object",
        "required": [
          "timezone",
          "as_of",
          "period",
          "today"
        ],
        "properties": {
          "timezone": {
            "type": "string",
            "enum": [
              "Africa/Kinshasa",
              "Africa/Lubumbashi"
            ]
          },
          "as_of": {
            "type": "string",
            "format": "date-time"
          },
          "period": {
            "$ref": "#/components/schemas/PaymentPeriod"
          },
          "today": {
            "$ref": "#/components/schemas/PaymentPeriod"
          }
        }
      },
      "PaymentLink": {
        "type": "object",
        "required": [
          "id",
          "code",
          "amount_mode",
          "amount",
          "minimum_amount",
          "maximum_amount",
          "currency",
          "usage",
          "description",
          "merchant_reference",
          "created_at",
          "expires_at",
          "cancelled_at",
          "status",
          "paid_count",
          "paid_amount",
          "version",
          "sandbox",
          "branch_id",
          "branch_code",
          "branch_name"
        ],
        "description": "A payment link and what it has collected, in this closed shape only. Lists and details never carry its share URL, a payer's capability or its branding.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "code": {
            "type": "string",
            "pattern": "^LK-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$",
            "description": "Names the link for people; it does not let anyone pay."
          },
          "amount_mode": {
            "type": "string",
            "enum": [
              "FIXED",
              "OPEN"
            ]
          },
          "amount": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "FIXED only, else null. Minor units; CDF in whole francs."
          },
          "minimum_amount": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "OPEN only, when set; else null."
          },
          "maximum_amount": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "OPEN only, when set, not below the minimum; else null."
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "usage": {
            "type": "string",
            "enum": [
              "SINGLE",
              "MULTIPLE"
            ]
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "merchant_reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set once the link is cancelled, and only then."
          },
          "status": {
            "type": "string",
            "enum": [
              "UNPAID",
              "OPEN",
              "PENDING",
              "UNCERTAIN",
              "PAID",
              "EXPIRED",
              "CANCELLED"
            ],
            "description": "CANCELLED; PAID (a single-use link's approved attempt); UNCERTAIN or PENDING (an attempt under way); EXPIRED (past its expiry, whatever it collected); OPEN (a multiple-use link that has received payments and takes more); otherwise UNPAID. A declined attempt frees the link."
          },
          "paid_count": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Approved attempts."
          },
          "paid_amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "description": "Their total in the link's currency, minor units."
          },
          "version": {
            "type": "integer",
            "minimum": 1
          },
          "sandbox": {
            "type": "boolean",
            "const": true
          },
          "branch_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The store the link was made for: branch_id, branch_code and branch_name together, or all null."
          },
          "branch_code": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Z0-9][A-Z0-9_-]{1,31}$"
          },
          "branch_name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 120
          },
          "payments": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/PaymentLinkAttempt"
            },
            "description": "The link's latest attempts, newest first: on its detail and on the answers to its creation and cancellation, never in a list."
          }
        },
        "additionalProperties": false
      },
      "PaymentLinkList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor",
          "counts"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentLink"
            },
            "maxItems": 25,
            "description": "Newest first, without their attempts."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The last link's id when the page is full; null on the last page."
          },
          "counts": {
            "type": "object",
            "required": [
              "links",
              "unpaid",
              "expiring"
            ],
            "properties": {
              "links": {
                "type": "integer",
                "format": "int64",
                "minimum": 0
              },
              "unpaid": {
                "type": "integer",
                "format": "int64",
                "minimum": 0,
                "description": "Links whose status is UNPAID."
              },
              "expiring": {
                "type": "integer",
                "format": "int64",
                "minimum": 0,
                "description": "Links still payable and not yet expired (unpaid, open, pending or uncertain, not cancelled) whose expiry falls before the end of tomorrow in Africa/Kinshasa."
              }
            },
            "additionalProperties": false
          }
        },
        "additionalProperties": false
      },
      "PaymentLinkShare": {
        "type": "object",
        "required": [
          "checkout_url"
        ],
        "properties": {
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "The link's share URL (`/p/{code}?token=…`): a bearer capability to pay through the link. Display only to an authorized link manager; do not cache or log.",
            "pattern": "^https?://[^/?#@]+(/[A-Za-z0-9._~-]+)*/p/LK-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}\\?token=[A-Za-z0-9_-]{43}$"
          }
        },
        "additionalProperties": false
      },
      "MerchantSettlement": {
        "type": "object",
        "required": [
          "id",
          "currency",
          "cutoff",
          "created_at",
          "captured_at",
          "state",
          "destination",
          "net",
          "snapshot_available",
          "sandbox",
          "basis",
          "policy",
          "gross",
          "refunds",
          "prior_settlements",
          "fees",
          "unavailable",
          "verified_destination",
          "reference",
          "period_start",
          "completed_at",
          "sponsor_reference",
          "period_methods",
          "period_refunds",
          "carried"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "cutoff": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "captured_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "state": {
            "type": "string",
            "enum": [
              "PROPOSED",
              "PROCESSING",
              "UNCERTAIN",
              "COMPLETED",
              "FAILED"
            ]
          },
          "destination": {
            "type": "string"
          },
          "net": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Exact minor units as decimal text."
          },
          "snapshot_available": {
            "type": "boolean"
          },
          "sandbox": {
            "type": "boolean",
            "const": true
          },
          "basis": {
            "type": "string",
            "enum": [
              "CUMULATIVE_ELIGIBLE_AT_PROPOSAL"
            ]
          },
          "policy": {
            "type": "string",
            "enum": [
              "synthetic-zero-fee-v1",
              "synthetic-verified-destination-v2",
              "synthetic-cycle-funded-v3"
            ]
          },
          "gross": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9]+$",
            "description": "Exact minor units. Null for historical batches without a captured statement."
          },
          "refunds": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9]+$",
            "description": "Exact minor units. Null for historical batches without a captured statement."
          },
          "prior_settlements": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9]+$",
            "description": "Exact minor units. Null for historical batches without a captured statement."
          },
          "fees": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9]+$",
            "description": "Exact minor units. Null for historical batches without a captured statement."
          },
          "unavailable": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9]+$",
            "description": "Exact minor units. Null for historical batches without a captured statement."
          },
          "verified_destination": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SettlementDestination"
              },
              {
                "type": "null"
              }
            ],
            "description": "This merchant’s frozen destination; null for historical v1 instructions."
          },
          "reference": {
            "type": "string",
            "pattern": "^RG-(USD|CDF)-[0-9A-F]{8}$",
            "description": "Futa's reference for the lot: RG, the currency and the first eight characters of its id."
          },
          "period_start": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cutoff of the previous lot shown to this merchant in the currency with an earlier cutoff (exclusive), or the merchant's first collection made by the cutoff for the first lot (inclusive); null when no collection precedes the cutoff. The period ends at the cutoff."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When Futa recorded the settlement as completed; null otherwise. Never a promised date."
          },
          "sponsor_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "The synthetic sponsor's transfer reference, once the outcome is recorded."
          },
          "period_methods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantSettlementPeriodMethod"
            },
            "description": "The period's recorded collections by method."
          },
          "period_refunds": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "count",
              "amount"
            ],
            "properties": {
              "count": {
                "type": "integer",
                "minimum": 0
              },
              "amount": {
                "type": "string",
                "pattern": "^[0-9]+$",
                "description": "Exact minor units as decimal text."
              }
            },
            "description": "The period's completed refunds."
          },
          "carried": {
            "type": "string",
            "pattern": "^-?[0-9]+$",
            "description": "Net minus the period's settled collections net of its refunds, in minor units: funds carried in from earlier periods less funds held back. The lines never say which payments the lot paid."
          },
          "lines": {
            "type": "array",
            "maxItems": 10000,
            "items": {
              "$ref": "#/components/schemas/MerchantSettlementLine"
            },
            "description": "Detail only: the period's wallet collections and completed refunds as recorded, oldest first; the first 100 (the CSV and PDF hold them all)."
          },
          "line_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Detail only: how many lines the period holds."
          },
          "events": {
            "type": "array",
            "maxItems": 100,
            "description": "Detail only: the lot's follow-up, oldest first, without actors or reasons.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "step",
                "at"
              ],
              "properties": {
                "step": {
                  "type": "string",
                  "enum": [
                    "PROPOSED",
                    "APPROVED",
                    "SENT",
                    "PROCESSING",
                    "UNCERTAIN",
                    "COMPLETED",
                    "FAILED"
                  ],
                  "description": "APPROVED is the second person's approval of the proposal."
                },
                "at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          }
        },
        "additionalProperties": false
      },
      "MerchantSettlementList": {
        "type": "object",
        "required": [
          "data",
          "next_cursor",
          "upcoming"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantSettlement"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "upcoming": {
            "type": "array",
            "maxItems": 2,
            "description": "Per currency, the newest lot shown that is neither completed nor failed: its net, never a date.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "currency",
                "id",
                "reference",
                "net",
                "state",
                "destination",
                "verified_destination"
              ],
              "properties": {
                "currency": {
                  "type": "string",
                  "enum": [
                    "USD",
                    "CDF"
                  ]
                },
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "reference": {
                  "type": "string",
                  "pattern": "^RG-(USD|CDF)-[0-9A-F]{8}$",
                  "description": "Futa's reference for the lot: RG, the currency and the first eight characters of its id."
                },
                "net": {
                  "type": "string",
                  "pattern": "^[0-9]+$",
                  "description": "Exact minor units as decimal text."
                },
                "state": {
                  "type": "string",
                  "enum": [
                    "PROPOSED",
                    "PROCESSING",
                    "UNCERTAIN"
                  ]
                },
                "destination": {
                  "type": "string"
                },
                "verified_destination": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/SettlementDestination"
                    },
                    {
                      "type": "null"
                    }
                  ],
                  "description": "This merchant’s frozen destination; null for historical v1 instructions."
                }
              }
            }
          }
        }
      },
      "SettlementDestination": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "merchant_id",
          "currency",
          "account_id",
          "account_version",
          "verification_id",
          "profile_version",
          "kyb_submission_id",
          "kyb_approval_version",
          "kind",
          "masked_reference"
        ],
        "properties": {
          "merchant_id": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{2,63}$"
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "account_id": {
            "type": "string",
            "format": "uuid"
          },
          "account_version": {
            "type": "integer",
            "minimum": 2,
            "maximum": 201
          },
          "verification_id": {
            "type": "string",
            "format": "uuid"
          },
          "profile_version": {
            "type": "integer",
            "minimum": 1
          },
          "kyb_submission_id": {
            "type": "string",
            "format": "uuid"
          },
          "kyb_approval_version": {
            "type": "integer",
            "minimum": 3
          },
          "kind": {
            "type": "string",
            "enum": [
              "BANK",
              "WALLET"
            ]
          },
          "masked_reference": {
            "type": "string",
            "pattern": "^SYNTH-••••-[A-Z0-9-]{4}$"
          }
        },
        "description": "Frozen synthetic destination evidence. Opaque IDs and masked reference only; no beneficiary names or full account reference."
      },
      "PaymentObligation": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "obligation_id",
          "merchant_reference",
          "amount",
          "currency",
          "merchant_expires_at",
          "head_payment_id",
          "collected_payment_id",
          "lineage_status",
          "synthetic",
          "attempts"
        ],
        "properties": {
          "obligation_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "merchant_reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "amount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ],
            "type": "string"
          },
          "merchant_expires_at": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?Z$"
          },
          "head_payment_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "collected_payment_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "lineage_status": {
            "enum": [
              "CONSISTENT",
              "INVESTIGATION"
            ],
            "type": "string"
          },
          "synthetic": {
            "const": true,
            "type": "boolean"
          },
          "attempts": {
            "type": "array",
            "minItems": 1,
            "maxItems": 65,
            "items": {
              "$ref": "#/components/schemas/PaymentObligationAttempt"
            }
          }
        },
        "description": "Capability-free synthetic obligation lineage. Either a root or child ID returns the same family. A collection identifies the actual attempt for receipts/refunds; original declined attempts remain declined. CONSISTENT describes ancestor closure fences, not a guarantee against all payment risk. Existing dashboards count attempts. CDF amounts are multiples of 100. Sequence 0 has null predecessor/choice; later attempts require their exact previous ID, synthetic choice and approval deadline. The standalone payment-obligation.schema.json and server validate these conditional invariants."
      },
      "PaymentObligationAttempt": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "payment_id",
          "predecessor_id",
          "sequence",
          "choice",
          "state",
          "created_at",
          "approval_deadline"
        ],
        "properties": {
          "payment_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "predecessor_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "sequence": {
            "type": "integer",
            "minimum": 0,
            "maximum": 64
          },
          "choice": {
            "enum": [
              null,
              "synthetic-success",
              "synthetic-decline",
              "synthetic-expiry",
              "synthetic-uncertain",
              "synthetic-late-success",
              "synthetic-unavailable"
            ],
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ],
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?Z$"
          },
          "approval_deadline": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?Z$"
          }
        }
      },
      "ObligationCollectionEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "schema_version",
          "occurred_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "type": {
            "type": "string",
            "enum": [
              "obligation.collected"
            ]
          },
          "schema_version": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "occurred_at": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?Z$"
          },
          "data": {
            "$ref": "#/components/schemas/ObligationCollectionData"
          }
        }
      },
      "ObligationCollectionData": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "obligation_id",
          "collected_payment_id",
          "predecessor_id",
          "approval_event_id",
          "attempt_ids",
          "merchant_id",
          "merchant_reference",
          "amount",
          "currency",
          "synthetic"
        ],
        "properties": {
          "obligation_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "collected_payment_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "predecessor_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "approval_event_id": {
            "type": "string",
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "attempt_ids": {
            "type": "array",
            "minItems": 2,
            "maxItems": 65,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "format": "uuid",
              "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
            }
          },
          "merchant_id": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{2,63}$"
          },
          "merchant_reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "amount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "type": "string",
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "synthetic": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "description": "New replacement collection only. Ordered unique attempt IDs start at obligation_id and end at collected_payment_id, with predecessor_id immediately before it. approval_event_id identifies the separate immutable payment.approved event. CDF amounts are multiples of 100. No retrospective events or root status rewrite."
      },
      "CancelledPayment": {
        "type": "object",
        "required": [
          "id",
          "reference",
          "merchant_reference",
          "branch_id",
          "branch_code",
          "branch_name",
          "merchant_name",
          "description",
          "amount",
          "currency",
          "payer_fee",
          "total",
          "state",
          "reason",
          "created_at",
          "expires_at",
          "approval_deadline",
          "checkout_url",
          "sandbox",
          "method",
          "version",
          "escalated"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": "string"
          },
          "merchant_reference": {
            "type": "string"
          },
          "branch_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "branch_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "branch_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "merchant_name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "payer_fee": {
            "type": "integer",
            "format": "int64",
            "const": 0
          },
          "total": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "state": {
            "type": "string",
            "enum": [
              "EXPIRED"
            ],
            "const": "EXPIRED"
          },
          "reason": {
            "type": "string",
            "enum": [
              "PAYMENT_CANCELLED",
              "LINK_CANCELLED"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "approval_deadline": {
            "type": "null"
          },
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "Bearer capability for this checkout only; do not log or share outside the intended payer."
          },
          "sandbox": {
            "type": "boolean",
            "const": true
          },
          "method": {
            "type": "null"
          },
          "version": {
            "type": "integer",
            "const": 1
          },
          "escalated": {
            "type": "boolean",
            "const": false
          }
        },
        "additionalProperties": false,
        "description": "Retained pre-submission cancellation payment representation; no provider or monetary command.",
        "if": {
          "properties": {
            "currency": {
              "const": "CDF"
            }
          }
        },
        "then": {
          "properties": {
            "amount": {
              "multipleOf": 100
            },
            "total": {
              "multipleOf": 100
            }
          }
        }
      },
      "MerchantPaymentDetail": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "payment_id",
          "reference",
          "merchant_reference",
          "state",
          "sequence",
          "created_at",
          "amount",
          "currency",
          "payer_method",
          "financials",
          "settlement",
          "synthetic"
        ],
        "properties": {
          "schema_version": {
            "const": 1,
            "type": "integer"
          },
          "payment_id": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": "string",
            "maxLength": 128,
            "minLength": 1
          },
          "merchant_reference": {
            "type": "string",
            "maxLength": 100,
            "minLength": 1
          },
          "state": {
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ],
            "type": "string"
          },
          "sequence": {
            "type": "integer",
            "minimum": 0
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "amount": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,12}$"
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ],
            "type": "string"
          },
          "payer_method": {
            "$ref": "#/components/schemas/MerchantPaymentPayerMethod"
          },
          "financials": {
            "$ref": "#/components/schemas/MerchantPaymentFinancials"
          },
          "settlement": {
            "$ref": "#/components/schemas/MerchantPaymentSettlement"
          },
          "synthetic": {
            "const": true,
            "type": "boolean"
          }
        },
        "description": "Passive synthetic evidence. Decimal strings in minor units. Net after refunds is not available settlement balance. Existing aggregate settlement batches cannot be attributed to individual payments. Missing payer identifiers are never inferred.",
        "allOf": [
          {
            "if": {
              "properties": {
                "state": {
                  "const": "APPROVED",
                  "type": "string"
                }
              }
            },
            "then": {
              "properties": {
                "settlement": {
                  "properties": {
                    "attribution": {
                      "const": "UNATTRIBUTED",
                      "type": "string"
                    }
                  }
                },
                "financials": {
                  "properties": {
                    "collected_amount": {
                      "pattern": "^[1-9][0-9]{0,12}$"
                    }
                  }
                }
              }
            },
            "else": {
              "properties": {
                "settlement": {
                  "properties": {
                    "attribution": {
                      "const": "NOT_COLLECTED",
                      "type": "string"
                    }
                  }
                },
                "financials": {
                  "properties": {
                    "collected_amount": {
                      "const": "0",
                      "type": "string"
                    },
                    "net_collected_amount": {
                      "const": "0",
                      "type": "string"
                    },
                    "refunded_amount": {
                      "const": "0",
                      "type": "string"
                    },
                    "refund_reserved_amount": {
                      "const": "0",
                      "type": "string"
                    },
                    "net_after_refunds": {
                      "const": "0",
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "MerchantPaymentTimeline": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "payment_id",
          "current_sequence",
          "data",
          "next_cursor",
          "synthetic"
        ],
        "properties": {
          "payment_id": {
            "type": "string",
            "format": "uuid"
          },
          "current_sequence": {
            "type": "integer",
            "minimum": 0
          },
          "data": {
            "type": "array",
            "maxItems": 25,
            "items": {
              "$ref": "#/components/schemas/MerchantPaymentEvent"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "synthetic": {
            "const": true,
            "type": "boolean"
          }
        },
        "description": "Oldest sequence first, at most 25 retained immutable events. Cursor is an event UUID belonging to this payment. New state events may appear in subsequent pages. No provider credentials or raw messages."
      },
      "MerchantPaymentPayerMethod": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code",
          "identifier_status",
          "masked_identifier"
        ],
        "properties": {
          "code": {
            "enum": [
              null,
              "simulator",
              "synthetic-bank"
            ],
            "type": [
              "string",
              "null"
            ]
          },
          "identifier_status": {
            "enum": [
              "NOT_SELECTED",
              "MASKED",
              "NOT_RETAINED"
            ],
            "type": "string"
          },
          "masked_identifier": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+243•••••[0-9]{4}$"
          }
        },
        "dependentSchemas": {
          "identifier_status": {
            "allOf": [
              {
                "if": {
                  "properties": {
                    "identifier_status": {
                      "const": "MASKED",
                      "type": "string"
                    }
                  }
                },
                "then": {
                  "properties": {
                    "masked_identifier": {
                      "type": "string"
                    },
                    "code": {
                      "const": "simulator",
                      "type": "string"
                    }
                  }
                },
                "else": {
                  "properties": {
                    "masked_identifier": {
                      "const": null,
                      "type": "null"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "identifier_status": {
                      "const": "NOT_SELECTED",
                      "type": "string"
                    }
                  }
                },
                "then": {
                  "properties": {
                    "code": {
                      "const": null,
                      "type": "null"
                    }
                  }
                },
                "else": {
                  "properties": {
                    "code": {
                      "enum": [
                        "simulator",
                        "synthetic-bank"
                      ],
                      "type": "string"
                    }
                  }
                }
              }
            ]
          }
        }
      },
      "MerchantPaymentFinancials": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "policy",
          "collected_amount",
          "merchant_fee",
          "net_collected_amount",
          "refunded_amount",
          "refund_reserved_amount",
          "net_after_refunds"
        ],
        "properties": {
          "policy": {
            "const": "SYNTHETIC_ZERO_FEE",
            "type": "string"
          },
          "collected_amount": {
            "type": "string",
            "pattern": "^(0|[1-9][0-9]{0,12})$"
          },
          "merchant_fee": {
            "const": "0",
            "type": "string"
          },
          "net_collected_amount": {
            "type": "string",
            "pattern": "^(0|[1-9][0-9]{0,12})$"
          },
          "refunded_amount": {
            "type": "string",
            "pattern": "^(0|[1-9][0-9]{0,12})$"
          },
          "refund_reserved_amount": {
            "type": "string",
            "pattern": "^(0|[1-9][0-9]{0,12})$"
          },
          "net_after_refunds": {
            "type": "string",
            "pattern": "^(0|[1-9][0-9]{0,12})$"
          }
        }
      },
      "MerchantPaymentSettlement": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "attribution",
          "batch_id"
        ],
        "properties": {
          "attribution": {
            "enum": [
              "NOT_COLLECTED",
              "UNATTRIBUTED"
            ],
            "type": "string"
          },
          "batch_id": {
            "const": null,
            "type": "null"
          }
        }
      },
      "MerchantPaymentEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "sequence",
          "state",
          "reason",
          "occurred_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sequence": {
            "type": "integer",
            "minimum": 0
          },
          "state": {
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ],
            "type": "string"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 128
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MerchantPaymentPhoneFilters": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "amount": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,12}$"
          },
          "state": {
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ]
          },
          "method": {
            "enum": [
              "simulator",
              "unselected"
            ]
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "maxLength": 128
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "maxLength": 128
          },
          "timezone": {
            "enum": [
              "Africa/Kinshasa",
              "Africa/Lubumbashi"
            ]
          },
          "branch_id": {
            "type": "string",
            "pattern": "^(unassigned|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$"
          }
        }
      },
      "MerchantPaymentPhoneExportFilters": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "amount": {
            "type": "string",
            "pattern": "^[1-9][0-9]{0,12}$"
          },
          "state": {
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ]
          },
          "method": {
            "enum": [
              "simulator",
              "unselected"
            ]
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "maxLength": 128
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "maxLength": 128
          },
          "timezone": {
            "enum": [
              "Africa/Kinshasa",
              "Africa/Lubumbashi"
            ]
          },
          "branch_id": {
            "type": "string",
            "pattern": "^(unassigned|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$"
          }
        },
        "required": [
          "from",
          "to"
        ]
      },
      "MerchantPaymentPhoneRequest": {
        "title": "Private body-only phone lookup",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "phone",
          "filters",
          "cursor"
        ],
        "properties": {
          "phone": {
            "type": "string",
            "pattern": "^\\+243[0-9]{9}$"
          },
          "filters": {
            "$ref": "#/components/schemas/MerchantPaymentPhoneFilters"
          },
          "cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      },
      "MerchantPaymentPhoneExportRequest": {
        "title": "Private body-only phone export",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "phone",
          "filters",
          "format"
        ],
        "properties": {
          "phone": {
            "type": "string",
            "pattern": "^\\+243[0-9]{9}$"
          },
          "filters": {
            "$ref": "#/components/schemas/MerchantPaymentPhoneExportFilters"
          },
          "format": {
            "enum": [
              "csv",
              "xlsx"
            ]
          }
        }
      },
      "MerchantPaymentPhoneRow": {
        "type": "object",
        "required": [
          "id",
          "reference",
          "merchant_reference",
          "amount",
          "currency",
          "state",
          "created_at",
          "method",
          "branch_id",
          "branch_code",
          "branch_name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "merchant_reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "method": {
            "const": "simulator"
          },
          "branch_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "branch_code": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100
          },
          "branch_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 160
          }
        },
        "additionalProperties": false,
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              }
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                }
              }
            }
          }
        ]
      },
      "MerchantPaymentPhonePage": {
        "title": "Indexed merchant payment phone search v1",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "data",
          "next_cursor",
          "coverage",
          "historical_complete"
        ],
        "properties": {
          "data": {
            "type": "array",
            "maxItems": 25,
            "items": {
              "$ref": "#/components/schemas/MerchantPaymentPhoneRow"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "coverage": {
            "const": "INDEXED_PHONE_ADMISSIONS"
          },
          "historical_complete": {
            "const": false
          }
        }
      },
      "PaymentLinkCreate": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "amount_mode",
          "currency",
          "usage",
          "description",
          "merchant_reference",
          "expires_in_hours"
        ],
        "description": "A payment link: a fixed amount, or an open amount the payer chooses within optional bounds, in one currency; single or multiple use; payable until it expires or is cancelled. Amounts are ISO minor units; CDF amounts are whole francs (divisible by 100). Creating a link starts no payment: each payer attempt is a payment bound to the link's terms.",
        "properties": {
          "amount_mode": {
            "type": "string",
            "enum": [
              "FIXED",
              "OPEN"
            ],
            "description": "FIXED takes `amount`; OPEN lets the payer choose, within `minimum_amount` and `maximum_amount` when set."
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "Required with FIXED, absent with OPEN."
          },
          "minimum_amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "OPEN only; optional."
          },
          "maximum_amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "OPEN only; optional; not below the minimum."
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "usage": {
            "type": "string",
            "enum": [
              "SINGLE",
              "MULTIPLE"
            ],
            "description": "SINGLE closes once paid; MULTIPLE takes payments until it expires or is cancelled."
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "merchant_reference": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "expires_in_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "description": "How long the link stays payable from its creation, computed by the server (at most thirty days)."
          },
          "return_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "pattern": "^https://",
            "description": "Optional. Each payment made through the link takes it as its return_url: the hosted checkout shows it as a « Retour à … » link on that payment's outcome pages that offer a way back to the shop, opened exactly as given, with nothing appended and no referrer, and never as proof of payment. Without it those pages offer no way back. It is an absolute https URL with a host and no userinfo; any other address, http included, is refused with 400 INVALID_REQUEST."
          },
          "branch_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Optional active branch owned by this merchant. Attribution is immutable."
          }
        }
      },
      "PaymentLinkAttempt": {
        "type": "object",
        "required": [
          "id",
          "reference",
          "amount",
          "currency",
          "state",
          "created_at"
        ],
        "description": "A payer's attempt through the link: an ordinary payment, read in full through the payments routes.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "reference": {
            "type": "string",
            "pattern": "^FT-[0-9A-F]{32}$",
            "description": "FT- and the payment's id."
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000,
            "description": "The link's amount when fixed, else within its bounds."
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD",
              "CDF"
            ],
            "description": "The link's currency."
          },
          "state": {
            "type": "string",
            "enum": [
              "CREATED",
              "PENDING_APPROVAL",
              "UNCERTAIN",
              "APPROVED",
              "DECLINED",
              "EXPIRED"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "MerchantSettlementPeriodMethod": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "method",
          "count",
          "amount",
          "settled"
        ],
        "properties": {
          "method": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "minimum": 1
          },
          "amount": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Exact minor units as decimal text."
          },
          "settled": {
            "type": "boolean",
            "description": "Whether this engine settles the method's collections; bank collections are counted, never settled here."
          }
        }
      },
      "MerchantSettlementLine": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "id",
          "reference",
          "merchant_reference",
          "method",
          "amount",
          "recorded_at"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "PAYMENT",
              "REFUND"
            ]
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The payment or refund."
          },
          "reference": {
            "type": "string",
            "description": "The payment's Futa reference."
          },
          "merchant_reference": {
            "type": "string"
          },
          "method": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "pattern": "^[0-9]+$",
            "description": "Exact minor units as decimal text."
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MerchantPayoutRequest": {
        "description": "switch-core's merchant-payout-request-v1, relayed in its closed shape. Sandbox only: synthetic participants, synthetic zero fees (synthetic-zero-fee-v1, shown as « [FRAIS] »), never a provider specification. One payout to a sandbox wallet number (+243 and nine digits) or synthetic bank reference (SIM-BANK-NNNN), in integer minor units of its currency (whole francs in CDF), with the name enquiry made for it within 24 hours (POST /v1/name-enquiries) and, when that enquiry could not give the holder's name, the preparer's explicit acknowledgement. Never logged.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "kind",
          "participant",
          "destination",
          "amount",
          "currency",
          "message",
          "merchant_reference",
          "name_enquiry_id",
          "acknowledge_unavailable_name"
        ],
        "properties": {
          "kind": {
            "enum": [
              "WALLET",
              "BANK"
            ]
          },
          "participant": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
          },
          "destination": {
            "type": "string",
            "pattern": "^(\\+243[0-9]{9}|SIM-BANK-[0-9]{4})$"
          },
          "amount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[^\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u061C\\u200B-\\u200F\\u2028-\\u202E\\u2060-\\u206F\\uFEFF\\uFFF9-\\uFFFB\\uD800-\\uDFFF]{1,100}$"
          },
          "merchant_reference": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9_./-]{1,64}$"
          },
          "name_enquiry_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "acknowledge_unavailable_name": {
            "type": "boolean"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "WALLET"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination": {
                  "pattern": "^\\+243[0-9]{9}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "BANK"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination": {
                  "pattern": "^SIM-BANK-[0-9]{4}$"
                }
              }
            }
          }
        ]
      },
      "MerchantPayout": {
        "description": "switch-core's merchant-payout-v1, relayed in its closed shape. Sandbox only: synthetic participants, synthetic zero fees (synthetic-zero-fee-v1, shown as « [FRAIS] »), never a provider specification. One payout: its amount, fee, total and the share of its reservation still held, in integer minor units of its currency, as the ledger sums them; its destination and holder masked, never in full; its state, its approval by a second person and its version. PENDING_APPROVAL holds its money reserved; FAILED, REJECTED and EXPIRED have returned it to the available balance; UNCERTAIN holds it until the participant's answer is known, and is never sent again under another key.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "id",
          "reference",
          "merchant_id",
          "kind",
          "participant",
          "destination_masked",
          "name",
          "amount",
          "fee",
          "total",
          "reserved",
          "currency",
          "fee_policy",
          "message",
          "merchant_reference",
          "state",
          "reason",
          "prepared_by",
          "key_holders",
          "threshold_version",
          "approval",
          "escalated",
          "version",
          "created_at",
          "updated_at",
          "paid_at",
          "sandbox"
        ],
        "properties": {
          "schema_version": {
            "const": 1
          },
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "reference": {
            "type": "string",
            "pattern": "^PO-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "merchant_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
          },
          "kind": {
            "enum": [
              "WALLET",
              "BANK"
            ]
          },
          "participant": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
          },
          "destination_masked": {
            "type": "string",
            "pattern": "^(\\+243•••••[0-9]{4}|SIM-BANK-••[0-9]{2})$"
          },
          "name": {
            "$ref": "#/components/schemas/PayoutNameCheck"
          },
          "amount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "fee": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9000000000000
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "maximum": 18000000000000
          },
          "reserved": {
            "type": "integer",
            "minimum": 0,
            "maximum": 18000000000000
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "fee_policy": {
            "const": "synthetic-zero-fee-v1"
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[^\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u061C\\u200B-\\u200F\\u2028-\\u202E\\u2060-\\u206F\\uFEFF\\uFFF9-\\uFFFB\\uD800-\\uDFFF]{1,100}$"
          },
          "merchant_reference": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9_./-]{1,64}$"
          },
          "state": {
            "enum": [
              "PENDING_APPROVAL",
              "QUEUED",
              "PROCESSING",
              "PAID",
              "FAILED",
              "UNCERTAIN",
              "REJECTED",
              "EXPIRED"
            ]
          },
          "reason": {
            "enum": [
              "UNKNOWN_ACCOUNT",
              "ACCOUNT_SUSPENDED",
              "CREDIT_REFUSED",
              "NAME_CHANGED",
              null
            ]
          },
          "prepared_by": {
            "type": "string",
            "pattern": "^(user|key):[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
          },
          "key_holders": {
            "$ref": "#/components/schemas/PayoutKeyHolders"
          },
          "threshold_version": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647
          },
          "approval": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PayoutApproval"
              }
            ]
          },
          "escalated": {
            "type": "boolean"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "sandbox": {
            "const": true
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                },
                "fee": {
                  "multipleOf": 100
                },
                "total": {
                  "multipleOf": 100
                },
                "reserved": {
                  "multipleOf": 100
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "fee_policy": {
                  "const": "synthetic-zero-fee-v1"
                }
              }
            },
            "then": {
              "properties": {
                "fee": {
                  "const": 0
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "WALLET"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination_masked": {
                  "pattern": "^\\+243•••••[0-9]{4}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "BANK"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination_masked": {
                  "pattern": "^SIM-BANK-••[0-9]{2}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "FAILED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "PENDING_APPROVAL",
                    "QUEUED",
                    "PROCESSING",
                    "PAID",
                    "UNCERTAIN",
                    "REJECTED",
                    "EXPIRED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "PAID"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "paid_at": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "PENDING_APPROVAL",
                    "QUEUED",
                    "PROCESSING",
                    "FAILED",
                    "UNCERTAIN",
                    "REJECTED",
                    "EXPIRED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "paid_at": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "PENDING_APPROVAL",
                    "REJECTED",
                    "EXPIRED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "approval": {
                  "type": "object"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "prepared_by": {
                  "pattern": "^user:"
                }
              }
            },
            "then": {
              "properties": {
                "key_holders": {
                  "properties": {
                    "known": {
                      "const": true
                    },
                    "holders": {
                      "maxItems": 0
                    }
                  }
                }
              }
            },
            "else": {
              "properties": {
                "key_holders": {
                  "not": {
                    "properties": {
                      "known": {
                        "const": true
                      },
                      "holders": {
                        "maxItems": 0
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "PayoutNameCheck": {
        "description": "The name check the payout was prepared with: never an unknown account; an unavailable name acknowledged.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "enquiry_id",
          "status",
          "holder_name_masked",
          "verdict",
          "checked_at",
          "acknowledged_unavailable"
        ],
        "properties": {
          "enquiry_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "status": {
            "enum": [
              "FOUND",
              "UNAVAILABLE"
            ]
          },
          "holder_name_masked": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "pattern": "^[^\\s*]+( [^\\s*]\\*\\*\\*)*$"
          },
          "verdict": {
            "enum": [
              "MATCH",
              "DIFFERENT",
              "UNAVAILABLE",
              null
            ]
          },
          "checked_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "acknowledged_unavailable": {
            "type": "boolean"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "verdict": {
                  "enum": [
                    "DIFFERENT",
                    "UNAVAILABLE"
                  ]
                }
              },
              "required": [
                "verdict"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "status": {
                  "const": "FOUND"
                },
                "verdict": {
                  "enum": [
                    "MATCH",
                    null
                  ]
                }
              },
              "required": [
                "status",
                "verdict"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "status": {
                  "enum": [
                    "FOUND"
                  ]
                }
              },
              "required": [
                "status"
              ]
            },
            "then": {
              "properties": {
                "acknowledged_unavailable": {
                  "const": false
                },
                "verdict": {
                  "enum": [
                    "MATCH",
                    "DIFFERENT",
                    null
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "status": {
                  "enum": [
                    "UNAVAILABLE"
                  ]
                }
              },
              "required": [
                "status"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                },
                "acknowledged_unavailable": {
                  "const": true
                },
                "verdict": {
                  "enum": [
                    "UNAVAILABLE",
                    null
                  ]
                }
              }
            }
          }
        ]
      },
      "PayoutApproval": {
        "description": "The wait for a second person: when it ends, and who decided, when and why; an expiry is no one's decision.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "expires_at",
          "approved_by",
          "rejected_by",
          "decided_at",
          "reason"
        ],
        "properties": {
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "approved_by": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^user:[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
          },
          "rejected_by": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^user:[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
          },
          "decided_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 10,
            "maxLength": 500,
            "pattern": "^[^\\s\\u0000-\\u001F\\u007F-\\u009F]([^\\u0000-\\u001F\\u007F-\\u009F]*[^\\s\\u0000-\\u001F\\u007F-\\u009F])?$"
          }
        },
        "allOf": [
          {
            "not": {
              "properties": {
                "approved_by": {
                  "type": "string"
                },
                "rejected_by": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "decided_at": {
                  "const": null
                }
              }
            },
            "then": {
              "properties": {
                "approved_by": {
                  "const": null
                },
                "rejected_by": {
                  "const": null
                },
                "reason": {
                  "const": null
                }
              }
            },
            "else": {
              "properties": {
                "reason": {
                  "type": "string"
                }
              },
              "anyOf": [
                {
                  "properties": {
                    "approved_by": {
                      "type": "string"
                    }
                  }
                },
                {
                  "properties": {
                    "rejected_by": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          }
        ]
      },
      "PayoutKeyHolders": {
        "description": "The holders of the keys that prepared it, from merchant-platform's key lineage (the people who created or rotated each key, following a key made with another key back to the people who held that one): none when no key prepared it; known false when any holder is not a known person, and then nobody can approve it, the known ones still listed so none of them decides it.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "known",
          "holders"
        ],
        "properties": {
          "known": {
            "type": "boolean"
          },
          "holders": {
            "type": "array",
            "maxItems": 50,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "pattern": "^user:[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
            }
          }
        }
      },
      "MerchantPayoutPage": {
        "description": "A page of payouts, newest first, and the next cursor: the last payout's id.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "data",
          "next_cursor"
        ],
        "properties": {
          "schema_version": {
            "const": 1
          },
          "data": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/MerchantPayout"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      },
      "MerchantPayoutBatch": {
        "description": "switch-core's merchant-payout-batch-v2, relayed in its closed shape; the switch answers v2 alone. Sandbox only: synthetic participants, synthetic zero fees (synthetic-zero-fee-v1, shown as « [FRAIS] »), never a provider specification. One payout batch: one currency, its lines counted by state, its money and its reservation, everyone who prepared it (each actor whose command created it, sent a chunk, decided a line, submitted or retried it, the first its creator) and the holders of every key among them, none of whom decides it, its approval by a second person and its version. LAPSED ends a batch never submitted that stayed seven days unchanged (SYNTHETIC): it never held money, and its place among the merchant's open batches is free again.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "id",
          "reference",
          "merchant_id",
          "currency",
          "state",
          "expected_lines",
          "received_lines",
          "counts",
          "totals",
          "fee_policy",
          "prepared_by",
          "preparers",
          "key_holders",
          "approval",
          "retry_of",
          "version",
          "created_at",
          "updated_at",
          "submitted_at",
          "finished_at",
          "sandbox"
        ],
        "properties": {
          "schema_version": {
            "const": 2
          },
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "reference": {
            "type": "string",
            "pattern": "^PB-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "merchant_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "state": {
            "enum": [
              "UPLOADED",
              "VALIDATING",
              "VALIDATED",
              "PENDING_APPROVAL",
              "APPROVED",
              "PROCESSING",
              "COMPLETED",
              "REJECTED",
              "EXPIRED",
              "LAPSED"
            ]
          },
          "expected_lines": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000
          },
          "received_lines": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "counts": {
            "$ref": "#/components/schemas/PayoutLineCounts"
          },
          "totals": {
            "$ref": "#/components/schemas/PayoutBatchTotals"
          },
          "fee_policy": {
            "const": "synthetic-zero-fee-v1"
          },
          "prepared_by": {
            "type": "string",
            "pattern": "^(user|key):[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
          },
          "preparers": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "pattern": "^(user|key):[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
            }
          },
          "key_holders": {
            "$ref": "#/components/schemas/PayoutKeyHolders"
          },
          "approval": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PayoutApproval"
              }
            ]
          },
          "retry_of": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "submitted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "sandbox": {
            "const": true
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "UPLOADED",
                    "VALIDATING",
                    "VALIDATED",
                    "LAPSED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "approval": {
                  "const": null
                },
                "submitted_at": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "PENDING_APPROVAL",
                    "APPROVED",
                    "PROCESSING",
                    "COMPLETED",
                    "REJECTED",
                    "EXPIRED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "approval": {
                  "type": "object"
                },
                "submitted_at": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "COMPLETED",
                    "REJECTED",
                    "EXPIRED",
                    "LAPSED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "finished_at": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "UPLOADED",
                    "VALIDATING",
                    "VALIDATED",
                    "PENDING_APPROVAL",
                    "APPROVED",
                    "PROCESSING"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "finished_at": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "preparers": {
                  "contains": {
                    "pattern": "^key:"
                  }
                }
              }
            },
            "then": {
              "properties": {
                "key_holders": {
                  "not": {
                    "properties": {
                      "known": {
                        "const": true
                      },
                      "holders": {
                        "maxItems": 0
                      }
                    }
                  }
                }
              }
            },
            "else": {
              "properties": {
                "key_holders": {
                  "properties": {
                    "known": {
                      "const": true
                    },
                    "holders": {
                      "maxItems": 0
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "PayoutLineCounts": {
        "description": "How many lines are in each state: lines, never money.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "valid",
          "review",
          "excluded",
          "pending_approval",
          "queued",
          "processing",
          "paid",
          "failed",
          "uncertain",
          "rejected",
          "expired"
        ],
        "properties": {
          "valid": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "review": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "excluded": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "pending_approval": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "queued": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "processing": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "paid": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "failed": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "uncertain": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "rejected": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          },
          "expired": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000
          }
        }
      },
      "PayoutBatchTotals": {
        "description": "The batch's money in its one currency: its lines' amounts and fees, then paid, returned to available and still reserved, which add up to the total once submitted.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "currency",
          "amount",
          "fee",
          "total",
          "paid",
          "returned",
          "reserved"
        ],
        "properties": {
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "amount": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          },
          "fee": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          },
          "paid": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          },
          "returned": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          },
          "reserved": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                },
                "fee": {
                  "multipleOf": 100
                },
                "total": {
                  "multipleOf": 100
                },
                "paid": {
                  "multipleOf": 100
                },
                "returned": {
                  "multipleOf": 100
                },
                "reserved": {
                  "multipleOf": 100
                }
              }
            }
          }
        ]
      },
      "PayoutBatchCreateRequest": {
        "description": "A new batch: its one currency and how many lines will arrive, at most 100,000.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "currency",
          "expected_lines"
        ],
        "properties": {
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "expected_lines": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000
          }
        }
      },
      "MerchantPayoutBatchPage": {
        "description": "A page of batches, newest first, and the next cursor: the last batch's id.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "data",
          "next_cursor"
        ],
        "properties": {
          "schema_version": {
            "const": 2
          },
          "data": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/MerchantPayoutBatch"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      },
      "PayoutVersionRequest": {
        "description": "A submission or a retry of the failed lines, at the version the person saw.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "expected_version"
        ],
        "properties": {
          "expected_version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          }
        }
      },
      "MerchantPayoutBatchLines": {
        "description": "switch-core's merchant-payout-batch-lines-v2, relayed in its closed shape; the switch answers v2 alone. Sandbox only: synthetic participants, synthetic zero fees (synthetic-zero-fee-v1, shown as « [FRAIS] »), never a provider specification. A page of a batch's lines by number, at most 1,000, and the next cursor: the last line's id.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "batch_id",
          "batch_reference",
          "currency",
          "data",
          "next_cursor"
        ],
        "properties": {
          "schema_version": {
            "const": 2
          },
          "batch_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "batch_reference": {
            "type": "string",
            "pattern": "^PB-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "data": {
            "type": "array",
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/PayoutBatchLine"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      },
      "PayoutBatchLine": {
        "description": "A batch line: its number, its reference (its batch's and its number, or its original line's for a retry), masked, its money in the batch's currency, its state, what the validation found and what the preparer decided. An excluded line keeps the values it arrived with. ROUTE_PAUSED: the line's credit route exists but was switched off, suspended or in maintenance when the line was checked, so it waits for review, answered only by a correction (checked again, its route and its name, once the route reopens) or a removal, never by ACCEPT or KEEP.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "number",
          "reference",
          "kind",
          "participant",
          "destination_masked",
          "holder_name_masked",
          "name_verdict",
          "amount",
          "fee",
          "currency",
          "message",
          "merchant_reference",
          "state",
          "issues",
          "decision",
          "reason",
          "retry_of",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "number": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000
          },
          "reference": {
            "type": "string",
            "pattern": "^PB-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}/[0-9]{6}$"
          },
          "kind": {
            "enum": [
              "WALLET",
              "BANK"
            ]
          },
          "participant": {
            "type": "string",
            "pattern": "^[\\x21-\\x7E]{1,64}$"
          },
          "destination_masked": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^(\\+243•••••[0-9]{4}|SIM-BANK-••[0-9]{2})$"
          },
          "holder_name_masked": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "pattern": "^[^\\s*]+( [^\\s*]\\*\\*\\*)*$"
          },
          "name_verdict": {
            "enum": [
              "MATCH",
              "DIFFERENT",
              "UNAVAILABLE",
              null
            ]
          },
          "amount": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "fee": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9000000000000
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200
          },
          "merchant_reference": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200
          },
          "state": {
            "enum": [
              "VALID",
              "REVIEW",
              "EXCLUDED",
              "PENDING_APPROVAL",
              "QUEUED",
              "PROCESSING",
              "PAID",
              "FAILED",
              "UNCERTAIN",
              "REJECTED",
              "EXPIRED"
            ]
          },
          "issues": {
            "type": "array",
            "maxItems": 13,
            "uniqueItems": true,
            "items": {
              "enum": [
                "NAME_DIFFERENT",
                "NAME_UNAVAILABLE",
                "DUPLICATE_PAYOUT",
                "DUPLICATE_REFERENCE",
                "ROUTE_PAUSED",
                "INVALID_DESTINATION",
                "ROUTE_UNAVAILABLE",
                "INVALID_AMOUNT",
                "MERCHANT_LIMIT_EXCEEDED",
                "UNKNOWN_ACCOUNT",
                "INVALID_EXPECTED_NAME",
                "INVALID_MESSAGE",
                "INVALID_REFERENCE"
              ]
            }
          },
          "decision": {
            "enum": [
              "ACCEPT",
              "KEEP",
              "REMOVE",
              "CORRECT",
              null
            ]
          },
          "reason": {
            "enum": [
              "UNKNOWN_ACCOUNT",
              "ACCOUNT_SUSPENDED",
              "CREDIT_REFUSED",
              "NAME_CHANGED",
              null
            ]
          },
          "retry_of": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          }
        },
        "allOf": [
          {
            "properties": {
              "fee": {
                "const": 0
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "WALLET"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination_masked": {
                  "pattern": "^\\+243•••••[0-9]{4}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "BANK"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination_masked": {
                  "pattern": "^SIM-BANK-••[0-9]{2}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "issues": {
                  "contains": {
                    "enum": [
                      "NAME_DIFFERENT",
                      "NAME_UNAVAILABLE",
                      "INVALID_DESTINATION",
                      "UNKNOWN_ACCOUNT"
                    ]
                  }
                }
              },
              "required": [
                "issues"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "name_verdict": {
                  "enum": [
                    "DIFFERENT",
                    "UNAVAILABLE"
                  ]
                }
              },
              "required": [
                "name_verdict"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "name_verdict": {
                  "enum": [
                    "MATCH"
                  ]
                }
              },
              "required": [
                "name_verdict"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "issues": {
                  "contains": {
                    "enum": [
                      "INVALID_DESTINATION",
                      "UNKNOWN_ACCOUNT",
                      "INVALID_EXPECTED_NAME"
                    ]
                  }
                }
              },
              "required": [
                "issues"
              ]
            },
            "then": {
              "properties": {
                "name_verdict": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "FAILED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "VALID",
                    "REVIEW",
                    "EXCLUDED",
                    "PENDING_APPROVAL",
                    "QUEUED",
                    "PROCESSING",
                    "PAID",
                    "UNCERTAIN",
                    "REJECTED",
                    "EXPIRED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "VALID",
                    "REVIEW",
                    "PENDING_APPROVAL",
                    "QUEUED",
                    "PROCESSING",
                    "PAID",
                    "FAILED",
                    "UNCERTAIN",
                    "REJECTED",
                    "EXPIRED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 9000000000000
                },
                "participant": {
                  "type": "string",
                  "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
                },
                "destination_masked": {
                  "type": "string",
                  "pattern": "^(\\+243•••••[0-9]{4}|SIM-BANK-••[0-9]{2})$"
                },
                "message": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "pattern": "^[^\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u061C\\u200B-\\u200F\\u2028-\\u202E\\u2060-\\u206F\\uFEFF\\uFFF9-\\uFFFB\\uD800-\\uDFFF]{1,100}$"
                },
                "merchant_reference": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "pattern": "^[A-Za-z0-9_./-]{1,64}$"
                }
              },
              "allOf": [
                {
                  "if": {
                    "properties": {
                      "currency": {
                        "const": "CDF"
                      }
                    },
                    "required": [
                      "currency"
                    ]
                  },
                  "then": {
                    "properties": {
                      "amount": {
                        "multipleOf": 100
                      }
                    }
                  }
                }
              ]
            }
          }
        ]
      },
      "PayoutLineInput": {
        "description": "A line as the file (futa-payout-batch-v1) or the API gives it: its number and its values as written, the amount already exact integer minor units. The validation judges them and reports a value outside the terms of a payout as an issue on the line; only their size is bound here. Never logged.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "number",
          "kind",
          "participant",
          "destination",
          "expected_name",
          "amount",
          "currency",
          "message",
          "merchant_reference"
        ],
        "properties": {
          "number": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000
          },
          "kind": {
            "enum": [
              "WALLET",
              "BANK"
            ]
          },
          "participant": {
            "type": "string",
            "pattern": "^[\\x21-\\x7E]{1,64}$"
          },
          "destination": {
            "type": "string",
            "pattern": "^[\\x21-\\x7E]{1,64}$"
          },
          "expected_name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200
          },
          "amount": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200
          },
          "merchant_reference": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200
          }
        }
      },
      "PayoutLineChunk": {
        "description": "One chunk of a batch's lines, at most 1,000, every line in the batch's currency; its number, from 1 to the number of lines the batch announced, keys it, so a chunk sent again changes nothing.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "chunk",
          "lines"
        ],
        "properties": {
          "chunk": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000
          },
          "lines": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/PayoutLineInput"
            }
          }
        }
      },
      "PayoutLineDecisionRequest": {
        "description": "The preparer's decision on a line at the batch's version: accept, keep, remove or correct; a correction carries the line's new values under its own number.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "expected_version",
          "decision",
          "correction"
        ],
        "properties": {
          "expected_version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "decision": {
            "enum": [
              "ACCEPT",
              "KEEP",
              "REMOVE",
              "CORRECT"
            ]
          },
          "correction": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PayoutLineInput"
              }
            ]
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "decision": {
                  "enum": [
                    "CORRECT"
                  ]
                }
              },
              "required": [
                "decision"
              ]
            },
            "then": {
              "properties": {
                "correction": {
                  "type": "object"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "decision": {
                  "enum": [
                    "ACCEPT",
                    "KEEP",
                    "REMOVE"
                  ]
                }
              },
              "required": [
                "decision"
              ]
            },
            "then": {
              "properties": {
                "correction": {
                  "const": null
                }
              }
            }
          }
        ]
      },
      "MerchantPayoutValidationReport": {
        "description": "switch-core's merchant-payout-validation-report-v2, relayed in its closed shape; the switch answers v2 alone. Sandbox only: synthetic participants, synthetic zero fees (synthetic-zero-fee-v1, shown as « [FRAIS] »), never a provider specification. A batch's validation: its real progress (lines checked over lines received), its counts (read, valid, to review, excluded), what its valid lines would reserve, and its issue lines by number, at most 1,000 a page, with the batch's version for the preparer's decisions. A LAPSED batch keeps its progress as the lapse left it.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "batch_id",
          "batch_reference",
          "currency",
          "state",
          "version",
          "progress",
          "counts",
          "totals",
          "issues",
          "next_cursor"
        ],
        "properties": {
          "schema_version": {
            "const": 2
          },
          "batch_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "batch_reference": {
            "type": "string",
            "pattern": "^PB-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "state": {
            "enum": [
              "UPLOADED",
              "VALIDATING",
              "VALIDATED",
              "PENDING_APPROVAL",
              "APPROVED",
              "PROCESSING",
              "COMPLETED",
              "REJECTED",
              "EXPIRED",
              "LAPSED"
            ]
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "progress": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "checked",
              "lines"
            ],
            "properties": {
              "checked": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000
              },
              "lines": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000
              }
            }
          },
          "counts": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "read",
              "valid",
              "review",
              "excluded"
            ],
            "properties": {
              "read": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000
              },
              "valid": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000
              },
              "review": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000
              },
              "excluded": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000
              }
            }
          },
          "totals": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "currency",
              "amount",
              "fee",
              "total"
            ],
            "properties": {
              "currency": {
                "enum": [
                  "CDF",
                  "USD"
                ]
              },
              "amount": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9223372036854776000
              },
              "fee": {
                "const": 0
              },
              "total": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9223372036854776000
              }
            },
            "allOf": [
              {
                "if": {
                  "properties": {
                    "currency": {
                      "const": "CDF"
                    }
                  },
                  "required": [
                    "currency"
                  ]
                },
                "then": {
                  "properties": {
                    "amount": {
                      "multipleOf": 100
                    },
                    "total": {
                      "multipleOf": 100
                    }
                  }
                }
              }
            ]
          },
          "issues": {
            "type": "array",
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/PayoutIssueLine"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      },
      "PayoutIssueLine": {
        "description": "A line the validation flagged, or the preparer removed: masked, with its issues and the decision taken. ROUTE_PAUSED is answered only by a correction or a removal.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "line_id",
          "number",
          "destination_masked",
          "holder_name_masked",
          "amount",
          "currency",
          "state",
          "issues",
          "decision"
        ],
        "properties": {
          "line_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "number": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100000
          },
          "destination_masked": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^(\\+243•••••[0-9]{4}|SIM-BANK-••[0-9]{2})$"
          },
          "holder_name_masked": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "pattern": "^[^\\s*]+( [^\\s*]\\*\\*\\*)*$"
          },
          "amount": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "state": {
            "enum": [
              "VALID",
              "REVIEW",
              "EXCLUDED"
            ]
          },
          "issues": {
            "type": "array",
            "maxItems": 13,
            "uniqueItems": true,
            "items": {
              "enum": [
                "NAME_DIFFERENT",
                "NAME_UNAVAILABLE",
                "DUPLICATE_PAYOUT",
                "DUPLICATE_REFERENCE",
                "ROUTE_PAUSED",
                "INVALID_DESTINATION",
                "ROUTE_UNAVAILABLE",
                "INVALID_AMOUNT",
                "MERCHANT_LIMIT_EXCEEDED",
                "UNKNOWN_ACCOUNT",
                "INVALID_EXPECTED_NAME",
                "INVALID_MESSAGE",
                "INVALID_REFERENCE"
              ]
            }
          },
          "decision": {
            "enum": [
              "ACCEPT",
              "KEEP",
              "REMOVE",
              "CORRECT",
              null
            ]
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "issues": {
                  "contains": {
                    "enum": [
                      "NAME_DIFFERENT",
                      "NAME_UNAVAILABLE",
                      "INVALID_DESTINATION",
                      "UNKNOWN_ACCOUNT"
                    ]
                  }
                }
              },
              "required": [
                "issues"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                }
              }
            }
          }
        ]
      },
      "MerchantPayoutBalance": {
        "description": "switch-core's merchant-payout-balance-v1, relayed in its closed shape. Sandbox only: synthetic participants, synthetic zero fees (synthetic-zero-fee-v1, shown as « [FRAIS] »), never a provider specification. The merchant's payout balance in both currencies, never summed and never converted: available and reserved as switch-core's ledger sums them, each currency's low-balance alert threshold, and the merchant's one funding reference PF-…, which a top-up carries. Collections and settlements never feed it.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "merchant_id",
          "funding_reference",
          "balances",
          "as_of",
          "sandbox"
        ],
        "properties": {
          "schema_version": {
            "const": 1
          },
          "merchant_id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
          },
          "funding_reference": {
            "type": "string",
            "pattern": "^PF-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "balances": {
            "type": "array",
            "minItems": 2,
            "maxItems": 2,
            "prefixItems": [
              {
                "$ref": "#/components/schemas/PayoutCurrencyBalance",
                "properties": {
                  "currency": {
                    "const": "CDF"
                  }
                }
              },
              {
                "$ref": "#/components/schemas/PayoutCurrencyBalance",
                "properties": {
                  "currency": {
                    "const": "USD"
                  }
                }
              }
            ],
            "items": false
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "sandbox": {
            "const": true
          }
        }
      },
      "PayoutCurrencyBalance": {
        "description": "One currency's payout balance as the ledger sums it, its alert threshold and whether the balance is below it; funded false before any top-up reached it.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "currency",
          "available",
          "reserved",
          "alert_threshold",
          "alert_version",
          "below_alert",
          "funded"
        ],
        "properties": {
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "available": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          },
          "reserved": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9223372036854776000
          },
          "alert_threshold": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 9000000000000
          },
          "alert_version": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647
          },
          "below_alert": {
            "type": "boolean"
          },
          "funded": {
            "type": "boolean"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "available": {
                  "multipleOf": 100
                },
                "reserved": {
                  "multipleOf": 100
                },
                "alert_threshold": {
                  "multipleOf": 100
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "alert_threshold": {
                  "const": null
                }
              }
            },
            "then": {
              "properties": {
                "below_alert": {
                  "const": false
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "funded": {
                  "const": false
                }
              }
            },
            "then": {
              "properties": {
                "available": {
                  "const": 0
                },
                "reserved": {
                  "const": 0
                }
              }
            }
          }
        ]
      },
      "PayoutNameEnquiry": {
        "description": "switch-core's payout-name-enquiry-v1, relayed in its closed shape. Sandbox only: synthetic participants, synthetic zero fees (synthetic-zero-fee-v1, shown as « [FRAIS] »), never a provider specification. A destination's holder as the participant answered, masked (holder_name_masked), never the full name, and the verdict against the expected name when one was given; a name that differs from the expected one, or that the participant could not give, is never shown, not even masked. A check is kept 24 hours; an enquiry within them answers the same check, cached. Never repeats the destination or the expected name.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "synthetic",
          "id",
          "kind",
          "participant",
          "destination_masked",
          "status",
          "holder_name_masked",
          "reason",
          "expected_name_given",
          "verdict",
          "checked_at",
          "expires_at",
          "cached"
        ],
        "properties": {
          "schema_version": {
            "const": 1
          },
          "synthetic": {
            "const": true
          },
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "kind": {
            "enum": [
              "WALLET",
              "BANK"
            ]
          },
          "participant": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
          },
          "destination_masked": {
            "type": "string",
            "pattern": "^(\\+243•••••[0-9]{4}|SIM-BANK-••[0-9]{2})$"
          },
          "status": {
            "enum": [
              "FOUND",
              "UNKNOWN_ACCOUNT",
              "UNAVAILABLE"
            ]
          },
          "holder_name_masked": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "pattern": "^[^\\s*]+( [^\\s*]\\*\\*\\*)*$"
          },
          "reason": {
            "enum": [
              "NOT_CONTRACTED",
              null
            ]
          },
          "expected_name_given": {
            "type": "boolean"
          },
          "verdict": {
            "enum": [
              "MATCH",
              "DIFFERENT",
              "UNAVAILABLE",
              null
            ]
          },
          "checked_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "cached": {
            "type": "boolean"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "WALLET"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination_masked": {
                  "pattern": "^\\+243•••••[0-9]{4}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "BANK"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination_masked": {
                  "pattern": "^SIM-BANK-••[0-9]{2}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "verdict": {
                  "enum": [
                    "DIFFERENT",
                    "UNAVAILABLE"
                  ]
                }
              },
              "required": [
                "verdict"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "status": {
                  "const": "FOUND"
                },
                "verdict": {
                  "enum": [
                    "MATCH",
                    null
                  ]
                }
              },
              "required": [
                "status",
                "verdict"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "type": "string"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "status": {
                  "enum": [
                    "FOUND"
                  ]
                }
              },
              "required": [
                "status"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "const": null
                },
                "verdict": {
                  "enum": [
                    "MATCH",
                    "DIFFERENT",
                    null
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "status": {
                  "enum": [
                    "UNKNOWN_ACCOUNT"
                  ]
                }
              },
              "required": [
                "status"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                },
                "reason": {
                  "const": null
                },
                "verdict": {
                  "enum": [
                    "UNAVAILABLE",
                    null
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "status": {
                  "enum": [
                    "UNAVAILABLE"
                  ]
                }
              },
              "required": [
                "status"
              ]
            },
            "then": {
              "properties": {
                "holder_name_masked": {
                  "const": null
                },
                "reason": {
                  "const": "NOT_CONTRACTED"
                },
                "verdict": {
                  "enum": [
                    "UNAVAILABLE",
                    null
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "expected_name_given": {
                  "const": false
                }
              }
            },
            "then": {
              "properties": {
                "verdict": {
                  "const": null
                }
              }
            },
            "else": {
              "properties": {
                "verdict": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "PayoutNameEnquiryRequest": {
        "description": "The destination in the body only, and the name the merchant expects, if any. Never logged.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "kind",
          "participant",
          "destination",
          "expected_name"
        ],
        "properties": {
          "kind": {
            "enum": [
              "WALLET",
              "BANK"
            ]
          },
          "participant": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
          },
          "destination": {
            "type": "string",
            "pattern": "^(\\+243[0-9]{9}|SIM-BANK-[0-9]{4})$"
          },
          "expected_name": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[^\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u061C\\u200B-\\u200F\\u2028-\\u202E\\u2060-\\u206F\\uFEFF\\uFFF9-\\uFFFB\\uD800-\\uDFFF]{1,100}$"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "WALLET"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination": {
                  "pattern": "^\\+243[0-9]{9}$"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "kind": {
                  "enum": [
                    "BANK"
                  ]
                }
              },
              "required": [
                "kind"
              ]
            },
            "then": {
              "properties": {
                "destination": {
                  "pattern": "^SIM-BANK-[0-9]{4}$"
                }
              }
            }
          }
        ]
      },
      "PayoutShortfall": {
        "description": "What the submission lacks to be reserved, in integer minor units of its currency.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9223372036854776000
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                }
              }
            }
          }
        ]
      },
      "MerchantPayoutError": {
        "description": "The body of every refusal a payout route's handler gives: switch-core's merchant-payout-error-v1, its codes and this API's own on the payout routes, each with this API's French and English messages. Only INSUFFICIENT_PAYOUT_BALANCE carries a detail, its shortfall in integer minor units of the submission's currency; every other code carries null. The refusals of the key, the scope, the body's size or media type and the request quota (401 INVALID_API_KEY, 403 INSUFFICIENT_SCOPE, 413 REQUEST_TOO_LARGE, 415 UNSUPPORTED_MEDIA_TYPE, 429 RATE_LIMITED) are the Error body, and a file the upload refuses is PayoutFileError.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "oneOf": [
              {
                "description": "The balance refusal, with its shortfall.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "code",
                  "message_en",
                  "message_fr",
                  "shortfall"
                ],
                "properties": {
                  "code": {
                    "const": "INSUFFICIENT_PAYOUT_BALANCE"
                  },
                  "message_en": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "message_fr": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "shortfall": {
                    "$ref": "#/components/schemas/PayoutShortfall"
                  }
                }
              },
              {
                "description": "Every other refusal, with no detail.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "code",
                  "message_en",
                  "message_fr",
                  "shortfall"
                ],
                "properties": {
                  "code": {
                    "enum": [
                      "PAYOUT_SECOND_APPROVER_REQUIRED",
                      "PAYOUT_REQUESTER_UNKNOWN",
                      "PAYOUT_APPROVER_PERSON_REQUIRED",
                      "PAYOUT_NOT_AWAITING_APPROVAL",
                      "PAYOUT_APPROVAL_EXPIRED",
                      "PAYOUT_VERSION_CHANGED",
                      "PAYOUT_BATCH_TOO_LARGE",
                      "PAYOUT_BATCH_INVALID",
                      "PAYOUT_BATCH_NOT_VALIDATED",
                      "PAYOUT_BATCH_CHANGED",
                      "PAYOUT_LINE_NOT_RETRYABLE",
                      "NAME_ENQUIRY_STALE",
                      "MERCHANT_LIMIT_EXCEEDED",
                      "PAYOUTS_HELD",
                      "INVALID_REQUEST",
                      "IDEMPOTENCY_CONFLICT",
                      "PAYOUT_NOT_FOUND",
                      "PAYOUT_BATCH_NOT_FOUND",
                      "PAYOUT_LINE_NOT_FOUND",
                      "NAME_ENQUIRY_NOT_FOUND",
                      "PAYOUT_ROUTE_UNAVAILABLE",
                      "PAYOUT_DESTINATION_UNKNOWN",
                      "NAME_ACKNOWLEDGEMENT_REQUIRED",
                      "PAYOUT_THRESHOLD_PROPOSAL_PENDING",
                      "MEMBERSHIP_REQUIRED",
                      "MEMBERSHIP_CHANGED",
                      "ROLE_DENIED",
                      "STEP_UP_REQUIRED",
                      "NAME_ENQUIRY_RATE_LIMITED",
                      "RESULT_NOT_READY",
                      "SWITCH_UNAVAILABLE",
                      "TOP_UP_UNAVAILABLE",
                      "PAYOUT_FILE_INVALID"
                    ]
                  },
                  "message_en": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "message_fr": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300
                  },
                  "shortfall": {
                    "type": "null"
                  }
                }
              }
            ]
          }
        }
      },
      "PayoutFileJob": {
        "description": "switch-core's merchant-payout-batch-v2 file_job: a batch's report or result file still being made, the answer (202) of its read until it is, asynchronously: ask again after Retry-After; the same GET then answers the file as the futa-payout-batch-v1 format writes it (« Files Futa writes »). Nothing else is promised about when.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "batch_id",
          "file",
          "requested_at",
          "state"
        ],
        "properties": {
          "batch_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "file": {
            "enum": [
              "REPORT",
              "RESULT"
            ]
          },
          "requested_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "state": {
            "const": "PREPARING"
          }
        }
      },
      "PayoutTopUpRequest": {
        "description": "A synthetic top-up of the sandbox payout balance: the amount the sponsor simulator notes as credited to the payout escrow under the merchant's funding reference, in integer minor units (whole francs in CDF). Test keys only; no real money and no withdrawal.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          }
        },
        "if": {
          "properties": {
            "currency": {
              "const": "CDF"
            }
          },
          "required": [
            "currency"
          ]
        },
        "then": {
          "properties": {
            "amount": {
              "multipleOf": 100
            }
          }
        }
      },
      "PayoutTopUp": {
        "description": "The sponsor simulator's synthetic credit notice for a top-up, carrying the merchant's funding reference: a notice, not the balance. switch-core reads the sponsor's notices in order and posts each once; GET /v1/balance shows the money only once it has, never before.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "synthetic",
          "notice_id",
          "funding_reference",
          "amount",
          "currency",
          "credited_at"
        ],
        "properties": {
          "schema_version": {
            "const": 1
          },
          "synthetic": {
            "const": true
          },
          "notice_id": {
            "type": "string",
            "pattern": "^SIM-SN-[0-9]{12}$"
          },
          "funding_reference": {
            "type": "string",
            "pattern": "^PF-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "amount": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "enum": [
              "CDF",
              "USD"
            ]
          },
          "credited_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$",
            "description": "When the synthetic escrow was credited, by the simulator."
          }
        },
        "if": {
          "properties": {
            "currency": {
              "const": "CDF"
            }
          },
          "required": [
            "currency"
          ]
        },
        "then": {
          "properties": {
            "amount": {
              "multipleOf": 100
            }
          }
        }
      },
      "PayoutEvent": {
        "title": "PayoutEvent",
        "description": "A payout reached a state a merchant is told of: payout.pending_approval, payout.completed (PAID), payout.failed, payout.rejected and payout.expired; QUEUED, PROCESSING and UNCERTAIN send none. Its data is the payout as switch-core's outbox writes it (payout-event-v1), payout.paid named payout.completed and its key holders reduced to whether all are known, as switch-core's own rules hold them. A notice, not the record: GET /v1/payouts/{id} is authoritative. Delivered since 0.59.17 to an endpoint that chose it; switch-core's queued, processing and uncertain states are never sent, nor held.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "schema_version",
          "occurred_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "type": {
            "enum": [
              "payout.pending_approval",
              "payout.completed",
              "payout.failed",
              "payout.rejected",
              "payout.expired"
            ]
          },
          "schema_version": {
            "const": 1
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "schema_version",
              "id",
              "reference",
              "merchant_id",
              "kind",
              "participant",
              "destination_masked",
              "name",
              "amount",
              "fee",
              "total",
              "reserved",
              "currency",
              "fee_policy",
              "message",
              "merchant_reference",
              "state",
              "reason",
              "prepared_by",
              "key_holders",
              "threshold_version",
              "approval",
              "escalated",
              "version",
              "created_at",
              "updated_at",
              "paid_at",
              "sandbox"
            ],
            "properties": {
              "schema_version": {
                "const": 1
              },
              "id": {
                "type": "string",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
              },
              "reference": {
                "type": "string",
                "pattern": "^PO-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
              },
              "merchant_id": {
                "type": "string",
                "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
              },
              "kind": {
                "enum": [
                  "WALLET",
                  "BANK"
                ]
              },
              "participant": {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
              },
              "destination_masked": {
                "type": "string",
                "pattern": "^(\\+243•••••[0-9]{4}|SIM-BANK-••[0-9]{2})$"
              },
              "name": {
                "description": "The name check the payout was prepared with: never an unknown account; an unavailable name acknowledged.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "enquiry_id",
                  "status",
                  "holder_name_masked",
                  "verdict",
                  "checked_at",
                  "acknowledged_unavailable"
                ],
                "properties": {
                  "enquiry_id": {
                    "type": "string",
                    "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
                  },
                  "status": {
                    "enum": [
                      "FOUND",
                      "UNAVAILABLE"
                    ]
                  },
                  "holder_name_masked": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 80,
                    "pattern": "^[^\\s*]+( [^\\s*]\\*\\*\\*)*$"
                  },
                  "verdict": {
                    "enum": [
                      "MATCH",
                      "DIFFERENT",
                      "UNAVAILABLE",
                      null
                    ]
                  },
                  "checked_at": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
                  },
                  "acknowledged_unavailable": {
                    "type": "boolean"
                  }
                },
                "allOf": [
                  {
                    "if": {
                      "properties": {
                        "verdict": {
                          "enum": [
                            "DIFFERENT",
                            "UNAVAILABLE"
                          ]
                        }
                      },
                      "required": [
                        "verdict"
                      ]
                    },
                    "then": {
                      "properties": {
                        "holder_name_masked": {
                          "const": null
                        }
                      }
                    }
                  },
                  {
                    "if": {
                      "properties": {
                        "status": {
                          "const": "FOUND"
                        },
                        "verdict": {
                          "enum": [
                            "MATCH",
                            null
                          ]
                        }
                      },
                      "required": [
                        "status",
                        "verdict"
                      ]
                    },
                    "then": {
                      "properties": {
                        "holder_name_masked": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  {
                    "if": {
                      "properties": {
                        "status": {
                          "enum": [
                            "FOUND"
                          ]
                        }
                      },
                      "required": [
                        "status"
                      ]
                    },
                    "then": {
                      "properties": {
                        "acknowledged_unavailable": {
                          "const": false
                        },
                        "verdict": {
                          "enum": [
                            "MATCH",
                            "DIFFERENT",
                            null
                          ]
                        }
                      }
                    }
                  },
                  {
                    "if": {
                      "properties": {
                        "status": {
                          "enum": [
                            "UNAVAILABLE"
                          ]
                        }
                      },
                      "required": [
                        "status"
                      ]
                    },
                    "then": {
                      "properties": {
                        "holder_name_masked": {
                          "const": null
                        },
                        "acknowledged_unavailable": {
                          "const": true
                        },
                        "verdict": {
                          "enum": [
                            "UNAVAILABLE",
                            null
                          ]
                        }
                      }
                    }
                  }
                ]
              },
              "amount": {
                "type": "integer",
                "minimum": 1,
                "maximum": 9000000000000
              },
              "fee": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9000000000000
              },
              "total": {
                "type": "integer",
                "minimum": 0,
                "maximum": 18000000000000
              },
              "reserved": {
                "type": "integer",
                "minimum": 0,
                "maximum": 18000000000000
              },
              "currency": {
                "enum": [
                  "CDF",
                  "USD"
                ]
              },
              "fee_policy": {
                "const": "synthetic-zero-fee-v1"
              },
              "message": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^[^\\u0000-\\u001F\\u007F-\\u009F\\u00AD\\u061C\\u200B-\\u200F\\u2028-\\u202E\\u2060-\\u206F\\uFEFF\\uFFF9-\\uFFFB\\uD800-\\uDFFF]{1,100}$"
              },
              "merchant_reference": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^[A-Za-z0-9_./-]{1,64}$"
              },
              "state": {
                "enum": [
                  "PENDING_APPROVAL",
                  "PAID",
                  "FAILED",
                  "REJECTED",
                  "EXPIRED"
                ]
              },
              "reason": {
                "enum": [
                  "UNKNOWN_ACCOUNT",
                  "ACCOUNT_SUSPENDED",
                  "CREDIT_REFUSED",
                  "NAME_CHANGED",
                  null
                ]
              },
              "prepared_by": {
                "type": "string",
                "pattern": "^(user|key):[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
              },
              "key_holders": {
                "description": "Whether every holder of the keys that prepared it is a known person: known false means nobody can approve it. The holders themselves are never sent to an endpoint; an authenticated read lists them.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "known"
                ],
                "properties": {
                  "known": {
                    "type": "boolean"
                  }
                }
              },
              "threshold_version": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2147483647
              },
              "approval": {
                "anyOf": [
                  {
                    "type": "null"
                  },
                  {
                    "description": "The wait for a second person: when it ends, and who decided, when and why; an expiry is no one's decision.",
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "expires_at",
                      "approved_by",
                      "rejected_by",
                      "decided_at",
                      "reason"
                    ],
                    "properties": {
                      "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
                      },
                      "approved_by": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "pattern": "^user:[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
                      },
                      "rejected_by": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "pattern": "^user:[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
                      },
                      "decided_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
                      },
                      "reason": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "minLength": 10,
                        "maxLength": 500,
                        "pattern": "^[^\\s\\u0000-\\u001F\\u007F-\\u009F]([^\\u0000-\\u001F\\u007F-\\u009F]*[^\\s\\u0000-\\u001F\\u007F-\\u009F])?$"
                      }
                    },
                    "allOf": [
                      {
                        "not": {
                          "properties": {
                            "approved_by": {
                              "type": "string"
                            },
                            "rejected_by": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      {
                        "if": {
                          "properties": {
                            "decided_at": {
                              "const": null
                            }
                          }
                        },
                        "then": {
                          "properties": {
                            "approved_by": {
                              "const": null
                            },
                            "rejected_by": {
                              "const": null
                            },
                            "reason": {
                              "const": null
                            }
                          }
                        },
                        "else": {
                          "properties": {
                            "reason": {
                              "type": "string"
                            }
                          },
                          "anyOf": [
                            {
                              "properties": {
                                "approved_by": {
                                  "type": "string"
                                }
                              }
                            },
                            {
                              "properties": {
                                "rejected_by": {
                                  "type": "string"
                                }
                              }
                            }
                          ]
                        }
                      }
                    ]
                  }
                ]
              },
              "escalated": {
                "type": "boolean"
              },
              "version": {
                "type": "integer",
                "minimum": 1,
                "maximum": 2147483647
              },
              "created_at": {
                "type": "string",
                "format": "date-time",
                "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time",
                "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
              },
              "paid_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
              },
              "sandbox": {
                "const": true
              }
            },
            "allOf": [
              {
                "if": {
                  "properties": {
                    "currency": {
                      "const": "CDF"
                    }
                  },
                  "required": [
                    "currency"
                  ]
                },
                "then": {
                  "properties": {
                    "amount": {
                      "multipleOf": 100
                    },
                    "fee": {
                      "multipleOf": 100
                    },
                    "total": {
                      "multipleOf": 100
                    },
                    "reserved": {
                      "multipleOf": 100
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "fee_policy": {
                      "const": "synthetic-zero-fee-v1"
                    }
                  }
                },
                "then": {
                  "properties": {
                    "fee": {
                      "const": 0
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "kind": {
                      "enum": [
                        "WALLET"
                      ]
                    }
                  },
                  "required": [
                    "kind"
                  ]
                },
                "then": {
                  "properties": {
                    "destination_masked": {
                      "pattern": "^\\+243•••••[0-9]{4}$"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "kind": {
                      "enum": [
                        "BANK"
                      ]
                    }
                  },
                  "required": [
                    "kind"
                  ]
                },
                "then": {
                  "properties": {
                    "destination_masked": {
                      "pattern": "^SIM-BANK-••[0-9]{2}$"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "FAILED"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "PENDING_APPROVAL",
                        "QUEUED",
                        "PROCESSING",
                        "PAID",
                        "UNCERTAIN",
                        "REJECTED",
                        "EXPIRED"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "reason": {
                      "const": null
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "PAID"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "paid_at": {
                      "type": "string"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "PENDING_APPROVAL",
                        "QUEUED",
                        "PROCESSING",
                        "FAILED",
                        "UNCERTAIN",
                        "REJECTED",
                        "EXPIRED"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "paid_at": {
                      "const": null
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "PENDING_APPROVAL",
                        "REJECTED",
                        "EXPIRED"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "approval": {
                      "type": "object"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "prepared_by": {
                      "pattern": "^user:"
                    }
                  }
                },
                "then": {
                  "properties": {
                    "key_holders": {
                      "properties": {
                        "known": {
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            ],
            "description": "The payout as GET /v1/payouts/{id} answers it at this state, its key holders reduced to whether all are known."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "type": {
                  "const": "payout.pending_approval"
                }
              }
            },
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "state": {
                      "const": "PENDING_APPROVAL"
                    }
                  }
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "payout.completed"
                }
              }
            },
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "state": {
                      "const": "PAID"
                    }
                  }
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "payout.failed"
                }
              }
            },
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "state": {
                      "const": "FAILED"
                    }
                  }
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "payout.rejected"
                }
              }
            },
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "state": {
                      "const": "REJECTED"
                    }
                  }
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "payout.expired"
                }
              }
            },
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "state": {
                      "const": "EXPIRED"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "PayoutBatchEvent": {
        "title": "PayoutBatchEvent",
        "description": "A payout batch was validated (its lines judged, ready for the preparer's decisions) or completed (every line final); its lines emit no event of their own. Its data is the batch as switch-core's outbox writes it (payout-batch-event-v1), its key holders reduced to whether all are known, as switch-core's own rules hold them. A notice, not the record: GET /v1/payout-batches/{id} is authoritative. Delivered since 0.59.17 to an endpoint that chose it; switch-core's queued, processing and uncertain states are never sent, nor held.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "schema_version",
          "occurred_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "type": {
            "enum": [
              "payout_batch.validated",
              "payout_batch.completed"
            ]
          },
          "schema_version": {
            "const": 1
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "schema_version",
              "id",
              "reference",
              "merchant_id",
              "currency",
              "state",
              "expected_lines",
              "received_lines",
              "counts",
              "totals",
              "fee_policy",
              "prepared_by",
              "preparers",
              "key_holders",
              "approval",
              "retry_of",
              "version",
              "created_at",
              "updated_at",
              "submitted_at",
              "finished_at",
              "sandbox"
            ],
            "properties": {
              "schema_version": {
                "const": 1
              },
              "id": {
                "type": "string",
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
              },
              "reference": {
                "type": "string",
                "pattern": "^PB-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
              },
              "merchant_id": {
                "type": "string",
                "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
              },
              "currency": {
                "enum": [
                  "CDF",
                  "USD"
                ]
              },
              "state": {
                "enum": [
                  "VALIDATED",
                  "COMPLETED"
                ]
              },
              "expected_lines": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100000
              },
              "received_lines": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100000
              },
              "counts": {
                "description": "How many lines are in each state: lines, never money.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "valid",
                  "review",
                  "excluded",
                  "pending_approval",
                  "queued",
                  "processing",
                  "paid",
                  "failed",
                  "uncertain",
                  "rejected",
                  "expired"
                ],
                "properties": {
                  "valid": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "review": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "excluded": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "pending_approval": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "queued": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "processing": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "paid": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "failed": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "uncertain": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "rejected": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  },
                  "expired": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 100000
                  }
                }
              },
              "totals": {
                "description": "The batch's money in its one currency: its lines' amounts and fees, then paid, returned to available and still reserved, which add up to the total once submitted.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "currency",
                  "amount",
                  "fee",
                  "total",
                  "paid",
                  "returned",
                  "reserved"
                ],
                "properties": {
                  "currency": {
                    "enum": [
                      "CDF",
                      "USD"
                    ]
                  },
                  "amount": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9223372036854776000
                  },
                  "fee": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9223372036854776000
                  },
                  "total": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9223372036854776000
                  },
                  "paid": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9223372036854776000
                  },
                  "returned": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9223372036854776000
                  },
                  "reserved": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9223372036854776000
                  }
                },
                "allOf": [
                  {
                    "if": {
                      "properties": {
                        "currency": {
                          "const": "CDF"
                        }
                      },
                      "required": [
                        "currency"
                      ]
                    },
                    "then": {
                      "properties": {
                        "amount": {
                          "multipleOf": 100
                        },
                        "fee": {
                          "multipleOf": 100
                        },
                        "total": {
                          "multipleOf": 100
                        },
                        "paid": {
                          "multipleOf": 100
                        },
                        "returned": {
                          "multipleOf": 100
                        },
                        "reserved": {
                          "multipleOf": 100
                        }
                      }
                    }
                  }
                ]
              },
              "fee_policy": {
                "const": "synthetic-zero-fee-v1"
              },
              "prepared_by": {
                "type": "string",
                "pattern": "^(user|key):[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
              },
              "preparers": {
                "type": "array",
                "minItems": 1,
                "maxItems": 100,
                "uniqueItems": true,
                "items": {
                  "type": "string",
                  "pattern": "^(user|key):[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
                }
              },
              "key_holders": {
                "description": "Whether every holder of the keys that prepared it is a known person: known false means nobody can approve it. The holders themselves are never sent to an endpoint; an authenticated read lists them.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "known"
                ],
                "properties": {
                  "known": {
                    "type": "boolean"
                  }
                }
              },
              "approval": {
                "anyOf": [
                  {
                    "type": "null"
                  },
                  {
                    "description": "The wait for a second person: when it ends, and who decided, when and why; an expiry is no one's decision.",
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "expires_at",
                      "approved_by",
                      "rejected_by",
                      "decided_at",
                      "reason"
                    ],
                    "properties": {
                      "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
                      },
                      "approved_by": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "pattern": "^user:[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
                      },
                      "rejected_by": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "pattern": "^user:[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
                      },
                      "decided_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
                      },
                      "reason": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "minLength": 10,
                        "maxLength": 500,
                        "pattern": "^[^\\s\\u0000-\\u001F\\u007F-\\u009F]([^\\u0000-\\u001F\\u007F-\\u009F]*[^\\s\\u0000-\\u001F\\u007F-\\u009F])?$"
                      }
                    },
                    "allOf": [
                      {
                        "not": {
                          "properties": {
                            "approved_by": {
                              "type": "string"
                            },
                            "rejected_by": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      {
                        "if": {
                          "properties": {
                            "decided_at": {
                              "const": null
                            }
                          }
                        },
                        "then": {
                          "properties": {
                            "approved_by": {
                              "const": null
                            },
                            "rejected_by": {
                              "const": null
                            },
                            "reason": {
                              "const": null
                            }
                          }
                        },
                        "else": {
                          "properties": {
                            "reason": {
                              "type": "string"
                            }
                          },
                          "anyOf": [
                            {
                              "properties": {
                                "approved_by": {
                                  "type": "string"
                                }
                              }
                            },
                            {
                              "properties": {
                                "rejected_by": {
                                  "type": "string"
                                }
                              }
                            }
                          ]
                        }
                      }
                    ]
                  }
                ]
              },
              "retry_of": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
              },
              "version": {
                "type": "integer",
                "minimum": 1,
                "maximum": 2147483647
              },
              "created_at": {
                "type": "string",
                "format": "date-time",
                "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
              },
              "updated_at": {
                "type": "string",
                "format": "date-time",
                "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
              },
              "submitted_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
              },
              "finished_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
              },
              "sandbox": {
                "const": true
              }
            },
            "allOf": [
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "UPLOADED",
                        "VALIDATING",
                        "VALIDATED"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "approval": {
                      "const": null
                    },
                    "submitted_at": {
                      "const": null
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "PENDING_APPROVAL",
                        "APPROVED",
                        "PROCESSING",
                        "COMPLETED",
                        "REJECTED",
                        "EXPIRED"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "approval": {
                      "type": "object"
                    },
                    "submitted_at": {
                      "type": "string"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "COMPLETED",
                        "REJECTED",
                        "EXPIRED"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "finished_at": {
                      "type": "string"
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "state": {
                      "enum": [
                        "UPLOADED",
                        "VALIDATING",
                        "VALIDATED",
                        "PENDING_APPROVAL",
                        "APPROVED",
                        "PROCESSING"
                      ]
                    }
                  },
                  "required": [
                    "state"
                  ]
                },
                "then": {
                  "properties": {
                    "finished_at": {
                      "const": null
                    }
                  }
                }
              },
              {
                "if": {
                  "properties": {
                    "preparers": {
                      "contains": {
                        "pattern": "^key:"
                      }
                    }
                  }
                },
                "else": {
                  "properties": {
                    "key_holders": {
                      "properties": {
                        "known": {
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            ],
            "description": "The batch as GET /v1/payout-batches/{id} answers it at this state, its key holders reduced to whether all are known; its lines emit no event of their own."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "type": {
                  "const": "payout_batch.validated"
                }
              }
            },
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "state": {
                      "const": "VALIDATED"
                    }
                  }
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "type": {
                  "const": "payout_batch.completed"
                }
              }
            },
            "then": {
              "properties": {
                "data": {
                  "properties": {
                    "state": {
                      "const": "COMPLETED"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "InvoiceBatchCreateRequest": {
        "description": "A batch of invoices to come: its label, its channels, its reminder policy and how many invoices will arrive, at most 50,000, in chunks of at most 1,000.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "label",
          "channels",
          "reminders",
          "expected_invoices"
        ],
        "properties": {
          "label": {
            "type": "string",
            "pattern": "^\\p{Zs}*[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}](?:[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}]{0,98}[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}])?\\p{Zs}*$",
            "description": "A text under the one text rule (futa-invoice-file-v1's « Texts »): refused when any of its characters is of general category Cc, Cf, Cs, Zl or Zp (Unicode 15.0), trimmed of White_Space at both ends, refused when nothing is left, and at most 100 code points once trimmed. Its pattern holds the whole rule, the length after trimming included: space separators may stand at either end, around 1 to 100 code points that begin and end with neither."
          },
          "channels": {
            "type": "array",
            "uniqueItems": true,
            "maxItems": 2,
            "items": {
              "enum": [
                "SMS",
                "WHATSAPP"
              ]
            },
            "description": "The channels each invoice's messages go through: SMS, WHATSAPP, both, or none (an empty list: the batch's links are made and no message is sent). Synthetic adapters only in the sandbox; no transport or sender is named."
          },
          "reminders": {
            "enum": [
              "BEFORE_AND_ON_DUE_DATE",
              "WEEKLY",
              "NONE"
            ],
            "description": "BEFORE_AND_ON_DUE_DATE (« J−3 et jour d'échéance »: 09:00 Kinshasa on D−3 and D), WEEKLY (« Chaque semaine »: 09:00 Kinshasa every seven days from the first message until D), each with one reminder on D+1 at 09:00 carrying a new link that ends at D+8 09:00 (SYNTHETIC); NONE (« Aucune »). An occurrence missed, a run late or a day held, sends only the latest one due, never two. A plan other than NONE with no channel is INVALID_REQUEST."
          },
          "expected_invoices": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000
          }
        }
      },
      "InvoiceBatchCounts": {
        "description": "A batch's invoices, never its money: its lines judged (a file's non-blank lines, or the invoices an API batch received), ready and rejected; once it is sent, those created, each counted once by its newest link as switch-core answers it (PAID when any of its links was paid, VERIFYING while its newest link has a pending or uncertain attempt, EXPIRED when its window ended unpaid, as Invoice says, CANCELLED when the send failed, with a link or without, once the close has answered, otherwise UNPAID), those not created (refused), and the undelivered (« Non délivrées »): the invoices whose delivery is UNDELIVERED alone, a message that reached no one (INVALID_NUMBER or UNREACHABLE) or that channels never sent before the window closed (SEND_WINDOW_CLOSED); UNCONFIRMED and NOT_SENT are not counted there. ready counts the ready invoices not yet created or refused. paid, verifying, unpaid, expired and cancelled add up to created; ready, refused and created add up to the invoices judged at the send.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "lines",
          "ready",
          "rejected",
          "refused",
          "created",
          "paid",
          "verifying",
          "unpaid",
          "expired",
          "cancelled",
          "undelivered"
        ],
        "properties": {
          "lines": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "ready": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "rejected": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "refused": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "created": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "paid": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "verifying": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "unpaid": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "expired": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "cancelled": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "undelivered": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          }
        }
      },
      "InvoiceCurrencyTotals": {
        "description": "One currency's invoices, ready or created, their amount, and those paid with the amount paid, in integer minor units of that currency, whole francs for CDF; never summed with another currency or converted.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "currency",
          "invoices",
          "amount",
          "paid_invoices",
          "paid_amount"
        ],
        "properties": {
          "currency": {
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "invoices": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "maximum": 450000000000000000,
            "description": "Integer minor units of the currency, whole francs for CDF. It can exceed 2^53, the largest integer a double holds exactly: parse it as a 64-bit integer, never as a floating-point number (TypeScript types generated from this contract give it number)."
          },
          "paid_invoices": {
            "type": "integer",
            "minimum": 0,
            "maximum": 50000
          },
          "paid_amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 0,
            "maximum": 450000000000000000,
            "description": "Integer minor units of the currency, whole francs for CDF. It can exceed 2^53, the largest integer a double holds exactly: parse it as a 64-bit integer, never as a floating-point number (TypeScript types generated from this contract give it number)."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                },
                "paid_amount": {
                  "multipleOf": 100
                }
              }
            }
          }
        ]
      },
      "InvoiceBatch": {
        "description": "One invoice batch. Its reference is BI- and two groups of four, never sequential. A batch made from a file is MAPPING until a mapping is confirmed, then VALIDATING, then READY; one made through the API is RECEIVING until every invoice it announced has arrived, then READY. « Créer N factures » makes it SENDING, then SENT once every invoice it judged at the send is created or refused and every first message has an outcome (accepted by channels, HELD, or NOT_SENT), not once every message is accepted, and at once when the send's judgment leaves no invoice: no link batch is made and every invoice reads REFUSED. SEND_FAILED, terminal, when switch-core refused a whole request during the send (send_failure): merchant-api then closes its link batch at switch-core, under invoice-batch-<id>-close, its body recorded first and replayed byte for byte while its outcome is unknown, which cancels every UNPAID link at once and keeps a link with an approved, pending or uncertain attempt, cancelling it if that attempt ends without approval (declined or expired), so no link of the batch becomes payable again. Only a link batch that was created is closed: a send whose creation was refused has none. The close always completes: its body is never discarded, a 4xx being a defect, operations alerted and the body kept and replayed once the defect is fixed, the one exception to a body discarded after a final 4xx; transient answers are retried without end. A SEND_FAILED batch's invoices are kept out of reminders, « Relancer », « Renvoyer » and the QR sheet, and keep their payment reads and the export: PAID with a paid link, VERIFYING with a pending or uncertain attempt, CANCELLED otherwise, once the close has answered, read from switch-core's links; until then each reads as it did, and with no link batch to close they read CANCELLED at once. A batch not yet sent may be discarded (DISCARDED, « Annuler »); one not sent seven days after its last change lapses (LAPSED, a SYNTHETIC choice): nothing of either was ever sent. A merchant holds at most 20 open batches, those MAPPING, VALIDATING, RECEIVING or READY (SYNTHETIC, INVOICE_BATCH_LIMIT_REACHED); merchant-api keeps at most 20 of the merchant's link batches in flight, a batch beyond them waiting SENDING, in order, before its link batch is made; closed and complete link batches do not count (SYNTHETIC), and a failed send's counts until switch-core answers its close CLOSED. version changes with each confirmed mapping, each chunk and each state, and is what mapping, sending and discarding name. label and reminders are null only while a file's batch waits for its mapping, or when such a batch lapsed or was discarded before one. Its counts come from merchant-api's projection; its paid counts and amounts per currency are read live from switch-core's link batch on each read of the batch. Uploaded files, payer data and lapsed or discarded batches are purged 90 days after they close, as tombstones (SYNTHETIC). Sandbox only.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "reference",
          "source",
          "label",
          "channels",
          "reminders",
          "language",
          "state",
          "version",
          "expected_invoices",
          "counts",
          "totals",
          "created_by",
          "created_at",
          "updated_at",
          "sent_at",
          "send_failure",
          "sandbox"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "reference": {
            "type": "string",
            "pattern": "^BI-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "source": {
            "enum": [
              "FILE",
              "API"
            ]
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 100,
            "pattern": "^[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}](?:[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}]*[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}])?$",
            "description": "A text under the one text rule (futa-invoice-file-v1's « Texts »): refused when any of its characters is of general category Cc, Cf, Cs, Zl or Zp (Unicode 15.0), trimmed of White_Space at both ends, refused when nothing is left, and at most 100 code points once trimmed. Kept as the rule trims it: neither end is a space separator, and that trimmed text is what merchant-api sends."
          },
          "channels": {
            "type": "array",
            "uniqueItems": true,
            "maxItems": 2,
            "items": {
              "enum": [
                "SMS",
                "WHATSAPP"
              ]
            },
            "description": "The channels each invoice's messages go through: SMS, WHATSAPP, both, or none (an empty list: the batch's links are made and no message is sent). Synthetic adapters only in the sandbox; no transport or sender is named."
          },
          "reminders": {
            "enum": [
              "BEFORE_AND_ON_DUE_DATE",
              "WEEKLY",
              "NONE",
              null
            ]
          },
          "language": {
            "enum": [
              "fr",
              "ln",
              "sw",
              "en"
            ],
            "description": "The language of the batch's messages: fr unless a send body named another; the portal shows no control for it, as the board has none."
          },
          "state": {
            "enum": [
              "MAPPING",
              "VALIDATING",
              "RECEIVING",
              "READY",
              "SENDING",
              "SENT",
              "SEND_FAILED",
              "LAPSED",
              "DISCARDED"
            ]
          },
          "version": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "expected_invoices": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 50000,
            "description": "What an API batch announced; null for a file's."
          },
          "counts": {
            "$ref": "#/components/schemas/InvoiceBatchCounts"
          },
          "totals": {
            "type": "array",
            "maxItems": 2,
            "items": {
              "$ref": "#/components/schemas/InvoiceCurrencyTotals"
            },
            "description": "USD first, then CDF, each at most once."
          },
          "created_by": {
            "type": "string",
            "pattern": "^(user|key):[\\x21-\\x2B\\x2D-\\x7E]{1,128}$"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "sent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "send_failure": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "code",
                  "correlation_id",
                  "failed_at"
                ],
                "description": "Why a send ended SEND_FAILED: switch-core refused a whole request during the asynchronous send (its 400, its 404, LINK_BATCH_TOO_LARGE, a position beyond the announced invoices, or a route-level IDEMPOTENCY_CONFLICT), which can only come from a defect once the rules agree; operations are alerted, the link batch is closed and nothing more is sent. LINK_BATCH_LIMIT_REACHED is never a failure: the batch waits SENDING.",
                "properties": {
                  "code": {
                    "const": "INVOICE_BATCH_SEND_FAILED"
                  },
                  "correlation_id": {
                    "type": "string",
                    "format": "uuid",
                    "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
                  },
                  "failed_at": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
                  }
                }
              }
            ]
          },
          "sandbox": {
            "const": true
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "state": {
                  "const": "MAPPING"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "source": {
                  "const": "FILE"
                },
                "label": {
                  "const": null
                },
                "reminders": {
                  "const": null
                },
                "channels": {
                  "maxItems": 0
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "VALIDATING",
                    "RECEIVING",
                    "READY",
                    "SENDING",
                    "SENT",
                    "SEND_FAILED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "label": {
                  "type": "string"
                },
                "reminders": {
                  "enum": [
                    "BEFORE_AND_ON_DUE_DATE",
                    "WEEKLY",
                    "NONE"
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "VALIDATING"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "source": {
                  "const": "FILE"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "RECEIVING"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "source": {
                  "const": "API"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "source": {
                  "const": "FILE"
                }
              },
              "required": [
                "source"
              ]
            },
            "then": {
              "properties": {
                "expected_invoices": {
                  "const": null
                }
              }
            },
            "else": {
              "properties": {
                "expected_invoices": {
                  "type": "integer"
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "SENDING",
                    "SENT",
                    "SEND_FAILED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "sent_at": {
                  "type": "string"
                }
              }
            },
            "else": {
              "properties": {
                "sent_at": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "SEND_FAILED"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "send_failure": {
                  "type": "object"
                }
              }
            },
            "else": {
              "properties": {
                "send_failure": {
                  "const": null
                }
              }
            }
          }
        ]
      },
      "InvoiceBatchPage": {
        "description": "A page of batches, newest first, and the next cursor: the last batch's id, null on the last page.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/InvoiceBatch"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      },
      "InvoiceInput": {
        "description": "One invoice as a chunk carries it, or as a file's line becomes once read (futa-invoice-file-v1): its line in the batch; reference, the merchant's reference for it (« Référence client »), held, case aside, by one invoice of the merchant at a time, which switch-core keeps as the link's merchant_reference; customer_reference, another reference the merchant keeps for the customer, never unique, never sent to the switch or in a message, and never filled from a file (its seven fields have none); the payer's name and phone (E.164), kept encrypted; the whole amount in integer minor units with its currency, whole francs for CDF; the due date, a Kinshasa calendar day from the day after the check to 90 days ahead (SYNTHETIC); and the description (« Libellé »).",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "line",
          "reference",
          "amount",
          "currency",
          "due_date"
        ],
        "properties": {
          "line": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000,
            "description": "The invoice's own line in its batch: the file's, with gaps where lines were rejected, or through the API 1 to the invoices the batch announced, each once. switch-core's position is another number, assigned in the send plan (Invoice.position)."
          },
          "reference": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_./-]{0,63}$",
            "description": "The merchant's reference, which switch-core keeps as the link's merchant_reference and the payer's page shows: it must hold no payer data. It is held, case aside, by every invoice of the merchant that is READY in a live batch (neither LAPSED nor DISCARDED) or that has a live link (UNPAID, PENDING or UNCERTAIN), whatever the invoice's or its batch's state, and freed when neither is true: a paid or expired invoice frees it for re-billing, and so does a cancelled one once switch-core's close has cancelled its last live link."
          },
          "customer_reference": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_./-]{0,63}$"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\p{Zs}*[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}](?:[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}]{0,98}[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}])?\\p{Zs}*$",
            "description": "A text under the one text rule (futa-invoice-file-v1's « Texts »): refused when any of its characters is of general category Cc, Cf, Cs, Zl or Zp (Unicode 15.0), trimmed of White_Space at both ends, refused when nothing is left, and at most 100 code points once trimmed. Its pattern holds the whole rule, the length after trimming included: space separators may stand at either end, around 1 to 100 code points that begin and end with neither."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+[1-9][0-9]{7,14}$"
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\p{Zs}*[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}](?:[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}]{0,198}[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}])?\\p{Zs}*$",
            "description": "A text under the one text rule (futa-invoice-file-v1's « Texts »): refused when any of its characters is of general category Cc, Cf, Cs, Zl or Zp (Unicode 15.0), trimmed of White_Space at both ends, refused when nothing is left, and at most 200 code points once trimmed. Its pattern holds the whole rule, the length after trimming included: space separators may stand at either end, around 1 to 200 code points that begin and end with neither. With none, the link and every message carry « Facture <reference> », the invoice's reference after « Facture », never a blank or invented text. It reaches switch-core's link and the payer's page, so it must hold no payer data. merchant-api keeps and sends it as the rule trims it, so what switch-core receives is bounded by the stored 200 code points and a chunk of 1,000 lines stays within switch-core's 6 MiB."
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                }
              }
            }
          }
        ]
      },
      "InvoiceChunk": {
        "description": "One chunk of an API batch's invoices: at most 1,000 and 512 KiB, its number from 1 to the invoices the batch announced, each line once.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "chunk",
          "invoices"
        ],
        "properties": {
          "chunk": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000
          },
          "invoices": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/InvoiceInput"
            }
          }
        }
      },
      "InvoiceChunkReceipt": {
        "description": "What a chunk gave: its lines, those ready and those rejected, and every problem of the rejected ones (at most four a line), judged as a file's line is. A chunk sent again answers the same receipt.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "batch_id",
          "chunk",
          "lines",
          "ready",
          "rejected",
          "problems"
        ],
        "properties": {
          "batch_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "chunk": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000
          },
          "lines": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "ready": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000
          },
          "rejected": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000
          },
          "problems": {
            "type": "array",
            "maxItems": 4000,
            "items": {
              "$ref": "#/components/schemas/InvoiceLineProblem"
            }
          }
        }
      },
      "InvoiceBatchVersionRequest": {
        "description": "The batch's version as the merchant last read it; another version is refused, never applied.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "expected_version"
        ],
        "properties": {
          "expected_version": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          }
        }
      },
      "InvoiceLineProblem": {
        "description": "One problem of one line (futa-invoice-file-v1): its line, numbered from 1 for the line after the header; its column from 1 and the Futa field that column fills, when it has them (a chunk's problem has no column); its code; and, for an amount, a currency or a due date only, the cell as written, cut to 39 characters and « … », null when empty or for UNSAFE_CELL. No name, phone, reference or description is ever repeated.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "line",
          "column",
          "field",
          "code",
          "value"
        ],
        "properties": {
          "line": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000
          },
          "column": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 50
          },
          "field": {
            "enum": [
              "reference",
              "name",
              "phone",
              "amount",
              "currency",
              "due_date",
              "description",
              null
            ]
          },
          "code": {
            "enum": [
              "COLUMN_COUNT",
              "UNSAFE_CELL",
              "REFERENCE_MISSING",
              "REFERENCE_INVALID",
              "INVOICE_REFERENCE_DUPLICATE",
              "TEXT_TOO_LONG",
              "PHONE_MISSING",
              "PHONE_INVALID",
              "CURRENCY_INVALID",
              "AMOUNT_INVALID",
              "AMOUNT_DECIMALS",
              "DUE_DATE_INVALID",
              "INVOICE_DUE_DATE_OUT_OF_RANGE",
              "MERCHANT_LIMIT_EXCEEDED"
            ]
          },
          "value": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 40
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "code": {
                  "const": "COLUMN_COUNT"
                }
              },
              "required": [
                "code"
              ]
            },
            "then": {
              "properties": {
                "column": {
                  "const": null
                },
                "field": {
                  "const": null
                }
              }
            },
            "else": {
              "anyOf": [
                {
                  "properties": {
                    "column": {
                      "type": "integer"
                    }
                  }
                },
                {
                  "properties": {
                    "field": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          },
          {
            "if": {
              "anyOf": [
                {
                  "properties": {
                    "code": {
                      "enum": [
                        "COLUMN_COUNT",
                        "UNSAFE_CELL"
                      ]
                    }
                  },
                  "required": [
                    "code"
                  ]
                },
                {
                  "properties": {
                    "field": {
                      "enum": [
                        "reference",
                        "name",
                        "phone",
                        "description",
                        null
                      ]
                    }
                  }
                }
              ]
            },
            "then": {
              "properties": {
                "value": {
                  "const": null
                }
              }
            }
          }
        ]
      },
      "MerchantInvoiceError": {
        "description": "The body of every refusal an invoice route's handler gives: its stable code and this API's French and English messages for it. Only SWITCH_UNAVAILABLE says to send the request again, under the same key; no other refusal invites a retry under another key. The framework's own 400, the authentication's 401, a key's 403 INSUFFICIENT_SCOPE, 413 REQUEST_TOO_LARGE and 429 RATE_LIMITED are Error.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "description": "The refusal.",
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "message_en",
              "message_fr"
            ],
            "properties": {
              "code": {
                "enum": [
                  "INVALID_REQUEST",
                  "REQUEST_INCOMPLETE",
                  "IDEMPOTENCY_CONFLICT",
                  "MEMBERSHIP_REQUIRED",
                  "MEMBERSHIP_CHANGED",
                  "ROLE_DENIED",
                  "INVOICE_BATCH_NOT_FOUND",
                  "INVOICE_NOT_FOUND",
                  "INVOICE_BATCH_TOO_LARGE",
                  "INVOICE_BATCH_CHANGED",
                  "INVOICE_BATCH_NOT_VALIDATED",
                  "INVOICE_MAPPING_REQUIRED",
                  "INVOICE_MAPPING_CHANGED",
                  "INVOICE_NOT_REMINDABLE",
                  "MESSAGES_NOT_CONFIGURED",
                  "RESULT_NOT_READY",
                  "MERCHANT_LIMIT_EXCEEDED",
                  "SWITCH_UNAVAILABLE",
                  "INVOICE_BATCH_LIMIT_REACHED",
                  "INVOICE_NO_CHANNEL",
                  "MERCHANT_NAME_INVALID",
                  "INVOICE_BATCH_SEND_FAILED",
                  "INVOICE_REFERENCE_DUPLICATE"
                ]
              },
              "message_en": {
                "type": "string",
                "minLength": 1,
                "maxLength": 300
              },
              "message_fr": {
                "type": "string",
                "minLength": 1,
                "maxLength": 300
              }
            }
          }
        }
      },
      "InvoiceLink": {
        "description": "One of an invoice's links as switch-core's link batch answers it (merchant-payment-link-batch-v1): fixed and single-use for the whole amount, its code, its place among the invoice's links (sequence 1 for the first, each replacement the one before plus one), its status from its attempts, when it was made, when it ends and when it was cancelled, and its approved attempt once paid, through a synthetic participant. It carries no capability: no share address. A message's link and the QR sheet's come from switch-core's share read of the link, made when the message or the sheet is written, and are never stored.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "code",
          "sequence",
          "status",
          "created_at",
          "expires_at",
          "cancelled_at",
          "paid_attempt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "code": {
            "type": "string",
            "pattern": "^LK-[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$"
          },
          "sequence": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "status": {
            "enum": [
              "UNPAID",
              "PENDING",
              "UNCERTAIN",
              "PAID",
              "EXPIRED",
              "CANCELLED"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$",
            "description": "When the link ends, as switch-core answered it and merchant-api recorded it. merchant-api asks for the end of the message's window, derived from the invoice and never from the time of the call: for a message on or before the due date D, D+1 00:00 Africa/Kinshasa; after it, D+8 09:00 Kinshasa, the after-due window's end (SYNTHETIC); for « Renvoyer » on an EXPIRED invoice, seven days after merchant-api recorded that resend. switch-core keeps the earlier of that and its own clock plus 720 hours."
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "paid_attempt": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "description": "The approved attempt: the payment's id and FT- reference, its synthetic participant, and when the switch approved it.",
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "payment_id",
                  "reference",
                  "participant",
                  "approved_at"
                ],
                "properties": {
                  "payment_id": {
                    "type": "string",
                    "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
                  },
                  "reference": {
                    "type": "string",
                    "pattern": "^FT-[0-9A-F]{32}$"
                  },
                  "participant": {
                    "enum": [
                      "simulator",
                      "synthetic-bank"
                    ]
                  },
                  "approved_at": {
                    "type": "string",
                    "format": "date-time",
                    "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
                  }
                }
              }
            ]
          }
        }
      },
      "InvoiceMessage": {
        "description": "An invoice's newest message on one channel (one message per occurrence and channel, so an occurrence on both channels is two messages under one key, invoice-<id>-request, -reminder-<n> or -resend-<k>, which channels scopes by merchant, channel and key and reads back by key, answering MESSAGE_NOT_FOUND when it holds none): its kind, its occurrence number among those of its kind, its state and why, the day it names and when it last changed. HELD waits in merchant-api: outside 08:00 to 19:55 Kinshasa, a message not answered by 19:55 included (SEND_WINDOW, the five minutes before 20:00 a SYNTHETIC margin), or beyond the merchant's 100,000 messages a Kinshasa day (DAILY_CAP, SYNTHETIC); it is sent at the next opening, in order, after its key is read first, payment is read from switch-core again, the window judged and the link replaced if needed. At that release a message whose invoice is no longer remindable (paid, in verification, « Expirée », CANCELLED, in a batch that ended SEND_FAILED, or its replacement refused LINK_NOT_REPLACEABLE or LINK_EXPIRY_OUT_OF_RANGE) is never sent: it ends NOT_SENT with NOT_REMINDABLE; transient answers there are retried without end, channels called only until 19:55. Once with channels it mirrors channel-message-v1: ACCEPTED; HELD, channels' own hold, which carries no reason there and reads SEND_WINDOW here; SENT; DELIVERED; UNDELIVERED (INVALID_NUMBER or UNREACHABLE); EXPIRED with SEND_WINDOW_CLOSED, never sent, when channels still held it at not_after, that day's 20:00 Kinshasa to the microsecond, or when it reached channels at or after not_after, which channels accepts and ends at once, a replay answering the same; or EXPIRED with no reason when channels sent it and no receipt came (« Envoyé, sans accusé de réception »). merchant-api reads each as the occurrence's outcome; sent_at is channels' own. merchant-api formats every value channels renders: the amount from integer minor units with its currency as a word of its own (« $ », « USD », « CDF » or « FC »), and expiry_date. NOT_SENT when channels refused it, with that code: its 400, INVALID_REQUEST, which after the text rule only a defect gives, so operations are alerted and the merchant's wording blames no request, or MESSAGES_NOT_CONFIGURED; when a replacement passed the per-payment limit in force under « Relancer » (answered 202 first), for a scheduled reminder or at a held message's release (MERCHANT_LIMIT_EXCEEDED), no message going, « Renvoyer », answered at once, answering 409 MERCHANT_LIMIT_EXCEEDED instead; or when its invoice was no longer remindable at its release (NOT_REMINDABLE). channels' 409 under the key is read back with the message read and its state adopted. Synthetic adapters only.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "channel",
          "kind",
          "occurrence",
          "state",
          "reason",
          "expiry_date",
          "sent_at",
          "updated_at"
        ],
        "properties": {
          "channel": {
            "enum": [
              "SMS",
              "WHATSAPP"
            ]
          },
          "kind": {
            "enum": [
              "REQUEST",
              "REMINDER",
              "RESEND"
            ]
          },
          "occurrence": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483647
          },
          "state": {
            "enum": [
              "HELD",
              "ACCEPTED",
              "SENT",
              "DELIVERED",
              "UNDELIVERED",
              "EXPIRED",
              "NOT_SENT"
            ]
          },
          "reason": {
            "enum": [
              "SEND_WINDOW",
              "DAILY_CAP",
              "INVALID_NUMBER",
              "UNREACHABLE",
              "SEND_WINDOW_CLOSED",
              "INVALID_REQUEST",
              "MESSAGES_NOT_CONFIGURED",
              "MERCHANT_LIMIT_EXCEEDED",
              "NOT_REMINDABLE",
              null
            ]
          },
          "expiry_date": {
            "type": "string",
            "format": "date",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
            "description": "The Kinshasa calendar day the message names as its link's last, sent to channels as expiry_date: the Kinshasa day of the link's expiry instant minus one microsecond. A link ending at the end of D names D, the after-due link D+8, a first link capped at 30 days its own day, and « Renvoyer » on an EXPIRED invoice its seventh day, so « Expire le … » never promises more time than the link has."
          },
          "sent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,6})?Z$",
            "description": "When channels sent it, as channel-message-v1 answers it: null while HELD or ACCEPTED, when NOT_SENT, and when EXPIRED with SEND_WINDOW_CLOSED, never sent; set once SENT, DELIVERED or UNDELIVERED, and when EXPIRED with no reason, sent without a receipt."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "state": {
                  "const": "HELD"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "enum": [
                    "SEND_WINDOW",
                    "DAILY_CAP"
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "UNDELIVERED"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "enum": [
                    "INVALID_NUMBER",
                    "UNREACHABLE"
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "EXPIRED"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "enum": [
                    "SEND_WINDOW_CLOSED",
                    null
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "NOT_SENT"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "enum": [
                    "INVALID_REQUEST",
                    "MESSAGES_NOT_CONFIGURED",
                    "MERCHANT_LIMIT_EXCEEDED",
                    "NOT_REMINDABLE"
                  ]
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "ACCEPTED",
                    "SENT",
                    "DELIVERED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "reason": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "HELD",
                    "ACCEPTED",
                    "NOT_SENT"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "sent_at": {
                  "const": null
                }
              }
            },
            "else": {
              "if": {
                "properties": {
                  "reason": {
                    "const": "SEND_WINDOW_CLOSED"
                  }
                },
                "required": [
                  "reason"
                ]
              },
              "then": {
                "properties": {
                  "sent_at": {
                    "const": null
                  }
                }
              },
              "else": {
                "properties": {
                  "sent_at": {
                    "type": "string"
                  }
                }
              }
            }
          }
        ]
      },
      "InvoiceDelivery": {
        "description": "« Envoi », apart from « Statut », rolled up from the newest occurrence's messages, one a channel: NONE when no message was asked (no channel, or not sent yet); DELIVERED when one reached the payer; otherwise PENDING while one has no outcome yet (HELD, ACCEPTED or SENT); and once every channel has an outcome, UNCONFIRMED when one was sent without a receipt (EXPIRED with no reason, « Envoyé, sans accusé de réception »), else UNDELIVERED when one ended UNDELIVERED or EXPIRED with SEND_WINDOW_CLOSED (« numéros à corriger » when the reason is INVALID_NUMBER), else NOT_SENT, channels having refused it on every channel or no message having gone, a held message no longer remindable at its release (NOT_REMINDABLE) an outcome too. It never stays PENDING once every channel has an outcome. reason is that of the message that gave the state, SMS before WHATSAPP when two did, null when it has none. A delivery failure never makes an invoice failed: it stays UNPAID.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "state",
          "reason",
          "messages"
        ],
        "properties": {
          "state": {
            "enum": [
              "NONE",
              "PENDING",
              "DELIVERED",
              "UNCONFIRMED",
              "UNDELIVERED",
              "NOT_SENT"
            ]
          },
          "reason": {
            "enum": [
              "SEND_WINDOW",
              "DAILY_CAP",
              "INVALID_NUMBER",
              "UNREACHABLE",
              "SEND_WINDOW_CLOSED",
              "INVALID_REQUEST",
              "MESSAGES_NOT_CONFIGURED",
              "MERCHANT_LIMIT_EXCEEDED",
              "NOT_REMINDABLE",
              null
            ]
          },
          "messages": {
            "type": "array",
            "maxItems": 2,
            "items": {
              "$ref": "#/components/schemas/InvoiceMessage"
            }
          }
        }
      },
      "Invoice": {
        "description": "One invoice as merchant-api answers it: its batch, its line and its fields as given, the phone masked; « Statut », its payment state: PAID and VERIFYING read live from switch-core's links read by invoice at each send, each reminder run and each GET /v1/invoices/{id}, lists showing merchant-api's projection of them, refreshed by paging that read (from_position); UNPAID and EXPIRED are merchant-api's own, judged by the invoice's window, never switch-core's expired count: READY until its batch is sent, REFUSED when it was not created (refusal says why: its due date had left the range on the day of the send, its reference was taken by another open invoice since, or the per-payment limit was lowered since), PAID when any of its links was paid, VERIFYING while its newest link has a pending or uncertain attempt (« En vérification », never paid or failed), EXPIRED when its after-due window ended unpaid, or under NONE when its link ended after the due date unpaid, CANCELLED when its batch ended SEND_FAILED and none of its links was paid or has a pending or uncertain attempt, its links cancelled by switch-core's close and never payable again: it reads so once the close has answered, from switch-core's links, as it read before until then, and at once when its batch's link batch was never created; otherwise UNPAID; and « Envoi » apart. Its links in order, the first and each replacement, and the payable one, if any: at most one at a time. No fee is shown. Personal fields (name, phone, description, customer_reference) are purged 90 days after the invoice closes (PAID, EXPIRED, CANCELLED or REFUSED) and the invoice is kept as a tombstone (SYNTHETIC).",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "batch_id",
          "line",
          "position",
          "reference",
          "customer_reference",
          "name",
          "phone",
          "amount",
          "currency",
          "due_date",
          "description",
          "state",
          "refusal",
          "links",
          "payable_link_id",
          "paid_at",
          "delivery",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "batch_id": {
            "type": "string",
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "line": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50000
          },
          "position": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 50000,
            "description": "Its place among the invoices the send announced to switch-core, from 1 to the link batch's expected_lines, fixed in the send plan before the link batch is made and sent as the link's position: null while the invoice is READY and for one merchant-api refused when it judged the batch at the send; one switch-core refused keeps the position it was announced at."
          },
          "reference": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_./-]{0,63}$"
          },
          "customer_reference": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9][A-Za-z0-9_./-]{0,63}$"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 100,
            "pattern": "^[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}](?:[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}]*[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}])?$",
            "description": "A text under the one text rule (futa-invoice-file-v1's « Texts »): refused when any of its characters is of general category Cc, Cf, Cs, Zl or Zp (Unicode 15.0), trimmed of White_Space at both ends, refused when nothing is left, and at most 100 code points once trimmed. Kept as the rule trims it: neither end is a space separator, and that trimmed text is what merchant-api sends."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^(\\+243)?•••••[0-9]{4}$",
            "description": "The phone masked: never more than its last four digits, after +243 for a DRC number and after nothing for another country's; null when none was given."
          },
          "amount": {
            "type": "integer",
            "format": "int64",
            "minimum": 1,
            "maximum": 9000000000000
          },
          "currency": {
            "enum": [
              "USD",
              "CDF"
            ]
          },
          "due_date": {
            "type": "string",
            "format": "date",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "pattern": "^[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}](?:[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}]*[^\\p{Cc}\\p{Cf}\\p{Cs}\\p{Zl}\\p{Zp}\\p{Zs}])?$",
            "description": "A text under the one text rule (futa-invoice-file-v1's « Texts »): refused when any of its characters is of general category Cc, Cf, Cs, Zl or Zp (Unicode 15.0), trimmed of White_Space at both ends, refused when nothing is left, and at most 200 code points once trimmed. Kept as the rule trims it: neither end is a space separator, and that trimmed text is what merchant-api sends."
          },
          "state": {
            "enum": [
              "READY",
              "REFUSED",
              "UNPAID",
              "VERIFYING",
              "PAID",
              "EXPIRED",
              "CANCELLED"
            ]
          },
          "refusal": {
            "enum": [
              "INVOICE_DUE_DATE_OUT_OF_RANGE",
              "INVOICE_REFERENCE_DUPLICATE",
              "MERCHANT_LIMIT_EXCEEDED",
              null
            ]
          },
          "links": {
            "type": "array",
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/InvoiceLink"
            }
          },
          "payable_link_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "delivery": {
            "$ref": "#/components/schemas/InvoiceDelivery"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          }
        },
        "allOf": [
          {
            "if": {
              "properties": {
                "currency": {
                  "const": "CDF"
                }
              },
              "required": [
                "currency"
              ]
            },
            "then": {
              "properties": {
                "amount": {
                  "multipleOf": 100
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "REFUSED"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "refusal": {
                  "enum": [
                    "INVOICE_DUE_DATE_OUT_OF_RANGE",
                    "INVOICE_REFERENCE_DUPLICATE",
                    "MERCHANT_LIMIT_EXCEEDED"
                  ]
                },
                "links": {
                  "maxItems": 0
                }
              }
            },
            "else": {
              "properties": {
                "refusal": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "READY"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "links": {
                  "maxItems": 0
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "UNPAID",
                    "VERIFYING",
                    "PAID",
                    "EXPIRED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "links": {
                  "minItems": 1
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "PAID"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "paid_at": {
                  "type": "string"
                },
                "payable_link_id": {
                  "const": null
                }
              }
            },
            "else": {
              "properties": {
                "paid_at": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "READY",
                    "REFUSED",
                    "VERIFYING",
                    "EXPIRED",
                    "CANCELLED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "payable_link_id": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "const": "READY"
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "position": {
                  "const": null
                }
              }
            }
          },
          {
            "if": {
              "properties": {
                "state": {
                  "enum": [
                    "UNPAID",
                    "VERIFYING",
                    "PAID",
                    "EXPIRED",
                    "CANCELLED"
                  ]
                }
              },
              "required": [
                "state"
              ]
            },
            "then": {
              "properties": {
                "position": {
                  "type": "integer"
                }
              }
            }
          }
        ]
      },
      "InvoicePage": {
        "description": "A page of a batch's invoices in line order, and the next cursor: the id of the last invoice the page read, null on the last page. A page of a payment state, a group or the undelivered, its invoices read again from switch-core, leaves out one that has left it meanwhile, so it may hold fewer invoices than it read, or none, with a next cursor.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "data",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/Invoice"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
          }
        }
      },
      "InvoiceReminderRunRequest": {
        "description": "The instant the sandbox runs the reminder schedule as of, from the database's clock to 100 days after it (SYNTHETIC). The calling window and the daily cap are judged on it, not on any clock, and a message the run sends carries 20:00 on its Kinshasa day as not_after.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "as_of"
        ],
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          }
        }
      },
      "InvoiceReminderRun": {
        "description": "What a sandbox run recorded, labelled synthetic: the caller's batches it looked at, the reminders it recorded (each under its key, so a run again records nothing twice), and the invoices it found EXPIRED.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "as_of",
          "synthetic",
          "batches",
          "reminders",
          "expired"
        ],
        "properties": {
          "as_of": {
            "type": "string",
            "format": "date-time",
            "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{1,9})?Z$"
          },
          "synthetic": {
            "const": true
          },
          "batches": {
            "type": "integer",
            "minimum": 0
          },
          "reminders": {
            "type": "integer",
            "minimum": 0
          },
          "expired": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "InvoiceSendRequest": {
        "description": "« Créer N factures »: the batch's version as the merchant last read it, another being refused, never applied, and the language of its messages: fr when absent.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "expected_version"
        ],
        "properties": {
          "expected_version": {
            "type": "integer",
            "format": "int64",
            "minimum": 1
          },
          "language": {
            "enum": [
              "fr",
              "ln",
              "sw",
              "en"
            ]
          }
        }
      }
    },
    "securitySchemes": {
      "MerchantKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Persisted hashed test key; permissions and per-merchant quotas are enforced. Never put in payer/browser code. sk_live_ keys are rejected."
      }
    },
    "responses": {
      "InternalError": {
        "description": "INTERNAL_ERROR: an unexpected failure only, logged and not detailed",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "UNSUPPORTED_MEDIA_TYPE: the request body is not JSON",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "MerchantKey": []
    }
  ],
  "tags": [
    {
      "name": "Payments",
      "description": "Sandbox checkout payments"
    },
    {
      "name": "Keys",
      "description": "Merchant-scoped restricted test credentials"
    },
    {
      "name": "Webhooks",
      "description": "Merchant-scoped signed delivery and replay"
    },
    {
      "name": "Refunds",
      "description": "Idempotent original-account refunds funded by unsettled merchant funds."
    },
    {
      "name": "MerchantPaymentPhone",
      "description": "Private exact phone search and matching reports; indexed admission coverage only."
    },
    {
      "name": "Payouts",
      "description": "The payout balance, single payouts and payout batches, declared in 0.59.0 and served since 0.59.7, each of them since the version the contract's description names. Every payout command is kept under its Idempotency-Key before switch-core is called, and sent to it under that key, which switch-core keeps with its answer for 720 hours, but for three: a chunk of lines, kept with its lines and sent under its own key (payout-batch-<id>-chunk-<n>); an uploaded file's batch, made under a key derived from the merchant, the person and the file (payout-file-…); and a sandbox top-up, sent to the sponsor simulator under its own id; a key starting with payout-batch- or payout-file-, which merchant-api makes for its own commands, is refused 400 INVALID_REQUEST. A key never makes a second payout while its payout's records are kept: past those 720 hours a command resent under its key is refused 409 IDEMPOTENCY_CONFLICT and never sent again, as switch-core, no longer holding its answer, would take it for a new one, judged by the key's age and never by whether merchant-api has purged anything; merchant-api keeps the command as its tombstone (the merchant, the actor, the key, the body's keyed fingerprint, when it was kept and what it made, never its body, its answer or anything personal) for ten years, and only then deletes it. An uploaded file follows the same rule: the key its batch is made under is never sent to make it past 720 hours from its upload; the file is kept, encrypted, until 720 hours after its upload last changed, and longer while its batch is open or a command younger than 720 hours is kept under its key; its upload is then kept as a tombstone for ten years, so the same file sent again never makes a second batch under that key. merchant-api keeps a report or result file 720 hours after it was asked for, a read after them asking for it anew, and a name enquiry the budget's hour. A command a key makes to prepare a payout or a batch states the key's holders to switch-core, from merchant-api's own key lineage, so none of them decides it; a key more than 50 people held cannot be stated so, and its preparing commands are refused 400 INVALID_REQUEST. Money is integer minor units with its currency, CDF in whole francs; every figure is switch-core's ledger's. An API key prepares and never approves: only a person other than every preparer and every holder of a preparing key approves or rejects, in the portal, after a recent sign-in. Synthetic sandbox only: the simulator's participants, synthetic zero fees, no real rails."
    },
    {
      "name": "Invoices",
      "description": "Bulk invoices and reminders, declared in 0.60.0, served before a send since 0.60.6 and the send since 0.60.8, reminders, « Relancer », « Renvoyer » and the sandbox reminder run since 0.60.12, the results workbook and the QR sheet since 0.60.14: invoice batches made from a file of the merchant's own columns (futa-invoice-file-v1) or through the API in chunks, each invoice paid through one fixed, single-use link at a time made by switch-core (merchant-payment-link-batch-v1), its « Demande de paiement » and reminders sent through channels' synthetic SMS and WhatsApp adapters, the results workbook (futa-invoice-results-v1) and the QR sheet. Sandbox only: synthetic participants, no transport, sender, fee or biller code. No webhook event is added: each paid invoice's payment raises payment.approved.\n\nHow switch-core's and channels' answers end, one row per answer and context. The contexts are the send (the link batch's creation, its chunks and, when the send fails, its close); a person's « Relancer » or « Renvoyer », answered at once; a scheduled reminder's replacement; a held message's release (its link's replacement, after payment is read again); a read in a run (switch-core's links and batch reads, channels' message read); a message to channels.\n\n| Answer | Context | merchant-api's outcome |\n| --- | --- | --- |\n| `MERCHANT_LIMIT_EXCEEDED`, a line | the send | the invoice reads REFUSED, refusal `MERCHANT_LIMIT_EXCEEDED` |\n| `LINK_EXPIRY_OUT_OF_RANGE`, a line | the send | the invoice reads REFUSED, refusal `INVOICE_DUE_DATE_OUT_OF_RANGE` (its window ends within the hour) |\n| `IDEMPOTENCY_CONFLICT`, a line | the send | a defect, each line's body being recorded and replayed byte for byte: operations are alerted, no invoice state is given; switch-core never keeps or counts it |\n| `INVALID_REQUEST` (400) | the send | the batch ends SEND_FAILED, send_failure with the correlation id, operations alerted, its link batch closed |\n| `LINK_BATCH_NOT_FOUND` (404) | the send | SEND_FAILED as above |\n| `LINK_BATCH_TOO_LARGE` (409) | the send | a position beyond the announced invoices, a defect: SEND_FAILED as above |\n| `IDEMPOTENCY_CONFLICT` (409) | the send | SEND_FAILED as above |\n| `LINK_BATCH_LIMIT_REACHED` (409) | the send | a wait, not a failure: the batch stays SENDING and the recorded creation body is replayed later |\n| `INVALID_REQUEST` (400), `LINK_BATCH_NOT_FOUND` (404), `IDEMPOTENCY_CONFLICT` (409), the close | the send | a defect: operations are alerted; the batch stays SEND_FAILED, and the close's body is kept, never discarded, and replayed once the defect is fixed |\n| 5xx, timeout, connection failure | the send | retried without end, operations alerted after 15 minutes; the batch stays SENDING |\n| 401 or 403, switch-core's access filter | the send | a configuration defect, merchant-api's key not switch-core's: operations are alerted at once, once a batch and code, and the body is kept and retried as a transient answer is; the batch keeps its state |\n| 5xx, timeout, connection failure, the close | the send | retried without end, operations alerted after 15 minutes; the batch stays SEND_FAILED, and its link batch counts in flight until switch-core answers it CLOSED |\n| `INVALID_REQUEST` (400), `LINK_BATCH_NOT_FOUND` (404), `LINK_NOT_FOUND` (404) | a person's « Relancer » or « Renvoyer » | a defect: operations are alerted; « Renvoyer » answers 409 `INVOICE_NOT_REMINDABLE`, and under « Relancer » that invoice gets no message |\n| `LINK_EXPIRY_OUT_OF_RANGE` (409) | a person's « Relancer » or « Renvoyer » | its window ends within the hour: « Renvoyer » answers 409 `INVOICE_NOT_REMINDABLE`, and under « Relancer » that invoice gets no message |\n| `MERCHANT_LIMIT_EXCEEDED` (409) | a person's « Relancer » or « Renvoyer » | « Renvoyer » answers 409 `MERCHANT_LIMIT_EXCEEDED`; under « Relancer », answered before its replacements, that invoice's reminder reads NOT_SENT with code `MERCHANT_LIMIT_EXCEEDED` |\n| `LINK_NOT_REPLACEABLE` (409) | a person's « Relancer » or « Renvoyer » | the invoice is not remindable: « Renvoyer » answers 409 `INVOICE_NOT_REMINDABLE`, and under « Relancer » that invoice gets no message |\n| `LINK_REPLACED` (409), `IDEMPOTENCY_CONFLICT` (409) | a person's « Relancer » or « Renvoyer » | a successor exists: it is read (links by invoice_id) and adopted, and the message goes with it |\n| 5xx, timeout, connection failure | a person's « Relancer » or « Renvoyer » | « Renvoyer » answers 503 `SWITCH_UNAVAILABLE`, its recorded body replayed when the same request comes again under its key; « Relancer », answered first, retries without end |\n| `INVALID_REQUEST` (400), `LINK_BATCH_NOT_FOUND` (404), `LINK_NOT_FOUND` (404) | a scheduled reminder's replacement | a defect: operations are alerted, no message goes and the body is discarded |\n| `LINK_EXPIRY_OUT_OF_RANGE` (409) | a scheduled reminder's replacement | no message: the window ends within the hour, and the invoice reads EXPIRED then; the body is discarded |\n| `MERCHANT_LIMIT_EXCEEDED` (409) | a scheduled reminder's replacement | no message goes: the occurrence reads NOT_SENT with code `MERCHANT_LIMIT_EXCEEDED` |\n| `LINK_NOT_REPLACEABLE` (409) | a scheduled reminder's replacement | no message: payment is read again and the invoice is reminded no more while it is not remindable; the body is discarded and the next reminder reuses n |\n| `LINK_REPLACED` (409), `IDEMPOTENCY_CONFLICT` (409) | a scheduled reminder's replacement | a successor exists: it is read (links by invoice_id) and adopted, and the reminder goes with it |\n| 5xx, timeout, connection failure | a scheduled reminder's replacement | retried without end, operations alerted after 15 minutes; the occurrence keeps its state |\n| no longer remindable, switch-core's links read | a held message's release | paid, in verification, « Expirée », CANCELLED or in a batch that ended SEND_FAILED as payment is read again: the message is never sent and ends NOT_SENT with reason `NOT_REMINDABLE` |\n| `INVALID_REQUEST` (400), `LINK_BATCH_NOT_FOUND` (404), `LINK_NOT_FOUND` (404) | a held message's release | a defect: operations are alerted and the message stays HELD, the body discarded; its release is tried again at the next opening |\n| `LINK_EXPIRY_OUT_OF_RANGE` (409) | a held message's release | the window ends within the hour, so the invoice is no longer remindable: the message is never sent and ends NOT_SENT with reason `NOT_REMINDABLE`; the body is discarded |\n| `MERCHANT_LIMIT_EXCEEDED` (409) | a held message's release | the message is never sent and ends NOT_SENT with code `MERCHANT_LIMIT_EXCEEDED` |\n| `LINK_NOT_REPLACEABLE` (409) | a held message's release | the invoice is no longer remindable, so the message is never sent and ends NOT_SENT with reason `NOT_REMINDABLE`; the body is discarded |\n| `LINK_REPLACED` (409), `IDEMPOTENCY_CONFLICT` (409) | a held message's release | a successor exists: it is read (links by invoice_id) and adopted, and the message goes with it |\n| 5xx, timeout, connection failure | a held message's release | retried without end, operations alerted after 15 minutes, channels called only until 19:55; the message stays HELD until answered, its key read first at the next opening |\n| `INVALID_REQUEST` (400), `LINK_BATCH_NOT_FOUND` (404), `LINK_NOT_FOUND` (404), switch-core's reads | a read in a run | a defect: operations are alerted; the run sends that batch no message and its figures stay as last read |\n| `MESSAGE_NOT_FOUND` (404), channels' message read | a read in a run | no message under that key, channel and merchant: a new body is recorded, with the new day's not_after, and sent under the same key |\n| `INVALID_REQUEST` (400), channels' message read | a read in a run | a defect: operations are alerted; the message stays HELD |\n| 5xx, timeout, connection failure | a read in a run | retried without end, operations alerted after 15 minutes |\n| 401 or 403, switch-core's access filter | a read in a run | a configuration defect, merchant-api's key not switch-core's: operations are alerted at once, once a batch and code, and the read is retried as a transient answer is; the figures stay as last read |\n| `INVALID_REQUEST` (400) | a message to channels | NOT_SENT with that code, operations alerted, since after the text rule only a defect gives it; the merchant's wording blames no request (« Message non envoyé : incident de notre côté, signalé »); the batch can still reach SENT |\n| `MESSAGES_NOT_CONFIGURED` (409) | a message to channels | NOT_SENT with that code, as above |\n| `IDEMPOTENCY_CONFLICT` (409) | a message to channels | channels holds a message under that key, possibly sent: it is read back from channels and its state adopted as the occurrence's outcome; operations alerted |\n| `MESSAGE_NOT_FOUND` (404), `INVALID_REQUEST` (400), the read after channels' 409 | a message to channels | a defect, channels' 409 having said it holds a message under that key: operations are alerted and the message keeps its state, never sent again under a new body |\n| 5xx, timeout, connection failure, the read after channels' 409 | a message to channels | retried until 19:55, as the message is; not answered by then, the message waits HELD with SEND_WINDOW and its key is read first at the next opening |\n| `EXPIRED`, `SEND_WINDOW_CLOSED` (202) | a message to channels | channels still held it at not_after, or it reached channels at or after not_after (ACCEPTED then EXPIRED at once, a replay answering the same): the occurrence's outcome, EXPIRED, never sent |\n| 401, channels' message request or its read | a message to channels | a configuration defect, merchant-api's key not channels': operations are alerted at once, the body is kept and retried as a transient answer is, and the message keeps its state, never NOT_SENT |\n| 5xx, timeout, connection failure | a message to channels | retried until 19:55; a message not answered by then waits HELD with SEND_WINDOW and its key is read first at the next opening |\n\nWhere each figure comes from: an invoice's PAID and VERIFYING live from switch-core's links read by invoice_id, at each send, each reminder run and each GET /v1/invoices/{id}; lists from merchant-api's projection, refreshed by paging switch-core's links from a position; a batch's paid counts and amounts per currency live from switch-core's batch read, its counts per state from the latest read of its invoices, read again in the background when switch-core's paid figures differ from them; UNPAID and EXPIRED from merchant-api's own judgment of each invoice's window, never switch-core's expired count. One message per occurrence and channel. A body is replayed only while its outcome is unknown; transient answers are retried until answered; merchant-api calls channels from 08:00 to 19:55 Kinshasa. While a merchant is disabled its sends wait: no call of its sends goes to switch-core or channels, operations are alerted once a batch, nothing is cancelled and each batch keeps its state until the merchant is enabled again. A file it validates meanwhile may still read switch-core's per-payment limit and the links of its references' holders. A send that fails closes its link batch, if one was created, its close never discarded, and once the close has answered its invoices read PAID, VERIFYING or CANCELLED. A held message whose invoice is no longer remindable at its release is never sent and ends NOT_SENT with NOT_REMINDABLE. Every text is kept and sent as the text rule trims it, so a chunk switch-core receives stays within its 6 MiB. At most 100,000 messages a merchant a Kinshasa day (SYNTHETIC), the rest HELD until the next day's opening, never refused. Personal fields are purged 90 days after an invoice closes, the invoice kept as a tombstone (SYNTHETIC). An invoice's link is not one of the merchant's own links: /v1/payment-links and its portal twin answer it 404, as an unknown link, and cannot cancel it; it is read through the invoice routes."
    }
  ]
}
