openapi: 3.1.0
info:
  title: Vanguard AI API
  version: "1.0.0"
  summary: Computer Vision & AI API for Lao documents.
  description: |
    The Vanguard AI API extracts structured data from Lao government documents and
    bank cards (OCR), transcribes and synthesises speech (STT / TTS), and exposes a
    multi-model LLM chat. All programmatic calls authenticate with an API key
    (`Authorization: Bearer nt_live_...`).

    - Control-plane calls use JSON. Scans upload `multipart/form-data`.
    - Every response follows the standard envelope: `{ "status": "success" | "error", ... }`.
    - This is the `v1` surface. New fields and endpoints are added compatibly within `/v1`.
  contact:
    name: Vanguard AI
    url: https://vanguardinitiative.com
servers:
  - url: https://api-ocr.vanguardinitiative.com
    description: Production
  - url: http://localhost:8000
    description: Local development
security:
  - apiKey: []
tags:
  - name: Auth
    description: Sign in and manage API keys.
  - name: OCR
    description: Extract structured data from document images.
  - name: Speech-to-Text
    description: Transcribe audio (asynchronous jobs).
  - name: Text-to-Speech
    description: Synthesise speech from text (asynchronous jobs).
  - name: Chat
    description: Multi-model LLM chat with streaming.
  - name: Jobs
    description: List, poll, rename and delete asynchronous jobs.
  - name: Projects
    description: Group jobs and chats.
  - name: Services
    description: Discover available services and pricing.
  - name: Account
    description: Balance, usage and storage configuration.
  - name: Billing
    description: Credit packages and top-ups.
  - name: System
    description: Health and liveness.

