openapi: 3.0.3
info:
  title: Tokia API
  version: "1.0"
  description: |
    Gateway brasileiro de IA com PIX, NF-e e markup transparente.

    **OpenAI-compatible**: troque `https://api.openai.com/v1` por
    `https://api.usetokia.com/v1` e qualquer SDK OpenAI funciona.

    Auth: Bearer token (`Authorization: Bearer sk-tokia-...`)

    Erros traduzidos pra PT-BR em [/docs/troubleshooting](https://www.usetokia.com/docs/troubleshooting).

    Sprint 297 — Para LLMs/agentes: descritor breve do site em
    [/llms.txt](https://www.usetokia.com/llms.txt) e índice estruturado em
    [/docs/index.json](https://www.usetokia.com/docs/index.json).
  contact:
    email: contato@usetokia.com
    url: https://www.usetokia.com
  license:
    name: Tokia Terms
    url: https://www.usetokia.com/terms

servers:
  - url: https://api.usetokia.com/v1
    description: Production
  - url: https://api-dev.usetokia.com/v1
    description: Dev/staging

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk-tokia-

  schemas:
    ChatCompletionRequest:
      type: object
      required: [model, messages]
      properties:
        model:
          type: string
          example: deepseek-v3
        messages:
          type: array
          items:
            type: object
            properties:
              role:
                type: string
                enum: [system, user, assistant]
              content:
                type: string
        max_tokens:
          type: integer
          minimum: 1
        temperature:
          type: number
          minimum: 0
          maximum: 2
        stream:
          type: boolean
          default: false
        tools:
          type: array
          description: Function calling (OpenAI-compatible)
          items:
            type: object

    ChatCompletionResponse:
      type: object
      properties:
        id:
          type: string
        object:
          type: string
          example: chat.completion
        created:
          type: integer
        model:
          type: string
        choices:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
              message:
                type: object
                properties:
                  role:
                    type: string
                  content:
                    type: string
              finish_reason:
                type: string
        usage:
          type: object
          properties:
            prompt_tokens:
              type: integer
            completion_tokens:
              type: integer
            total_tokens:
              type: integer

    EmbeddingRequest:
      type: object
      required: [model, input]
      properties:
        model:
          type: string
          example: text-embedding-3-small
        input:
          oneOf:
            - type: string
            - type: array
              items: { type: string }

    ImageRequest:
      type: object
      required: [model, prompt]
      properties:
        model:
          type: string
          example: flux-schnell
        prompt:
          type: string
        size:
          type: string
          example: "1024x1024"
        n:
          type: integer
          default: 1

    Balance:
      type: object
      properties:
        balance_brl:
          type: number
        total_topped_up_brl:
          type: number
        total_spent_brl:
          type: number

    Model:
      type: object
      properties:
        id:
          type: string
        display_name:
          type: string
        category:
          type: string
          enum: [llm, image, video]
        upstream:
          type: string
        markup_multiplier:
          type: number
        description:
          type: string
          nullable: true

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            message:
              type: string
            type:
              type: string
            code:
              type: string

paths:
  /chat/completions:
    post:
      summary: Chat completion (OpenAI-compatible)
      tags: [Chat]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatCompletionRequest'
            examples:
              simple:
                summary: Pergunta simples
                value:
                  model: deepseek-v3
                  messages:
                    - role: user
                      content: Explica PIX em 1 frase
              streaming:
                summary: Com streaming
                value:
                  model: gpt-4o-mini
                  messages: [{ role: user, content: Conta uma historia curta }]
                  stream: true
      responses:
        "200":
          description: Resposta completa (não-streaming) OU stream SSE
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatCompletionResponse'
            text/event-stream:
              schema:
                type: string
        "401":
          description: API key inválida (sk-tokia-...)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        "402":
          description: Saldo BRL insuficiente
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        "429":
          description: Rate limit excedido
        "5XX":
          description: Provider upstream falhou

  /embeddings:
    post:
      summary: Embeddings (RAG, search semântica)
      tags: [Embeddings]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EmbeddingRequest'
      responses:
        "200":
          description: Vetores 1536-dim
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        embedding:
                          type: array
                          items: { type: number }

  /images/generations:
    post:
      summary: Geração de imagem (Flux Schnell via Fal.ai)
      tags: [Images]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImageRequest'
      responses:
        "200":
          description: URLs das imagens
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        url: { type: string }

  /audio/transcriptions:
    post:
      summary: Transcrição áudio (Whisper)
      tags: [Audio]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                model:
                  type: string
                  default: whisper-1
      responses:
        "200":
          description: Texto transcrito
          content:
            application/json:
              schema:
                type: object
                properties:
                  text: { type: string }

  /usage:
    get:
      summary: Histórico de uso + saldo (Tokia-specific)
      tags: [Tokia]
      responses:
        "200":
          description: Usage events + balance embutido
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance:
                    $ref: '#/components/schemas/Balance'
                  usage_events:
                    type: array
                    items: { type: object }

  /usage/export.csv:
    get:
      summary: Export uso CSV (compliance/contabilidade)
      tags: [Tokia]
      parameters:
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
      responses:
        "200":
          description: CSV UTF-8 com BOM (Excel BR-friendly)
          content:
            text/csv: {}

  /usage/export.json:
    get:
      summary: Export uso JSON (Sprint 182 — drop-in BI/Zapier/IFTTT)
      tags: [Tokia]
      parameters:
        - in: query
          name: from
          schema: { type: string, format: date }
        - in: query
          name: to
          schema: { type: string, format: date }
      responses:
        "200":
          description: JSON estruturado com eventos do período
          content:
            application/json:
              schema:
                type: object
                properties:
                  period: { type: object }
                  total_events: { type: integer }
                  events:
                    type: array
                    items: { type: object }

  /me/usage-summary:
    get:
      summary: Sprint 188 — resumo compacto em 1 chamada (saldo + totals + top 3 modelos)
      tags: [Tokia]
      parameters:
        - in: query
          name: days
          schema: { type: integer, default: 30, minimum: 1, maximum: 365 }
      responses:
        "200":
          description: Resumo do período
          content:
            application/json:
              schema:
                type: object
                properties:
                  period_days: { type: integer }
                  balance_brl: { type: number }
                  total_topped_up_brl: { type: number }
                  total_spent_brl: { type: number }
                  period_totals:
                    type: object
                    properties:
                      requests: { type: integer }
                      cost_billed_brl: { type: number }
                      cost_upstream_usd: { type: number }
                      prompt_tokens: { type: integer }
                      completion_tokens: { type: integer }
                  top_models:
                    type: array
                    items:
                      type: object
                      properties:
                        model_id: { type: string }
                        model_name: { type: string }
                        requests: { type: integer }
                        cost_billed_brl: { type: number }
                  unique_models_used: { type: integer }

  /me/dashboard-stats:
    get:
      summary: Sprint 281 — snapshot consolidado (1 call) pra dashboards mobile/SDK
      tags: [Tokia]
      responses:
        "200":
          description: Snapshot
          content:
            application/json:
              schema:
                type: object
                properties:
                  profile: { type: object, nullable: true }
                  balance_brl: { type: number }
                  keys:
                    type: object
                    properties:
                      active: { type: integer }
                      revoked: { type: integer }
                  topups:
                    type: object
                    properties:
                      paid: { type: integer }
                      total: { type: integer }
                  notifications:
                    type: object
                    properties:
                      failed: { type: integer }
                      total: { type: integer }

  /me/runway:
    get:
      summary: Sprint 261 — runway estimado em dias (saldo / gasto médio diário)
      tags: [Tokia]
      parameters:
        - in: query
          name: days
          description: Período pra calcular gasto médio diário
          schema: { type: integer, default: 30, minimum: 1, maximum: 90 }
      responses:
        "200":
          description: Estimativa de runway
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance_brl: { type: number }
                  period_days: { type: integer }
                  daily_avg_brl: { type: number }
                  runway_days:
                    type: integer
                    nullable: true
                    description: null quando não há gasto recente o suficiente pra estimar.
                  critical:
                    type: boolean
                    description: true quando runway_days < 7.

  /me/notifications:
    get:
      summary: Sprint 194 — histórico de emails transacionais recebidos pelo user
      tags: [Tokia]
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
      responses:
        "200":
          description: Lista das últimas notificações enviadas
          content:
            application/json:
              schema:
                type: object
                properties:
                  total: { type: integer }
                  notifications:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        template: { type: string }
                        sent_at: { type: string, format: date-time }
                        status: { type: string, enum: [sent, failed] }
                        error: { type: string, nullable: true }

  /me/export:
    get:
      summary: Sprint 251 — export LGPD (Art. 18) do dado do user em JSON
      tags: [Tokia]
      responses:
        "200":
          description: Snapshot JSON com profile + saldo + notificações
          content:
            application/json:
              schema:
                type: object
                properties:
                  export_version: { type: integer }
                  generated_at: { type: string, format: date-time }
                  profile: { type: object }
                  balance: { type: object }
                  notifications:
                    type: array
                    items: { type: object }
                  note: { type: string }

  /models:
    get:
      summary: Catálogo curado Tokia (27+ modelos)
      tags: [Tokia]
      security: []
      parameters:
        - in: query
          name: category
          description: Sprint 181 — filtra por categoria. `chat` é alias de `llm` (compat OpenAI).
          schema: { type: string, enum: [llm, image, video, embedding, audio, chat] }
      responses:
        "200":
          description: Lista de modelos disponíveis
          content:
            application/json:
              schema:
                type: object
                properties:
                  models:
                    type: array
                    items: { $ref: '#/components/schemas/Model' }

  /me/profile:
    get:
      summary: Perfil do user autenticado
      tags: [Tokia]
      responses:
        "200":
          description: Perfil + flags

  /me/referrals:
    get:
      summary: Stats programa de indicações
      tags: [Tokia · Referrals]
      responses:
        "200":
          description: Código + lista de indicados + stats

  /me/teams:
    get:
      summary: Lista teams do user
      tags: [Tokia · Teams]
    post:
      summary: Cria novo team
      tags: [Tokia · Teams]

  /me/alerts:
    get:
      summary: Spending alerts customizáveis
      tags: [Tokia · Alerts]
    post:
      summary: Cria alerta custom
      tags: [Tokia · Alerts]

  /me/refunds:
    get:
      summary: Histórico refund self-service
      tags: [Tokia · Refunds]
    post:
      summary: Cria pedido de refund (mín R$ 5)
      tags: [Tokia · Refunds]

  /healthz:
    get:
      summary: Health check
      tags: [Public]
      security: []
      responses:
        "200":
          description: Status do serviço

  /healthz/db:
    get:
      summary: Sprint 286 — health check isolado do Postgres (SELECT 1)
      tags: [Public]
      security: []
      responses:
        "200":
          description: DB responde corretamente
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  latency_ms: { type: integer }
                  timestamp: { type: string, format: date-time }
        "503":
          description: DB inacessível

  /incidents/active:
    get:
      summary: Incidents ativos (status page)
      tags: [Public]
      security: []
      responses:
        "200":
          description: Lista de incidents abertos

  # ===== Sprint 87 — Per-API-key drill-down =====
  /usage/by-key:
    get:
      summary: Breakdown de uso por API key
      tags: [Tokia · Usage]
      parameters:
        - name: days
          in: query
          schema: { type: integer, default: 30, minimum: 1, maximum: 365 }
      responses:
        "200":
          description: Lista de keys com agregados (requests, billed, top modelos)

  # ===== Sprint 88 — Webhook rotate signing secret =====
  /webhooks/{id}/rotate-secret:
    post:
      summary: Rotaciona signing_secret (invalida antigo)
      tags: [Tokia · Webhooks]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Novo signing_secret (mostrado 1× só)

  # ===== Sprint 90 — Upstream health =====
  /upstream-health:
    get:
      summary: Saúde dos upstreams IA (públic)
      tags: [Public]
      security: []
      parameters:
        - name: days
          in: query
          schema: { type: integer, default: 30, minimum: 1, maximum: 90 }
      responses:
        "200":
          description: Uptime% e latência p50/p95 por provider

  # ===== Sprint 91 — Recommendations =====
  /me/recommendations:
    get:
      summary: Recomendações de modelos mais baratos
      tags: [Tokia · Recommendations]
      parameters:
        - name: days
          in: query
          schema: { type: integer, default: 30, minimum: 7, maximum: 90 }
      responses:
        "200":
          description: Sugestões baseadas em custo real BRL/1k tokens

  # ===== Sprint 92 — Audit log export =====
  /admin/audit-log/export.csv:
    get:
      summary: Export CSV do audit log (admin only)
      tags: [Tokia · Admin]
      parameters:
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
      responses:
        "200":
          description: CSV BOM UTF-8 com até 10k rows
          content:
            text/csv: {}

  # ===== Sprint 93 — Webhook replay =====
  /webhooks/attempts/{attemptId}/replay:
    post:
      summary: Cria novo attempt clonando payload existente
      tags: [Tokia · Webhooks]
      parameters:
        - name: attemptId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Novo attempt enfileirado com delivery_id novo

  # ===== Sprint 94 — Rate limit usage =====
  /usage/rate-limit:
    get:
      summary: RPM/TPM consumo atual por key
      tags: [Tokia · Usage]
      parameters:
        - name: minutes
          in: query
          schema: { type: integer, default: 60, minimum: 5, maximum: 720 }
      responses:
        "200":
          description: Por key com headroom%

  # ===== Sprint 96 — Bulk revoke =====
  /api-keys/bulk-revoke:
    post:
      summary: Revoga várias keys de uma vez (emergency rotation)
      tags: [Tokia · API Keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [all]
                  properties:
                    all: { type: boolean, enum: [true] }
                - type: object
                  required: [ids]
                  properties:
                    ids:
                      type: array
                      maxItems: 100
                      items: { type: string, format: uuid }
      responses:
        "200":
          description: Counts (revoked, alreadyInactive, litellmFailures)

  # ===== Sprint 607 — Sessions (group requests by conversation) =====
  /me/sessions:
    get:
      summary: Lista sessions (agrupa usage_events por session_id)
      description: |
        Cliente envia header `x-tokia-session-id: <uuid>` em cada POST /v1/chat/completions.
        Proxy propaga via metadata.session_id; reconcile_spend materializa em
        usage_events.session_id. Este endpoint agrupa por session_id retornando
        request_count, custo total, tokens, modelos, primeira/última request, duração.
      tags: [Tokia · Sessions]
      parameters:
        - in: query
          name: days
          schema: { type: integer, minimum: 1, maximum: 90, default: 30 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        "200":
          description: Lista de sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  days: { type: integer }
                  count: { type: integer }
                  sessions:
                    type: array
                    items:
                      type: object
                      properties:
                        session_id: { type: string }
                        request_count: { type: integer }
                        total_billed_brl: { type: number }
                        total_prompt_tokens: { type: integer }
                        total_completion_tokens: { type: integer }
                        total_tokens: { type: integer }
                        models: { type: array, items: { type: string } }
                        first_request_at: { type: string, format: date-time }
                        last_request_at: { type: string, format: date-time }
                        duration_ms: { type: integer }

  /me/sessions/{id}:
    get:
      summary: Detalhe de 1 session (eventos cronológicos)
      tags: [Tokia · Sessions]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, maxLength: 80 }
      responses:
        "200":
          description: Eventos da session
          content:
            application/json:
              schema:
                type: object
                properties:
                  session_id: { type: string }
                  request_count: { type: integer }
                  total_billed_brl: { type: number }
                  events:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        model_id: { type: string }
                        api_key_id: { type: string, format: uuid, nullable: true }
                        prompt_tokens: { type: integer, nullable: true }
                        completion_tokens: { type: integer, nullable: true }
                        cost_billed_brl: { type: number }
                        cost_upstream_usd: { type: number }
                        created_at: { type: string, format: date-time }
        "404": { description: session_not_found }

  # ===== Sprint 607-608 — Prompts library =====
  /me/prompts:
    get:
      summary: Lista prompts salvos do user
      tags: [Tokia · Prompts]
      responses:
        "200":
          description: Lista
    post:
      summary: Cria novo prompt (versão 1)
      tags: [Tokia · Prompts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, body]
              properties:
                name: { type: string, minLength: 3, maxLength: 120 }
                body: { type: string, minLength: 1, maxLength: 32000, description: "Suporta placeholders {{var}}" }
                description: { type: string, nullable: true }
                variables: { type: array, items: { type: string, pattern: "^[a-zA-Z0-9_]{1,40}$" } }
                tags: { type: array, items: { type: string, maxLength: 24 } }
                default_model: { type: string, nullable: true, example: "gpt-4o-mini" }
      responses:
        "201": { description: Created }

  /me/prompts/{id}:
    get:
      summary: Detalhe do prompt
      tags: [Tokia · Prompts]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: OK }
        "404": { description: prompt_not_found }
    patch:
      summary: Atualiza prompt (cria nova versão se body mudou)
      tags: [Tokia · Prompts]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                body: { type: string }
                description: { type: string, nullable: true }
                variables: { type: array, items: { type: string } }
                tags: { type: array, items: { type: string } }
                default_model: { type: string, nullable: true }
                change_note: { type: string, maxLength: 200 }
      responses:
        "200": { description: OK }
    delete:
      summary: Remove prompt (cascata em prompt_versions)
      tags: [Tokia · Prompts]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: OK }

  /me/prompts/{id}/versions:
    get:
      summary: Histórico de versões do prompt (append-only)
      tags: [Tokia · Prompts]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Lista de versões }

  /me/prompts/{id}/clone:
    post:
      summary: Clona prompt existente
      description: Slug recebe sufixo `-copia` (collision-safe). Versão volta pra 1, histórico separado.
      tags: [Tokia · Prompts]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        "201": { description: Created }

  /me/prompts/{id}/runs:
    get:
      summary: Histórico de runs do prompt (reusa Sessions)
      description: |
        Cada run do playground cria session com prefixo `prompt-{id}-`.
        Este endpoint agrupa por session_id retornando model, tokens, custo.
      tags: [Tokia · Prompts]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
      responses:
        "200": { description: Lista de runs }

  /me/prompts/{id}/run:
    post:
      summary: Renderiza variáveis + executa via /v1/chat/completions
      description: |
        Substitui `{{var}}` pelos values, chama /v1/chat/completions interno com
        x-tokia-session-id=`prompt-{id}-{ts}`. Cobra do saldo BRL da api_key.
      tags: [Tokia · Prompts]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [api_key]
              properties:
                values: { type: object, additionalProperties: { type: string } }
                model: { type: string, nullable: true }
                api_key: { type: string, pattern: "^sk-" }
                max_tokens: { type: integer, default: 1024 }
                temperature: { type: number, default: 0.7 }
      responses:
        "200": { description: Resposta upstream }

  # ===== Sprint 98 — Account preferences =====
  /me/preferences:
    put:
      summary: Atualiza timezone, locale, weekly digest opt-out
      tags: [Tokia · Me]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                timezone:
                  type: string
                  example: America/Sao_Paulo
                preferred_locale:
                  type: string
                  pattern: "^[a-z]{2}(-[A-Z]{2})?$"
                  example: pt-BR
                weekly_digest_enabled:
                  type: boolean
      responses:
        "200":
          description: Preferences atualizadas
