{
  "openapi": "3.1.0",
  "info": {
    "title": "Noghtex Public API",
    "version": "1.0.0",
    "summary": "REST API of Noghtex (نقطکس), the live shared pixel canvas.",
    "description": "Noghtex is a live 1000×1000 shared pixel canvas where every pixel is an ownable asset with a server-authoritative price. This API is the cold surface of that product: reads, takeovers and wallet operations.\n\n## Authentication model\n\nThere are **no API keys**. Every non-public route acts as one real, authenticated user:\n\n1. `POST /auth/otp/request` sends a one-time code to the user's phone.\n2. `POST /auth/login` exchanges the code for two `__Host-` cookies (session id + CSRF token) and returns the CSRF token in the body.\n3. Every mutating request must send the cookies, the CSRF token in `X-CSRF-Token`, an `Idempotency-Key` (UUID), and an allowed `Origin` (`https://app.noghtex.ir`).\n\nWithdrawals additionally require a WebAuthn step-up assertion in `X-Noghtex-StepUp-Assertion`.\n\nAn agent may only drive these routes on behalf of a user who explicitly handed over that login.\n\n## Errors\n\nEvery error response — including 404 and 405 — is JSON with a stable machine code:\n\n```json\n{\"code\": \"NOT_FOUND\", \"message\": \"not found\", \"hint\": \"See /openapi.json for the list of valid endpoints.\"}\n```\n\nBranch on `code`, never on `message`.\n\n## Versioning and deprecation\n\nThis surface is major version **1**, and every response carries an `API-Version` header naming it (`API-Version: 1`). The current unversioned paths **are** the v1 contract; they will not change incompatibly. A future breaking major ships under a new path prefix (`/v2/...`) and runs alongside v1 for at least **12 months** before v1 is removed. When a single operation is scheduled for removal it is marked `deprecated: true` in this document and its responses carry `Deprecation: true` plus a `Sunset` HTTP date until it is gone. Non-breaking changes (new optional request fields, new response fields, new operations, new values appended to the error-code taxonomy) can land at any time without notice, so clients must ignore unknown fields and codes.\n\n## Rate limiting\n\nBudgeted responses carry `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (seconds until the current window expires) and `RateLimit-Policy` (`burst;w=window-seconds`), so an agent can self-throttle in real time from the first response. A refused call answers `429 RATE_LIMITED` with the same headers at zero remaining plus `Retry-After` in seconds. Edge limits are separate from these per-route budgets and looser.\n\n**Observing them requires no account:** `GET /api/stats` is public and carries live headers on every answer — `curl -si https://api.noghtex.ir/api/stats`. Session-gated routes emit the same headers once authenticated.\n\n## Money on the wire\n\nEvery amount is a JSON **string** (e.g. `\"1200\"`), never a float, so precision survives clients that promote large integers to double. The unit is the Iranian rial.",
    "contact": {
      "name": "Noghtex support",
      "email": "support@noghtex.ir",
      "url": "https://noghtex.ir/contact/"
    },
    "termsOfService": "https://noghtex.ir/privacy/"
  },
  "servers": [
    {
      "url": "https://api.noghtex.ir",
      "description": "Production"
    }
  ],
  "x-api-version": "1",
  "x-versioning": {
    "current": "1",
    "signal": "Every response carries an API-Version header naming its major version.",
    "pathRule": "The current unversioned paths are the v1 contract. A breaking major ships under a new path prefix (/v2/...) alongside v1 for at least 12 months before removal.",
    "deprecation": {
      "specMarker": "deprecated: true on the operation object",
      "headers": ["Deprecation", "Sunset"],
      "headerComponentRefs": [
        "#/components/headers/Deprecation",
        "#/components/headers/Sunset"
      ],
      "minimumNoticeMonths": 12
    },
    "nonBreakingChanges": "New optional request fields, new response fields, new operations and new error-code values can appear at any time; clients must ignore unknown fields and codes."
  },
  "tags": [
    {
      "name": "Public",
      "description": "Readable without a session. Rate-limited per source address."
    },
    {
      "name": "Auth",
      "description": "OTP login and session lifecycle. Mutating routes need the session cookies plus X-CSRF-Token."
    },
    {
      "name": "Board",
      "description": "Reads and takeovers on the pixel board."
    },
    {
      "name": "Wallet",
      "description": "Balance, deposits via the payment gateway, and hardware-verified withdrawals."
    },
    {
      "name": "Realtime",
      "description": "Connect tickets for the WebTransport/WebSocket stream (not part of this REST surface)."
    },
    {
      "name": "Step-up",
      "description": "WebAuthn ceremonies protecting withdrawals."
    },
    {
      "name": "Notifications",
      "description": "Which channels may tell you your pixel changed hands, and when."
    },
    {
      "name": "Health",
      "description": "Liveness and readiness probes."
    }
  ],
  "paths": {
    "/api/stats": {
      "get": {
        "operationId": "getPublicStats",
        "tags": ["Public"],
        "summary": "Public activity counters",
        "description": "Two aggregates that reveal nothing about any individual: distinct users with an authenticated request in the last five minutes, and successful non-replayed takeovers in the trailing hour. Served from a short cache; a degraded store yields zeroes rather than an error, so this route never fails.",
        "security": [],
        "responses": {
          "200": {
            "description": "The current counters.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublicStatsView" },
                "example": { "online": 12, "sales_last_hour": 340 }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "tags": ["Public"],
        "summary": "This document",
        "description": "The OpenAPI 3.1 description of this API. Also mirrored at `https://noghtex.ir/openapi.json`.",
        "security": [],
        "responses": {
          "200": {
            "description": "This specification.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OpenApiDocument" }
              }
            }
          }
        }
      }
    },
    "/auth/otp/request": {
      "post": {
        "operationId": "requestOtp",
        "tags": ["Auth"],
        "summary": "Send a one-time login code",
        "description": "Step one of login. Sends a one-time code to the given phone number. The response deliberately does not say whether the number is registered — that would make the endpoint a user-enumeration oracle. Requires an allowed `Origin` header; the per-address brute-force budget applies.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/OtpRequest" },
              "example": { "phone": "+989121234567" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The challenge is armed. Delivery itself is out of band (SMS).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OtpAccepted" },
                "example": { "expires_in_secs": 120, "resend_after_secs": 60 }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/auth/login": {
      "post": {
        "operationId": "login",
        "tags": ["Auth"],
        "summary": "Exchange phone + code for a session",
        "description": "Step two of login. On success the server sets two `__Host-` cookies (`__Host-ngx_sid`, `__Host-ngx_csrf`) — `Secure`, `HttpOnly`, `SameSite=Strict`, host-locked — and returns the session identity including the CSRF token, which every later mutating request echoes in `X-CSRF-Token`. Requires an allowed `Origin` header.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LoginRequest" },
              "example": { "phone": "+989121234567", "code": "12345" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A new session. The cookies are on the response; the body carries the identity and the CSRF token.",
            "headers": {
              "Set-Cookie": {
                "description": "Two cookies: `__Host-ngx_sid` and `__Host-ngx_csrf`.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MeView" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": {
            "description": "Wrong or expired code.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } }
            }
          },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "operationId": "logout",
        "tags": ["Auth"],
        "summary": "Revoke the current session",
        "description": "Revokes the session the cookies name, server-side, and clears the cookies. Idempotent: logging out twice succeeds once and then answers like any request without a session.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "responses": {
          "204": { "description": "The session is gone." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/auth/logout-all": {
      "post": {
        "operationId": "logoutAllSessions",
        "tags": ["Auth"],
        "summary": "Revoke every session of the current user",
        "description": "Empties the user's session index — every device, not just the caller. Use after a suspected cookie theft; the device-binding anomaly detector revokes on its own when a stolen cookie is replayed from a different fingerprint.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "responses": {
          "204": { "description": "Every session of the user is gone." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/api/me": {
      "get": {
        "operationId": "getSessionIdentity",
        "tags": ["Auth"],
        "summary": "Who the session belongs to",
        "description": "Answered by the BFF from the session it already holds; it does not reach an internal service. The `csrf` field is the same value as the `__Host-ngx_csrf` cookie, handed over in the body because the cookie is host-locked to the API origin and a bundle served from another origin cannot read it. A `GET` needs no CSRF header, so this route stays reachable when every mutating one would be refused.",
        "security": [{ "cookieAuth": [] }],
        "responses": {
          "200": {
            "description": "The session's identity.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MeView" },
                "example": { "user_id": 1001, "handle": "آبی", "csrf": "3f9c…" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/wallet/deposit/{authority}": {
      "get": {
        "operationId": "getDepositReceipt",
        "tags": ["Wallet"],
        "summary": "One of the caller's own deposit receipts",
        "description": "Everything a payer is shown after the bank comes from here, over their own session — never off the return URL, which is a string a foreign origin handed their device. Answers `404` for an authority this account did not start, which is the same answer it gives for one that does not exist: whether a stranger's payment reference is real is not a fact this endpoint discloses. A `pending` receipt resolves within seconds; poll briefly rather than reporting failure.",
        "security": [{ "cookieAuth": [] }],
        "parameters": [
          {
            "name": "authority",
            "in": "path",
            "required": true,
            "description": "The provider's identifier for the payment session, as returned by `startWalletCharge`.",
            "schema": { "type": "string", "minLength": 1, "maxLength": 64 }
          }
        ],
        "responses": {
          "200": {
            "description": "The receipt, in whatever state it is in.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DepositReceipt" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "No such deposit for this account.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
          }
        }
      }
    },
    "/api/wallet/withdraw": {
      "post": {
        "operationId": "withdrawFunds",
        "tags": ["Wallet"],
        "summary": "Request a payout (WebAuthn step-up required)",
        "description": "The money-out path. Requires, on top of the session and CSRF token: an `Idempotency-Key`, and a WebAuthn step-up assertion — hardware-bound proof that the user physically approved this exact payout — carried as base64url JSON in `X-Noghtex-StepUp-Assertion`. The assertion binds to the amount and destination in this body, so an approval cannot be replayed onto a second withdrawal. The destination is an opaque handle; the real account details are decrypted only inside the signing enclave.",
        "security": [
          { "cookieAuth": [] },
          { "csrfHeader": [] },
          { "idempotencyKey": [] },
          { "stepUpAssertion": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WithdrawRequest" },
              "example": { "amount": "250000", "destination_ref": "sheba:IR…" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The withdrawal is accepted and settles asynchronously. The body is the engine's settlement receipt.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithdrawSettledView" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": {
            "description": "The guarded debit found the balance too low.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
          },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": {
            "description": "The `Idempotency-Key` was seen before with a different body.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/notify/prefs": {
      "get": {
        "operationId": "getNotifyPrefs",
        "tags": ["Notifications"],
        "summary": "Read this account's notification settings",
        "description": "Which channels may interrupt you, the smallest profit worth telling you about, and your quiet hours. An account that has never opened the settings panel has no stored row and reads the defaults: in-app alerts on, browser notifications on, SMS off, a 50,000 rial floor, and no quiet hours.",
        "security": [{ "cookieAuth": [] }],
        "responses": {
          "200": {
            "description": "The current settings.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/NotifyPrefsView" },
                "example": { "live_alerts": true, "push_enabled": true, "sms_enabled": false, "min_profit": "50000" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "put": {
        "operationId": "setNotifyPrefs",
        "tags": ["Notifications"],
        "summary": "Save this account's notification settings",
        "description": "Replaces the stored settings and answers with what is now held — a value a constraint refused is never echoed back as though it had been saved. `quiet_from_min` and `quiet_to_min` are minutes of day in Tehran local time (a fixed +03:30; Iran has observed no DST since 2022) and must be sent together or not at all: half a quiet-hours setting is a permanent mute nobody chose. `from > to` wraps past midnight, which is the common case. `sms_enabled` is read-only here — no component of this system stores a phone number, so nothing can send one.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/NotifyPrefsView" },
              "example": { "live_alerts": true, "push_enabled": true, "min_profit": "100000", "quiet_from_min": 1380, "quiet_to_min": 420 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings as now stored.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotifyPrefsView" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/api/notify/subscribe": {
      "post": {
        "operationId": "subscribePush",
        "tags": ["Notifications"],
        "summary": "Register this browser for Web Push",
        "description": "Stores one browser's push endpoint and its RFC 8291 keys, exactly as `PushSubscription.toJSON()` produces them. The endpoint must be `https` on a known push service; the keys must decode to 65 bytes (an uncompressed P-256 point, `0x04`-led) and 16 bytes. Registering an endpoint that already exists updates it in place, because a browser that re-subscribes returns the same endpoint and two rows would be two notifications for one device. Rate limited to 10 per hour per account.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PushSubscribeRequest" },
              "example": { "endpoint": "https://fcm.googleapis.com/fcm/send/…", "p256dh": "BCVxsr7N…", "auth": "BTBZMqHH6r4Tts7J_aSIgg", "label": "Pixel 8" }
            }
          }
        },
        "responses": {
          "204": { "description": "Registered." },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "operationId": "unsubscribePush",
        "tags": ["Notifications"],
        "summary": "Retire this browser's push endpoint",
        "description": "Removes one endpoint from this account. Answers 204 whether or not a row was removed: whether a given endpoint belongs to you is not a fact this route discloses.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PushUnsubscribeRequest" },
              "example": { "endpoint": "https://fcm.googleapis.com/fcm/send/…" }
            }
          }
        },
        "responses": {
          "204": { "description": "Retired, or there was nothing to retire." },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/rt/ticket": {
      "post": {
        "operationId": "createRealtimeTicket",
        "tags": ["Realtime"],
        "summary": "Mint a connect ticket for the live stream",
        "description": "Returns a short-lived, single-use, fingerprint-bound PASETO the realtime gateway accepts at `wss://rt.noghtex.ir` (WebTransport over UDP/443 to the same host, with WSS as the fallback). The ticket is for the live canvas stream, which is a separate protocol from this REST surface.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "responses": {
          "200": {
            "description": "The connect ticket.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ConnectTicketView" },
                "example": { "ticket": "v4.public.…", "expires_in_secs": 30 }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/auth/stepup/challenge": {
      "post": {
        "operationId": "createStepUpChallenge",
        "tags": ["Step-up"],
        "summary": "Request a WebAuthn step-up challenge",
        "description": "Arms a single-use, short-lived challenge the browser's platform authenticator signs for a withdrawal approval. The signed result travels on the withdrawal request itself, as `X-Noghtex-StepUp-Assertion`.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "responses": {
          "200": {
            "description": "The challenge for `navigator.credentials.get()`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebAuthnChallenge" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/auth/webauthn/register/challenge": {
      "post": {
        "operationId": "createWebAuthnRegistrationChallenge",
        "tags": ["Step-up"],
        "summary": "Request a WebAuthn registration challenge",
        "description": "Arms the challenge for enrolling a new hardware credential. `allow_credentials` is empty on registration by definition.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "responses": {
          "200": {
            "description": "The challenge for `navigator.credentials.create()`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebAuthnChallenge" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/auth/webauthn/register": {
      "post": {
        "operationId": "registerWebAuthnCredential",
        "tags": ["Step-up"],
        "summary": "Store a verified WebAuthn credential",
        "description": "The browser's answer to a registration challenge. `public_key` is the SPKI DER the platform authenticator returns from `getPublicKey()`; only public material crosses here — a credential has no secret half outside the authenticator.",
        "security": [{ "cookieAuth": [] }, { "csrfHeader": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RegistrationResponse" }
            }
          }
        },
        "responses": {
          "204": { "description": "The credential is enrolled." },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/auth/webauthn/credentials": {
      "get": {
        "operationId": "listWebAuthnCredentials",
        "tags": ["Step-up"],
        "summary": "List enrolled WebAuthn credentials",
        "description": "Every credential the caller may assert with: id, public key, COSE algorithm, counter and label.",
        "security": [{ "cookieAuth": [] }],
        "responses": {
          "200": {
            "description": "The credential list.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CredentialListResponse" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/pay/callback": {
      "get": {
        "operationId": "paymentGatewayReturn",
        "tags": ["Wallet"],
        "summary": "The payment gateway's browser return leg",
        "description": "Where the payer's browser lands after the gateway. Deliberately sessionless — a top-level cross-site navigation carries no `SameSite=Strict` cookie — and deliberately believing nothing it is told: the `Status` query parameter is a hint written by a redirect, so the outcome is re-verified server-to-server and the payer is then redirected to the app with a `?pay=` hint to look up the real receipt over their authenticated session. Programmatic clients should ignore this route and poll `getDepositReceipt` instead.",
        "security": [],
        "parameters": [
          {
            "name": "Authority",
            "in": "query",
            "required": false,
            "description": "The provider's payment-session identifier. Lowercase `authority` is accepted too — proxies in this market have been observed lowercasing query keys. Neither spelling is trusted.",
            "schema": { "type": "string" }
          },
          {
            "name": "Status",
            "in": "query",
            "required": false,
            "description": "The provider's `OK`/`NOK` hint, verbatim. Not trusted; verification is server-to-server.",
            "schema": { "type": "string", "enum": ["OK", "NOK"] }
          }
        ],
        "responses": {
          "303": {
            "description": "Redirect to the app with a `?pay=ok|failed|pending|unknown` hint and the authority.",
            "headers": {
              "Location": { "description": "The app origin with the payment hint.", "schema": { "type": "string", "format": "uri" } }
            }
          }
        }
      }
    },
    "/healthz/live": {
      "get": {
        "operationId": "getLiveness",
        "tags": ["Health"],
        "summary": "Liveness probe",
        "description": "The process is up and its executor is scheduling. Consults nothing, by design: a liveness probe that fails when a dependency is down restarts every replica at once.",
        "security": [],
        "responses": {
          "200": {
            "description": "Alive.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthView" } } }
          }
        }
      }
    },
    "/healthz/ready": {
      "get": {
        "operationId": "getReadiness",
        "tags": ["Health"],
        "summary": "Readiness probe",
        "description": "Both required dependencies are reachable: Dragonfly (the session store — without it every request would 401) and the engine (the thing the BFF fronts).",
        "security": [],
        "responses": {
          "200": {
            "description": "Ready to serve.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthView" } } }
          },
          "503": {
            "description": "A required dependency is down.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthView" } } }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "Retry-After": {
        "description": "Seconds until the budget that refused this call resets. Present on every `429 RATE_LIMITED`.",
        "schema": { "type": "integer" }
      },
      "Deprecation": {
        "description": "Rides responses of an operation scheduled for removal (marked `deprecated: true` in this document), naming the convention: the value is `true` from the moment deprecation is announced.",
        "schema": { "type": "string", "enum": ["true"] }
      },
      "Sunset": {
        "description": "The HTTP-date after which a deprecated operation will no longer answer. Announced at least 12 months ahead — see the versioning policy above.",
        "schema": { "type": "string", "example": "Wed, 31 Dec 2027 23:59:59 GMT" }
      },
      "RateLimit-Limit": {
        "description": "The burst ceiling of the budget applied to this route.",
        "schema": { "type": "integer" }
      },
      "RateLimit-Remaining": {
        "description": "Requests left in the current window; zero on a refused call.",
        "schema": { "type": "integer" }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the current window expires.",
        "schema": { "type": "integer" }
      },
      "RateLimit-Policy": {
        "description": "The budget as `burst;w=window-seconds`.",
        "schema": { "type": "string" }
      },
      "API-Version": {
        "description": "The major version of this API on every response, including errors. Currently `1`.",
        "schema": { "type": "string", "enum": ["1"] }
      }
    },
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-ngx_sid",
        "description": "The session cookie. `__Host-` prefixed: `Secure`, `HttpOnly`, `SameSite=Strict`, no `Domain` attribute, host-locked to api.noghtex.ir."
      },
      "csrfHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-CSRF-Token",
        "description": "The double-submit half of the CSRF check: the same token the `__Host-ngx_csrf` cookie carries, echoed in this header on every mutating request. The token is also returned in the body of `login` and `getSessionIdentity`."
      },
      "idempotencyKey": {
        "type": "apiKey",
        "in": "header",
        "name": "Idempotency-Key",
        "description": "A client-generated UUID. Replaying the same key returns the original result instead of executing a second purchase. The BFF neither generates nor rewrites it."
      },
      "stepUpAssertion": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Noghtex-StepUp-Assertion",
        "description": "Base64url JSON of the signed WebAuthn assertion (`AssertionResponse` schema), binding the payout to a hardware approval."
      }
    },
    "parameters": {
      "PixelX": {
        "name": "x",
        "in": "path",
        "required": true,
        "description": "Board x coordinate.",
        "schema": { "type": "integer", "minimum": 0, "maximum": 999 }
      },
      "PixelY": {
        "name": "y",
        "in": "path",
        "required": true,
        "description": "Board y coordinate.",
        "schema": { "type": "integer", "minimum": 0, "maximum": 999 }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request itself was malformed. The specific reason stays in the log; `hint` names what to fix.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "Unauthorized": {
        "description": "No session, an expired one, or a binding mismatch (the cookie was replayed from a different device and the session was revoked).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "Forbidden": {
        "description": "The origin wall or the CSRF double-submit check refused the request. The body never says which — the reason goes to the security log, not to a possible attacker.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "NotFound": {
        "description": "No such resource — or it exists and is not yours; the two are the same answer.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      },
      "RateLimited": {
        "description": "A token bucket is exhausted. `Retry-After` (seconds) rides the response alongside the same `RateLimit-*` headers every budgeted response carries, now at zero remaining; the body carries `retry_after_ms`.",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/Retry-After" },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimit-Limit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimit-Remaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimit-Reset" },
          "RateLimit-Policy": { "$ref": "#/components/headers/RateLimit-Policy" }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorBody" } } }
      }
    },
    "schemas": {
      "Money": {
        "description": "A monetary amount as a decimal string, e.g. `\"1200\"`. A string, never a number: a balance above 2^53 loses precision as a JSON number. Unit: Iranian rial.",
        "type": "string",
        "pattern": "^-?[0-9]+$",
        "examples": ["1200"]
      },
      "Rgb": {
        "type": "object",
        "description": "A colour as three bytes, the same encoding that rides the binary plane.",
        "properties": {
          "r": { "type": "integer", "minimum": 0, "maximum": 255 },
          "g": { "type": "integer", "minimum": 0, "maximum": 255 },
          "b": { "type": "integer", "minimum": 0, "maximum": 255 }
        },
        "required": ["r", "g", "b"],
        "additionalProperties": false
      },
      "NotifyPrefsView": {
        "type": "object",
        "description": "Which channels may interrupt an account, and when. Nothing here is inferred from behaviour: a notification preference derived from what somebody did would be a side channel.",
        "properties": {
          "live_alerts": {
            "type": "boolean",
            "description": "The in-app plane, over the live stream. Free, instant, and off only if you say so."
          },
          "push_enabled": {
            "type": "boolean",
            "description": "Web Push. Opt-out: it costs nothing and the browser's own permission prompt is the real consent gate."
          },
          "sms_enabled": {
            "type": "boolean",
            "description": "Read-only, and always false. The column exists so the preference survives a product decision either way; no component of this system stores a phone number, so nothing can send an SMS."
          },
          "min_profit": {
            "$ref": "#/components/schemas/Money",
            "description": "The smallest profit worth a notification. Nobody should be woken for 900 rial."
          },
          "quiet_from_min": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1439,
            "description": "Minutes of day, Tehran local time (a fixed +03:30). Sent with `quiet_to_min` or not at all."
          },
          "quiet_to_min": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1439,
            "description": "The end of the quiet window, exclusive. A value below `quiet_from_min` wraps past midnight, which is the common case."
          }
        },
        "required": ["live_alerts", "push_enabled", "sms_enabled", "min_profit"],
        "additionalProperties": false
      },
      "PushSubscribeRequest": {
        "type": "object",
        "description": "One browser's Web Push registration, as `PushSubscription.toJSON()` produces it. Both keys are base64url.",
        "properties": {
          "endpoint": {
            "type": "string",
            "format": "uri",
            "maxLength": 512,
            "description": "The push service's URL. Must be `https` on a known push service host — an unvalidated endpoint is a request this cluster would later make on a stranger's behalf."
          },
          "p256dh": {
            "type": "string",
            "description": "The user agent's public key: an uncompressed P-256 point, 65 bytes, `0x04`-led."
          },
          "auth": {
            "type": "string",
            "description": "The user agent's 16-byte auth secret (RFC 8291 §3.2)."
          },
          "label": {
            "type": "string",
            "maxLength": 64,
            "description": "Optional. Purely so a person can recognise their own devices in the settings list."
          }
        },
        "required": ["endpoint", "p256dh", "auth"],
        "additionalProperties": false
      },
      "PushUnsubscribeRequest": {
        "type": "object",
        "description": "Names the endpoint to retire. A browser knows its own endpoint and nothing else, which is why this is not an id.",
        "properties": {
          "endpoint": { "type": "string", "format": "uri", "maxLength": 512 }
        },
        "required": ["endpoint"],
        "additionalProperties": false
      },
      "ErrorBody": {
        "type": "object",
        "description": "The one error shape of the whole API. `code` is a stable machine identifier to branch on; `message` is a stable English developer string, never user-facing copy; `hint` names what to do about it.",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "PIXEL_MOVED",
              "INSUFFICIENT_FUNDS",
              "IDEMPOTENCY_REPLAY",
              "DUPLICATE_IN_PROGRESS",
              "IDEMPOTENCY_KEY_REUSED",
              "RATE_LIMITED",
              "UNAUTHORIZED",
              "FORBIDDEN",
              "UNDER_REVIEW",
              "NOT_FOUND",
              "BAD_REQUEST",
              "METHOD_NOT_ALLOWED",
              "INTERNAL"
            ],
            "description": "PIXEL_MOVED (409): lost the takeover race, body carries the fresh quote. INSUFFICIENT_FUNDS (402): guarded debit refused. RATE_LIMITED (429): Retry-After present. UNAUTHORIZED (401): re-authenticate. FORBIDDEN (403): origin wall or CSRF. UNDER_REVIEW (403): a withdrawal held for a person to review; the balance is untouched and nothing moved — information, not a failure, and it never says why. NOT_FOUND (404). BAD_REQUEST (400). IDEMPOTENCY_KEY_REUSED (422): same key, different body. DUPLICATE_IN_PROGRESS (409): the same key is still running elsewhere — on a withdrawal it can also mean a check has not caught up; retrying the same key after Retry-After resumes it. IDEMPOTENCY_REPLAY (200): the original result was returned. INTERNAL (500)."
          },
          "message": { "type": "string" },
          "hint": {
            "type": "string",
            "description": "A resolution hint. Present on the errors an informed caller can act on (NOT_FOUND, BAD_REQUEST, METHOD_NOT_ALLOWED); absent where a hint would help an attacker."
          },
          "pixel": {
            "description": "Present only on `PIXEL_MOVED`.",
            "type": "object",
            "properties": {
              "new_price": { "$ref": "#/components/schemas/Money" },
              "new_version": { "type": "integer", "description": "The fresh optimistic-concurrency version." }
            },
            "required": ["new_price", "new_version"],
            "additionalProperties": false
          },
          "retry_after_ms": {
            "type": "integer",
            "description": "Present on RATE_LIMITED and DUPLICATE_IN_PROGRESS."
          }
        },
        "required": ["code", "message"],
        "additionalProperties": false
      },
      "PublicStatsView": {
        "type": "object",
        "description": "The public counters under the login card. Carries nothing that is not already public: two aggregates, no identities, no board state.",
        "properties": {
          "online": { "type": "integer", "minimum": 0, "description": "Distinct users with an authenticated request in the last five minutes." },
          "sales_last_hour": { "type": "integer", "minimum": 0, "description": "Successful, non-replayed takeovers in the trailing hour." }
        },
        "required": ["online", "sales_last_hour"],
        "additionalProperties": false
      },
      "OtpRequest": {
        "type": "object",
        "properties": {
          "phone": { "type": "string", "description": "E.164 phone number, e.g. `+989121234567`." }
        },
        "required": ["phone"],
        "additionalProperties": false
      },
      "OtpAccepted": {
        "type": "object",
        "description": "What the client needs to render the countdown — deliberately not whether the number is registered, which would make the endpoint an enumeration oracle.",
        "properties": {
          "expires_in_secs": { "type": "integer", "minimum": 1 },
          "resend_after_secs": { "type": "integer", "minimum": 1 }
        },
        "required": ["expires_in_secs", "resend_after_secs"],
        "additionalProperties": false
      },
      "LoginRequest": {
        "type": "object",
        "properties": {
          "phone": { "type": "string" },
          "code": { "type": "string", "description": "The one-time code delivered by SMS." }
        },
        "required": ["phone", "code"],
        "additionalProperties": false
      },
      "MeView": {
        "type": "object",
        "description": "Session identity, answered by the BFF from the session it already holds.",
        "properties": {
          "user_id": { "type": "integer" },
          "handle": { "type": "string", "description": "The public display name." },
          "csrf": { "type": "string", "description": "The session's CSRF token — the same value as the `__Host-ngx_csrf` cookie, also delivered here because the cookie is host-locked to the API origin." }
        },
        "required": ["user_id", "handle", "csrf"],
        "additionalProperties": false
      },
      "TakeRequest": {
        "type": "object",
        "properties": {
          "x": { "type": "integer", "minimum": 0, "maximum": 999 },
          "y": { "type": "integer", "minimum": 0, "maximum": 999 },
          "color": { "$ref": "#/components/schemas/Rgb" },
          "client_price_seen": {
            "$ref": "#/components/schemas/Money",
            "description": "Advisory only. The engine recomputes the price from the authoritative row; this is used to notice that the caller was rendering a stale board, and for nothing else."
          }
        },
        "required": ["x", "y", "color"],
        "additionalProperties": false
      },
      "TakeOk": {
        "type": "object",
        "description": "The engine's takeover receipt, passed through unread by the BFF.",
        "properties": {
          "x": { "type": "integer" },
          "y": { "type": "integer" },
          "price": { "$ref": "#/components/schemas/Money" },
          "version": { "type": "integer" }
        },
        "required": ["x", "y", "price", "version"],
        "additionalProperties": true
      },
      "PixelMeta": {
        "type": "object",
        "description": "Everything known about one pixel. `next_price` is the fresh ask — what a retry would have to pay — not the price the winner just paid; pricing is server-authoritative, so a client cannot derive it.",
        "properties": {
          "x": { "type": "integer", "minimum": 0, "maximum": 999 },
          "y": { "type": "integer", "minimum": 0, "maximum": 999 },
          "owner_handle": { "type": "string", "description": "Absent when nobody owns it yet and it sits at the floor price." },
          "color": { "$ref": "#/components/schemas/Rgb" },
          "price": { "$ref": "#/components/schemas/Money" },
          "version": { "type": "integer", "description": "The optimistic-concurrency version." },
          "next_price": { "$ref": "#/components/schemas/Money", "description": "What a taker pays now." },
          "seller_profit": { "$ref": "#/components/schemas/Money", "description": "What the current owner receives if someone takes it at next_price." },
          "platform_fee": { "$ref": "#/components/schemas/Money" }
        },
        "required": ["x", "y", "color", "price", "version", "next_price", "seller_profit", "platform_fee"],
        "additionalProperties": false
      },
      "PixelRef": {
        "type": "object",
        "properties": {
          "x": { "type": "integer", "minimum": 0, "maximum": 999 },
          "y": { "type": "integer", "minimum": 0, "maximum": 999 }
        },
        "required": ["x", "y"],
        "additionalProperties": false
      },
      "PixelQuoteRequest": {
        "type": "object",
        "properties": {
          "pixels": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PixelRef" },
            "maxItems": 256,
            "minItems": 1
          }
        },
        "required": ["pixels"],
        "additionalProperties": false
      },
      "PixelQuoteResponse": {
        "type": "object",
        "properties": {
          "pixels": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PixelMeta" },
            "description": "One metadata row per requested cell, in the order asked."
          }
        },
        "required": ["pixels"],
        "additionalProperties": false
      },
      "MarketFeedView": {
        "type": "object",
        "properties": {
          "events": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MarketEventView" },
            "description": "Newest first, at most 40."
          }
        },
        "required": ["events"],
        "additionalProperties": false
      },
      "MarketEventView": {
        "type": "object",
        "description": "One settled resale, as the live-market feed line needs it.",
        "properties": {
          "x": { "type": "integer", "minimum": 0, "maximum": 999 },
          "y": { "type": "integer", "minimum": 0, "maximum": 999 },
          "seller_gets": {
            "$ref": "#/components/schemas/Money",
            "description": "Principal + profit — what the previous owner received. NOT the price the buyer paid, which carries the platform fee on top: the profit percentage is seller_profit / (seller_gets - seller_profit), and only this value makes that difference the seller's principal."
          },
          "seller_profit": { "$ref": "#/components/schemas/Money" },
          "seller_handle": { "type": "string", "description": "The seller named in the feed line." },
          "at_ms": { "type": "integer", "description": "Unix milliseconds the sale was recorded." }
        },
        "required": ["x", "y", "seller_gets", "seller_profit", "seller_handle", "at_ms"],
        "additionalProperties": false
      },
      "LeaderboardView": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/LeaderboardEntryView" },
            "description": "Ranked, best first. Render the position from the index rather than re-sorting."
          }
        },
        "required": ["entries"],
        "additionalProperties": false
      },
      "LeaderboardEntryView": {
        "type": "object",
        "description": "One place on the holdings leaderboard.",
        "properties": {
          "handle": { "type": "string", "description": "The holder named in the list." },
          "pixels": { "type": "integer", "minimum": 0, "maximum": 1000000, "description": "How many pixels of the board this account currently owns." },
          "value": {
            "$ref": "#/components/schemas/Money",
            "description": "What those pixels cost: the sum of their current prices. Acquisition cost, NOT a mark-to-market valuation and NOT realised profit — a pixel's price is what its current owner paid for it."
          }
        },
        "required": ["handle", "pixels", "value"],
        "additionalProperties": false
      },
      "WalletView": {
        "type": "object",
        "properties": {
          "balance": { "$ref": "#/components/schemas/Money" },
          "entries": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/LedgerEntryView" }
          }
        },
        "required": ["balance", "entries"],
        "additionalProperties": false
      },
      "LedgerEntryView": {
        "type": "object",
        "description": "One row of the user's own statement. Negative is money leaving the wallet.",
        "properties": {
          "txn_id": { "type": "string", "format": "uuid" },
          "amount": { "$ref": "#/components/schemas/Money" },
          "kind": { "type": "string", "enum": ["takeover_debit", "takeover_credit", "fee", "deposit", "withdrawal"] },
          "ref_x": { "type": "integer", "description": "The pixel this entry is about, when there is one." },
          "ref_y": { "type": "integer", "description": "The pixel this entry is about, when there is one." },
          "created_at_ms": { "type": "integer", "description": "Unix milliseconds." }
        },
        "required": ["txn_id", "amount", "kind", "created_at_ms"],
        "additionalProperties": false
      },
      "ChargeRequest": {
        "type": "object",
        "properties": {
          "amount": { "$ref": "#/components/schemas/Money" }
        },
        "required": ["amount"],
        "additionalProperties": false
      },
      "ChargeStarted": {
        "type": "object",
        "description": "The opened payment session. **No money has moved** — the intent is parked and the payer has not been to the bank.",
        "properties": {
          "txn_id": { "type": "string", "format": "uuid", "description": "The caller's Idempotency-Key." },
          "amount": { "$ref": "#/components/schemas/Money" },
          "authority": { "type": "string", "description": "The provider's identifier for this session; the only thing the payer's return leg carries." },
          "payment_url": { "type": "string", "format": "uri", "description": "Where to send the browser." },
          "status": { "$ref": "#/components/schemas/DepositStatus" },
          "provider": { "type": "string", "description": "e.g. `zarinpal`. Named so a second provider is a value change, not a client change." },
          "expires_in_secs": { "type": "integer", "description": "How long the authority stays payable." }
        },
        "required": ["txn_id", "amount", "authority", "payment_url", "status", "provider", "expires_in_secs"],
        "additionalProperties": false
      },
      "DepositStatus": {
        "type": "string",
        "enum": ["pending", "verified", "failed", "expired"],
        "description": "pending: opened, not resolved (includes 'the payer paid but verification has not landed' — poll, don't fail). verified: the only status under which money exists. failed: the gateway refused or the payer cancelled. expired: never completed inside the window."
      },
      "DepositReceipt": {
        "type": "object",
        "description": "Everything a payer is shown after the bank. Always fetched over the payer's own session — never off the return URL.",
        "properties": {
          "authority": { "type": "string" },
          "txn_id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/DepositStatus" },
          "amount": { "$ref": "#/components/schemas/Money" },
          "ref_id": { "type": "string", "description": "«شماره پیگیری» — the provider's reference. Present once verified." },
          "card_pan": { "type": "string", "description": "Masked at the source: `502229******1234`." },
          "balance": { "$ref": "#/components/schemas/Money", "description": "The authoritative balance after settlement. Only on a verified receipt." },
          "provider_code": { "type": "integer", "description": "The gateway's own code, for support. Never rendered as-is." },
          "created_at_ms": { "type": "integer", "description": "Unix milliseconds." }
        },
        "required": ["authority", "txn_id", "status", "amount", "created_at_ms"],
        "additionalProperties": false
      },
      "WithdrawRequest": {
        "type": "object",
        "properties": {
          "amount": { "$ref": "#/components/schemas/Money" },
          "destination_ref": {
            "type": "string",
            "description": "Opaque handle for the payout destination. The real account details are PII, decrypted only enclave-side; nothing outside the enclave ever carries the number."
          }
        },
        "required": ["amount", "destination_ref"],
        "additionalProperties": false
      },
      "ConnectTicketView": {
        "type": "object",
        "properties": {
          "ticket": { "type": "string", "description": "A PASETO v4.public token, audience `noghtex-gateway`." },
          "expires_in_secs": { "type": "integer" }
        },
        "required": ["ticket", "expires_in_secs"],
        "additionalProperties": false
      },
      "WebAuthnChallenge": {
        "type": "object",
        "description": "What the browser needs to run a WebAuthn ceremony.",
        "properties": {
          "challenge": { "type": "string", "description": "32 random bytes, base64url. Single-use, server-side, short-lived." },
          "rp_id": { "type": "string", "description": "The Relying Party id the assertion must be scoped to (the registrable domain)." },
          "allow_credentials": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Which credentials the browser may use. Empty on registration."
          },
          "timeout_ms": { "type": "integer", "minimum": 1 }
        },
        "required": ["challenge", "rp_id", "timeout_ms"],
        "additionalProperties": false
      },
      "AssertionResponse": {
        "type": "object",
        "description": "The browser's answer to a step-up challenge, carried on the withdrawal request as `X-Noghtex-StepUp-Assertion` (base64url JSON). Every field is base64url because that is what `PublicKeyCredential` produces.",
        "properties": {
          "credential_id": { "type": "string" },
          "authenticator_data": { "type": "string" },
          "client_data_json": { "type": "string" },
          "signature": { "type": "string" }
        },
        "required": ["credential_id", "authenticator_data", "client_data_json", "signature"],
        "additionalProperties": false
      },
      "RegistrationResponse": {
        "type": "object",
        "description": "The browser's answer to a registration challenge. `public_key` is SPKI DER from `AuthenticatorAttestationResponse.getPublicKey()`.",
        "properties": {
          "credential_id": { "type": "string" },
          "authenticator_data": { "type": "string" },
          "client_data_json": { "type": "string" },
          "public_key": { "type": "string" }
        },
        "required": ["credential_id", "authenticator_data", "client_data_json", "public_key"],
        "additionalProperties": false
      },
      "CredentialView": {
        "type": "object",
        "properties": {
          "credential_id": { "type": "string", "description": "The authenticator's opaque handle, base64url." },
          "public_key": { "type": "string", "description": "SubjectPublicKeyInfo DER, base64url. A public key." },
          "cose_alg": { "type": "integer", "description": "COSE algorithm identifier: -7 (ES256) or -8 (EdDSA)." },
          "sign_count": { "type": "integer" },
          "label": { "type": "string", "description": "The user's own label for the key. Cosmetic." }
        },
        "required": ["credential_id", "public_key", "cose_alg", "sign_count", "label"],
        "additionalProperties": false
      },
      "CredentialListResponse": {
        "type": "object",
        "properties": {
          "credentials": { "type": "array", "items": { "$ref": "#/components/schemas/CredentialView" } }
        },
        "required": ["credentials"],
        "additionalProperties": false
      },
      "OpenApiDocument": {
        "type": "object",
        "description": "The OpenAPI 3.1 document itself. The document cannot fully describe its own internals, so only the envelope is typed here; fetch and parse it for the whole contract.",
        "properties": {
          "openapi": { "type": "string", "const": "3.1.0" },
          "info": {
            "type": "object",
            "description": "Title, version (`1.0.0`), the auth model, the error taxonomy, the versioning/deprecation policy and the rate-limit conventions."
          },
          "servers": { "type": "array", "items": { "type": "object" } },
          "tags": { "type": "array", "items": { "type": "object" } },
          "paths": { "type": "object", "description": "One operation object per route, each with a unique operationId." },
          "components": { "type": "object", "description": "Reusable schemas, parameters, responses and security schemes." }
        },
        "required": ["openapi", "info", "paths"],
        "additionalProperties": true
      },
      "WithdrawSettledView": {
        "type": "object",
        "description": "The engine's settlement receipt for an accepted withdrawal. The payout itself settles asynchronously; this says the intent was approved and the ledger legs posted.",
        "properties": {
          "txn_id": { "type": "string", "format": "uuid", "description": "The caller's `Idempotency-Key`, echoed." },
          "amount": { "$ref": "#/components/schemas/Money" },
          "kind": {
            "type": "string",
            "enum": ["deposit", "withdrawal"],
            "description": "`withdrawal` on this operation; shared with the deposit receipt shape."
          },
          "balance": { "$ref": "#/components/schemas/Money", "description": "The authoritative balance after the movement." }
        },
        "required": ["txn_id", "amount", "kind", "balance"],
        "additionalProperties": false
      },
      "HealthView": {
        "type": "object",
        "description": "The one probe body. `ok` under a 200, `unavailable` under a 503 — so a client never has to infer health from the status code alone.",
        "properties": {
          "status": { "type": "string", "enum": ["ok", "unavailable"] }
        },
        "required": ["status"],
        "additionalProperties": false
      }
    }
  }
}
