openapi: 3.1.0
info:
  title: API pública de KOP
  version: '1'
  description: >-
    La entrada desde fuera de la app: un sistema de la organización crea
    órdenes y les adjunta documentos (kop_platform/SPEC.md §5.5). Lo que sale
    de KOP hacia ese sistema es el webhook, abajo (kop_integrations/SPEC.md
    §7.2). Lo publicado solo se amplía: un cambio que rompa es un /v2.
servers:
  - url: https://kopproject.com/api/v1
security:
  - apiKey: []
paths:
  /templates:
    get:
      summary: Las plantillas no archivadas de la organización, con sus parámetros.
      operationId: listTemplates
      responses:
        '200':
          description: Lo que hay que mandar al crear una orden.
          content:
            application/json:
              schema:
                type: object
                required: [templates]
                properties:
                  templates:
                    type: array
                    items:
                      $ref: '#/components/schemas/Template'
        '401':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /workOrders:
    post:
      summary: Crea una orden, con sus documentos.
      description: >-
        Lo mismo que la callable createWorkOrder. Repetir el requestId
        devuelve la misma orden con 200, sin crear otra ni volver a descargar
        sus documentos.
      operationId: createWorkOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWorkOrder'
      responses:
        '201':
          description: Orden creada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedWorkOrder'
        '200':
          description: Ya existía con ese requestId.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedWorkOrder'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '409':
          $ref: '#/components/responses/Error'
        '413':
          $ref: '#/components/responses/Error'
        '422':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /workOrders/{workOrderId}/documents:
    post:
      summary: Adjunta un documento a una orden que no está cerrada.
      operationId: addDocument
      parameters:
        - name: workOrderId
          in: path
          required: true
          schema:
            type: string
        - name: name
          in: query
          description: Nombre del fichero, cuando el cuerpo es el PDF.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentByUrl'
          application/pdf:
            schema:
              type: string
              format: binary
      responses:
        '201':
          description: Documento adjunto.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
        '413':
          $ref: '#/components/responses/Error'
        '422':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /auth/device:
    post:
      summary: Empieza una autorización desde el móvil (RFC 8628).
      operationId: startDeviceAuthorization
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [clientName]
              properties:
                clientName:
                  type: string
                  maxLength: 80
                  description: Cómo se llama la herramienta, que es lo que lee quien aprueba.
      responses:
        '200':
          description: El código que enseñar y con el que sondear.
          content:
            application/json:
              schema:
                type: object
                required:
                  [
                    deviceCode,
                    userCode,
                    verificationUri,
                    verificationUriComplete,
                    expiresIn,
                    interval,
                  ]
                properties:
                  deviceCode:
                    type: string
                    description: Secreto; se queda en la herramienta.
                  userCode:
                    type: string
                    pattern: '^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$'
                  verificationUri:
                    type: string
                    format: uri
                  verificationUriComplete:
                    type: string
                    format: uri
                  expiresIn:
                    type: integer
                    description: Segundos (600).
                  interval:
                    type: integer
                    description: Segundos mínimos entre sondeos (5).
        '429':
          $ref: '#/components/responses/RateLimited'
  /auth/token:
    post:
      summary: Recoge la clave de una autorización aprobada.
      description: >-
        Mientras nadie contesta, 400 authorization_pending; sondear antes del
        interval, slow_down; rechazada, access_denied; caducada o ya
        entregada, expired_token. Aprobada, la clave sale una sola vez.
      operationId: pollDeviceToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [deviceCode]
              properties:
                deviceCode:
                  type: string
      responses:
        '200':
          description: La clave, una sola vez.
          content:
            application/json:
              schema:
                type: object
                required: [apiKey, organizationId, organizationName]
                properties:
                  apiKey:
                    type: string
                    pattern: '^kop_[A-Za-z0-9]+_[A-Za-z0-9_-]+$'
                  organizationId:
                    type: string
                  organizationName:
                    type: string
        '400':
          description: Todavía no, o ya nunca.
          content:
            application/json:
              schema:
                type: object
                required: [error]
                properties:
                  error:
                    type: string
                    enum: [authorization_pending, slow_down, access_denied, expired_token]
