openapi: 3.1.0
info:
  title: Nescio ID API
  version: "1.0.0"
  summary: Proof of real, unique, 18+ players — without PII.
  description: |
    **Nescio ID** verifies that a player is a genuine, one-of-one, of-age human and
    hands your game a stable, anonymous **per-game `subjectId`** — no names, no emails,
    no documents ever touch your servers or ours.

    > **New here?** Follow the step-by-step set-up guide — with a real, copy-paste
    > example (a simplified game like Arena) — in your
    > [studio dashboard](https://dashboard.nescio.dev). **This page is the full
    > reference**: every endpoint, parameter, and response, with live samples.

    ## How you integrate
    Your **backend** holds an API key (`nsc_<keyId>_<secret>`) and calls these
    endpoints over plain HTTPS. The browser never sees the key; you never see PII.

    1. **Start a verification** → `POST /v1/game/verify/sessions`. Render the returned
       `client` payload as a QR for the player to scan with the Self app.
    2. **Poll the session** → `GET /v1/game/verify/sessions/{sessionId}`. When
       `status` is `verified` you get a `subjectId` (stable for this player, in *this
       game only*) and a `duplicate` flag (true = the same human already verified —
       your Sybil signal).
    3. **Read claims** anytime → `GET /v1/game/players/{subjectId}` →
       `{ verified_human, over_18, issuing_country }`.

    Same human → same `subjectId`, forever — and meaningless to any other game. Ban,
    gate ranked, or grant rewards against the identity, not the account.
  contact:
    name: Nescio ID
    url: https://nescio.dev
servers:
  - url: https://api.nescio.dev
    description: Production
tags:
  - name: Verification
    description: Start and poll a player's verification session.
  - name: Players
    description: Look up a verified player's non-PII claims.
  - name: Health
    description: Liveness.
security:
  - apiKey: []
paths:
  /v1/game/verify/sessions:
    post:
      tags: [Verification]
      operationId: startVerification
      summary: Start a verification session
      description: |
        Begins a verification for one of your players. Render the returned `client`
        payload as a QR; the player scans it with the Self app, which proves on-device
        and posts the proof to Nescio. Then poll the session.
      responses:
        "201":
          description: Session created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NewSession" }
              example:
                sessionId: "b2f1c8e0-4a6d-4c3f-9b5e-2a1d7c9f4e6b"
                status: "pending"
                mode: "live"
                expiresAt: "2026-09-26T21:10:00.000Z"
                client:
                  kind: "self"
                  universalLink: "https://redirect.self.xyz?selfApp=…"
                  selfApp: { scope: "nescio-prod-v1", endpoint: "https://api.nescio.dev/v1/verify/callback" }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/GameSuspended" }
        "502": { $ref: "#/components/responses/ProviderUnavailable" }
  /v1/game/verify/sessions/{sessionId}:
    get:
      tags: [Verification]
      operationId: getVerification
      summary: Poll a verification session
      description: |
        Returns the session status. When `verified`, includes the game-scoped
        `subjectId` and a `duplicate` flag (true = this human already had an identity —
        block or flag the second account).
      parameters:
        - $ref: "#/components/parameters/sessionId"
      responses:
        "200":
          description: Current session state.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Session" }
              examples:
                pending: { summary: Still waiting, value: { sessionId: "b2f1…", status: "pending" } }
                verified:
                  summary: Verified — a unique human
                  value: { sessionId: "b2f1…", status: "verified", subjectId: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", duplicate: false }
                duplicate:
                  summary: Verified — but a repeat human (smurf)
                  value: { sessionId: "b2f1…", status: "verified", subjectId: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", duplicate: true }
                failed: { summary: Proof rejected, value: { sessionId: "b2f1…", status: "failed", reason: "under_18" } }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/GameSuspended" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/game/players/{subjectId}:
    get:
      tags: [Players]
      operationId: getPlayer
      summary: Get a player's claims
      description: Resolve one of your `subjectId`s to its non-PII claims. Never returns PII.
      parameters:
        - name: subjectId
          in: path
          required: true
          description: A per-game subject id you received from a verified session.
          schema: { type: string }
      responses:
        "200":
          description: The player's claims.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Player" }
              example:
                subjectId: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
                claims: { verified_human: true, over_18: true, issuing_country: "FRA" }
        "401": { $ref: "#/components/responses/InvalidApiKey" }
        "403": { $ref: "#/components/responses/GameSuspended" }
        "404": { $ref: "#/components/responses/NotFound" }
  /healthz:
    get:
      tags: [Health]
      operationId: health
      summary: Liveness
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { type: object, properties: { ok: { type: boolean } } }
              example: { ok: true }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "Your game API key: `Authorization: Bearer nsc_<keyId>_<secret>`. Server-side only."
  parameters:
    sessionId:
      name: sessionId
      in: path
      required: true
      description: The session id returned by startVerification.
      schema: { type: string, format: uuid }
  schemas:
    NewSession:
      type: object
      required: [sessionId, status, client]
      properties:
        sessionId: { type: string, format: uuid }
        status: { type: string, enum: [pending] }
        mode: { type: string, enum: [live, sandbox], description: "sandbox = the API runs in dev/mock mode and accepts MOCK passports only; render a notice telling players to use a mock document in Self. live in production." }
        expiresAt: { type: string, format: date-time }
        client:
          type: object
          description: Opaque render payload for the verification rail (a Self QR config). Render as a QR; do not parse.
          additionalProperties: true
    Session:
      type: object
      required: [sessionId, status]
      properties:
        sessionId: { type: string, format: uuid }
        status: { type: string, enum: [pending, verified, failed], description: "verified → subjectId present; failed → reason present." }
        mode: { type: string, enum: [live, sandbox], description: "sandbox = dev/mock mode (mock passports only)." }
        subjectId: { type: string, description: "Stable per-game id. Present when verified. Key your player records on this." }
        duplicate: { type: boolean, description: "true = this human already verified for your game (Sybil / smurf signal)." }
        reason: { type: string, description: "Failure reason, e.g. under_18 | proof_invalid." }
    Player:
      type: object
      required: [subjectId, claims]
      properties:
        subjectId: { type: string }
        claims: { $ref: "#/components/schemas/Claims" }
    Claims:
      type: object
      description: Non-PII booleans/enums only — never a name, DOB, or document number.
      properties:
        verified_human: { type: boolean }
        over_18: { type: boolean }
        issuing_country: { type: string, description: "ISO 3166 alpha-3, when disclosed." }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
        detail: { type: string }
  responses:
    InvalidApiKey:
      description: Missing or invalid API key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" }, example: { error: invalid_api_key } } }
    GameSuspended:
      description: This game has been suspended by an operator.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" }, example: { error: game_suspended } } }
    NotFound:
      description: No such session or player.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" }, example: { error: not_found } } }
    ProviderUnavailable:
      description: The verification provider is temporarily unavailable.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" }, example: { error: provider_unavailable } } }
