openapi: 3.1.0
info:
  title: Kanan External Financial API
  version: 1.1.0
  description: >-
    API HTTPS para consultar y operar espacios financieros autorizados. Los cambios 1.1 son
    aditivos; las credenciales y rutas 1.0 conservan su comportamiento.
  contact:
    name: Kanan
    url: https://kanan.app
externalDocs:
  description: Guias publicas de la API de Kanan
  url: https://docs.kanan.app
servers:
  - url: https://api.kanan.app/v1/financial
    description: Produccion
x-readme:
  explorer-enabled: false
  proxy-enabled: false
  samples-languages:
    - shell
security:
  - bearerAuth: []
tags:
  - { name: Context, description: Seleccion del espacio financiero efectivo. }
  - { name: Catalog, description: 'Categorias, cuentas, cajas y saldos.' }
  - { name: Budgets, description: Presupuestos y partidas presupuestales. }
  - { name: Movements, description: Gastos e ingresos del tracker seleccionado. }
  - { name: Transfers, description: Transferencias atomicas entre cuentas y cajas. }
  - { name: API keys, description: Administracion de credenciales desde una sesion de usuario. }
  - { name: Legacy integrations, description: Superficie 1.0 conservada temporalmente por compatibilidad. }
paths:
  /context:
    get:
      operationId: getFinancialContext
      tags: [Context]
      summary: Consultar el contexto financiero efectivo
      description: Devuelve el principal autenticado, los trackers autorizados y el tracker efectivo.
      x-kanan-scopes: [context:read]
      parameters: [{ $ref: '#/components/parameters/TrackerHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
  /context/active-tracker:
    put:
      operationId: setActiveTracker
      deprecated: true
      tags: [Context]
      summary: Cambiar el tracker activo del principal heredado
      description: Compatibilidad con clientes 1.0; los clientes nuevos deben preferir X-Kanan-Tracker-Id.
      x-kanan-scopes: [context:write]
      requestBody: { $ref: '#/components/requestBodies/TrackerSelection' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
  /context/default-tracker:
    put:
      operationId: setDefaultTracker
      tags: [Context]
      summary: Cambiar el tracker predeterminado del usuario
      description: Actualiza el tracker que se usa cuando una sesion de usuario no envia seleccion explicita.
      x-kanan-scopes: [context:write]
      requestBody: { $ref: '#/components/requestBodies/TrackerSelection' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '403': { $ref: '#/components/responses/Error' }
  /trackers:
    get:
      operationId: listTrackers
      tags: [Context]
      summary: Listar trackers autorizados
      description: Lista unicamente los espacios que el principal puede utilizar.
      x-kanan-scopes: [trackers:read]
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /categories:
    get:
      operationId: listCategories
      tags: [Catalog]
      summary: Listar categorias visibles
      description: Lista las categorias activas visibles en el tracker seleccionado.
      x-kanan-scopes: [catalog:read]
      parameters: [{ $ref: '#/components/parameters/TrackerHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      operationId: createCategory
      tags: [Catalog]
      summary: Crear una categoria de tracker o global del usuario
      description: El scope requerido depende de si body.scope es tracker o user.
      x-kanan-scopes: [catalog:write:tracker, catalog:write:user]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateCategory' }
      responses:
        '201': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
  /categories/{categoryId}:
    get:
      operationId: getCategory
      tags: [Catalog]
      summary: Consultar una categoria visible
      description: Devuelve una categoria si es visible desde el tracker seleccionado.
      x-kanan-scopes: [catalog:read]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { name: categoryId, in: path, required: true, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/Error' }
  /accounts:
    get:
      operationId: listAccounts
      tags: [Catalog]
      summary: Listar cuentas y saldos
      description: Lista cuentas financieras activas y sus saldos en el tracker seleccionado.
      x-kanan-scopes: [accounts:read]
      parameters: [{ $ref: '#/components/parameters/TrackerHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /accounts/{accountId}/cashboxes:
    get:
      operationId: listAccountCashboxes
      tags: [Catalog]
      summary: Listar cajas de una cuenta
      description: Lista las cajas activas pertenecientes a una cuenta visible.
      x-kanan-scopes: [accounts:read]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/AccountId' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/Error' }
  /balances:
    get:
      operationId: listBalances
      tags: [Catalog]
      summary: Consultar saldos por cuenta y caja
      description: Devuelve los saldos actuales desglosados por cuenta y caja.
      x-kanan-scopes: [balances:read]
      parameters: [{ $ref: '#/components/parameters/TrackerHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /budgets:
    get:
      operationId: listBudgets
      tags: [Budgets]
      summary: Listar presupuestos
      description: Lista presupuestos del tracker con filtros opcionales de modo y estado.
      x-kanan-scopes: [budgets:read]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { name: mode, in: query, schema: { type: string, enum: [tracking, planning] } }
        - { name: status, in: query, schema: { type: string, enum: [draft, active, paused, closed, archived] } }
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      operationId: createBudget
      tags: [Budgets]
      summary: Crear un presupuesto en borrador
      description: Crea un presupuesto de seguimiento o planeacion de forma idempotente.
      x-kanan-scopes: [budgets:write]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateBudget' }
      responses:
        '201': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
  /budgets/{budgetId}:
    get:
      operationId: getBudget
      tags: [Budgets]
      summary: Consultar presupuesto, partidas, resumen y ejecucion
      description: Devuelve el detalle completo de un presupuesto visible en el tracker.
      x-kanan-scopes: [budgets:read]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/BudgetId' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/Error' }
  /budgets/{budgetId}/lines:
    post:
      operationId: createBudgetLine
      tags: [Budgets]
      summary: Agregar una partida presupuestal
      description: Agrega una partida al presupuesto indicado de forma idempotente.
      x-kanan-scopes: [budgets:write]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/BudgetId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateBudgetLine' }
      responses:
        '201': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
  /movements:
    get:
      operationId: listMovements
      tags: [Movements]
      summary: Listar movimientos filtrados
      description: Lista gastos, ingresos o transferencias mediante filtros combinables y paginacion.
      x-kanan-scopes: [movements:read]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { name: type, in: query, schema: { type: string, enum: [expense, income, transfer] } }
        - { name: categoryId, in: query, schema: { type: string } }
        - { name: categoryIds, in: query, description: Hasta cinco IDs separados por coma, schema: { type: string } }
        - { name: accountId, in: query, schema: { type: string, format: uuid } }
        - { name: cashboxId, in: query, schema: { type: string, format: uuid } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
        - { name: offset, in: query, schema: { type: integer, minimum: 0, default: 0 } }
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      operationId: createMovement
      tags: [Movements]
      summary: Registrar gasto o ingreso
      description: Registra un gasto o ingreso; un reintento debe conservar Idempotency-Key.
      x-kanan-scopes: [movements:write]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateMovement' }
      responses:
        '201': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
  /movements/{movementId}:
    get:
      operationId: getMovement
      tags: [Movements]
      summary: Consultar un movimiento y su ETag
      description: Devuelve el movimiento y el ETag necesario para una correccion posterior.
      x-kanan-scopes: [movements:read]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/MovementId' }
      responses:
        '200':
          description: Movimiento visible
          headers: { ETag: { schema: { type: string } } }
          content: { application/json: { schema: { $ref: '#/components/schemas/SuccessEnvelope' } } }
        '404': { $ref: '#/components/responses/Error' }
    patch:
      operationId: updateMovement
      tags: [Movements]
      summary: Corregir monto, categoria, descripcion o comercio
      description: Corrige un movimiento mediante concurrencia optimista con If-Match.
      x-kanan-scopes: [movements:write]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/MovementId' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
        - { name: If-Match, in: header, required: true, schema: { type: string } }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/PatchMovement' } } }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
  /transfers:
    post:
      operationId: createTransfer
      tags: [Transfers]
      summary: Crear una transferencia
      description: Registra de forma atomica la salida y la entrada entre cuentas o cajas.
      x-kanan-scopes: [transfers:write]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateTransfer' } } }
      responses:
        '201': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
  /transfers/{groupId}:
    get:
      operationId: getTransfer
      tags: [Transfers]
      summary: Consultar una transferencia por su grupo
      description: Devuelve ambos movimientos que forman la transferencia solicitada.
      x-kanan-scopes: [movements:read]
      parameters:
        - { $ref: '#/components/parameters/TrackerHeader' }
        - { name: groupId, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/Error' }
  /api-keys:
    get:
      operationId: listApiKeys
      tags: [API keys]
      summary: Listar claves y metadatos
      description: Solo JWT de usuario. Nunca devuelve secretos ni hashes.
      x-kanan-principal: user
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '403': { $ref: '#/components/responses/Error' }
    post:
      operationId: createApiKey
      tags: [API keys]
      summary: Crear una clave segura
      description: Solo JWT con TOTP/AAL2 reciente y sesion activa. El secreto se devuelve una vez.
      x-kanan-principal: user
      x-kanan-step-up: true
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateApiKey' } } }
      responses:
        '201': { $ref: '#/components/responses/ApiKeyCreated' }
        '400': { $ref: '#/components/responses/Error' }
        '403': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }
  /api-keys/{clientId}/rotate:
    post:
      operationId: rotateApiKey
      tags: [API keys]
      summary: Crear un reemplazo sin revocar la clave anterior
      description: Solo JWT con TOTP/AAL2 reciente y sesion activa.
      x-kanan-principal: user
      x-kanan-step-up: true
      parameters: [{ $ref: '#/components/parameters/ClientId' }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/RotateApiKey' } } }
      responses:
        '201': { $ref: '#/components/responses/ApiKeyCreated' }
        '403': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '409': { $ref: '#/components/responses/Error' }
  /api-keys/{clientId}:
    delete:
      operationId: revokeApiKey
      tags: [API keys]
      summary: Revocar inmediatamente una clave
      description: Solo JWT; no exige step-up para permitir contencion de incidentes.
      x-kanan-principal: user
      parameters: [{ $ref: '#/components/parameters/ClientId' }]
      responses:
        '204': { description: Clave revocada }
        '404': { $ref: '#/components/responses/Error' }
  /integration-clients:
    get:
      operationId: listLegacyIntegrationClients
      deprecated: true
      tags: [Legacy integrations]
      summary: Listar credenciales mediante el contrato 1.0
      description: Endpoint heredado para sesiones de usuario; no usar en integraciones nuevas.
      x-kanan-principal: user
      responses: { '200': { $ref: '#/components/responses/Success' } }
    post:
      operationId: createLegacyIntegrationClient
      deprecated: true
      tags: [Legacy integrations]
      summary: Crear una credencial mediante el contrato 1.0
      description: Endpoint heredado conservado durante la ventana de compatibilidad.
      x-kanan-principal: user
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/CreateIntegrationClient' } } }
      responses: { '201': { $ref: '#/components/responses/Success' } }
  /integration-clients/{clientId}:
    delete:
      operationId: revokeLegacyIntegrationClient
      deprecated: true
      tags: [Legacy integrations]
      summary: Revocar una credencial mediante el contrato 1.0
      description: Revoca una credencial 1.0 perteneciente al usuario autenticado.
      x-kanan-principal: user
      parameters: [{ $ref: '#/components/parameters/ClientId' }]
      responses:
        '204': { description: Credencial revocada }
        '404': { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT de Kanan o secreto `kni_live_*` para operaciones financieras.
  parameters:
    TrackerHeader:
      name: X-Kanan-Tracker-Id
      in: header
      required: false
      description: >-
        Tracker de la solicitud. Es obligatorio para una clave segura cuando tiene mas de un
        tracker elegible; JWT y credenciales 1.0 conservan sus fallbacks de contexto.
      schema: { type: string, format: uuid }
    AccountId: { name: accountId, in: path, required: true, schema: { type: string, format: uuid } }
    MovementId: { name: movementId, in: path, required: true, schema: { type: string, format: uuid } }
    BudgetId: { name: budgetId, in: path, required: true, schema: { type: string, format: uuid } }
    ClientId: { name: clientId, in: path, required: true, schema: { type: string, format: uuid } }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Identificador estable para reintentar la misma intencion sin duplicarla.
      schema: { type: string, minLength: 1, maxLength: 200 }
  requestBodies:
    TrackerSelection:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [trackerId]
            additionalProperties: false
            properties: { trackerId: { type: string, format: uuid } }
            example: { trackerId: 90d814c8-5ec2-4f33-9729-48669f34b701 }
  schemas:
    Scope:
      type: string
      enum: [context:read, context:write, trackers:read, catalog:read, catalog:write:tracker, catalog:write:user, movements:read, movements:write, budgets:read, budgets:write, accounts:read, balances:read, transfers:write]
    CreateApiKey:
      type: object
      required: [name, scopes, access]
      additionalProperties: false
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        expiresAt: { type: string, format: date-time, description: Por defecto 90 dias; maximo de servidor hasta 365 dias. }
        scopes: { type: array, minItems: 1, uniqueItems: true, items: { $ref: '#/components/schemas/Scope' } }
        access:
          oneOf:
            - type: object
              required: [mode, trackerIds]
              additionalProperties: false
              properties:
                mode: { const: selected }
                trackerIds: { type: array, minItems: 1, maxItems: 20, uniqueItems: true, items: { type: string, format: uuid } }
            - type: object
              required: [mode]
              additionalProperties: false
              properties: { mode: { const: all_administered } }
      example:
        name: Automatizacion contable
        scopes: [balances:read, movements:write, catalog:read]
        access:
          mode: selected
          trackerIds: [90d814c8-5ec2-4f33-9729-48669f34b701]
    RotateApiKey:
      type: object
      additionalProperties: false
      properties: { expiresAt: { type: string, format: date-time } }
      example: { expiresAt: '2026-12-31T23:59:59Z' }
    CreateCategory:
      type: object
      required: [scope, name, type]
      additionalProperties: false
      properties:
        scope: { type: string, enum: [tracker, user] }
        name: { type: string, minLength: 1, maxLength: 120 }
        type: { type: string, enum: [expense, income] }
        description: { type: [string, 'null'], maxLength: 500 }
        aiEnabled: { type: boolean }
        icon: { type: [string, 'null'], maxLength: 80 }
        color: { type: [string, 'null'], pattern: '^#[0-9A-Fa-f]{6}$' }
      example:
        scope: tracker
        name: Transporte
        type: expense
        description: Traslados y movilidad
        aiEnabled: true
        icon: car
        color: '#2563EB'
    CreateBudget:
      oneOf:
        - type: object
          required: [mode, name, period, startsOn]
          additionalProperties: false
          properties:
            mode: { const: tracking }
            name: { type: string, minLength: 1, maxLength: 120 }
            period: { type: string, enum: [month, bimester, quadrimester, year] }
            startsOn: { type: string, format: date }
        - type: object
          required: [mode, name, startsOn, endsOn]
          additionalProperties: false
          properties:
            mode: { const: planning }
            name: { type: string, minLength: 1, maxLength: 120 }
            startsOn: { type: string, format: date }
            endsOn: { type: string, format: date }
      example:
        mode: tracking
        name: Presupuesto mensual
        period: month
        startsOn: '2026-09-01'
    CreateBudgetLine:
      type: object
      required: [categoryId, concept, lineType, amountDecimal]
      additionalProperties: false
      properties:
        categoryId: { type: string }
        concept: { type: string, minLength: 1, maxLength: 160 }
        lineType: { type: string, enum: [expense, income] }
        lineKind: { type: string, enum: [fixed, variable_estimate, reserve], default: variable_estimate }
        amountDecimal: { type: string, examples: ['125.50'] }
        frequency: { type: string, enum: [day, week, month, bimester, quarter, quadrimester, semester, year, custom], default: month }
        frequencyInterval: { type: integer, minimum: 1, default: 1 }
        notes: { type: [string, 'null'], maxLength: 500 }
      example:
        categoryId: transport
        concept: Transporte mensual
        lineType: expense
        lineKind: variable_estimate
        amountDecimal: '1500.00'
        frequency: month
    CreateMovement:
      type: object
      required: [type, amountDecimal, accountId]
      additionalProperties: false
      properties:
        type: { type: string, enum: [expense, income] }
        amountDecimal: { type: string, examples: ['125.50'] }
        accountId: { type: string, format: uuid }
        cashboxId: { type: [string, 'null'], format: uuid }
        categoryId: { type: [string, 'null'] }
        currencyCode: { type: string, minLength: 3, maxLength: 3 }
        occurredOn: { type: string, format: date }
        description: { type: [string, 'null'], maxLength: 500 }
        merchantName: { type: [string, 'null'], maxLength: 200 }
      example:
        type: expense
        amountDecimal: '36.00'
        accountId: d2f1d450-4278-46cf-94a1-45de5ff713f8
        categoryId: transport
        currencyCode: MXN
        occurredOn: '2026-09-04'
        description: Traslado a casa
        merchantName: Didi
    PatchMovement:
      type: object
      minProperties: 1
      additionalProperties: false
      properties:
        amountDecimal: { type: string }
        categoryId: { type: [string, 'null'] }
        description: { type: [string, 'null'], maxLength: 500 }
        merchantName: { type: [string, 'null'], maxLength: 200 }
      example:
        amountDecimal: '42.50'
        categoryId: transport
        description: Descripcion corregida
        merchantName: Comercio corregido
    CreateTransfer:
      type: object
      required: [amountDecimal, fromAccountId, toAccountId]
      additionalProperties: false
      properties:
        amountDecimal: { type: string }
        fromAccountId: { type: string, format: uuid }
        fromCashboxId: { type: [string, 'null'], format: uuid }
        toAccountId: { type: string, format: uuid }
        toCashboxId: { type: [string, 'null'], format: uuid }
        currencyCode: { type: string, minLength: 3, maxLength: 3 }
        occurredOn: { type: string, format: date }
        description: { type: [string, 'null'], maxLength: 500 }
      example:
        amountDecimal: '500.00'
        fromAccountId: 06ec3eb0-5c3e-49d9-8d7d-3c04b4df1fe8
        toAccountId: 32c110f3-dddc-4d1e-900d-78d14d90b558
        currencyCode: MXN
        occurredOn: '2026-09-04'
        description: Fondeo de efectivo
    CreateIntegrationClient:
      deprecated: true
      type: object
      required: [name, scopes]
      additionalProperties: false
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        scopes: { type: array, minItems: 1, items: { type: string } }
        allowedTrackerIds: { type: [array, 'null'], items: { type: string, format: uuid } }
        defaultTrackerId: { type: [string, 'null'], format: uuid }
        expiresAt: { type: [string, 'null'], format: date-time }
    SuccessEnvelope:
      type: object
      required: [data, meta]
      properties:
        data: {}
        meta:
          type: object
          required: [apiVersion, requestId]
          properties:
            apiVersion: { const: '1' }
            requestId: { type: string, format: uuid }
            trackerId: { type: string, format: uuid }
            idempotentReplay: { type: boolean }
      example:
        data: {}
        meta:
          apiVersion: '1'
          requestId: b04d4f0f-413f-4678-921a-2c7b45ed975e
          trackerId: 90d814c8-5ec2-4f33-9729-48669f34b701
    ErrorEnvelope:
      type: object
      required: [error, meta]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details: { type: object, additionalProperties: true }
        meta:
          type: object
          required: [apiVersion, requestId]
          properties:
            apiVersion: { const: '1' }
            requestId: { type: string, format: uuid }
      example:
        error:
          code: VALIDATION_ERROR
          message: Solicitud invalida
          details: {}
        meta:
          apiVersion: '1'
          requestId: b04d4f0f-413f-4678-921a-2c7b45ed975e
  responses:
    Success:
      description: Operacion exitosa
      content: { application/json: { schema: { $ref: '#/components/schemas/SuccessEnvelope' } } }
    ApiKeyCreated:
      description: Clave creada; data.secret solo aparece en esta respuesta.
      content: { application/json: { schema: { $ref: '#/components/schemas/SuccessEnvelope' } } }
    Error:
      description: Solicitud rechazada de forma segura
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