paths:
  /v1/auth/signup:
    post:
      tags: [Auth]
      summary: Create an organization + owner user
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                orgName: { type: string }
      responses:
        "200":
          description: Session token issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  token: { type: string }
                  orgId: { type: string, format: uuid }
                  userId: { type: string, format: uuid }
        "400": { $ref: "#/components/responses/BadRequest" }

  /v1/auth/login:
    post:
      tags: [Auth]
      summary: Log in
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
      responses:
        "200":
          description: Session token issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  token: { type: string }
                  orgId: { type: string, format: uuid }
                  userId: { type: string, format: uuid }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/keys:
    get:
      tags: [Auth]
      summary: List API keys
      description: Requires a JWT session token (dashboard action). Hashes are never returned.
      responses:
        "200":
          description: The organization's keys.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  keys:
                    type: array
                    items: { $ref: "#/components/schemas/ApiKey" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Auth]
      summary: Mint an API key
      description: |
        Requires a JWT session token. The plaintext key is returned **once** and stored
        only as a hash — surface a "copy now" step to the user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: production }
                scopes:
                  type: array
                  items: { type: string, enum: [ocr, stt, tts, chat] }
                  default: [ocr]
      responses:
        "200":
          description: Key created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  id: { type: string, format: uuid }
                  apiKey: { type: string, example: nt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
                  prefix: { type: string, example: nt_live_xxxxxxxx }
                  scopes: { type: array, items: { type: string } }
                  warning: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/keys/{id}:
    delete:
      tags: [Auth]
      summary: Revoke an API key
      parameters:
        - $ref: "#/components/parameters/PathId"
      responses:
        "200":
          description: Revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  revoked: { type: string, format: uuid }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/services:
    get:
      tags: [Services]
      summary: List available services
      security: []
      responses:
        "200":
          description: Enabled services and their credit prices.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  services:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, example: ocr }
                        credits: { type: integer, example: 1 }
                        scope: { type: string, example: ocr }

  /v1/ocr/scan:
    post:
      tags: [OCR]
      summary: Run OCR on a document
      description: |
        Extract structured fields from a Lao document image or PDF. `type` is an optional
        hint; when omitted the service auto-detects the document type. Opt into result
        storage with the `X-Storage-Option` header (PRO/ENTERPRISE plans only).
      parameters:
        - $ref: "#/components/parameters/StorageOption"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
                  description: Document image or PDF. Max 15 MB.
                type:
                  type: string
                  enum: [PASSPORT, ID_CARD_OLD, ID_CARD_NEW, DRIVING_LICENSE, BANK_CARD, ENTERPRISE_REGISTRATION]
                projectId:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Extraction result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  service: { type: string, example: ocr }
                  data:
                    type: object
                    description: Extracted fields (shape depends on the detected document type).
                    additionalProperties: true
                  credits: { type: integer, example: 1 }
                  latencyMs: { type: integer, example: 842 }
                  stored:
                    type: [object, "null"]
                    description: Storage descriptor when a store option was requested, else null.
        "402": { $ref: "#/components/responses/QuotaExceeded" }
        "403": { $ref: "#/components/responses/ScopeForbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/{service}/scan:
    post:
      tags: [OCR]
      summary: Run a computer-vision service (generic)
      description: Generic form of the scan endpoint. `service` resolves against the registry (e.g. `ocr`).
      parameters:
        - name: service
          in: path
          required: true
          schema: { type: string, example: ocr }
        - $ref: "#/components/parameters/StorageOption"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image: { type: string, format: binary }
                type: { type: string }
      responses:
        "200":
          description: Extraction result.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScanResult" }
        "404": { $ref: "#/components/responses/UnknownService" }

  /v1/stt/jobs:
    post:
      tags: [Speech-to-Text]
      summary: Transcribe audio
      description: |
        Enqueue an asynchronous transcription job. Returns `202` with a `jobId`; poll
        `GET /v1/jobs/{id}` until `status` is `success` or `error`. Send either a
        `multipart/form-data` file or a JSON body with `audioUrl`. Costs 5 credits.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: Audio file, up to 200 MB. }
                language: { type: string, example: lo }
                projectId: { type: string, format: uuid }
          application/json:
            schema:
              type: object
              required: [audioUrl]
              properties:
                audioUrl: { type: string, format: uri }
                language: { type: string, example: lo }
                projectId: { type: string, format: uuid }
      responses:
        "202":
          description: Job accepted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobAccepted" }
        "402": { $ref: "#/components/responses/QuotaExceeded" }
        "403": { $ref: "#/components/responses/ScopeForbidden" }

  /v1/tts/jobs:
    post:
      tags: [Text-to-Speech]
      summary: Synthesise speech
      description: |
        Enqueue an asynchronous speech-synthesis job. Returns `202` with a `jobId`;
        the stored audio is available from the finished job. Costs 2 credits.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
                voice: { type: string }
                languageCode: { type: string, example: lo-LA }
                format: { type: string, enum: [mp3, wav, ogg], default: mp3 }
                ssml: { type: boolean, default: false }
                speakingRate: { type: number, example: 1.0 }
                pitch: { type: number, example: 0.0 }
                projectId: { type: string, format: uuid }
      responses:
        "202":
          description: Job accepted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobAccepted" }
        "402": { $ref: "#/components/responses/QuotaExceeded" }

  /v1/jobs:
    get:
      tags: [Jobs]
      summary: List jobs
      parameters:
        - { name: projectId, in: query, schema: { type: string, format: uuid } }
        - { name: serviceId, in: query, schema: { type: string, example: stt } }
        - { name: status, in: query, schema: { type: string, enum: [pending, processing, success, error] } }
        - { name: q, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: A page of jobs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  jobs:
                    type: array
                    items: { $ref: "#/components/schemas/Job" }

  /v1/jobs/{id}:
    get:
      tags: [Jobs]
      summary: Get a job
      parameters: [{ $ref: "#/components/parameters/PathId" }]
      responses:
        "200":
          description: The job.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  job: { $ref: "#/components/schemas/Job" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Jobs]
      summary: Rename a job
      parameters: [{ $ref: "#/components/parameters/PathId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
    delete:
      tags: [Jobs]
      summary: Delete a job
      parameters: [{ $ref: "#/components/parameters/PathId" }]
      responses:
        "200":
          description: Deleted (storage cleaned up).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }

  /v1/jobs/{id}/summarize:
    post:
      tags: [Jobs]
      summary: Summarise a transcript
      description: Generate a meeting-style summary of a completed transcription. Costs 1 credit.
      parameters: [{ $ref: "#/components/parameters/PathId" }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                preset: { type: string, example: meeting }
                force: { type: boolean, default: false }
      responses:
        "200":
          description: Summary produced.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  summary: { type: string }

  /v1/projects:
    get:
      tags: [Projects]
      summary: List projects
      responses:
        "200":
          description: Projects.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  projects:
                    type: array
                    items: { $ref: "#/components/schemas/Project" }
    post:
      tags: [Projects]
      summary: Create a project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  project: { $ref: "#/components/schemas/Project" }

  /v1/projects/{id}:
    patch:
      tags: [Projects]
      summary: Rename a project
      parameters: [{ $ref: "#/components/parameters/PathId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
    delete:
      tags: [Projects]
      summary: Delete a project
      parameters: [{ $ref: "#/components/parameters/PathId" }]
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }

  /v1/chat/models:
    get:
      tags: [Chat]
      summary: List chat models
      security: []
      responses:
        "200":
          description: Available models grouped by provider.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  models:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, example: gpt-5 }
                        provider: { type: string, example: openai }
                        label: { type: string }

  /v1/chat/conversations:
    get:
      tags: [Chat]
      summary: List conversations
      responses:
        "200":
          description: Conversations.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
    post:
      tags: [Chat]
      summary: Create a conversation
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                model: { type: string, example: gpt-5 }
                projectId: { type: string, format: uuid }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  conversation: { $ref: "#/components/schemas/Conversation" }

  /v1/chat/conversations/{id}/messages:
    post:
      tags: [Chat]
      summary: Send a message (streaming)
      description: |
        Append a user message and stream the assistant's reply as
        Server-Sent Events (`text/event-stream`). If the connection drops, reconnect
        with `GET /v1/chat/conversations/{id}/stream`.
      parameters: [{ $ref: "#/components/parameters/PathId" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content: { type: string }
                model: { type: string }
      responses:
        "200":
          description: An SSE token stream.
          content:
            text/event-stream:
              schema: { type: string }

  /v1/account/balance:
    get:
      tags: [Account]
      summary: Get credit balance
      responses:
        "200":
          description: Balance and plan.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  balance: { type: integer, example: 100 }
                  plan: { type: string, example: FREE }

  /v1/account/usage:
    get:
      tags: [Account]
      summary: List usage events
      parameters:
        - { name: service, in: query, schema: { type: string, example: ocr } }
        - { name: status, in: query, schema: { type: string, enum: [success, error] } }
        - { name: apiKeyId, in: query, schema: { type: string, format: uuid } }
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Recent events and a rollup summary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  recent:
                    type: array
                    items: { $ref: "#/components/schemas/UsageEvent" }
                  summary:
                    type: array
                    items:
                      type: object
                      properties:
                        service: { type: string }
                        calls: { type: integer }
                        credits: { type: integer }

  /v1/account/usage/stats:
    get:
      tags: [Account]
      summary: Aggregated usage stats
      responses:
        "200":
          description: Aggregates by service, key, status and a time series.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }

  /v1/account/storage:
    get:
      tags: [Account]
      summary: Get storage configuration
      responses:
        "200":
          description: The org's bring-your-own storage config (secrets redacted).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }
    post:
      tags: [Account]
      summary: Set bring-your-own S3 storage
      description: Configure an S3-compatible bucket for stored results. PRO/ENTERPRISE only.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StorageConfig" }
      responses:
        "200":
          description: Saved.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }

  /v1/billing/packages:
    get:
      tags: [Billing]
      summary: List credit packages
      security: []
      responses:
        "200":
          description: Purchasable credit packages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { const: success }
                  packages:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, example: standard }
                        credits: { type: integer, example: 5000 }
                        amountLak: { type: integer, example: 200000 }

  /v1/billing/topup:
    post:
      tags: [Billing]
      summary: Start a credit purchase
      description: Returns the payment gateway QR / deeplink to render.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [packageId]
              properties:
                packageId: { type: string, example: standard }
                provider: { type: string, default: laoqr }
      responses:
        "200":
          description: Payment intent created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Success" }

  /health:
    get:
      tags: [System]
      summary: Health check
      security: []
      responses:
        "200":
          description: Service healthy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
        "503":
          description: Service unhealthy.

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: nt_live_xxx
      description: |
        Send your API key as `Authorization: Bearer nt_live_...`. Dashboard/self-serve
        endpoints accept a JWT session token instead; programmatic calls should always
        use an API key.

  parameters:
    PathId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    StorageOption:
      name: X-Storage-Option
      in: header
      required: false
      description: "Opt into result storage: `none` (default), `platform`, or `byo`. PRO/ENTERPRISE only."
      schema: { type: string, enum: [none, platform, byo] }

  responses:
    BadRequest:
      description: Invalid input.
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    Unauthorized:
      description: Missing or invalid credential (`AUTH_MISSING` / `AUTH_INVALID`).
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    QuotaExceeded:
      description: Out of credits (`QUOTA_EXCEEDED`).
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    ScopeForbidden:
      description: Key lacks the required scope (`SCOPE_FORBIDDEN`).
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    NotFound:
      description: Resource not found.
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    UnknownService:
      description: No such service (`UNKNOWN_SERVICE`).
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    PayloadTooLarge:
      description: Upload over the size limit (`PAYLOAD_TOO_LARGE`).
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    UnsupportedMediaType:
      description: File type not allowed (`UNSUPPORTED_MEDIA_TYPE`).
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    RateLimited:
      description: Too many requests this minute (`RATE_LIMITED`).
      headers:
        X-RateLimit-Limit: { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
      content:
        application/json: { schema: { $ref: "#/components/schemas/Error" } }

  schemas:
    Success:
      type: object
      properties:
        status: { const: success }
      required: [status]
    Error:
      type: object
      required: [status, code, message]
      properties:
        status: { const: error }
        code: { type: string, example: QUOTA_EXCEEDED }
        message: { type: string, example: Out of credits }
    ApiKey:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        prefix: { type: string, example: nt_live_xxxx }
        scopes: { type: array, items: { type: string } }
        lastUsedAt: { type: [string, "null"], format: date-time }
        revokedAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
    ScanResult:
      type: object
      properties:
        status: { const: success }
        service: { type: string }
        data: { type: object, additionalProperties: true }
        credits: { type: integer }
        latencyMs: { type: integer }
        stored: { type: [object, "null"] }
    JobAccepted:
      type: object
      properties:
        status: { const: success }
        jobId: { type: string, format: uuid }
        state: { type: string, example: pending }
    Job:
      type: object
      properties:
        id: { type: string, format: uuid }
        serviceId: { type: string, example: stt }
        name: { type: string }
        status: { type: string, enum: [pending, processing, success, error] }
        projectId: { type: [string, "null"], format: uuid }
        result: { type: [object, "null"], additionalProperties: true }
        createdAt: { type: string, format: date-time }
    Project:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        createdAt: { type: string, format: date-time }
    Conversation:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        model: { type: string }
        projectId: { type: [string, "null"], format: uuid }
        createdAt: { type: string, format: date-time }
    UsageEvent:
      type: object
      properties:
        id: { type: string, format: uuid }
        service: { type: string, example: ocr }
        credits: { type: integer }
        latencyMs: { type: integer }
        status: { type: string, enum: [success, error] }
        createdAt: { type: string, format: date-time }
    StorageConfig:
      type: object
      required: [endpoint, bucket, accessKeyId, secretAccessKey]
      properties:
        endpoint: { type: string, format: uri }
        region: { type: string }
        bucket: { type: string }
        accessKeyId: { type: string }
        secretAccessKey: { type: string }