webhooks:
  integrationEvent:
    post:
      summary: Lo que KOP entrega a la URL del webhook de la organización.
      description: >-
        Al menos una vez: se deduplica por KOP-Event-Id. La firma es el
        HMAC-SHA256 de "{t}.{cuerpo sin parsear}" con el secreto whsec_ de la
        integración; un t de hace más de cinco minutos se descarta. Al
        conectar llega un ping con la misma firma.
      parameters:
        - name: KOP-Event-Id
          in: header
          required: true
          schema:
            type: string
        - name: KOP-Signature
          in: header
          required: true
          schema:
            type: string
            pattern: '^t=[0-9]+,v1=[0-9a-f]{64}$'
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - $ref: 'https://raw.githubusercontent.com/kopproject/kop/main/schemas/integration-event.schema.yaml'
                - $ref: '#/components/schemas/Ping'
      responses:
        '2XX':
          description: Recibido. Cualquier otra respuesta se reintenta si es 408, 429 o 5xx.
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        kop_{keyId}_{secret}, de una organización. La crea un admin en la app o
        sale de /auth/token. Revocada, 401.
  responses:
    Error:
      description: Error con un code estable.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Cuota de la clave agotada (o de la IP en /auth/device).
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - unauthorized
                - not_found
                - conflict
                - too_large
                - unprocessable
                - rate_limited
                - unavailable
                - internal
            message:
              type: string
              description: En inglés, para quien integra.
    Template:
      type: object
      required: [id, name, version, parameters]
      properties:
        id:
          type: string
        name:
          type: string
        version:
          type: integer
        parameters:
          type: array
          description: >-
            Los parámetros como los declara la plantilla (workorder-template:
            name, schema, description, required…); se mandan por su name en
            template.parameters al crear.
          items:
            type: object
            required: [name]
            additionalProperties: true
    CreateWorkOrder:
      type: object
      required: [requestId, template, description]
      properties:
        requestId:
          type: string
          format: uuid
          description: Id de la orden; repetirlo no crea otra.
        template:
          type: object
          required: [id]
          properties:
            id:
              type: string
            parameters:
              type: object
              description: >-
                Por nombre. Una ubicación va resuelta (placeId, lat, lng),
                solo con address (se resuelve en el callejero; si no da un
                solo candidato con el municipio dicho, 422 con los
                candidatos) o con lat y lng sin placeId: un sitio sin
                dirección, guardado por sus coordenadas, con address como
                su nombre si lo trae.
              additionalProperties: true
        description:
          type: string
          description: El título de la orden.
        observations:
          type: string
        code:
          type: string
        dueDate:
          type: string
          format: date-time
        startAfterDate:
          type: string
          format: date-time
        assignees:
          type: array
          description: Teléfonos en E.164 de miembros de la organización.
          items:
            type: string
            pattern: '^\+[1-9][0-9]{6,14}$'
        documents:
          type: array
          maxItems: 10
          items:
            $ref: '#/components/schemas/DocumentByUrl'
    DocumentByUrl:
      type: object
      required: [name, url]
      properties:
        name:
          type: string
          maxLength: 200
        url:
          type: string
          format: uri
          description: https a una dirección pública; hoy solo PDF.
    Document:
      type: object
      required: [knowledgeId, name]
      properties:
        knowledgeId:
          type: string
        name:
          type: string
    CreatedWorkOrder:
      type: object
      required: [workOrderId, documents]
      properties:
        workOrderId:
          type: string
        documents:
          type: array
          items:
            $ref: '#/components/schemas/Document'
    Ping:
      type: object
      required: [type, id, occurredAt, organizationId]
      properties:
        type:
          const: ping
        id:
          type: string
        occurredAt:
          type: string
          format: date-time
        organizationId:
          type: string
