{
  "openapi": "3.0.3",
  "info": {
    "title": "PA Payments API",
    "version": "1.0.0",
    "description": "Public payments API of the PA Core payment aggregation platform.\n\nThe API exposes the PA model only (payments, refunds, payment instruments, payouts, merchant onboarding, audit trail, webhook delivery log, tenant key lifecycle). PSP/connector concepts never cross this boundary.\n\nConventions:\n- Authentication: `Authorization: Bearer <tenant_api_key>` for every `/v1` route (see `securitySchemes.bearerAuth`). Health probes and the docs portal are unauthenticated.\n- Money: integer minor units (cents) + ISO 4217 alpha-3 currency.\n- Identifiers: type-safe prefixed UUIDs (`pay_…`, `mer_…`, `pin_…`, `pout_…`, `tn_…`, `key_…`).\n- Idempotency: mutating requests take an `Idempotency-Key` header; a retry with the same key and body replays the stored result (`Idempotent-Replayed: true`), a reused key with a different body is rejected with `422 idempotency_key_reuse`.\n- Errors: RFC 7807 `application/problem+json` with stable PA `code` values; PSP error codes are never exposed.\n- Rate limiting: every authenticated request consumes a sliding-window slot (default 100 rpm per tenant+endpoint); over-limit returns `429 rate_limited` with `Retry-After`.\n- Interactive reference: `GET /docs` (branded Redocly reference page) served by the same API instance; `GET /docs/swagger-ui` is the try-it-out console (Swagger UI); this document is `GET /docs/openapi.json`.\n- Guides (PA-74): the same `/docs` reference page renders the operator handover guides INSIDE the sidebar «Guides» group — Handover Package, Onboarding Checklist (PA-61), SDK + Webhooks (Secure Fields) and SDK README — built from the versioned repository documents by `docs/api/build-redocly.sh`; `/docs` is the single documentation entry point, the raw markdown surface stays at `GET /portal/docs/{doc}`.\n\nRU documentation of the contract lives in the repository under `docs/api/` (Russian); this spec is the EN-facing reference for external merchants.",
    "x-logo": {
      "url": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIiByb2xlPSJpbWciIGFyaWEtbGFiZWw9IlBBIGxvZ28iPgogIDxyZWN0IHg9IjEiIHk9IjEiIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIgcng9IjciIGZpbGw9IiMxZDI0MzgiLz4KICA8cmVjdCB4PSIxIiB5PSIxIiB3aWR0aD0iMzAiIGhlaWdodD0iMzAiIHJ4PSI3IiBmaWxsPSJub25lIiBzdHJva2U9IiMzMzRCRkYiIHN0cm9rZS13aWR0aD0iMiIvPgogIDx0ZXh0IHg9IjE2IiB5PSIyMS41IiBmb250LWZhbWlseT0iQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2ZmZmZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgbGV0dGVyLXNwYWNpbmc9IjAuNSI+UEE8L3RleHQ+CiAgPGNpcmNsZSBjeD0iMjUuNSIgY3k9IjI1LjUiIHI9IjIuNSIgZmlsbD0iIzMzNEJGRiIvPgo8L3N2Zz4=",
      "altText": "PA Payments API",
      "backgroundColor": "#f5f6f9"
    }
  },
  "servers": [
    {
      "url": "https://payments.playpulse.tech",
      "description": "Production (PlayPulse platform; PA-64 — default)"
    },
    {
      "url": "https://sandbox.playpulse.tech",
      "description": "Sandbox (isolated sandbox environment, PA-79 — dedicated pa-api instance and database; legacy alias: https://167-233-117-108.sslip.io)"
    }
  ],
  "tags": [
    {
      "name": "Payments",
      "description": "Payment lifecycle (create, read). Status machine: PENDING → AUTHORIZED → CAPTURED → PARTIALLY_REFUNDED → REFUNDED; FAILED; UNKNOWN_PENDING_SYNC."
    },
    {
      "name": "Refunds",
      "description": "Idempotent refunds of authorized/captured payments."
    },
    {
      "name": "Payment Instruments",
      "description": "Card instrument registration from single-use vault tokens (Secure Fields capture surface). No PAN/CVV ever reaches the backend API."
    },
    {
      "name": "Payouts",
      "description": "Payout domain (approval gate, limits, idempotent mutations). The whole surface is gated by the fail-closed `payouts_enabled` feature flag — 404 `feature_disabled` while off."
    },
    {
      "name": "Merchant Onboarding",
      "description": "Admin surface: merchants, providers (MIDs), route targets, webhook registration."
    },
    {
      "name": "Webhooks",
      "description": "Signed payment events delivered by the outbox worker + delivery log."
    },
    {
      "name": "Audit",
      "description": "Immutable audit trail of configuration changes."
    },
    {
      "name": "Tenant Keys",
      "description": "Tenant API key lifecycle: rotation with 24h grace, immediate revocation."
    },
    {
      "name": "Merchant Portal",
      "description": "Self-service surface of the casino merchant portal (PA-65 + PA-66): dashboard statistics, payments list, payment details, CSV report, webhook delivery health, the self-profile read (PA-66: access triple, base URLs, webhook config without the secret) and the webhook URL re-target (PA-66). MVP auth: the same admin Bearer as the operator surfaces (single-principal model) — a merchant-scoped portal key is the production follow-up."
    },
    {
      "name": "Ops",
      "description": "Unauthenticated health probes for orchestrators."
    },
    {
      "name": "Docs",
      "description": "This documentation portal."
    },
    {
      "name": "PSP Portal",
      "description": "Processor-scoped self-service surface (PA-70). The Bearer key IS the connector scope: every route under /v1/psp is filtered by that connector code on the SQL side; a foreign MCA reference is a plain 404 (never 403). see docs/operations/psp-portal.md."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "tags": [
          "Ops"
        ],
        "summary": "Liveness probe",
        "description": "Returns `200` while the server process is alive. Deliberately performs NO database check: restarting the process cannot heal PostgreSQL, so liveness stays independent of it. Unauthenticated.",
        "security": [],
        "operationId": "healthz",
        "responses": {
          "200": {
            "description": "Process is alive",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                }
              }
            }
          }
        }
      }
    },
    "/ready": {
      "get": {
        "tags": [
          "Ops"
        ],
        "summary": "Readiness probe",
        "description": "Returns `200` while PostgreSQL answers a probe query (`SELECT 1`, 3s timeout), `503` when it does not. The orchestrator removes an unready instance from traffic rotation WITHOUT restarting it; readiness self-heals when the database recovers. Unauthenticated.",
        "security": [],
        "operationId": "ready",
        "responses": {
          "200": {
            "description": "Database is reachable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadinessStatus"
                }
              }
            }
          },
          "503": {
            "description": "Database is unreachable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadinessStatus"
                }
              }
            }
          }
        }
      }
    },
    "/docs": {
      "get": {
        "tags": [
          "Docs"
        ],
        "summary": "API reference (branded Redocly page)",
        "description": "Serves the branded, self-contained Redocly reference page (PA-73) built from this document. Unauthenticated; safe to bookmark for integration engineers.",
        "security": [],
        "operationId": "getDocs",
        "responses": {
          "200": {
            "description": "Redocly reference HTML page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/docs/openapi.json": {
      "get": {
        "tags": [
          "Docs"
        ],
        "summary": "OpenAPI 3.0 document of this API",
        "description": "The versioned machine-readable contract (same bytes as the repository file `docs/api/openapi.json`). Unauthenticated.",
        "security": [],
        "operationId": "getOpenapiSpec",
        "responses": {
          "200": {
            "description": "OpenAPI 3.0.3 document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/docs/static/{asset}": {
      "get": {
        "tags": [
          "Docs"
        ],
        "summary": "Static asset of the docs portal (vendored Swagger UI)",
        "description": "Serves the vendored swagger-ui-dist assets (`swagger-ui.css`, `swagger-ui-bundle.js`) from a closed allowlist — any other asset name is a plain 404. Unauthenticated.",
        "security": [],
        "operationId": "getDocsStaticAsset",
        "parameters": [
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "swagger-ui.css",
                "swagger-ui-bundle.js"
              ]
            },
            "description": "Vendored asset file name."
          }
        ],
        "responses": {
          "200": {
            "description": "Asset content (CSS or JavaScript)",
            "content": {
              "text/css": {
                "schema": {
                  "type": "string"
                }
              },
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown asset name (closed allowlist)"
          }
        }
      }
    },
    "/admin": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Admin console (PA-52)",
        "description": "Serves the same-origin admin console page (static SPA: onboarding checklist, merchants overview, payout approval). Unauthenticated as a surface; every API call from the page carries the admin Bearer to the authenticated `/v1/...` routes itself. Operator surface — see `docs/operations/admin-ui.md`.",
        "security": [],
        "operationId": "getAdminConsole",
        "responses": {
          "200": {
            "description": "Admin console HTML page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/admin/static/{asset}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Admin console static asset",
        "description": "Serves `admin.css` / `admin.js` of the console from a closed allowlist; anything else is a plain 404 (no path traversal surface).",
        "security": [],
        "operationId": "getAdminConsoleAsset",
        "parameters": [
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "description": "Static asset file name (allowlist: admin.css, admin.js).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requested asset (CSS or JavaScript)",
            "content": {
              "text/css": {
                "schema": {
                  "type": "string"
                }
              },
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown asset (closed allowlist)"
          }
        }
      }
    },
    "/portal": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Merchant portal console",
        "description": "PA-65: the self-service merchant portal console at `/portal` — a static, same-origin SPA served by the API itself (the PA-52 `/admin` pattern: embedded at compile time, no filesystem lookup). The surface is UNAUTHENTICATED on the serving side: it carries no tenant data of its own — every API call the SPA makes goes to the authenticated `/v1` routes with the Bearer key typed into the login box (kept in sessionStorage). A strict CSP (same-origin scripts/styles/connect only, no framing) is delivered by the HTML itself plus hardening headers. This surface is NOT part of the API contract; it is pinned by the router manifest.",
        "operationId": "merchantPortalConsole",
        "security": [],
        "responses": {
          "200": {
            "description": "Merchant portal console page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/portal/static/{asset}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Merchant portal static assets",
        "description": "PA-65: the static assets of the `/portal` console (portal.css, portal.js). The allowlist is closed — anything else is a plain 404 (no path traversal surface, no directory listing).",
        "operationId": "merchantPortalStaticAsset",
        "security": [],
        "parameters": [
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "description": "Static asset file name of the portal console.",
            "schema": {
              "type": "string",
              "enum": [
                "portal.css",
                "portal.js"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Static asset",
            "content": {
              "text/css": {
                "schema": {
                  "type": "string"
                }
              },
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown asset (closed allowlist)"
          }
        }
      }
    },
    "/v1/payments": {
      "post": {
        "tags": [
          "Payments"
        ],
        "summary": "Create a payment",
        "description": "Idempotent payment creation. The request runs the routing cascade (eligible Route Targets by currency/capture method) through the execution engine; the response is the logical payment of the PA model (attempt journal collapsed into `attempts_summary`, no PSP internals).\n\nDeterministic client errors (`unknown_merchant`, `no_eligible_route`, validation failures) are stored with the idempotency record and are replayed identically on retry with the same key.\n\nRequires the `Idempotency-Key` header.",
        "operationId": "createPayment",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment created. Idempotent replay: the same `Idempotency-Key` + body returns the ORIGINAL stored result — the same `201` status and body, header `Idempotent-Replayed: true`, no new logical payment (a stored deterministic error problem replays with its original status and problem body).",
            "headers": {
              "Idempotent-Replayed": {
                "description": "`false` on a fresh execution, `true` on a replay of the stored result (present on every response of this idempotent POST).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Another request with the same idempotency key is still in flight (`idempotency_in_flight`); `Retry-After` carries the claim window",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failure (`invalid_request`), unknown merchant (`unknown_merchant`), no eligible route target (`no_eligible_route`), unsupported method (`unsupported_payment_method`) or idempotency key reuse (`idempotency_key_reuse`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payments/{id}": {
      "get": {
        "tags": [
          "Payments"
        ],
        "summary": "Fetch a payment",
        "description": "Returns the last confirmed state of the payment with the collapsed attempt journal. Fresh reads never call the PSP; while the state is `PENDING` or `UNKNOWN_PENDING_SYNC` the response carries `next_action` and a `Retry-After` header (synchronization is asynchronous — the source of truth is this GET).",
        "operationId": "getPayment",
        "parameters": [
          {
            "$ref": "#/components/parameters/PaymentId"
          }
        ],
        "responses": {
          "200": {
            "description": "Payment state",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Payment not found in this tenant (`payment_not_found`) — a foreign-tenant payment is a plain 404",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/refunds": {
      "post": {
        "tags": [
          "Refunds"
        ],
        "summary": "Create a refund",
        "description": "Idempotent full or partial refund of a payment in `AUTHORIZED` / `CAPTURED` / `PARTIALLY_REFUNDED` / `REFUNDED` state. The refund amount never exceeds the remaining refundable amount; the payment status is recomputed (`PARTIALLY_REFUNDED` → `REFUNDED`). Requires the `Idempotency-Key` header.",
        "operationId": "createRefund",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRefundRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Refund created. Idempotent replay: the same `Idempotency-Key` + body returns the ORIGINAL stored result — the same `201` status and body, header `Idempotent-Replayed: true`, no second refund (a stored deterministic error problem replays with its original status and problem body).",
            "headers": {
              "Idempotent-Replayed": {
                "description": "`false` on a fresh execution, `true` on a replay of the stored result (present on every response of this idempotent POST).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefundResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Idempotency key in flight (`idempotency_in_flight`) with `Retry-After`",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failure, refund exceeds the remaining amount (`refund_exceeds_amount`) or invalid payment state (`invalid_state`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payment-instruments": {
      "post": {
        "tags": [
          "Payment Instruments"
        ],
        "summary": "Register a payment instrument from a single-use vault token",
        "description": "PCI boundary: the browser-side capture surface (PA Secure Fields SDK → PA Locker) tokenizes PAN/CVV into a single-use `vault_token`. This endpoint registers the instrument in PA — the request struct carries NO card fields and cannot accept PAN/CVV; PA resolves the masked metadata (masked PAN, brand, expiry) from the vault itself. The vault token is a credential: it is consumed for the metadata lookup and stored as a reference only, never serialized back.\n\nRequires the `Idempotency-Key` header.",
        "operationId": "createPaymentInstrument",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentInstrumentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Instrument registered. Idempotent replay: the same `Idempotency-Key` + body returns the ORIGINAL stored result — the same `201` status and body, header `Idempotent-Replayed: true`, no second instrument (a stored deterministic error problem replays with its original status and problem body).",
            "headers": {
              "Idempotent-Replayed": {
                "description": "`false` on a fresh execution, `true` on a replay of the stored result (present on every response of this idempotent POST).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentInstrumentResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Idempotency key in flight (`idempotency_in_flight`) or this vault token is already registered as a payment instrument of the tenant (`vault_token_already_registered`)",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Unknown vault token (`vault_token_unknown`), unknown merchant (`unknown_merchant`), validation failure (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payment-instruments/{id}": {
      "get": {
        "tags": [
          "Payment Instruments"
        ],
        "summary": "Fetch a payment instrument (masked view)",
        "description": "Returns masked card metadata only (masked PAN `****<last4>`, brand, optional expiry) and the instrument lifecycle status. No vault token is ever returned.",
        "operationId": "getPaymentInstrument",
        "parameters": [
          {
            "$ref": "#/components/parameters/InstrumentId"
          }
        ],
        "responses": {
          "200": {
            "description": "Masked instrument",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentInstrumentResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Instrument not found in this tenant (`instrument_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payouts": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Create a payout",
        "description": "Creates a payout (card-based receiver by `instrument_id` in the MVP; masked receiver metadata only — PAN/CVV payloads are rejected). Payouts follow the approval-gate lifecycle: created payouts wait for an operator decision (`POST /v1/payouts/{id}/approve|decline`) before dispatch.\n\nThe whole surface is gated by the fail-closed `payouts_enabled` feature flag: while off, every payout route answers 404 `feature_disabled`.\n\nRequires the `Idempotency-Key` header.",
        "operationId": "createPayout",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePayoutRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payout created. Idempotent replay: the same `Idempotency-Key` + body returns the ORIGINAL stored result — the same `201` status and body, header `Idempotent-Replayed: true`, no second payout (a stored deterministic error problem replays with its original status and problem body).",
            "headers": {
              "Idempotent-Replayed": {
                "description": "`false` on a fresh execution, `true` on a replay of the stored result (present on every response of this idempotent POST).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true",
                    "false"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Payouts disabled for this deployment (`feature_disabled`) or merchant not found (`unknown_merchant`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key in flight (`idempotency_in_flight`) with `Retry-After`",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failure (`invalid_request` — incl. PAN/CVV card data fields in the receiver/risk_context payload: the problem message names the offending field, e.g. `receiver must not contain card data fields (PAN/CVV)`; there is no separate card-data error code), unknown merchant (`unknown_merchant`), pre-flight limit failure (`limit_exceeded`) or idempotency key reuse (`idempotency_key_reuse`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      },
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "List payouts (the console approval queue, PA-69)",
        "description": "The tenant payout list page (PA-69, research §5 P0): newest first, filter-scoped `total`, clamped page. The console approval queue filters `status=AWAITING_APPROVAL`; the gate actions stay the existing `POST /v1/payouts/{id}/approve|decline|cancel` endpoints (PA-27) — this read adds no second mutation surface. Same fail-closed `payouts_enabled` feature flag as the rest of the payout domain (404 `feature_disabled` while off). Read-only: no audit rows.",
        "operationId": "listPayouts",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Optional payout-status filter (the canonical registry).",
            "schema": {
              "type": "string",
              "enum": [
                "CREATED",
                "AWAITING_APPROVAL",
                "APPROVED",
                "PROCESSING",
                "COMPLETED",
                "DECLINED",
                "FAILED_TECHNICAL",
                "CANCELLED",
                "UNKNOWN_PENDING_SYNC"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (clamped to 200).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Page offset (0-based).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payout list page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "description": "Payouts are not enabled for this tenant (`feature_disabled`, fail-closed)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payouts/{id}": {
      "get": {
        "tags": [
          "Payouts"
        ],
        "summary": "Fetch a payout",
        "description": "Returns the payout aggregate: status, collapsed attempt summary, approval decisions (optimistic-concurrency `version` included). 404 `feature_disabled` while the feature flag is off.",
        "operationId": "getPayout",
        "parameters": [
          {
            "$ref": "#/components/parameters/PayoutId"
          }
        ],
        "responses": {
          "200": {
            "description": "Payout state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Feature disabled (`feature_disabled`) or payout not found in this tenant (`payout_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payouts/{id}/approve": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Approve a payout (approval gate)",
        "description": "Fixes the operator decision `APPROVED` under the configured approval policy (fail-closed gate). Idempotent: the same key replays the stored decision. Empty body allowed.",
        "operationId": "approvePayout",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/PayoutId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutMutationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decision recorded / replayed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Feature disabled (`feature_disabled`) or payout not found (`payout_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key in flight (`idempotency_in_flight`) or a payout state that does not allow this transition (`invalid_state_transition` — incl. concurrent modification; RFC 7807, ADR-0004 §4.2 rule 6 — NOT stored as an idempotency result: a retry re-evaluates the current state)",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Idempotency key reuse (`idempotency_key_reuse`) or the approval-time limit re-check failure (`limit_exceeded`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payouts/{id}/decline": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Decline a payout (approval gate)",
        "description": "Fixes the operator decision `DECLINED`. Idempotent. Empty body allowed; `reason` is optional.",
        "operationId": "declinePayout",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/PayoutId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutMutationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decision recorded / replayed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Feature disabled (`feature_disabled`) or payout not found (`payout_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key in flight (`idempotency_in_flight`) or a payout state that does not allow this transition (`invalid_state_transition` — incl. concurrent modification; RFC 7807, ADR-0004 §4.2 rule 6 — NOT stored as an idempotency result: a retry re-evaluates the current state)",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Idempotency key reuse (`idempotency_key_reuse`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/payouts/{id}/cancel": {
      "post": {
        "tags": [
          "Payouts"
        ],
        "summary": "Cancel a payout before dispatch",
        "description": "Cancels a payout before dispatch (ADR-0004 §4.7.1). Idempotent. Empty body allowed.",
        "operationId": "cancelPayout",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/PayoutId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutMutationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decision recorded / replayed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Feature disabled (`feature_disabled`) or payout not found (`payout_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency key in flight (`idempotency_in_flight`) or a payout state that does not allow this transition (`invalid_state_transition` — incl. concurrent modification; RFC 7807, ADR-0004 §4.2 rule 6 — NOT stored as an idempotency result: a retry re-evaluates the current state)",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Idempotency key reuse (`idempotency_key_reuse`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants": {
      "post": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Create a merchant",
        "description": "Creates the merchant with its onboarding profile (default currency scope of the routing configuration). The routing configuration starts empty — route targets are added explicitly. Audited as `create_merchant` in the same transaction as the change.",
        "operationId": "createMerchant",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateMerchantRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Merchant created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateMerchantResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Validation failure (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      },
      "get": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "List the tenant merchants (admin console registry, PA-69)",
        "description": "The server-side merchant registry of the tenant (PA-69, the research doc `admin-console-research.md` §5 P0 gap): newest first, strictly tenant-scoped, with per-merchant provider (PA-59 execution model + connector status) and route-target lifecycle (PA-58) summaries. Optional `with_stats=1` attaches the 7d turnover snapshot per merchant (the PA-65 stats aggregate — GROSS EUR turnover of the approved family in minor units, previous-period delta, approval ratio; one aggregate query per page row). Replaces the PA-52 `localStorage` registry: the server is the source of truth.\n\n`q` is a substring filter over the name and the `mer_`-prefixed id (`%`/`_` are escaped — a literal match). Read-only: no audit rows (the console polls it).",
        "operationId": "listMerchants",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (clamped to 200).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Page offset (0-based).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Substring filter over the merchant name and the `mer_…` id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "with_stats",
            "in": "query",
            "required": false,
            "description": "Attach the 7d turnover snapshot per merchant (`1|true|yes` / `0|false|no`).",
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true",
                "yes",
                "0",
                "false",
                "no"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant registry page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}": {
      "get": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Fetch the merchant configuration",
        "description": "Full configuration of the merchant: onboarding profile, providers (with MIDs), route targets with filters and capability snapshot, registered webhook endpoint (URL only — the signing secret is never returned by a read).",
        "operationId": "getMerchant",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant configuration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantConfigResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/providers": {
      "post": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Link a provider (MID) to the merchant",
        "description": "Links one provider account (MID) to the merchant. The provider (connector) is reused when it already exists for the merchant, otherwise it is created first. `credentials_ref` is a PA reference only — connector credentials never enter PA. Audited as `create_provider` + `create_provider_account`.",
        "operationId": "addMerchantProvider",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddProviderRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Provider account linked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddProviderResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failure (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/route-targets": {
      "post": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Add a route target",
        "description": "Adds a Route Target (one routable terminal: MID + priority + weight + filters + capability snapshot). The MID must belong to a provider OF the merchant (scoped lookup, 404 otherwise). Audited as `update_routing` on the merchant.",
        "operationId": "addMerchantRouteTarget",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddRouteTargetRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Route target created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddRouteTargetResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant or provider account not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failure (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/webhook": {
      "post": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Register the webhook endpoint",
        "description": "Registers (or replaces) the HTTPS endpoint of the merchant backend that receives signed payment events. PA generates the HMAC signing secret server-side and returns it EXACTLY ONCE in this response — it is never surfaced again and never logged. Audited as `set_merchant_webhook` (details carry the URL only, never the secret).",
        "operationId": "setMerchantWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetMerchantWebhookRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook registered; `webhook_secret` is shown exactly once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetMerchantWebhookResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid URL (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/stats": {
      "get": {
        "tags": [
          "Merchant Portal"
        ],
        "summary": "Merchant dashboard statistics",
        "description": "Dashboard aggregates of one merchant over a trailing window (`7d` / `30d`, default `7d`): Turnover EUR (with the previous-period delta), Approval Ratio, Transaction Count, Pay Ins, Credits/Refunds, Payment Option Distribution, the full payment-state histogram and the daily rollup (the Reconciliation view).\n\nSemantics: the **approved family** is `AUTHORIZED`, `CAPTURED`, `PARTIALLY_REFUNDED`, `REFUNDED` (refunds are approvals returned later); `turnover_eur` sums the `AUTHORIZED | CAPTURED` volume in MINOR units (cents — the same unit as every amount in this API) with currency EUR only — money IN, while refunded volume is reported as Credits/Refunds (`refunds_eur`, SUCCEEDED refunds from the `refunds` table); other currencies are reported per-currency in `turnover_by_currency` and are never mixed; `previous_turnover_eur` is the same aggregate over the PREVIOUS period of the same length (`[now-2N, now-N)`) — the delta basis of the Turnover card; `approval_ratio` is approved / (approved + FAILED) in percent (two decimals) — the apcopay formula `authorized / (authorized + declined + failed)`: `PENDING` and `UNKNOWN_PENDING_SYNC` are still in flight and stay out of the denominator; `payins` counts the approved payments (research §4.5) and the Deposits card carries its own EUR line (`deposits_eur`, `AUTHORIZED | CAPTURED` only — money confirmed IN, not yet refunded, with `previous_deposits_eur` as the delta basis); `payment_method_distribution` groups the window payments per `payment_method_type` (count only).\n\n`daily` groups by UTC calendar day and per currency; `by_state` is always the full state registry (zero-filled) so the portal renders a deterministic grid. Read-only aggregate over the existing tables — no new schema.\n\nAuth model (MVP): the same admin Bearer key as the operator surfaces — the merchant id comes from the path and is resolved strictly inside the authenticated tenant; a foreign or unknown merchant is a plain `404`.\n\nPA-49 G1: pass `from`+`to` (both, YYYY-MM-DD, inclusive) for a custom period — the response echoes `window: \"custom\"` and `window_days` as the inclusive day count.",
        "operationId": "getMerchantStats",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          },
          {
            "name": "window",
            "in": "query",
            "required": false,
            "description": "Trailing window of the aggregates. Ignored when `from`+`to` are given (PA-49: the custom range wins).",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d"
              ],
              "default": "7d"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "PA-49 G1: custom range start (YYYY-MM-DD, inclusive, UTC). Requires `to`; `from < to`, span at most 90 days, else `400 invalid_request`.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "PA-49 G1: custom range end (YYYY-MM-DD, inclusive, UTC). Requires `from`.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Merchant statistics snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantStatsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unknown `window` value (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/payments": {
      "get": {
        "tags": [
          "Merchant Portal"
        ],
        "summary": "Merchant payments list",
        "description": "The Transactions grid of the merchant portal (apcopay-portal layout): the merchant's payments, newest first (`created_at DESC`, id as the deterministic tie-break), with an optional `status` filter and `limit`/`offset` pagination. `total` counts the payments matching the filter independently of the page.\n\nSummary projection with the apcopay columns: payment id, status, amount (minor units), currency, created_at, the latest attempt's provider + PSP reference, the payment option, the masked card metadata (PA-13 projection — brand + last4 + expiry, no PAN segments), the best-effort merchant context (`order_reference` from the request metadata, `email` from the request customer). No attempt journal — the full detail is the dedicated details read.\n\nSame admin Bearer (single-principal MVP); a foreign or unknown merchant is a plain `404`.",
        "operationId": "listMerchantPayments",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Optional payment-status filter (the canonical registry).",
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "AUTHORIZED",
                "CAPTURED",
                "PARTIALLY_REFUNDED",
                "REFUNDED",
                "FAILED",
                "UNKNOWN_PENDING_SYNC"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (clamped to 200).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Page offset (0-based).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "PA-49 G4: free-text search (contains match, ILIKE) over payment_id, PSP transaction reference, order_reference and email; at most 100 characters (else `400 invalid_request`). The `total` is search-scoped.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payments page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPaymentListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unknown `status` value or invalid pagination (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/payments/{payment_id}": {
      "get": {
        "tags": [
          "Merchant Portal"
        ],
        "summary": "Merchant payment details",
        "description": "The Transaction Details view of the merchant portal (apcopay-portal layout): the grid summary plus the PSP details (the attempt journal WITH the forensic provider facts — provider status / code / transaction reference, ADR-0002 §6.1; the collapse of the public `GET /v1/payments/{id}` summary does not bind this merchant-scoped read of the merchant's OWN data), the related refunds and the merchant's free-form request objects (`metadata`, `customer` — the expandable JSON of the details page). The Events timeline of the UI composes `attempts` (this response) with the webhook deliveries of `GET /v1/merchants/{id}/webhooks/deliveries?payment_id=…`.\n\nStrictly tenant+merchant scoped: a payment of another merchant (or an unknown id) is a plain `404`. Same admin Bearer.",
        "operationId": "getMerchantPaymentDetails",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          },
          {
            "name": "payment_id",
            "in": "path",
            "required": true,
            "description": "Payment id (`pay_…`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Payment details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantPaymentDetailsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Payment not found in this merchant (`payment_not_found` / `merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/payments/report.csv": {
      "get": {
        "tags": [
          "Merchant Portal"
        ],
        "summary": "Merchant transactions report (CSV)",
        "description": "The Transaction Report export: the same filter (`status`) as the payments list, delivered as a CSV attachment (`Content-Type: text/csv; charset=utf-8`, `Content-Disposition: attachment`). Columns: `payment_id`, `status`, `amount_minor`, `currency`, `capture_method`, `payment_method_type`, `provider`, `psp_reference`, `card_brand`, `card_last4`, `card_expiry`, `order_reference`, `email`, `created_at`, `updated_at` (RFC 3339 UTC timestamps, RFC 4180 quoting; the card columns carry the PA-13 masked projection only — never a PAN segment).\n\nPA-49 G2: `columns=` selects groups and/or individual columns (Choose Columns).\n\nPA-49 G5: the export is a STREAMING body walking keyset pages of 1000 rows (newest first) with a flat memory profile — the former bounded 10k snapshot is gone; any row count exports. A store error mid-stream aborts the body. Same admin Bearer; a foreign or unknown merchant is a plain `404`.",
        "operationId": "merchantPaymentsReportCsv",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Optional payment-status filter (the canonical registry).",
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "AUTHORIZED",
                "CAPTURED",
                "PARTIALLY_REFUNDED",
                "REFUNDED",
                "FAILED",
                "UNKNOWN_PENDING_SYNC"
              ]
            }
          },
          {
            "name": "columns",
            "in": "query",
            "required": false,
            "description": "PA-49 G2: comma-separated column keys and/or column-group keys (`transaction`, `bank`, `card`, `customer`). Columns: `payment_id`, `status`, `amount_minor`, `currency`, `capture_method`, `payment_method_type`, `provider`, `psp_reference`, `error_code`, `card_brand`, `card_last4`, `card_expiry`, `order_reference`, `email`, `created_at`, `updated_at`. Omitted → the full default set. The output order is always the canonical column order. Unknown keys / an empty selection → `400 invalid_request`.",
            "schema": {
              "type": "string",
              "example": "transaction,bank"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV export",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "payment_id,status,amount_minor,currency,capture_method,payment_method_type,provider,psp_reference,card_brand,card_last4,card_expiry,order_reference,email,created_at,updated_at\npay_01900000-0000-7000-8000-00000000000a,AUTHORIZED,1050,EUR,automatic,card,payadmit,mock-txn,do_not_honor,VISA,4242,08/2028,order-77,player@example.com,2025-09-24T10:00:00Z,2025-09-24T10:00:05Z"
              }
            }
          },
          "400": {
            "description": "Unknown `status` value (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/webhooks/deliveries": {
      "get": {
        "tags": [
          "Merchant Portal"
        ],
        "summary": "Merchant webhook delivery health",
        "description": "The webhook delivery health of the merchant (portal Integration tab): the most recent deliveries of ALL payments of the merchant, newest first, with an optional `payment_id` filter. Same projection discipline as `GET /v1/webhooks/deliveries` — the stored event payload (and with it every secret) is never projected into reads.\n\nDelivery is at-least-once (outbox worker, PA-40): `PENDING` rows are retried with backoff and dead-letter as `DEAD`; `DELIVERED` means the receiver acknowledged with a 2xx. The receiver verifies the `PA-Webhook-Signature` HMAC on its side — this log carries the PA-side delivery health (lifecycle state, attempt counter, last error class), not the receiver's verification result. Same admin Bearer; a foreign or unknown merchant is a plain `404`.",
        "operationId": "listMerchantWebhookDeliveries",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          },
          {
            "name": "payment_id",
            "in": "query",
            "required": false,
            "description": "Optional payment id (`pay_…`) to narrow the view to one payment's events.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of deliveries (clamped to 100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery health",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantWebhookDeliveryListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unknown `payment_id` value or invalid pagination (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/webhooks/deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Webhook delivery log of one payment",
        "description": "Delivery history of the webhook outbox for one payment: lifecycle status (`PENDING` / `DELIVERED` / `DEAD`), attempt counter, last error class and delivery time per event, oldest event first. Strictly tenant-scoped (a foreign payment is a plain 404). The stored event payload (and with it every secret) is never projected into reads.\n\nEvents are delivered asynchronously by the outbox worker: `payment.authorized`, `payment.captured`, `payment.refunded`, `payment.failed`. Delivery is at-least-once; receivers dedup by `event_id` and verify `PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, webhook_secret))>` over the raw delivered bytes.",
        "operationId": "listWebhookDeliveries",
        "parameters": [
          {
            "name": "payment_id",
            "in": "query",
            "required": true,
            "description": "Payment id (`pay_…`) whose delivery log is requested.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery history",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `payment_id` query parameter (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Payment not found in this tenant (`payment_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/audit": {
      "get": {
        "tags": [
          "Audit"
        ],
        "summary": "Audit trail read",
        "description": "Immutable audit trail of configuration changes (append-only at the database level; UPDATE/DELETE are rejected by the schema). Newest first. Filters combine conjunctively; `action` must be one of the closed registry codes (`create_merchant`, `create_provider`, `create_provider_account`, `update_routing`, `rotate_key`, `revoke_key`, `create_payment_instrument`, `set_merchant_webhook`, `create_payout`, `approve_payout`, `decline_payout`, `cancel_payout`, `housekeeping_run`).",
        "operationId": "listAudit",
        "parameters": [
          {
            "name": "entity_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by entity type (`merchant`, `provider`, `provider_account`, `route_target`, `tenant_key`, `payment_instrument`, `payout`, `housekeeping`)."
          },
          {
            "name": "entity_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by public entity id (`mer_…`, `prv_…`, …)."
          },
          {
            "name": "action",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by one audit action code of the closed registry. Note: `rotate_key` / `revoke_key` are already valid filter codes; their emission sites (tenant key lifecycle) land with PA-24 — until then the filter is valid but the filtered result is empty."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            },
            "description": "Page size (default 100, hard cap 1000)."
          }
        ],
        "responses": {
          "200": {
            "description": "Audit events, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuditListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unknown audit action or non-integer limit (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/tenants/{id}/rotate-key": {
      "post": {
        "tags": [
          "Tenant Keys"
        ],
        "summary": "Rotate the tenant API key",
        "description": "Generates a new tenant API key: it is valid IMMEDIATELY, the replaced key stays working for a 24h grace period (`old_key_expires_at`), then expires. The raw `new_key` is returned EXACTLY ONCE and is never stored (only its SHA-256 hash is). Keys are issued per tenant — the authenticated key must belong to the tenant named in the path.",
        "operationId": "rotateTenantKey",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          }
        ],
        "responses": {
          "200": {
            "description": "New key issued; the old key enters the grace period",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RotateKeyResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Tenant not found (`tenant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/tenants/{id}/keys/{key_id}": {
      "delete": {
        "tags": [
          "Tenant Keys"
        ],
        "summary": "Revoke a tenant API key",
        "description": "Immediate revocation: the key answers `401` starting from the next request (no grace). Revoking the key you are currently authenticated with locks out the caller — rotate first, then revoke the old `key_id`.",
        "operationId": "revokeTenantKey",
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantId"
          },
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Key id of the key to revoke (`key_…`)."
          }
        ],
        "responses": {
          "204": {
            "description": "Key revoked"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Tenant or key not found (`tenant_not_found` / `key_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/route-targets/{rt_id}/status": {
      "patch": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Change route target status",
        "description": "Lifecycle transition of one Route Target (PA-58, PA-55 §8): ENABLED / DRAINING / DEGRADED / DISABLED. Legal manual transitions follow the soft-disable flow — ENABLED ↔ DRAINING ↔ DISABLED with the return paths DISABLED → ENABLED or through DRAINING; same-state requests are `409 invalid_state_transition`. DEGRADED is set by the system only (limit machinery, ADR-0004 §4.5): an admin request to set it is rejected; the manual surface may reset a degraded target (`DEGRADED → ENABLED`) or take it further out (→ DRAINING/DISABLED). DRAINING and DISABLED exclude NEW payments from routing (422 `no_eligible_route` when nothing else remains) while in-flight attempts keep resolving (sync/refund/webhooks). Audited as `route_target.status_change` with before/after states.",
        "operationId": "changeRouteTargetStatus",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          },
          {
            "name": "rt_id",
            "in": "path",
            "required": true,
            "description": "Route target identifier (`rt_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RouteTargetStatusChangeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lifecycle status changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteTargetStatusChangeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant or route target not found in this tenant (`merchant_not_found`, `route_target_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Lifecycle transition not allowed (`invalid_state_transition`) — matrix violation or a concurrent status change",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failure (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/rotate-webhook-secret": {
      "post": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Rotate the webhook signing secret",
        "description": "Replaces the HMAC signing secret of the registered webhook endpoint (PA-34). The NEW secret is generated server-side and returned EXACTLY ONCE in this response — deliveries are signed with it immediately, while the replaced secret stays verifiable receiver-side until `old_secret_expires_at` (`PA_WEBHOOK_SECRET_GRACE_DAYS`, default 7 days): verify BOTH secrets during the grace window (dual-verify). Audited as `rotate_webhook_secret` (details carry the URL and the rotation time, never a secret).",
        "operationId": "rotateMerchantWebhookSecret",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "responses": {
          "201": {
            "description": "Secret rotated; `webhook_secret` is the NEW active secret, shown exactly once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RotateWebhookSecretResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Merchant has no webhook endpoint registered (`webhook_not_registered`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/selfprofile": {
      "get": {
        "tags": [
          "Merchant Portal"
        ],
        "summary": "Self-service profile of the merchant",
        "description": "One tenant-scoped read with everything the handover promised (PA-66, the `/portal` Profile tab): the access triple — `api_key` (the SAME bearer of THIS request echoed back: in the single-principal MVP the tenant API key IS the admin key the merchant already holds), `merchant_id` and the production/sandbox base URLs — plus the webhook configuration and the routing data. `webhook_url` is the registered endpoint (`null` when none); `webhook_secret_status` is `issued` (a webhook is registered — the secret was shown exactly once at registration/last rotation and is NEVER returned again; the only path to a new secret value is the PA-34 rotation) or `never_shown` (no webhook registered). `providers` lists the linked PSPs (connector + MID, currency scope, connector lifecycle status) and `test_cards` carries the documented sandbox terminal fixtures. Audited as `selfprofile.read` with masked details (merchant id only).",
        "operationId": "getMerchantSelfProfile",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "responses": {
          "200": {
            "description": "Self-service profile of the merchant (`api_key` = the bearer of this request)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MerchantSelfProfileResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/merchants/{id}/webhook-url": {
      "patch": {
        "tags": [
          "Merchant Portal"
        ],
        "summary": "Retarget the webhook endpoint (self-service)",
        "description": "Changes the registered webhook endpoint URL WITHOUT rotating the signing secret (PA-66 self-service): the active secret and the PA-34 grace slot stay byte-identical, so the receiver keeps verifying with the key it already holds. HTTPS-only validation — plain http, garbage or overlong values are rejected; requires an EXISTING webhook registration (URL and secret are a pair). Audited as `merchant.update_webhook_url` (details carry the new URL, never a secret).",
        "operationId": "updateMerchantWebhookUrl",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookUrlRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Endpoint retargeted; the signing secret is unchanged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateWebhookUrlResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Merchant has no webhook endpoint registered (`webhook_not_registered`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid URL — not https, garbage or over 2048 characters (`invalid_request`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/ops/reconciliation-health": {
      "get": {
        "tags": [
          "Ops"
        ],
        "summary": "Tenant reconciliation health (admin console, PA-69)",
        "description": "The tenant-wide current-state reconciliation snapshot (PA-69; research §3.5/§5): the PA-46 dispatch-journal unresolved count with the oldest unresolved dispatch (compare against `sync_recovery_stale_secs` — `PA_SYNC_RECOVERY_STALE_SECS`), the stuck-checkout candidates (the PA-36 predicate, threshold `PA_RECONCILE_AFTER_MINS`), the `UNKNOWN_PENDING_SYNC` backlog and the webhook-outbox health. No window — monitoring asks what is stuck NOW; the per-merchant windowed counterpart is `GET /v1/merchants/{id}/stats`. Read-only: no audit rows.",
        "operationId": "getOpsReconciliationHealth",
        "responses": {
          "200": {
            "description": "The tenant-wide reconciliation snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReconciliationHealthResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/ops/outbox/dead": {
      "get": {
        "tags": [
          "Ops"
        ],
        "summary": "List webhook dead letters (admin console, PA-69)",
        "description": "The PA-40 outbox dead-letter page of the tenant (PA-69; research §3.5/§5): the most recent DEAD deliveries (newest first) with the delivery facts (attempt counter, last error class — the stored event payload is never projected) plus the honest total backlog count. Delivery is at-least-once; receivers verify the HMAC signature on their side — this log is the PA-side delivery health. Read-only: no audit rows.",
        "operationId": "getOpsOutboxDead",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size of the letters list (clamped to 200).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dead-letter page plus the total backlog count",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OutboxDeadResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/ops/rate-limit": {
      "get": {
        "tags": [
          "Ops"
        ],
        "summary": "Rate-limit snapshot (admin console, PA-69)",
        "description": "The PA-47 read-only rate-limit snapshot (PA-69; research §3.5/§5): which backend is active (`memory`/`redis`), the sliding-window length, the default and per-endpoint limits, the unauthenticated per-IP limit and — for the memory backend — the live per-endpoint usage of the CALLING tenant plus the process-level tracked-bucket counters. The Redis backend holds the shared buckets in Redis; the snapshot reports the configuration only (`tenant_buckets`/`tracked` are absent — honest MVP gap, research §7 open question 4). Read-only, no secrets.",
        "operationId": "getOpsRateLimit",
        "responses": {
          "200": {
            "description": "The rate-limit configuration and (memory backend) live tenant usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitSnapshotResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/psp/me": {
      "get": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "PSP profile (scope identity, capabilities, fixtures)",
        "description": "The scope identity of the authenticated PSP key: the connector code (resolved FROM the key — the scope is not an input), the display name and the PA-59 execution snapshot of the connector rows, the EFFECTIVE capability snapshot (the last PSP anкета confirmation over the static PA-53 default snapshot), the production/sandbox base URLs and the sandbox test-card fixtures of the connector. Works for a PSP without provider rows yet (honest empty state).",
        "operationId": "getPspProfile",
        "responses": {
          "200": {
            "description": "PSP profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PspProfileResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/psp/me/mcas": {
      "get": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "The PSP's own connections (merchant × MID × MCA)",
        "description": "Every connection of the PSP across ALL merchants: providers of the connector code joined to the merchants and the provider accounts (the fork MCA reference), with the link scope (currencies/countries), the PA-59 execution snapshot and the PA-58 Route Target lifecycle statuses of every MID. No credentials (the MCA reference is a proxy address), no route priorities/weights (operator data), no other PSPs.",
        "operationId": "listPspMcas",
        "responses": {
          "200": {
            "description": "The PSP's own connections",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PspMcaListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/psp/me/dispatch": {
      "get": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "The PSP's own dispatches (attempts routed to the connector)",
        "description": "The PSP's own attempts (`attempts.provider_connector_code` = the key's scope), newest first, joined to the payment: PA state, amount/currency (minor units, never mixed), the PSP's own reference (`provider_transaction_id` — the pspid of the Apcopay model), the verbatim provider status/code, the final PA error code, merchant and MID. The PSP sees its ATTEMPTS — never the payment route as a whole (no competing PSPs). No customer PII.",
        "operationId": "listPspDispatches",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "description": "Page size (clamped to 200)."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Page offset."
          }
        ],
        "responses": {
          "200": {
            "description": "The dispatch grid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PspDispatchListResponse"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — non-positive limit / negative offset",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/psp/me/health": {
      "get": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "Health block of the PSP",
        "description": "Windowed health over the PSP's own dispatches: attempt-status counts (zero-filled registry + success rate over DECIDED attempts — in-flight and UNKNOWN stay out of the denominator; an undecided window is `null`, never 0%), the unresolved PA-46 dispatch-journal entries (in-flight or crashed dispatches — the money may be on the PSP without a recorded outcome), the decline breakdown by VERBATIM bank codes and the daily (UTC-day × currency) rollup of the dispatch volume.",
        "operationId": "getPspHealth",
        "parameters": [
          {
            "name": "window",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d"
              ],
              "default": "7d"
            },
            "description": "Aggregation window."
          }
        ],
        "responses": {
          "200": {
            "description": "Health block",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PspHealthResponse"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — unknown window",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/psp/me/capabilities": {
      "get": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "The effective capability snapshot",
        "description": "The PA-53 capability snapshot of the PSP: the LAST `psp.capabilities_confirmed` audit event (the PSP's own anкета confirmation, evidence `CONFIRMED_BY_PSP`) over the static PA-53 default snapshot (research facts; read-only baseline). `source` states which one is returned; a malformed stored snapshot falls back to the default (never fabricate flags).",
        "operationId": "getPspCapabilities",
        "responses": {
          "200": {
            "description": "Effective capability snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PspCapabilitiesView"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "put": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "Confirm the capability anкeta (PA-53)",
        "description": "The PSP confirms its capability flags (all six REQUIRED — a partial anкета would silently erase the rest of the snapshot). The flags are non-secret and land as the append-only audit event `psp.capabilities_confirmed` (actor `psp:<connector_code>`, entity `connector`, evidence `CONFIRMED_BY_PSP`) — the LAST confirmation is the durable value read back by GET (a dedicated configs/kv store is the PA-24 extension). PA-70 stores no migration; the append-only audit trail (PA-25) is the storage.",
        "operationId": "putPspCapabilities",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PspCapabilitiesPutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The confirmed snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PspCapabilitiesView"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — malformed body",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/v1/psp/me/mca/{mca_id}/credentials": {
      "patch": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "Rotate the MCA credentials (fork admin API proxy)",
        "description": "The PSP rotates the credentials of one of ITS MCAs. The MCA reference is resolved strictly inside the connector scope (a foreign MCA is 404). The values (the COMPLETE SignatureKey set — the fork's `MerchantConnectorUpdate.connector_account_details` REPLACES the whole credential object, so partial rotations are rejected with 422 to avoid clearing omitted fields) are STREAMED to the fork admin API (`POST /account/{merchant}/connectors/{mca}`) and NEVER persisted in PA — not in the Store, not in the audit trail, not in logs. The audit event `psp.credentials_rotated` carries the rotated FIELD NAMES only. Error mapping: fork rejection → `502 fork_admin_rejected` (status number only, the fork body is never passed through), fork unreachable → `502 fork_admin_unreachable`, proxy not configured → `503 psp_proxy_not_configured`.",
        "operationId": "rotatePspMcaCredentials",
        "parameters": [
          {
            "name": "mca_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "description": "The fork MCA reference (`provider_accounts.external_mca_ref`, e.g. `mca_…`). Resolved strictly inside the PSP connector scope — a foreign MCA is 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PspCredentialsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was rotated (field NAMES only)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PspCredentialsRotatedResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "`mca_not_found` — the MCA reference is outside the connector scope (foreign PSP)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_request` — partial/empty/overlong credential set",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "`fork_admin_rejected` | `fork_admin_unreachable`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "`psp_proxy_not_configured`",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/psp": {
      "get": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "PSP portal console",
        "description": "PA-70: the processor's self-service console at `/psp` — a static, same-origin SPA served by the API itself (the PA-52 `/admin` and PA-65 `/portal` pattern: embedded at compile time, no filesystem lookup). The surface is UNAUTHENTICATED on the serving side: it carries no PSP data of its own — every API call the SPA makes goes to the authenticated `/v1/psp/*` routes with the PSP Bearer typed into the login box (kept in sessionStorage); the key IS the connector scope. A strict CSP (same-origin scripts/styles/connect only, no framing) is delivered by the HTML itself plus hardening headers.",
        "operationId": "pspPortalConsole",
        "security": [],
        "responses": {
          "200": {
            "description": "PSP portal console page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/psp/static/{asset}": {
      "get": {
        "tags": [
          "PSP Portal"
        ],
        "summary": "PSP portal static assets",
        "description": "PA-70: the static assets of the `/psp` console (psp.css, psp.js). The allowlist is closed — anything else is a plain 404 (no path traversal surface, no directory listing).",
        "operationId": "pspPortalStaticAsset",
        "security": [],
        "parameters": [
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "description": "Static asset file name of the PSP console.",
            "schema": {
              "type": "string",
              "enum": [
                "psp.css",
                "psp.js"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Static asset",
            "content": {
              "text/css": {
                "schema": {
                  "type": "string"
                }
              },
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown asset (closed allowlist)"
          }
        }
      }
    },
    "/v1/sdk/capture": {
      "post": {
        "tags": [
          "SDK Capture"
        ],
        "summary": "Browser card capture — tokenize PAN/CVV into a single-use vault token (PA-48)",
        "description": "The real Locker capture surface of the PA Secure Fields SDK: the sandboxed SDK iframe posts PAN/CVV DIRECTLY here and receives a single-use vault token. PA forwards the card bundle to the configured capture client (fork Locker proxy) and holds the values only in flight — nothing card-shaped is logged, persisted or echoed.\n\nAuth: the PUBLIC publishable key (`X-Publishable-Key: pk_...`, body `publishable_key` fallback) — derived per merchant at onboarding, re-displayed by the merchant APIs. It binds the capture to a merchant but carries no bearer power.\n\nLoad control: hard per-key sliding window (`PA_SDK_CAPTURE_RPM`, default 30/min) + per-IP window; over-limit answers `429 rate_limited` + `Retry-After` before any card data is processed. Origin allowlist (`PA_SDK_CAPTURE_ALLOWED_ORIGINS`) applies when configured. No Idempotency-Key: every call mints a FRESH vault token (each token is consumed exactly once by the instrument registration).",
        "operationId": "sdkCapture",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SdkCaptureRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Card tokenized. Response headers carry `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer` and a restrictive CSP — token material is never cacheable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SdkCaptureResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "Missing/unknown publishable key (`invalid_publishable_key`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "Request origin not on the configured allowlist (`origin_not_allowed`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Validation failure (`invalid_request`) or the capture surface rejected the card (`capture_rejected`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Per-key or per-IP capture rate limit exceeded (`rate_limited`)",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "Capture surface unreachable/unavailable (`capture_unavailable`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "options": {
        "tags": [
          "SDK Capture"
        ],
        "summary": "CORS preflight of the capture endpoint",
        "description": "Never authenticated (browsers send no custom headers on preflights). Answers `204` with `Access-Control-Allow-*` headers when the Origin is acceptable (wildcard posture when no allowlist is configured), `403 origin_not_allowed` otherwise.",
        "operationId": "sdkCapturePreflight",
        "security": [],
        "responses": {
          "204": {
            "description": "Preflight accepted (no body)"
          },
          "403": {
            "description": "Origin not allowed (`origin_not_allowed`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchants/{id}/api-key": {
      "post": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Issue a merchant-scoped API key (PA-72)",
        "description": "Generates the merchant's OWN Bearer key and returns the raw value EXACTLY ONCE (PA-24 once-semantics); only the SHA-256 hash is stored (`merchants.api_key_hash`, migration 0019). Tenant isolation: from now on the casino uses THIS key on the `/portal` and the `/v1/merchants/{own_id}/*` surface — it can read and rotate ONLY its own merchant (a foreign id is `404 merchant_not_found`), cannot list `/v1/merchants` and cannot reach `/v1/ops/*` (both `403 forbidden`). The legacy tenant key keeps working as the admin principal (operator + PlayPulse compatibility) until every casino holds its own key. Issuing a key again REPLACES the current hash (the old key dies immediately) and is audited `merchant.api_key_issued` (hashes only, never raw values). Admin Bearer only.",
        "operationId": "issueMerchantApiKey",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "responses": {
          "201": {
            "description": "Key issued; `api_key` is the raw key, shown exactly once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssueMerchantKeyResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "403": {
            "description": "The Bearer is a merchant-scoped key, not the admin (tenant) key (`forbidden`) — PA-72 tenant isolation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/merchants/{id}/api-key/rotate": {
      "post": {
        "tags": [
          "Merchant Onboarding"
        ],
        "summary": "Rotate the merchant-scoped API key (PA-72)",
        "description": "Generates a NEW merchant-scoped key, returns it EXACTLY ONCE and keeps the replaced key verifiable until `old_key_expires_at` (24h grace, the PA-24 window). The merchant's own scoped key may rotate ITSELF; the admin key may rotate any tenant merchant; a merchant-scoped key on a foreign merchant is a plain `404`. Audited `merchant.api_key_rotated` (hashes + grace expiry only).",
        "operationId": "rotateMerchantApiKey",
        "parameters": [
          {
            "$ref": "#/components/parameters/MerchantId"
          }
        ],
        "responses": {
          "201": {
            "description": "Key rotated; `api_key` is the NEW raw key (shown once); the old key works until `old_key_expires_at`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IssueMerchantKeyResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Merchant not found in this tenant (`merchant_not_found`)",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/portal/docs/{doc}": {
      "get": {
        "tags": [
          "Admin"
        ],
        "summary": "Merchant documentation (markdown, this origin)",
        "description": "PA-72 follow-up (casino feedback #3): the merchant-facing documentation served from THIS origin — the reviewed docs only, never the GitLab. Allowlist: handover-package.md, casino-onboarding-checklist.md, pa-payments-api.md, pa-secure-fields.md, sdk-readme.md. PA-74: the same documents render INSIDE the `/docs` Redocly reference (sidebar «Guides» group) — this endpoint stays as the raw-markdown compatibility surface and is no longer linked from the portal UI.",
        "operationId": "merchantPortalStaticAsset",
        "security": [],
        "parameters": [
          {
            "name": "asset",
            "in": "path",
            "required": true,
            "description": "Static asset file name of the portal console.",
            "schema": {
              "type": "string",
              "enum": [
                "portal.css",
                "portal.js"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Static asset",
            "content": {
              "text/css": {
                "schema": {
                  "type": "string"
                }
              },
              "application/javascript": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown asset (closed allowlist)"
          }
        }
      }
    },
    "/docs/swagger-ui": {
      "get": {
        "tags": [
          "Docs"
        ],
        "summary": "Try-it-out console (Swagger UI)",
        "description": "Serves the Swagger UI console bound to `GET /docs/openapi.json` — the interactive counterpart of the `GET /docs` reference page (authorize with the tenant Bearer key, execute requests against this origin). Unauthenticated.",
        "security": [],
        "operationId": "getDocsSwaggerUi",
        "responses": {
          "200": {
            "description": "Swagger UI HTML page",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/": {
      "get": {
        "tags": [
          "Docs"
        ],
        "summary": "Landing redirect to the API reference",
        "description": "Redirects (302) to `/docs` — the branded API reference and integration guides. Lets the bare API base URL (sandbox or production) serve as the documentation entry point for integration engineers.",
        "security": [],
        "operationId": "rootRedirect",
        "responses": {
          "302": {
            "description": "Redirect to `/docs`",
            "headers": {
              "Location": {
                "description": "The reference page URL (`/docs`)",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Tenant API key (admin, issued at bootstrap/rotation) OR a merchant-scoped API key (PA-72, `mer_sk_…`): the merchant key is limited to its own merchant id on `/v1/merchants/{id}/*` (a foreign id → 404) and has no access to `/v1/merchants` (list) or `/v1/ops/*` (403 forbidden). Keys are stored as SHA-256 hashes only; the raw key is shown exactly once and never logged."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        },
        "description": "Client-generated unique key of the operation. A retry with the same key + same body replays the stored result of the original request — the ORIGINAL status code (e.g. `201` for a successful creation) and body, with header `Idempotent-Replayed: true` (a fresh execution answers `false`; no new domain event is written). Same key + different body is a `422 idempotency_key_reuse`; a stored deterministic 4xx problem replays as the same problem. TTL 24h."
      },
      "PaymentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Payment id (`pay_…`)."
      },
      "InstrumentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Payment instrument id (`pin_…`)."
      },
      "PayoutId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Payout id (`pout_…`)."
      },
      "MerchantId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Merchant id (`mer_…`)."
      },
      "TenantId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Tenant id (`tn_…`)."
      }
    },
    "headers": {
      "RetryAfter": {
        "description": "Seconds the client should wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid or expired bearer key (`unauthorized`) with `WWW-Authenticate: Bearer`",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Malformed request (`invalid_request`), e.g. missing idempotency key or unparseable body",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Sliding-window limit exceeded (`rate_limited`) with `Retry-After`",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Internal": {
        "description": "Internal error (`internal`) — the detailed cause goes to the log stream, never to the client",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 7807 problem report. `type` is always `about:blank`; `code` is the stable PA error code (never a PSP code).",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "message"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "RFC 7807 type URI (`about:blank`)."
          },
          "title": {
            "type": "string",
            "example": "Bad Request"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code repeated in the body.",
            "example": 422
          },
          "code": {
            "type": "string",
            "description": "Stable PA error code: `unauthorized`, `invalid_request`, `idempotency_key_required`, `idempotency_key_reuse`, `idempotency_in_flight`, `unknown_merchant`, `no_eligible_route`, `payment_not_found`, `invalid_state`, `invalid_state_transition`, `vault_token_already_registered`, `refund_exceeds_amount`, `limit_exceeded`, `rate_limited`, `feature_disabled`, `internal`, …",
            "example": "unknown_merchant"
          },
          "message": {
            "type": "string"
          },
          "payment_id": {
            "type": "string",
            "description": "Set when the error refers to a payment (e.g. idempotency races)."
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          }
        }
      },
      "ReadinessStatus": {
        "type": "object",
        "required": [
          "status",
          "checks"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "unavailable"
            ]
          },
          "checks": {
            "type": "object",
            "required": [
              "database"
            ],
            "properties": {
              "database": {
                "type": "string",
                "enum": [
                  "up",
                  "down"
                ]
              }
            }
          }
        }
      },
      "CreatePaymentRequest": {
        "type": "object",
        "required": [
          "merchant_id",
          "amount",
          "currency"
        ],
        "properties": {
          "merchant_id": {
            "type": "string",
            "description": "Merchant of this tenant (`mer_…`). A merchant outside the tenant of the Bearer key → `422 unknown_merchant`."
          },
          "amount": {
            "type": "integer",
            "description": "Positive integer in minor units (cents); `0` or negative → `422 invalid_request`.",
            "minimum": 1,
            "example": 1050
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 alpha-3. Part of the routing eligibility: when no Route Target of the merchant supports the currency, the payment answers `422 no_eligible_route` without starting an attempt.",
            "example": "EUR"
          },
          "payment_method": {
            "type": "object",
            "required": [
              "type"
            ],
            "description": "Payment method selection. Card payments reference a PA instrument (`pin_…`) registered from a single-use vault token; PAN/CVV are never accepted here.",
            "properties": {
              "type": {
                "type": "string",
                "description": "Payment method type. `card` is the only supported value; any other value → `422 unsupported_payment_method`.",
                "example": "card"
              },
              "instrument_id": {
                "type": "string",
                "description": "PA instrument reference (`pin_…`) from `POST /v1/payment-instruments`. SINGLE-USE: the instrument is consumed (`ACTIVE` → `USED`) atomically with the payment creation, so reusing the same `pin_…` for a second payment → `422 instrument_not_active` (also `unknown_instrument` for an id outside the tenant, `instrument_merchant_mismatch` for another merchant's instrument). Required for the dispatch: without it the connector cannot be called and the attempt ends `FAILED_TECHNICAL`.",
                "example": "pin_01900000-0000-7000-8000-000000000001"
              }
            }
          },
          "capture_method": {
            "type": "string",
            "enum": [
              "automatic",
              "manual"
            ],
            "default": "automatic",
            "description": "`automatic` (default) — sale model: the PSP dispatch is PURC, so PA `AUTHORIZED` already means the provider captured the money (provider_status `COMPLETED`). `manual` — authorize-only; the capture endpoint is NOT part of the public API surface, and Route Targets are capability-filtered: a target without the manual-capture capability never serves the request (`422 no_eligible_route` when nothing remains; the pilot merchant routes are configured `automatic` only)."
          },
          "customer": {
            "type": "object",
            "description": "Customer context (identity/contact); risk fields belong to `risk_context`. Must be a JSON object (or absent/`null`) — any other JSON type → `422 invalid_request`. FREE-FORM and WRITE-ONLY: the object is stored verbatim and projected to the PSP connector call, but the payment response NEVER echoes it (see `PaymentResponse`; the merchant portal Transaction Details / CSV report are the read surface).",
            "properties": {
              "reference_id": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "number"
                  }
                ],
                "description": "Casino-side player reference (ownership context of the instruments). A JSON number is canonicalized to its decimal form before validation; explicit `null` = absent. Opaque-reference rules: at most 255 characters, charset `[A-Za-z0-9_.@-]`, and a Luhn-valid 13–19 digit run — including a formatted PAN such as `4111-1111-1111-1111` — is rejected as card data (`422 invalid_request`; the message names the constraint, never the value). This is NOT a PAN field.",
                "example": "usr_123"
              },
              "first_name": {
                "type": "string",
                "description": "Player first name; projected into the PSP billing address (required by the Apcopay S2S DEPOSIT profile)."
              },
              "last_name": {
                "type": "string",
                "description": "Player last name; projected into the PSP billing address (required by the Apcopay S2S DEPOSIT profile)."
              },
              "email": {
                "type": "string",
                "description": "Player email; projected to the PSP as the billing/email contact (required by the Apcopay S2S DEPOSIT profile)."
              },
              "phone": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "object"
                  }
                ],
                "description": "Player phone. REQUIRED by the Apcopay S2S DEPOSIT profile — without it the connector rejects the payment with its missing-param error (the fork `IR_04` family). Flat string form: digits only, a leading `+` is stripped (`\"+35712345678\"` → `35712345678`). Structured form `{\"country_code\": \"357\", \"number\": \"12345678\"}` is projected verbatim; `country_code` defaults from the `billing_address` country when absent.",
                "example": "35712345678"
              }
            }
          },
          "risk_context": {
            "type": "object",
            "description": "Casino-supplied risk context (KYC flags, deposit statistics). Free-form, stored with the payment, write-only (never echoed). The MVP routing does not consume it; the connector projection is data-minimized."
          },
          "session_context": {
            "type": "object",
            "description": "Device/session context of the payment. Free-form, stored, write-only (never echoed); it does not affect the MVP routing — EXCEPT the `browser` block, which is projected into the PSP `browser_info` of the dispatch (the Apcopay DirectConnect PURC requires `UserAgent`/`TimeZone`; absent context → the dispatch carries no browser data).",
            "properties": {
              "browser": {
                "type": "object",
                "description": "3DS/browser data collected by the casino client (PAN-free). Keys projected to the PSP: `user_agent`, `accept_header`, `language` (strings), `java_enabled`, `java_script_enabled` (booleans), `screen_width`, `screen_height`, `color_depth` (integers), `time_zone` (JS `getTimezoneOffset()` minutes offset), `ip_address` (must parse as an IP)."
              }
            }
          },
          "billing_address": {
            "type": "object",
            "description": "Billing address; free-form and write-only (never echoed by the payment response). Two consumers: (1) the routing country filter — `country_code`/`country` scopes Route Target selection, and a payment WITHOUT a country never matches a country-filtered target (an all-filtered cascade → `422 no_eligible_route`); (2) the PSP billing projection (`address.line1` etc. — the Apcopay S2S DEPOSIT profile requires `addressLine1`, `city`, `countryCode`, `postalCode`).",
            "properties": {
              "address_line1": {
                "type": "string",
                "description": "Address line 1 (projected to the PSP `line1`; required by the Apcopay S2S DEPOSIT profile)."
              },
              "address_line2": {
                "type": "string",
                "description": "Address line 2 (projected to the PSP `line2`)."
              },
              "city": {
                "type": "string",
                "description": "City (required by the Apcopay S2S DEPOSIT profile)."
              },
              "state": {
                "type": "string",
                "description": "State/region (projected to the PSP `state`)."
              },
              "postal_code": {
                "type": "string",
                "description": "Postal code (projected to the PSP `zip`; required by the Apcopay S2S DEPOSIT profile)."
              },
              "country_code": {
                "type": "string",
                "description": "ISO-2 country code — the routing country-filter input (preferred over `country`)."
              },
              "country": {
                "type": "string",
                "description": "Alternative key of the billing country (read when `country_code` is absent); ISO-2."
              }
            }
          },
          "metadata": {
            "type": "object",
            "description": "Free-form client metadata (e.g. `order_id`); stored with the payment, WRITE-ONLY: never echoed by the payment response. Convention: the external order reference belongs in `metadata.order_reference` — that key surfaces in the merchant portal (Order Reference column, free-text search `q`, CSV report).",
            "additionalProperties": true
          }
        }
      },
      "PaymentResponse": {
        "type": "object",
        "description": "Projection of the logical PA payment — the creation response and `GET /v1/payments/{id}` share it. The request contexts (`customer`, `billing_address`, `metadata`, `risk_context`, `session_context`) are WRITE-ONLY and are NEVER echoed here; the merchant portal (Transaction Details `GET /v1/merchants/{id}/payments/{payment_id}`, CSV report) is the read surface for the merchant's own context. PSP internals stay collapsed: `attempts_summary` is the public projection of the attempt journal.",
        "required": [
          "payment_id",
          "status",
          "amount",
          "currency",
          "capture_method",
          "attempts_summary",
          "created_at"
        ],
        "properties": {
          "payment_id": {
            "type": "string",
            "example": "pay_01900000-0000-7000-8000-00000000000a"
          },
          "status": {
            "type": "string",
            "description": "PA payment status (ADR-0002 §2.2).",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED",
              "UNKNOWN_PENDING_SYNC"
            ]
          },
          "amount": {
            "type": "integer",
            "example": 1050
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "capture_method": {
            "type": "string",
            "enum": [
              "automatic",
              "manual"
            ]
          },
          "next_action": {
            "$ref": "#/components/schemas/NextAction"
          },
          "attempts_summary": {
            "type": "array",
            "description": "Collapsed attempt journal: per-attempt sequence number, the public Route Target projection (provider + MID label only, no PSP secrets) and the attempt status. The decline class is NOT part of the summary — it is carried by the full attempt domain model internally, never by this public projection.",
            "items": {
              "type": "object",
              "required": [
                "seq",
                "route_target",
                "status"
              ],
              "properties": {
                "seq": {
                  "type": "integer"
                },
                "route_target": {
                  "type": "object",
                  "description": "Public Route Target projection of the attempt.",
                  "required": [
                    "provider",
                    "mid"
                  ],
                  "properties": {
                    "provider": {
                      "type": "string",
                      "example": "payadmit"
                    },
                    "mid": {
                      "type": "string",
                      "example": "mid_1"
                    }
                  }
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "PENDING",
                    "AUTHORIZED",
                    "DECLINED_SOFT",
                    "DECLINED_HARD",
                    "UNKNOWN",
                    "FAILED_TECHNICAL"
                  ]
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NextAction": {
        "description": "Typed union (`type` discriminator): what the client should do next. `none` — nothing to do; `wait` carries `retry_after_secs` (synchronize later); `redirect` carries the hosted-page URL (3DS / hosted payment page); `collect_data` asks the client to gather additional data from the player; `approval` marks an operator action (payout gate).",
        "required": [
          "type"
        ],
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "none"
                ]
              }
            }
          },
          {
            "type": "object",
            "required": [
              "retry_after_secs",
              "reason"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "wait"
                ]
              },
              "retry_after_secs": {
                "type": "integer"
              },
              "reason": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "redirect_url",
              "method"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "redirect"
                ]
              },
              "redirect_url": {
                "type": "string"
              },
              "method": {
                "type": "string"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "required_fields"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "collect_data"
                ],
                "description": "Discriminator: the client must collect additional data."
              },
              "required_fields": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Field names the processor asked the client to collect from the player (e.g. `email`, `phone`)."
              }
            },
            "description": "The payment processor requests additional data from the player before it can proceed. `required_fields` lists the field names the client must collect (e.g. via the Secure Fields SDK or a follow-up form). After collecting them, create a new payment (or repeat the confirm call) supplying the requested data. In the current live flows the platform answers with `none`, `redirect` or `wait`; `collect_data` is the contract member reserved for processor flows that need extra input."
          },
          {
            "type": "object",
            "required": [
              "resource",
              "allowed_actions"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "approval"
                ]
              },
              "resource": {
                "type": "string"
              },
              "allowed_actions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        ]
      },
      "CreateRefundRequest": {
        "type": "object",
        "required": [
          "payment_id",
          "amount"
        ],
        "properties": {
          "payment_id": {
            "type": "string",
            "example": "pay_01900000-0000-7000-8000-00000000000a"
          },
          "amount": {
            "type": "integer",
            "description": "Minor units; must not exceed the remaining refundable amount.",
            "minimum": 1
          },
          "reason": {
            "type": "string"
          }
        }
      },
      "RefundResponse": {
        "type": "object",
        "required": [
          "refund_id",
          "payment_id",
          "amount",
          "currency",
          "status",
          "payment_status"
        ],
        "properties": {
          "refund_id": {
            "type": "string"
          },
          "payment_id": {
            "type": "string"
          },
          "amount": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Refund status.",
            "enum": [
              "PENDING",
              "SUCCEEDED",
              "FAILED"
            ]
          },
          "payment_status": {
            "type": "string",
            "description": "Recomputed payment status.",
            "enum": [
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED"
            ]
          }
        }
      },
      "CreatePaymentInstrumentRequest": {
        "type": "object",
        "required": [
          "merchant_id",
          "vault_token"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "vault_token": {
            "type": "string",
            "description": "Opaque single-use token from the Secure Fields capture surface (credential — never logged, never serialized back)."
          },
          "customer_reference": {
            "type": "string",
            "description": "Casino-side player reference (ownership context; optional)."
          }
        }
      },
      "CardMetaResponse": {
        "type": "object",
        "required": [
          "masked_pan",
          "brand"
        ],
        "properties": {
          "masked_pan": {
            "type": "string",
            "description": "Masked PAN `****<last4>`; PA deliberately does not keep the first digits.",
            "example": "****4242"
          },
          "brand": {
            "type": "string",
            "example": "VISA"
          },
          "exp_month": {
            "type": "string"
          },
          "exp_year": {
            "type": "string"
          }
        }
      },
      "PaymentInstrumentResponse": {
        "type": "object",
        "required": [
          "instrument_id",
          "merchant_id",
          "status",
          "card_meta",
          "created_at"
        ],
        "properties": {
          "instrument_id": {
            "type": "string",
            "example": "pin_01900000-0000-7000-8000-000000000001"
          },
          "merchant_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "ACTIVE",
              "USED",
              "EXPIRED",
              "DEACTIVATED",
              "PURGED"
            ]
          },
          "card_meta": {
            "$ref": "#/components/schemas/CardMetaResponse"
          },
          "customer_reference": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "used_at": {
            "type": "string",
            "format": "date-time",
            "description": "First payment usage of the single-use instrument."
          }
        }
      },
      "CreatePayoutRequest": {
        "type": "object",
        "required": [
          "merchant_id",
          "amount",
          "currency",
          "customer_reference"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "minimum": 1
          },
          "currency": {
            "type": "string"
          },
          "customer_reference": {
            "type": "string",
            "description": "Casino-side player reference."
          },
          "instrument_id": {
            "type": "string",
            "description": "Card-based receiver: PA instrument (`pin_…`); required together with (or replaced by) `receiver`."
          },
          "receiver": {
            "type": "object",
            "description": "Masked receiver metadata; PAN/CVV keys are rejected with `422 invalid_request` (`receiver must not contain card data fields (PAN/CVV)`)."
          },
          "risk_context": {
            "type": "object"
          },
          "retry_of_payout_id": {
            "type": "string",
            "description": "Original payout when retrying after a confirmed DECLINED (a new payout, not a second financial attempt)."
          }
        }
      },
      "PayoutMutationRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string"
          }
        }
      },
      "PayoutResponse": {
        "type": "object",
        "required": [
          "payout_id",
          "status",
          "amount",
          "currency",
          "customer_reference",
          "attempts_summary",
          "version",
          "created_at"
        ],
        "properties": {
          "payout_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "PA payout status (ADR-0004 §4.2 state machine). Phase-1 reachable through this API: `AWAITING_APPROVAL` (manual gate), `APPROVED` (auto-approve policy — decision fixed, PSP dispatch pending), `DECLINED`, `CANCELLED`. `CREATED` is the domain-level initial state (validation and pre-flight limits passed) that a created payout leaves immediately — creation maps it straight onto `AWAITING_APPROVAL` / `APPROVED`. `PROCESSING` and `UNKNOWN_PENDING_SYNC` (as well as `COMPLETED` / `FAILED_TECHNICAL`) become reachable only after the Phase-2 PSP dispatch; `UNKNOWN_PENDING_SYNC` syncs first (ADR-0002 §3 applied to payouts).",
            "enum": [
              "CREATED",
              "AWAITING_APPROVAL",
              "APPROVED",
              "PROCESSING",
              "COMPLETED",
              "DECLINED",
              "FAILED_TECHNICAL",
              "CANCELLED",
              "UNKNOWN_PENDING_SYNC"
            ]
          },
          "amount": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "customer_reference": {
            "type": "string"
          },
          "next_action": {
            "$ref": "#/components/schemas/NextAction"
          },
          "attempts_summary": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "seq": {
                  "type": "integer"
                },
                "status": {
                  "type": "string"
                }
              }
            }
          },
          "approval_decisions": {
            "type": "array",
            "description": "Approval-gate decisions (present after a gate mutation).",
            "items": {
              "type": "object",
              "required": [
                "decision",
                "policy_id",
                "actor",
                "role",
                "decided_at"
              ],
              "properties": {
                "decision": {
                  "type": "string",
                  "enum": [
                    "APPROVED",
                    "DECLINED"
                  ]
                },
                "policy_id": {
                  "type": "string"
                },
                "actor": {
                  "type": "string"
                },
                "role": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                },
                "snapshot_hash": {
                  "type": "string",
                  "description": "Hash of the immutable policy snapshot the decision was made against."
                },
                "decided_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "version": {
            "type": "integer",
            "description": "Optimistic-concurrency version of the aggregate."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateMerchantRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 255
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2."
          },
          "currencies": {
            "type": "array",
            "description": "ISO 4217 alpha-3 codes (default routing currency scope).",
            "items": {
              "type": "string"
            },
            "maxItems": 50
          },
          "legal_entity": {
            "type": "object",
            "description": "Free-form legal entity metadata snapshot."
          }
        }
      },
      "CreateMerchantResponse": {
        "type": "object",
        "required": [
          "currencies",
          "merchant_id",
          "name",
          "publishable_key"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "legal_entity": {
            "type": "object"
          },
          "publishable_key": {
            "type": "string",
            "description": "PA-48: the derived publishable key of the merchant (`pk_<merchant-uuid>_<digest>`). PUBLIC by design — it lives in the casino frontend and binds browser captures to this merchant; it is not a credential. Deterministic: re-derived on every read (same value as at onboarding)."
          }
        }
      },
      "AddProviderRequest": {
        "type": "object",
        "required": [
          "provider_code",
          "mid",
          "credentials_ref"
        ],
        "properties": {
          "provider_code": {
            "type": "string",
            "description": "Connector code (lowercase identifier, e.g. `payadmit`); normalized to lowercase."
          },
          "mid": {
            "type": "string",
            "description": "Merchant account (MID) label of this provider."
          },
          "credentials_ref": {
            "type": "string",
            "description": "PA-side reference of the connector credentials — the credentials themselves stay in the provider's secret store, never in PA."
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ISO 4217 alpha-3 codes."
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ISO 3166-1 alpha-2 codes."
          }
        }
      },
      "AddProviderResponse": {
        "type": "object",
        "required": [
          "provider_id",
          "provider_account_id",
          "connector_code",
          "mid_label"
        ],
        "properties": {
          "provider_id": {
            "type": "string"
          },
          "provider_account_id": {
            "type": "string"
          },
          "connector_code": {
            "type": "string"
          },
          "mid_label": {
            "type": "string"
          }
        }
      },
      "AddRouteTargetRequest": {
        "type": "object",
        "required": [
          "provider_account_id",
          "priority"
        ],
        "properties": {
          "provider_account_id": {
            "type": "string",
            "description": "MID (`mid_…`) of a provider OF this merchant."
          },
          "priority": {
            "type": "integer",
            "description": "Cascade order (lower = earlier)."
          },
          "weight": {
            "type": "integer",
            "description": "Weighted split within the same priority (default 1)."
          },
          "filters": {
            "type": "object",
            "description": "Routing filters; a scalar or a list is accepted for each field.",
            "properties": {
              "country": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                ]
              },
              "currency": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                ]
              },
              "method": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                ]
              }
            }
          },
          "capabilities": {
            "type": "object",
            "description": "Capability snapshot of the terminal (e.g. `redirect_hpp`, `s2s_card`, `lookup_by_reference`); absent fields = not declared.",
            "properties": {
              "redirect_hpp": {
                "type": "boolean"
              },
              "s2s_card": {
                "type": "boolean"
              },
              "lookup_by_reference": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "AddRouteTargetResponse": {
        "type": "object",
        "required": [
          "route_target_id",
          "merchant_id",
          "provider_id",
          "provider_account_id",
          "priority",
          "weight",
          "filters",
          "capabilities",
          "status"
        ],
        "properties": {
          "route_target_id": {
            "type": "string"
          },
          "merchant_id": {
            "type": "string"
          },
          "provider_id": {
            "type": "string"
          },
          "provider_account_id": {
            "type": "string"
          },
          "priority": {
            "type": "integer"
          },
          "weight": {
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/RouteTargetStatus",
            "description": "Lifecycle state (PA-58): a fresh Route Target is always ENABLED."
          },
          "filters": {
            "$ref": "#/components/schemas/RouteTargetFilters"
          },
          "capabilities": {
            "type": "object",
            "properties": {
              "redirect_hpp": {
                "type": "boolean"
              },
              "s2s_card": {
                "type": "boolean"
              },
              "lookup_by_reference": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "RouteTargetFilters": {
        "type": "object",
        "required": [
          "countries",
          "currencies",
          "methods"
        ],
        "properties": {
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "methods": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "automatic",
                "manual"
              ]
            }
          }
        }
      },
      "ProviderAccountResponse": {
        "type": "object",
        "required": [
          "provider_account_id",
          "mid_label",
          "credentials_ref"
        ],
        "properties": {
          "provider_account_id": {
            "type": "string"
          },
          "mid_label": {
            "type": "string"
          },
          "credentials_ref": {
            "type": "string"
          }
        }
      },
      "ProviderConfigResponse": {
        "type": "object",
        "required": [
          "provider_id",
          "connector_code",
          "display_name",
          "currencies",
          "countries",
          "accounts"
        ],
        "properties": {
          "provider_id": {
            "type": "string"
          },
          "connector_code": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProviderAccountResponse"
            }
          }
        }
      },
      "RouteTargetConfigResponse": {
        "type": "object",
        "required": [
          "route_target_id",
          "provider_id",
          "provider_account_id",
          "priority",
          "weight",
          "active",
          "filters",
          "capabilities",
          "status"
        ],
        "properties": {
          "route_target_id": {
            "type": "string"
          },
          "provider_id": {
            "type": "string"
          },
          "provider_account_id": {
            "type": "string"
          },
          "priority": {
            "type": "integer"
          },
          "weight": {
            "type": "integer"
          },
          "status": {
            "$ref": "#/components/schemas/RouteTargetStatus",
            "description": "Lifecycle state (PA-58, PA-55 §8). Source of truth for routing eligibility; the legacy `active` flag is its derived projection."
          },
          "active": {
            "type": "boolean"
          },
          "filters": {
            "$ref": "#/components/schemas/RouteTargetFilters"
          },
          "capabilities": {
            "type": "object",
            "description": "Capability snapshot; absent fields = not declared.",
            "properties": {
              "redirect_hpp": {
                "type": "boolean"
              },
              "s2s_card": {
                "type": "boolean"
              },
              "lookup_by_reference": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "MerchantConfigResponse": {
        "type": "object",
        "required": [
          "created_at",
          "currencies",
          "merchant_id",
          "name",
          "providers",
          "publishable_key",
          "route_targets"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "legal_entity": {
            "type": "object"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProviderConfigResponse"
            }
          },
          "route_targets": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "route_target_id",
                "provider_id",
                "provider_account_id",
                "priority",
                "weight",
                "active",
                "filters",
                "capabilities"
              ],
              "properties": {
                "route_target_id": {
                  "type": "string"
                },
                "provider_id": {
                  "type": "string"
                },
                "provider_account_id": {
                  "type": "string"
                },
                "priority": {
                  "type": "integer"
                },
                "weight": {
                  "type": "integer"
                },
                "active": {
                  "type": "boolean"
                },
                "filters": {
                  "$ref": "#/components/schemas/RouteTargetFilters"
                },
                "capabilities": {
                  "type": "object",
                  "properties": {
                    "redirect_hpp": {
                      "type": "boolean"
                    },
                    "s2s_card": {
                      "type": "boolean"
                    },
                    "lookup_by_reference": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "webhook_url": {
            "type": "string",
            "description": "Registered webhook endpoint (URL only — the signing secret is never returned by a read)."
          },
          "publishable_key": {
            "type": "string",
            "description": "PA-48: the derived publishable key of the merchant (`pk_<merchant-uuid>_<digest>`). PUBLIC by design — it lives in the casino frontend and binds browser captures to this merchant; it is not a credential. Deterministic: re-derived on every read (same value as at onboarding)."
          }
        }
      },
      "SetMerchantWebhookRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2048,
            "description": "HTTP(S) endpoint of the merchant backend that receives signed payment events."
          }
        }
      },
      "SetMerchantWebhookResponse": {
        "type": "object",
        "required": [
          "merchant_id",
          "webhook_url",
          "webhook_secret"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "webhook_url": {
            "type": "string"
          },
          "webhook_secret": {
            "type": "string",
            "description": "HMAC signing secret (`whs_…`) — shown EXACTLY ONCE in this response, never surfaced again, never logged. Verify `PA-Webhook-Signature: v1=<hex(HMAC-SHA256(raw_body, secret))>` over the raw delivered bytes."
          }
        }
      },
      "WebhookDeliveryListResponse": {
        "type": "object",
        "required": [
          "payment_id",
          "count",
          "deliveries"
        ],
        "properties": {
          "payment_id": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "deliveries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDeliveryResponse"
            }
          }
        }
      },
      "WebhookDeliveryResponse": {
        "type": "object",
        "description": "One webhook delivery of the outbox history. Delivery is asynchronous and at-least-once: PENDING → (retries with exponential backoff) → DELIVERED on the first 2xx, DEAD after the attempt budget.",
        "required": [
          "event_id",
          "event_type",
          "status",
          "attempts",
          "created_at"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Unique event id — the receiver-side dedup key (`PA-Event-Id` header)."
          },
          "event_type": {
            "type": "string",
            "enum": [
              "payment.authorized",
              "payment.captured",
              "payment.refunded",
              "payment.failed"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "DELIVERED",
              "DEAD"
            ]
          },
          "attempts": {
            "type": "integer"
          },
          "last_error": {
            "type": "string",
            "description": "Stable error class of the last failed attempt (`timeout`, `connect`, `status:<code>`); absent after a successful delivery."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "delivered_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AuditListResponse": {
        "type": "object",
        "required": [
          "events",
          "count"
        ],
        "properties": {
          "count": {
            "type": "integer"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuditEventResponse"
            }
          }
        }
      },
      "AuditEventResponse": {
        "type": "object",
        "required": [
          "id",
          "created_at",
          "actor",
          "action",
          "entity_type",
          "entity_id"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "Monotonic audit id."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "actor": {
            "type": "string",
            "description": "Authenticated principal (`tenant:<uuid>`); raw keys are never recorded."
          },
          "action": {
            "type": "string",
            "description": "Closed registry code (`create_merchant`, `update_routing`, `rotate_key`, …)."
          },
          "entity_type": {
            "type": "string"
          },
          "entity_id": {
            "type": "string"
          },
          "details": {
            "type": "object",
            "description": "Structured context of the change; credentials are never part of it."
          }
        }
      },
      "RotateKeyResponse": {
        "type": "object",
        "required": [
          "new_key",
          "old_key_expires_at",
          "key_id"
        ],
        "properties": {
          "new_key": {
            "type": "string",
            "description": "Raw tenant API key — valid immediately, shown EXACTLY ONCE, stored only as a SHA-256 hash."
          },
          "old_key_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "End of the 24h grace period of the replaced key."
          },
          "key_id": {
            "type": "string",
            "description": "Id of the new key (for `DELETE /v1/tenants/{id}/keys/{key_id}`)."
          }
        }
      },
      "RouteTargetStatus": {
        "type": "string",
        "description": "Lifecycle state of a Route Target (PA-58, PA-55 §8).",
        "enum": [
          "ENABLED",
          "DRAINING",
          "DEGRADED",
          "DISABLED"
        ]
      },
      "RouteTargetStatusChangeRequest": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "$ref": "#/components/schemas/RouteTargetStatus"
          }
        }
      },
      "MerchantStateCount": {
        "type": "object",
        "description": "One state of the window histogram (the full registry is always present, zero-filled).",
        "required": [
          "state",
          "count"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED",
              "UNKNOWN_PENDING_SYNC"
            ]
          },
          "count": {
            "type": "integer",
            "description": "Payments of the window in this state."
          }
        }
      },
      "MerchantDailyStateAmount": {
        "type": "object",
        "description": "Per-state amount of one daily rollup row (full registry, zero-filled; minor units).",
        "required": [
          "state",
          "amount_minor"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED",
              "UNKNOWN_PENDING_SYNC"
            ]
          },
          "amount_minor": {
            "type": "integer",
            "description": "Sum of amounts in this state on that day, minor units."
          }
        }
      },
      "MerchantDailySummary": {
        "type": "object",
        "description": "One (UTC day, currency) row of the daily rollup — the Reconciliation view. Amounts stay per-currency (never mixed).",
        "required": [
          "date",
          "currency",
          "count",
          "total_amount_minor",
          "by_state"
        ],
        "properties": {
          "date": {
            "type": "string",
            "description": "UTC calendar day, `YYYY-MM-DD`.",
            "example": "2025-09-24"
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "count": {
            "type": "integer",
            "description": "All payments of the day+currency (any state)."
          },
          "total_amount_minor": {
            "type": "integer",
            "description": "Sum of all payments of the day+currency, minor units."
          },
          "by_state": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantDailyStateAmount"
            }
          }
        }
      },
      "MerchantCurrencyAmount": {
        "type": "object",
        "description": "Turnover volume of one currency within the window (minor units).",
        "required": [
          "currency",
          "amount_minor"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "amount_minor": {
            "type": "integer",
            "description": "Sum of `AUTHORIZED | CAPTURED` amounts in this currency, minor units."
          }
        }
      },
      "MerchantPaymentMethodShare": {
        "type": "object",
        "description": "One payment option of the distribution card: count of the window payments with this `payment_method_type` (currently `card`).",
        "required": [
          "method",
          "count",
          "volume_minor"
        ],
        "properties": {
          "method": {
            "type": "string",
            "example": "card"
          },
          "count": {
            "type": "integer",
            "description": "Window payments of this payment option."
          },
          "volume_minor": {
            "type": "integer",
            "description": "Volume of the approved payments of this option, minor units."
          }
        }
      },
      "MerchantStatsResponse": {
        "type": "object",
        "description": "Dashboard snapshot of the merchant portal (Turnover EUR with previous-period delta, Approval Ratio, Transaction Count, Pay Ins, Credits/Refunds, Payment Option Distribution, state histogram, daily rollup).",
        "required": [
          "merchant_id",
          "window",
          "window_days",
          "turnover_eur",
          "previous_turnover_eur",
          "approval_ratio",
          "transaction_count",
          "approved_count",
          "failed_count",
          "payins",
          "refunds_count",
          "refunds_eur",
          "by_state",
          "turnover_by_currency",
          "payment_method_distribution",
          "daily",
          "generated_at",
          "deposits_eur",
          "previous_deposits_eur"
        ],
        "properties": {
          "merchant_id": {
            "type": "string",
            "example": "mer_01900000-0000-7000-8000-00000000000a"
          },
          "window": {
            "type": "string",
            "enum": [
              "7d",
              "30d"
            ],
            "description": "Echoed window value."
          },
          "window_days": {
            "type": "integer",
            "enum": [
              7,
              30
            ],
            "description": "Window length in days."
          },
          "turnover_eur": {
            "type": "integer",
            "description": "Turnover EUR: `AUTHORIZED | CAPTURED` volume in EUR, MINOR units (cents). Money IN — refunded volume is reported as Credits/Refunds, not turnover.",
            "example": 3050
          },
          "previous_turnover_eur": {
            "type": "integer",
            "description": "Same turnover aggregate over the PREVIOUS period of the same length (`[now-2N, now-N)`) — the 'Previously' line of the Turnover card.",
            "example": 0
          },
          "approval_ratio": {
            "type": "number",
            "format": "double",
            "description": "Approved / (approved + FAILED) payments of the window, percent 0-100, two decimals — the apcopay formula `authorized / (authorized + declined + failed)`. In-flight states (`PENDING`, `UNKNOWN_PENDING_SYNC`) stay out of the denominator; an undecided window answers 0.",
            "example": 75.0
          },
          "transaction_count": {
            "type": "integer",
            "description": "All payments of the window (any state).",
            "example": 4
          },
          "approved_count": {
            "type": "integer",
            "description": "Approved-family payments of the window (the ratio numerator).",
            "example": 3
          },
          "failed_count": {
            "type": "integer",
            "description": "FAILED payments of the window (the ratio decline side).",
            "example": 1
          },
          "payins": {
            "type": "integer",
            "description": "Pay Ins: count of `AUTHORIZED | CAPTURED` payments (successful deposits).",
            "example": 3
          },
          "deposits_eur": {
            "type": "integer",
            "description": "Deposits €-line: `AUTHORIZED | CAPTURED` volume in EUR, minor units — the money confirmed IN of the Deposits card.",
            "example": 3050
          },
          "previous_deposits_eur": {
            "type": "integer",
            "description": "Deposits €-line of the PREVIOUS period (the delta basis of the Deposits card).",
            "example": 0
          },
          "refunds_count": {
            "type": "integer",
            "description": "Credits/Refunds: count of SUCCEEDED refunds (any currency).",
            "example": 0
          },
          "refunds_eur": {
            "type": "integer",
            "description": "Credits/Refunds: sum of SUCCEEDED refunds in EUR, minor units.",
            "example": 0
          },
          "by_state": {
            "type": "array",
            "description": "Full state registry of the window, zero-filled.",
            "items": {
              "$ref": "#/components/schemas/MerchantStateCount"
            }
          },
          "turnover_by_currency": {
            "type": "array",
            "description": "Turnover volume per currency (minor units) — the honest companion of the EUR-only turnover card.",
            "items": {
              "$ref": "#/components/schemas/MerchantCurrencyAmount"
            }
          },
          "payment_method_distribution": {
            "type": "array",
            "description": "Payment Option Distribution of the window (per `payment_method_type`, count only).",
            "items": {
              "$ref": "#/components/schemas/MerchantPaymentMethodShare"
            }
          },
          "daily": {
            "type": "array",
            "description": "Daily rollup of the window (UTC days, per currency) — the Reconciliation view.",
            "items": {
              "$ref": "#/components/schemas/MerchantDailySummary"
            }
          },
          "generated_at": {
            "type": "string",
            "description": "RFC 3339 moment the snapshot was computed."
          }
        }
      },
      "MerchantCardMeta": {
        "type": "object",
        "description": "Masked instrument metadata of a payment (PA-13 projection — no PAN segments, no CVV, no holder name).",
        "required": [
          "brand",
          "last4"
        ],
        "properties": {
          "brand": {
            "type": "string",
            "example": "VISA"
          },
          "last4": {
            "type": "string",
            "example": "4242"
          },
          "exp_month": {
            "type": "string",
            "example": "08"
          },
          "exp_year": {
            "type": "string",
            "example": "2028"
          }
        }
      },
      "MerchantPaymentSummary": {
        "type": "object",
        "description": "One payment of the merchant portal Transactions grid (apcopay layout — summary projection with PSP and method columns; no attempt journal).",
        "required": [
          "payment_id",
          "status",
          "amount",
          "currency",
          "created_at",
          "payment_method_type"
        ],
        "properties": {
          "payment_id": {
            "type": "string",
            "example": "pay_01900000-0000-7000-8000-00000000000a"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED",
              "UNKNOWN_PENDING_SYNC"
            ]
          },
          "amount": {
            "type": "integer",
            "description": "Minor units of `currency`.",
            "example": 1050
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "created_at": {
            "type": "string",
            "description": "RFC 3339."
          },
          "provider": {
            "type": "string",
            "description": "Latest attempt's connector code (`payadmit`, …); absent without attempts.",
            "example": "payadmit"
          },
          "psp_reference": {
            "type": "string",
            "description": "Latest attempt's PSP transaction reference; absent without attempts.",
            "example": "mock-txn"
          },
          "error_code": {
            "type": "string",
            "description": "Latest attempt's PSP decline/response code (the verbatim Error Code column); absent when the attempt carries none.",
            "example": "do_not_honor"
          },
          "payment_method_type": {
            "type": "string",
            "example": "card"
          },
          "card": {
            "$ref": "#/components/schemas/MerchantCardMeta"
          },
          "order_reference": {
            "type": "string",
            "description": "Best-effort merchant context from the free-form request metadata (`order_reference`)."
          },
          "email": {
            "type": "string",
            "description": "Best-effort player contact from the request customer object."
          }
        }
      },
      "MerchantPaymentListResponse": {
        "type": "object",
        "description": "Payments page of the merchant portal (newest first).",
        "required": [
          "merchant_id",
          "total",
          "limit",
          "offset",
          "count",
          "payments"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED",
              "UNKNOWN_PENDING_SYNC"
            ],
            "description": "Echoed status filter (absent when unfiltered)."
          },
          "total": {
            "type": "integer",
            "description": "Total payments matching the filter, independent of the page."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "count": {
            "type": "integer",
            "description": "Rows on this page."
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantPaymentSummary"
            }
          }
        }
      },
      "MerchantAttemptDetail": {
        "type": "object",
        "description": "One attempt of the details view: the public summary fields PLUS the forensic provider facts of the merchant's own payment (provider status / code / transaction reference) — the collapse of the public payments API does not bind this merchant-scoped read.",
        "required": [
          "seq",
          "provider",
          "mid",
          "status",
          "started_at"
        ],
        "properties": {
          "seq": {
            "type": "integer"
          },
          "provider": {
            "type": "string",
            "description": "Connector code of the Route Target snapshot.",
            "example": "payadmit"
          },
          "mid": {
            "type": "string",
            "description": "MID label of the Route Target snapshot.",
            "example": "mid_portal"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "DECLINED_SOFT",
              "DECLINED_HARD",
              "UNKNOWN",
              "FAILED_TECHNICAL"
            ]
          },
          "provider_status": {
            "type": "string",
            "description": "Normalized PSP status, when present."
          },
          "provider_code": {
            "type": "string",
            "description": "Raw PSP response code, when present."
          },
          "provider_transaction_id": {
            "type": "string",
            "description": "PSP transaction reference, when present."
          },
          "started_at": {
            "type": "string",
            "description": "RFC 3339."
          },
          "finished_at": {
            "type": "string",
            "description": "RFC 3339; present for finished attempts."
          }
        }
      },
      "MerchantRefundSummary": {
        "type": "object",
        "description": "One related refund of the details view (Related Transactions).",
        "required": [
          "refund_id",
          "amount",
          "currency",
          "status",
          "created_at"
        ],
        "properties": {
          "refund_id": {
            "type": "string",
            "example": "ref_01900000-0000-7000-8000-00000000000a"
          },
          "amount": {
            "type": "integer",
            "description": "Minor units of `currency`.",
            "example": 500
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "SUCCEEDED",
              "FAILED"
            ]
          },
          "created_at": {
            "type": "string",
            "description": "RFC 3339."
          }
        }
      },
      "MerchantPaymentDetailsResponse": {
        "type": "object",
        "description": "Transaction Details view of the merchant portal: grid summary + PSP details (attempt journal with the forensic provider facts) + related refunds + the free-form request objects.",
        "required": [
          "merchant_id",
          "payment_id",
          "status",
          "amount",
          "currency",
          "capture_method",
          "payment_method_type",
          "correlation_id",
          "created_at",
          "updated_at",
          "attempts",
          "refunds"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "payment_id": {
            "type": "string",
            "example": "pay_01900000-0000-7000-8000-00000000000a"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED",
              "UNKNOWN_PENDING_SYNC"
            ]
          },
          "amount": {
            "type": "integer",
            "description": "Minor units of `currency`.",
            "example": 1050
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "capture_method": {
            "type": "string",
            "enum": [
              "automatic",
              "manual"
            ]
          },
          "payment_method_type": {
            "type": "string",
            "example": "card"
          },
          "provider": {
            "type": "string",
            "description": "Latest attempt's connector code; absent without attempts.",
            "example": "payadmit"
          },
          "psp_reference": {
            "type": "string",
            "description": "Latest attempt's PSP transaction reference; absent without attempts."
          },
          "error_code": {
            "type": "string",
            "description": "Latest attempt's PSP decline/response code (the verbatim Error Code of the Transactions grid); absent when the attempt carries none."
          },
          "card": {
            "$ref": "#/components/schemas/MerchantCardMeta"
          },
          "order_reference": {
            "type": "string",
            "description": "Best-effort merchant context from the request metadata."
          },
          "email": {
            "type": "string",
            "description": "Best-effort player contact from the request customer."
          },
          "correlation_id": {
            "type": "string",
            "description": "Correlation id shared with the PA trace of the payment creation."
          },
          "metadata": {
            "type": "object",
            "description": "The merchant's free-form request metadata (verbatim)."
          },
          "customer": {
            "type": "object",
            "description": "The merchant's free-form customer object (verbatim)."
          },
          "created_at": {
            "type": "string",
            "description": "RFC 3339."
          },
          "updated_at": {
            "type": "string",
            "description": "RFC 3339."
          },
          "attempts": {
            "type": "array",
            "description": "Attempt journal, oldest first (the Events timeline + PSP details).",
            "items": {
              "$ref": "#/components/schemas/MerchantAttemptDetail"
            }
          },
          "refunds": {
            "type": "array",
            "description": "Related refunds, oldest first (Related Transactions).",
            "items": {
              "$ref": "#/components/schemas/MerchantRefundSummary"
            }
          }
        }
      },
      "MerchantWebhookDeliveryResponse": {
        "type": "object",
        "description": "One delivery of the merchant webhook health view (same projection discipline as WebhookDeliveryResponse — the payload is never projected).",
        "required": [
          "event_id",
          "event_type",
          "payment_id",
          "status",
          "attempts",
          "created_at"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Receiver-side dedup key (UUIDv7)."
          },
          "event_type": {
            "type": "string",
            "enum": [
              "payment.authorized",
              "payment.captured",
              "payment.refunded",
              "payment.failed"
            ]
          },
          "payment_id": {
            "type": "string",
            "example": "pay_01900000-0000-7000-8000-00000000000a"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "DELIVERED",
              "DEAD"
            ]
          },
          "attempts": {
            "type": "integer",
            "description": "Delivery attempts made so far."
          },
          "last_error": {
            "type": "string",
            "description": "Last error class (`timeout`, `connect`, `status:<code>`); absent after a success."
          },
          "created_at": {
            "type": "string",
            "description": "RFC 3339."
          },
          "delivered_at": {
            "type": "string",
            "description": "RFC 3339; present when DELIVERED."
          }
        }
      },
      "MerchantWebhookDeliveryListResponse": {
        "type": "object",
        "description": "Delivery health of the merchant (newest first).",
        "required": [
          "merchant_id",
          "count",
          "deliveries"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "payment_id": {
            "type": "string",
            "description": "Echoed payment filter (absent when unfiltered)."
          },
          "count": {
            "type": "integer",
            "description": "Rows on this page."
          },
          "deliveries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantWebhookDeliveryResponse"
            }
          }
        }
      },
      "RouteTargetStatusChangeResponse": {
        "type": "object",
        "required": [
          "route_target_id",
          "merchant_id",
          "previous_status",
          "status",
          "active"
        ],
        "properties": {
          "route_target_id": {
            "type": "string",
            "description": "Public identifier of the changed Route Target (`rt_...`)."
          },
          "merchant_id": {
            "type": "string"
          },
          "previous_status": {
            "$ref": "#/components/schemas/RouteTargetStatus"
          },
          "status": {
            "$ref": "#/components/schemas/RouteTargetStatus"
          },
          "active": {
            "type": "boolean",
            "description": "Legacy traffic projection of the NEW state (ENABLED/DEGRADED → true; DRAINING/DISABLED → false)."
          }
        }
      },
      "UpdateWebhookUrlRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2048,
            "description": "New HTTPS endpoint of the casino backend receiving signed events (plain http is not accepted for self-service)."
          }
        }
      },
      "UpdateWebhookUrlResponse": {
        "type": "object",
        "required": [
          "merchant_id",
          "webhook_url"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "webhook_url": {
            "type": "string",
            "description": "The new endpoint; the signing secret is unchanged."
          }
        }
      },
      "RotateWebhookSecretResponse": {
        "type": "object",
        "required": [
          "merchant_id",
          "webhook_url",
          "webhook_secret",
          "old_secret_expires_at",
          "rotated_at"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "webhook_url": {
            "type": "string"
          },
          "webhook_secret": {
            "type": "string",
            "description": "The NEW active HMAC signing secret (`whs_…`) — shown EXACTLY ONCE, never surfaced again, never logged. From now on deliveries are signed with it immediately."
          },
          "old_secret_expires_at": {
            "type": "string",
            "description": "RFC 3339 end of the receiver-side grace window of the replaced secret: verify BOTH secrets until it (dual-verify), only the new one after."
          },
          "rotated_at": {
            "type": "string",
            "description": "RFC 3339 rotation time."
          }
        }
      },
      "WebhookSecretStatus": {
        "type": "string",
        "enum": [
          "issued",
          "never_shown"
        ],
        "description": "PA-13 once-semantics marker: `issued` = a webhook (URL+secret) is registered — the secret was shown exactly once and is never returned by a read; `never_shown` = no webhook registered yet."
      },
      "MerchantBaseUrls": {
        "type": "object",
        "required": [
          "production",
          "sandbox"
        ],
        "properties": {
          "production": {
            "type": "string",
            "description": "Production API base URL (PA-64 domain)."
          },
          "sandbox": {
            "type": "string",
            "description": "Sandbox stand base URL."
          }
        }
      },
      "ApcopayTestCard": {
        "type": "object",
        "required": [
          "number",
          "expiry",
          "holder"
        ],
        "properties": {
          "number": {
            "type": "string",
            "description": "PAN of the sandbox terminal card (public fixture)."
          },
          "expiry": {
            "type": "string"
          },
          "holder": {
            "type": "string"
          }
        }
      },
      "PayadmitTestCard": {
        "type": "object",
        "required": [
          "number",
          "expiry",
          "cvc"
        ],
        "properties": {
          "number": {
            "type": "string",
            "description": "PAN of the sandbox terminal card (public fixture)."
          },
          "expiry": {
            "type": "string"
          },
          "cvc": {
            "type": "string"
          }
        }
      },
      "MerchantTestCards": {
        "type": "object",
        "required": [
          "apcopay",
          "payadmit"
        ],
        "properties": {
          "apcopay": {
            "$ref": "#/components/schemas/ApcopayTestCard"
          },
          "payadmit": {
            "$ref": "#/components/schemas/PayadmitTestCard"
          }
        }
      },
      "MerchantSelfProfileProvider": {
        "type": "object",
        "required": [
          "connector_code",
          "mid_label",
          "currencies",
          "status"
        ],
        "properties": {
          "connector_code": {
            "type": "string",
            "description": "Connector code of the linked PSP (`apcopay`, `payadmit`, …)."
          },
          "mid_label": {
            "type": "string",
            "description": "MID label of the linked account."
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Currency scope of the connector (ISO 4217 alpha-3)."
          },
          "status": {
            "type": "string",
            "description": "Connector lifecycle status (PA-55 §8): `ENABLED | DEPRECATED | RETIRED`."
          }
        }
      },
      "MerchantSelfProfileResponse": {
        "type": "object",
        "required": [
          "merchant_id",
          "name",
          "api_key",
          "base_urls",
          "webhook_secret_status",
          "providers",
          "test_cards"
        ],
        "properties": {
          "merchant_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "api_key": {
            "type": "string",
            "description": "The bearer key of THIS request (single-principal MVP echo). Never logged, never persisted by PA."
          },
          "base_urls": {
            "$ref": "#/components/schemas/MerchantBaseUrls"
          },
          "webhook_url": {
            "type": "string",
            "nullable": true,
            "description": "Registered webhook endpoint; `null` when none (then `webhook_secret_status` is `never_shown`)."
          },
          "webhook_secret_status": {
            "$ref": "#/components/schemas/WebhookSecretStatus"
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantSelfProfileProvider"
            }
          },
          "test_cards": {
            "$ref": "#/components/schemas/MerchantTestCards"
          }
        }
      },
      "MerchantListProvider": {
        "type": "object",
        "description": "One linked PSP of the registry row: the PA-59 execution snapshot only (the console renders the model/status badges, not the MID details).",
        "required": [
          "connector_code",
          "execution_model",
          "status"
        ],
        "properties": {
          "connector_code": {
            "type": "string",
            "description": "Connector code (`payadmit`, `apcopay`, …)."
          },
          "execution_model": {
            "type": "string",
            "enum": [
              "native_hyperswitch",
              "external_ucs"
            ],
            "description": "PA-59 execution model of the connector dispatch."
          },
          "status": {
            "type": "string",
            "enum": [
              "ENABLED",
              "DEPRECATED",
              "RETIRED"
            ],
            "description": "Connector lifecycle status (PA-59)."
          }
        }
      },
      "MerchantListStats": {
        "type": "object",
        "description": "Optional per-merchant turnover snapshot (`with_stats=1`): the 7d aggregate of `GET /v1/merchants/{id}/stats` (PA-65 semantics — GROSS EUR turnover of the approved family, minor units, previous-period delta, approval ratio).",
        "required": [
          "window",
          "window_days",
          "turnover_eur",
          "previous_turnover_eur",
          "transaction_count",
          "approved_count",
          "failed_count"
        ],
        "properties": {
          "window": {
            "type": "string",
            "enum": [
              "7d"
            ],
            "description": "Fixed snapshot window of the list."
          },
          "window_days": {
            "type": "integer",
            "enum": [
              7
            ]
          },
          "turnover_eur": {
            "type": "integer",
            "description": "Approved EUR volume, minor units (GROSS)."
          },
          "previous_turnover_eur": {
            "type": "integer",
            "description": "Same aggregate over the PREVIOUS period of the same length."
          },
          "transaction_count": {
            "type": "integer",
            "description": "All payments of the window (any state)."
          },
          "approved_count": {
            "type": "integer",
            "description": "Approved-family payments of the window."
          },
          "failed_count": {
            "type": "integer",
            "description": "FAILED payments of the window."
          },
          "approval_ratio": {
            "type": "number",
            "nullable": true,
            "description": "Approved / (approved + FAILED), percent two decimals; `null` when nothing is decided in the window."
          }
        }
      },
      "MerchantListEntry": {
        "type": "object",
        "description": "One merchant of the registry page.",
        "required": [
          "merchant_id",
          "name",
          "currencies",
          "providers",
          "route_targets_count",
          "route_targets_statuses",
          "webhook_configured",
          "created_at"
        ],
        "properties": {
          "merchant_id": {
            "type": "string",
            "description": "Prefixed merchant id (`mer_…`)."
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "nullable": true,
            "description": "Onboarding jurisdiction (ISO code), when set."
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Processing currency scope of the onboarding profile."
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantListProvider"
            }
          },
          "route_targets_count": {
            "type": "integer",
            "description": "Total route targets (any lifecycle state)."
          },
          "route_targets_statuses": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Route-target lifecycle status counts (PA-58 states, sorted by status)."
          },
          "webhook_configured": {
            "type": "boolean",
            "description": "PA-13 registration invariant: URL+secret pair registered (the secret itself is never projected)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "stats": {
            "$ref": "#/components/schemas/MerchantListStats"
          }
        }
      },
      "MerchantListResponse": {
        "type": "object",
        "description": "The server-side merchant registry of the tenant (PA-69) — replaces the PA-52 localStorage registry.",
        "required": [
          "tenant_id",
          "total",
          "limit",
          "offset",
          "count",
          "with_stats",
          "merchants"
        ],
        "properties": {
          "tenant_id": {
            "type": "string",
            "description": "The authenticated tenant (the Bearer principal) — the console uses it to prefill the PA-24 tenant-key rotation form."
          },
          "total": {
            "type": "integer",
            "description": "Total merchants matching the filter (independent of the page)."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "count": {
            "type": "integer",
            "description": "Merchants on this page."
          },
          "q": {
            "type": "string",
            "nullable": true,
            "description": "Echoed search filter (`null` = no filter)."
          },
          "with_stats": {
            "type": "boolean",
            "description": "Echoed `with_stats` flag."
          },
          "merchants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantListEntry"
            }
          }
        }
      },
      "PayoutListItem": {
        "type": "object",
        "description": "One payout of the list page: the queue/grid columns (gate details — decisions and attempts — stay on the per-payout read).",
        "required": [
          "payout_id",
          "merchant_id",
          "status",
          "amount",
          "currency",
          "customer_reference",
          "version",
          "created_at"
        ],
        "properties": {
          "payout_id": {
            "type": "string",
            "description": "Prefixed payout id (`po_…`)."
          },
          "merchant_id": {
            "type": "string",
            "description": "Prefixed merchant id (`mer_…`)."
          },
          "status": {
            "type": "string",
            "enum": [
              "CREATED",
              "AWAITING_APPROVAL",
              "APPROVED",
              "PROCESSING",
              "COMPLETED",
              "DECLINED",
              "FAILED_TECHNICAL",
              "CANCELLED",
              "UNKNOWN_PENDING_SYNC"
            ],
            "description": "ADR-0004 §4.2 payout lifecycle state."
          },
          "amount": {
            "type": "integer",
            "description": "Amount in minor units."
          },
          "currency": {
            "type": "string"
          },
          "customer_reference": {
            "type": "string",
            "description": "Casino-side player reference."
          },
          "version": {
            "type": "integer",
            "description": "Optimistic-concurrency version of the aggregate."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PayoutListResponse": {
        "type": "object",
        "description": "The tenant payout list page (PA-69) — the console approval queue base.",
        "required": [
          "total",
          "limit",
          "offset",
          "count",
          "payouts"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Total payouts matching the filter (independent of the page)."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "count": {
            "type": "integer",
            "description": "Payouts on this page."
          },
          "status": {
            "type": "string",
            "enum": [
              "CREATED",
              "AWAITING_APPROVAL",
              "APPROVED",
              "PROCESSING",
              "COMPLETED",
              "DECLINED",
              "FAILED_TECHNICAL",
              "CANCELLED",
              "UNKNOWN_PENDING_SYNC"
            ],
            "nullable": true,
            "description": "Echoed status filter (`null` = no filter)."
          },
          "payouts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PayoutListItem"
            }
          }
        }
      },
      "DispatchJournalHealth": {
        "type": "object",
        "description": "PA-46 dispatch-journal health block of the reconciliation snapshot.",
        "required": [
          "unresolved_count"
        ],
        "properties": {
          "unresolved_count": {
            "type": "integer",
            "description": "Unresolved (potentially in-flight) dispatches of the tenant; 0 = nothing for the sync-worker recovery to do."
          },
          "oldest_unresolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Oldest unresolved dispatch (`null` = none)."
          },
          "oldest_unresolved_age_secs": {
            "type": "integer",
            "nullable": true,
            "description": "Age of the oldest unresolved dispatch in seconds — compare against `sync_recovery_stale_secs`."
          }
        }
      },
      "ReconciliationHealthResponse": {
        "type": "object",
        "description": "The tenant-wide current-state reconciliation snapshot (PA-69) — no window: monitoring asks what is stuck NOW.",
        "required": [
          "tenant_id",
          "dispatch_journal",
          "stuck_checkouts",
          "unknown_pending_sync",
          "webhook_pending",
          "webhook_dead",
          "stale_checkout_threshold_mins",
          "sync_recovery_stale_secs",
          "generated_at"
        ],
        "properties": {
          "tenant_id": {
            "type": "string"
          },
          "dispatch_journal": {
            "$ref": "#/components/schemas/DispatchJournalHealth"
          },
          "stuck_checkouts": {
            "type": "integer",
            "description": "PENDING payments with a PENDING latest attempt older than `stale_checkout_threshold_mins` (the PA-36 reconcile candidates)."
          },
          "unknown_pending_sync": {
            "type": "integer",
            "description": "Payments in `UNKNOWN_PENDING_SYNC`."
          },
          "webhook_pending": {
            "type": "integer",
            "description": "Outbox rows currently awaiting delivery."
          },
          "webhook_dead": {
            "type": "integer",
            "description": "Dead-lettered outbox rows (the page view: `GET /v1/ops/outbox/dead`)."
          },
          "stale_checkout_threshold_mins": {
            "type": "integer",
            "description": "The reconcile threshold in effect (`PA_RECONCILE_AFTER_MINS`)."
          },
          "sync_recovery_stale_secs": {
            "type": "integer",
            "description": "The recovery staleness guard in effect (`PA_SYNC_RECOVERY_STALE_SECS`)."
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OutboxDeadLetter": {
        "type": "object",
        "description": "One dead-lettered delivery of the tenant (attempts/last-error facts — the stored payload is never projected).",
        "required": [
          "event_id",
          "event_type",
          "payment_id",
          "merchant_id",
          "attempts",
          "created_at"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "description": "PA event id (`PA-Event-Id` of the delivery)."
          },
          "event_type": {
            "type": "string",
            "description": "PA event type (`payment.authorized`, …)."
          },
          "payment_id": {
            "type": "string"
          },
          "merchant_id": {
            "type": "string"
          },
          "attempts": {
            "type": "integer",
            "description": "Delivery attempts made."
          },
          "last_error": {
            "type": "string",
            "nullable": true,
            "description": "Last error class of the delivery."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OutboxDeadResponse": {
        "type": "object",
        "description": "The bounded newest-first dead-letter page plus the honest total backlog count (PA-69).",
        "required": [
          "tenant_id",
          "dead_count",
          "limit",
          "count",
          "letters",
          "generated_at"
        ],
        "properties": {
          "tenant_id": {
            "type": "string"
          },
          "dead_count": {
            "type": "integer",
            "description": "ALL dead letters of the tenant (independent of the page)."
          },
          "limit": {
            "type": "integer"
          },
          "count": {
            "type": "integer",
            "description": "Letters on this page."
          },
          "letters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OutboxDeadLetter"
            }
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "RateLimitBucketUsage": {
        "type": "object",
        "description": "Live usage of one endpoint bucket of the calling tenant (memory backend).",
        "required": [
          "endpoint",
          "used",
          "limit"
        ],
        "properties": {
          "endpoint": {
            "type": "string",
            "description": "Route-pattern bucket key (instance paths collapse onto it)."
          },
          "used": {
            "type": "integer",
            "description": "Requests inside the current window."
          },
          "limit": {
            "type": "integer",
            "description": "Effective limit (override or default)."
          }
        }
      },
      "RateLimitSnapshotResponse": {
        "type": "object",
        "description": "The PA-47 read-only rate-limit snapshot (PA-69). `tenant_buckets`/`tracked` are the live memory-backend state; on the Redis backend they are absent (the shared buckets live in Redis — honest MVP gap).",
        "required": [
          "backend",
          "window_secs",
          "default_rpm",
          "overrides",
          "unauth_ip_rpm",
          "generated_at"
        ],
        "properties": {
          "backend": {
            "type": "string",
            "enum": [
              "memory",
              "redis"
            ],
            "description": "PA-47 backend selection."
          },
          "window_secs": {
            "type": "integer",
            "description": "Sliding-window length (seconds)."
          },
          "default_rpm": {
            "type": "integer",
            "description": "Default per-tenant per-endpoint limit (requests per window)."
          },
          "overrides": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Per-endpoint overrides (sorted by endpoint)."
          },
          "unauth_ip_rpm": {
            "type": "integer",
            "description": "Unauthenticated per-IP window limit (PA-47)."
          },
          "tenant_buckets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RateLimitBucketUsage"
            },
            "nullable": true,
            "description": "Live per-endpoint usage of the CALLING tenant (absent on the Redis backend)."
          },
          "tracked": {
            "type": "object",
            "nullable": true,
            "description": "Process-wide tracked-bucket counters `{tenant_buckets, ip_buckets}` (absent on the Redis backend)."
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PspCapabilities": {
        "type": "object",
        "description": "PA-53 capability anкета flags (MVP, booleans). Non-secret provider-level product claims the PSP confirms through the portal.",
        "required": [
          "s2s_card",
          "redirect_hpp",
          "three_ds",
          "refund",
          "idempotency",
          "lookup"
        ],
        "properties": {
          "s2s_card": {
            "type": "boolean",
            "description": "S2S raw-card processing (DirectConnect)."
          },
          "redirect_hpp": {
            "type": "boolean",
            "description": "Hosted/redirect HPP processing."
          },
          "three_ds": {
            "type": "boolean",
            "description": "3DS (frictionless/challenge) support."
          },
          "refund": {
            "type": "boolean",
            "description": "Refund (incl. partial) support."
          },
          "idempotency": {
            "type": "boolean",
            "description": "Native idempotency key semantics (safe retries)."
          },
          "lookup": {
            "type": "boolean",
            "description": "Lookup by PSP reference (recovery)."
          }
        }
      },
      "PspCapabilitiesSource": {
        "type": "string",
        "enum": [
          "default_snapshot",
          "psp_confirmed"
        ],
        "description": "`default_snapshot` = the static PA-53 research snapshot; `psp_confirmed` = the PSP's own anкета confirmation (evidence CONFIRMED_BY_PSP)."
      },
      "PspCapabilitiesView": {
        "type": "object",
        "description": "The effective capability snapshot: the confirmed flags (or the static default), their source and the confirmation time.",
        "allOf": [
          {
            "$ref": "#/components/schemas/PspCapabilities"
          },
          {
            "type": "object",
            "properties": {
              "source": {
                "$ref": "#/components/schemas/PspCapabilitiesSource"
              },
              "confirmed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "RFC 3339 UTC time of the last confirmation (absent for the default snapshot)."
              }
            }
          }
        ]
      },
      "PspCapabilitiesPutRequest": {
        "type": "object",
        "required": [
          "capabilities"
        ],
        "properties": {
          "capabilities": {
            "$ref": "#/components/schemas/PspCapabilities"
          }
        }
      },
      "PspTestCard": {
        "type": "object",
        "description": "One sandbox test card of the PSP's terminal (public PSP fixture — not a credential).",
        "required": [
          "label",
          "number",
          "expiry",
          "note"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "number": {
            "type": "string"
          },
          "expiry": {
            "type": "string"
          },
          "holder": {
            "type": "string",
            "nullable": true
          },
          "cvc": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string",
            "description": "What the fixture proves."
          }
        }
      },
      "PspProfileResponse": {
        "type": "object",
        "required": [
          "connector_code",
          "display_name",
          "execution_model",
          "connector_status",
          "capabilities",
          "base_urls",
          "test_cards"
        ],
        "properties": {
          "connector_code": {
            "type": "string",
            "description": "The connector scope of the key (resolved FROM the key, not an input)."
          },
          "display_name": {
            "type": "string",
            "description": "The display name of the connector rows (fallback: the connector code)."
          },
          "execution_model": {
            "type": "string",
            "enum": [
              "native_hyperswitch",
              "external_ucs"
            ],
            "description": "PA-59 execution snapshot."
          },
          "connector_status": {
            "type": "string",
            "enum": [
              "ENABLED",
              "DEPRECATED",
              "RETIRED"
            ],
            "description": "PA-59 connector lifecycle status."
          },
          "connector_version": {
            "type": "string",
            "nullable": true,
            "description": "PA-59 connector version (fork MR commit); null = not recorded."
          },
          "capabilities": {
            "$ref": "#/components/schemas/PspCapabilitiesView"
          },
          "base_urls": {
            "$ref": "#/components/schemas/MerchantBaseUrls"
          },
          "test_cards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PspTestCard"
            }
          }
        }
      },
      "PspRouteTargetBrief": {
        "type": "object",
        "required": [
          "rt_id",
          "status"
        ],
        "properties": {
          "rt_id": {
            "type": "string",
            "description": "PA Route Target id (`rt_…`)."
          },
          "status": {
            "type": "string",
            "enum": [
              "ENABLED",
              "DRAINING",
              "DEGRADED",
              "DISABLED"
            ],
            "description": "PA-58 lifecycle state of the Route Target (no priorities/weights — operator data)."
          }
        }
      },
      "PspMca": {
        "type": "object",
        "required": [
          "mca_id",
          "merchant_id",
          "merchant_name",
          "mid_label",
          "currencies",
          "countries",
          "execution_model",
          "connector_status",
          "route_targets"
        ],
        "properties": {
          "mca_id": {
            "type": "string",
            "description": "The fork MCA reference (`provider_accounts.external_mca_ref`) — the proxy address of the rotation, not a secret."
          },
          "merchant_id": {
            "type": "string"
          },
          "merchant_name": {
            "type": "string"
          },
          "mid_label": {
            "type": "string"
          },
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "execution_model": {
            "type": "string",
            "enum": [
              "native_hyperswitch",
              "external_ucs"
            ]
          },
          "connector_status": {
            "type": "string",
            "enum": [
              "ENABLED",
              "DEPRECATED",
              "RETIRED"
            ]
          },
          "connector_version": {
            "type": "string",
            "nullable": true
          },
          "route_targets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PspRouteTargetBrief"
            }
          }
        }
      },
      "PspMcaListResponse": {
        "type": "object",
        "required": [
          "connector_code",
          "count",
          "mcas"
        ],
        "properties": {
          "connector_code": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "mcas": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PspMca"
            }
          }
        }
      },
      "PspDispatchSummary": {
        "type": "object",
        "required": [
          "payment_id",
          "state",
          "attempt_status",
          "amount",
          "currency",
          "merchant_id",
          "mid_label",
          "created_at"
        ],
        "properties": {
          "payment_id": {
            "type": "string"
          },
          "psp_reference": {
            "type": "string",
            "nullable": true,
            "description": "The PSP's own transaction reference (attempts.provider_transaction_id — the pspid of the Apcopay model)."
          },
          "state": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "CAPTURED",
              "PARTIALLY_REFUNDED",
              "REFUNDED",
              "FAILED",
              "UNKNOWN_PENDING_SYNC"
            ],
            "description": "PA payment state."
          },
          "attempt_status": {
            "type": "string",
            "enum": [
              "PENDING",
              "AUTHORIZED",
              "DECLINED_SOFT",
              "DECLINED_HARD",
              "UNKNOWN",
              "FAILED_TECHNICAL"
            ],
            "description": "The PSP's own attempt status."
          },
          "provider_code": {
            "type": "string",
            "nullable": true,
            "description": "Verbatim provider (bank) code."
          },
          "provider_status": {
            "type": "string",
            "nullable": true,
            "description": "Verbatim provider status echo."
          },
          "amount": {
            "type": "integer",
            "description": "Minor units (cents)."
          },
          "currency": {
            "type": "string"
          },
          "merchant_id": {
            "type": "string"
          },
          "mid_label": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "Attempt start (dispatch time), RFC 3339 UTC."
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "PspDispatchListResponse": {
        "type": "object",
        "required": [
          "connector_code",
          "total",
          "limit",
          "offset",
          "count",
          "payments"
        ],
        "properties": {
          "connector_code": {
            "type": "string"
          },
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PspDispatchSummary"
            }
          }
        }
      },
      "PspDispatchStats": {
        "type": "object",
        "required": [
          "pending",
          "authorized",
          "declined_soft",
          "declined_hard",
          "unknown",
          "failed_technical"
        ],
        "properties": {
          "pending": {
            "type": "integer"
          },
          "authorized": {
            "type": "integer"
          },
          "declined_soft": {
            "type": "integer"
          },
          "declined_hard": {
            "type": "integer"
          },
          "unknown": {
            "type": "integer"
          },
          "failed_technical": {
            "type": "integer"
          },
          "success_rate": {
            "type": "number",
            "nullable": true,
            "description": "authorized / (authorized + declined_soft + declined_hard), percent with two decimals; null = nothing decided in the window (never 0%)."
          }
        }
      },
      "PspDeclineCodeCount": {
        "type": "object",
        "required": [
          "provider_code",
          "count"
        ],
        "properties": {
          "provider_code": {
            "type": "string",
            "description": "Verbatim bank code (empty = no code recorded)."
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "PspDailyRollup": {
        "type": "object",
        "required": [
          "date",
          "currency",
          "count",
          "amount_minor"
        ],
        "properties": {
          "date": {
            "type": "string",
            "description": "UTC day (YYYY-MM-DD)."
          },
          "currency": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "description": "Dispatch count that day."
          },
          "amount_minor": {
            "type": "integer",
            "description": "Sum of payment amounts, minor units (never mixed across currencies)."
          }
        }
      },
      "PspHealthResponse": {
        "type": "object",
        "required": [
          "connector_code",
          "window",
          "window_days",
          "dispatch_stats",
          "journal_unresolved",
          "declines_breakdown",
          "daily",
          "generated_at"
        ],
        "properties": {
          "connector_code": {
            "type": "string"
          },
          "window": {
            "type": "string",
            "enum": [
              "7d",
              "30d"
            ]
          },
          "window_days": {
            "type": "integer"
          },
          "dispatch_stats": {
            "$ref": "#/components/schemas/PspDispatchStats"
          },
          "journal_unresolved": {
            "type": "integer",
            "description": "Unresolved PA-46 dispatch-journal rows (in-flight or crashed dispatches)."
          },
          "declines_breakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PspDeclineCodeCount"
            }
          },
          "daily": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PspDailyRollup"
            }
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PspCredentialsRequest": {
        "type": "object",
        "description": "The NEW credential values of the MCA (the COMPLETE SignatureKey set — the fork replaces the whole credential object; partial sets are rejected with 422). NEVER persisted in PA: streamed to the fork admin API, dropped after the call, never logged; the audit event carries the field NAMES only.",
        "properties": {
          "api_key": {
            "type": "string",
            "maxLength": 4096,
            "description": "API key of the PSP terminal (SignatureKey.api_key). Absent/empty → 422 (the rotation replaces the FULL credential set on the fork)."
          },
          "key1": {
            "type": "string",
            "maxLength": 4096,
            "description": "Terminal/brand key (SignatureKey.key1). Absent/empty → 422 (the rotation replaces the FULL credential set on the fork)."
          },
          "api_secret": {
            "type": "string",
            "maxLength": 4096,
            "description": "API secret (SignatureKey.api_secret). Absent/empty → 422 (the rotation replaces the FULL credential set on the fork)."
          }
        }
      },
      "PspCredentialsRotatedResponse": {
        "type": "object",
        "required": [
          "connector_code",
          "mca_id",
          "merchant_id",
          "mid_label",
          "rotated_fields",
          "rotated_at"
        ],
        "properties": {
          "connector_code": {
            "type": "string"
          },
          "mca_id": {
            "type": "string"
          },
          "merchant_id": {
            "type": "string"
          },
          "mid_label": {
            "type": "string"
          },
          "rotated_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The rotated FIELD NAMES only — the values are never echoed."
          },
          "rotated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SdkCaptureRequest": {
        "type": "object",
        "required": [
          "card_number",
          "cvc",
          "expiry_month",
          "expiry_year"
        ],
        "properties": {
          "card_number": {
            "type": "string",
            "description": "PAN, digits only (12-19, no separators, Luhn-checked server-side). PCI: exists only in flight — never logged, persisted or echoed."
          },
          "cvc": {
            "type": "string",
            "description": "CVC, 3-4 digits. PCI: exists only in flight."
          },
          "expiry_month": {
            "type": "string",
            "description": "Expiry month, `01`-`12`."
          },
          "expiry_year": {
            "type": "string",
            "description": "Expiry year, 4 digits (`2030`)."
          },
          "card_holder_name": {
            "type": "string",
            "nullable": true,
            "description": "Optional cardholder name (max 128 chars), forwarded to the capture surface."
          },
          "publishable_key": {
            "type": "string",
            "nullable": true,
            "description": "Optional fallback transport of the publishable key (header `X-Publishable-Key` is canonical)."
          }
        }
      },
      "SdkCaptureResponse": {
        "type": "object",
        "required": [
          "vault_token",
          "brand",
          "last4"
        ],
        "properties": {
          "vault_token": {
            "type": "string",
            "description": "Single-use vault token of the freshly tokenized card (fork `pm_...`). The casino frontend forwards it to its backend, which registers the instrument via `POST /v1/payment-instruments`. Consumed exactly once."
          },
          "brand": {
            "type": "string",
            "description": "Masked card brand from the capture surface (`UNKNOWN` fallback)."
          },
          "last4": {
            "type": "string",
            "description": "Last four digits of the PAN (masked metadata)."
          },
          "exp_month": {
            "type": "string",
            "nullable": true
          },
          "exp_year": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "IssueMerchantKeyResponse": {
        "type": "object",
        "required": [
          "merchant_id",
          "api_key",
          "created_at"
        ],
        "properties": {
          "merchant_id": {
            "type": "string",
            "description": "Merchant id (`mer_…`) the key is scoped to."
          },
          "api_key": {
            "type": "string",
            "description": "The raw merchant-scoped API key (`mer_sk_…`) — shown EXACTLY ONCE, never surfaced again, never logged; only its SHA-256 hash is persisted (PA-72)."
          },
          "created_at": {
            "type": "string",
            "description": "RFC 3339 issue/rotate time of the key."
          }
        }
      }
    }
  }
}
