openapi: 3.1.0

info:
  title: Managora Services API
  version: "1"
  summary: B2B channel for preparing case files for nautical administrative procedures.
  description: |
    Your system sends the DATA for a procedure. We create the case file, validate it field by
    field and email your end customer a hosted link where they review it, sign the mandate and
    pay. You follow progress through this API or through webhooks.

    Filing with the public administration is carried out by a Managora professional. The API
    automates the preparation of the case file, not the filing.

    The signing link is NEVER returned to you: it goes by email to your customer, it is
    single-use and it expires.

    AUTHENTICATION. Header `Authorization: Bearer mgk_<environment>_<prefix>_<secret>`. A key
    starting with `mgk_sandbox_` belongs to the TEST environment; one starting with `mgk_live_`
    takes real money. The server and the paths are the same in both environments.

    TEST ENVIRONMENT. With an `mgk_sandbox_` key these are real: the case file, the validation,
    the EMAIL to your customer (it really is sent), the customer's screen, the mandate signature,
    the documents, the webhooks and the written submission. These are not: the charge, the
    invoice and the filing with the public administration. Test only with mailboxes of your own.

    RESPONSE HEADERS. Every response from the channel carries `X-Managora-Api-Version`. Every
    response that is actually served also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`
    and `X-RateLimit-Reset`.
  contact:
    name: Managora
    email: tramites@managora.net
    url: https://managora.net
  termsOfService: https://managora.net/terminos

servers:
  - url: https://managora.net
    description: |
      The only server for the channel. The environment (test or live) is decided by the key, not
      by the URL.

security:
  - bearerAuth: []

tags:
  - name: Identity
    description: Who your key belongs to and what it can do.
  - name: Catalogue
    description: What you can sell, and the field contract for each item.
  - name: Orders
    description: Create a case file, follow it, resend the link and cancel it.
  - name: Webhook endpoints
    description: Register, edit and test your endpoints, rotate their secret and reactivate them.
  - name: Deliveries
    description: History of the notifications we have sent you, and redelivery of a single one.

paths:

  /api/partner/v1/me:
    get:
      tags: [Identity]
      operationId: quienSoy
      summary: Identity of the key
      description: |
        The first call of any integration. It tells you whether the key is valid, which
        environment it belongs to, which permissions and quota it has and, above all, whether it
        takes real money.
      responses:
        "200":
          description: The key is valid.
          headers:
            X-Managora-Api-Version:
              $ref: "#/components/headers/ApiVersion"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                type: object
                required: [partner, clave, modelo_cobro, cuota, servicios_disponibles, cobra_de_verdad, aviso]
                properties:
                  partner:
                    type: object
                    properties:
                      id: { type: string }
                      nombre: { type: string }
                  clave:
                    type: object
                    properties:
                      entorno:
                        type: string
                        enum: [live, sandbox]
                      scopes:
                        type: array
                        items: { type: string }
                  modelo_cobro:
                    type: string
                    description: Always `cliente_final_paga` for now.
                  cuota:
                    type: object
                    properties:
                      peticiones_por_minuto: { type: integer, examples: [120] }
                      ventana_segundos: { type: integer, examples: [60] }
                  servicios_disponibles:
                    type: array
                    items: { type: string }
                  cobra_de_verdad:
                    type: boolean
                    description: "`false` with a test key."
                  aviso: { type: string }
              examples:
                pruebas:
                  summary: Test key
                  value:
                    partner: { id: "prt_8f2c", nombre: "Náutica Ejemplo, S.L." }
                    clave:
                      entorno: sandbox
                      scopes: ["catalogo:leer", "pedidos:crear", "pedidos:leer"]
                    modelo_cobro: cliente_final_paga
                    cuota: { peticiones_por_minuto: 120, ventana_segundos: 60 }
                    servicios_disponibles: ["inscripcion_buque_recreo", "titulo_nautico_per", "despacho_embarcacion_recreo"]
                    cobra_de_verdad: false
                    aviso: "ENTORNO DE PRUEBAS. Los pedidos que cree con esta clave recorren el trámite entero y mandan correos REALES a la dirección que usted declare, pero el pago es simulado: no se cobra nada y no presentamos nada ante la Administración."
        "401":
          $ref: "#/components/responses/NoAutenticado"
        "403":
          $ref: "#/components/responses/Prohibido"
        "429":
          $ref: "#/components/responses/CuotaExcedida"
        "500":
          $ref: "#/components/responses/ErrorInterno"
        "503":
          $ref: "#/components/responses/CanalNoConfigurado"

  /api/partner/v1/services:
    get:
      tags: [Catalogue]
      operationId: listarServicios
      summary: Procedures you can sell
      description: |
        With the price broken down and without the field contract. For the fields of a specific
        procedure, use `GET /api/partner/v1/services/{tramiteId}`.
      parameters:
        - name: category
          in: query
          required: false
          description: Commercial category. There is one for now, `nautica`.
          schema: { type: string, examples: ["nautica"] }
        - name: lang
          in: query
          required: false
          description: |
            Language of what you READ (title, description, labels and help text). The VALUES you
            have to send back are always in Spanish, including with `lang=en`.
          schema:
            type: string
            enum: [es, en]
            default: es
      responses:
        "200":
          description: The catalogue open to your account.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  categorias:
                    type: array
                    items: { type: string }
                  servicios:
                    type: array
                    items: { $ref: "#/components/schemas/Servicio" }
        "400":
          description: "`lang` not supported. Code `idioma_no_soportado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "No such category. Code `categoria_no_disponible`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/services/{tramiteId}:
    get:
      tags: [Catalogue]
      operationId: obtenerServicio
      summary: Field contract for one procedure
      description: |
        THIS IS THE LIVE LIST. It comes from the same definition that validates the form, so it
        overrides any example in this documentation. It gives you what to send in `datos`, with
        types, literal options, help text and visibility conditions; what your customer will be
        asked for after paying; and the cases in which the procedure does not apply.
      parameters:
        - name: tramiteId
          in: path
          required: true
          schema: { type: string, examples: ["titulo_nautico_per"] }
        - name: lang
          in: query
          required: false
          schema:
            type: string
            enum: [es, en]
            default: es
      responses:
        "200":
          description: The service with its fields.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  servicio: { $ref: "#/components/schemas/Servicio" }
        "400":
          description: "`lang` not supported. Code `idioma_no_soportado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "The procedure is not open to your account. Code `servicio_no_disponible`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/orders:
    post:
      tags: [Orders]
      operationId: crearPedido
      summary: Create an order
      description: |
        Creates the case file and sends your customer, BY EMAIL, the link where they review it,
        sign the mandate and pay. The link does not travel in this response: it is the access
        credential to one person's case file.

        `Idempotency-Key` is MANDATORY. A retry with the SAME body returns the same 201 again
        with `reintento: true`; with a different body it returns 409
        `idempotency_key_reutilizada`.

        Nothing is created until these checks have passed, in this order: idempotency, the
        channel allowlist, the sales guards, the values in `datos` against the field definition,
        the mandatory visible fields and the generator of the written submission. An order that
        reaches 201 is an order we know how to draft.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: Unique per order, between 8 and 200 characters.
          schema: { type: string, minLength: 8, maxLength: 200 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NuevoPedido" }
            examples:
              titulo_nautico_per:
                summary: PER licence, first issue
                description: |
                  Every mandatory field of the `titulo_nautico_per` definition. The values of the
                  `select` fields are literals taken from that definition. We set
                  `fecha_presentacion_prevista` ourselves, which is why it is not here.
                value:
                  tramite: titulo_nautico_per
                  referencia: "PED-2026-4417"
                  idioma: es
                  datos:
                    solicitante_nombre: Ana
                    solicitante_apellido1: Ruiz
                    solicitante_apellido2: Serrano
                    solicitante_dni_nie: "12345678Z"
                    solicitante_domicilio: "Calle del Puerto 14, 3.o B, 07015 Palma"
                    solicitante_email: ana.ruiz@example.com
                    solicitante_telefono: "+34600111222"
                    solicitante_fecha_nacimiento: "1988-04-12"
                    ccaa_o_dgmm_competente: "Illes Balears"
                    clase_tramite: expedicion_inicial
                    titulo_solicitado: per_patron_embarcaciones_recreo
                    ha_superado_examen_teorico: "Sí"
                    fecha_examen_teorico: "2026-06-21"
                    ha_realizado_practicas_basicas_seguridad_navegacion: "Sí"
                    ha_realizado_practicas_radiocomunicaciones: "Sí"
                    ha_certificado_aptitud_psicofisica: "Sí"
                    ha_pagado_tasa: "No"
      responses:
        "201":
          description: |
            Order created. This is also the response to a retry with the same `Idempotency-Key`
            and the same body, and in that case it carries `reintento: true`.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                required: [pedido, enlace_enviado, aviso]
                properties:
                  pedido: { $ref: "#/components/schemas/Pedido" }
                  precio:
                    oneOf:
                      - $ref: "#/components/schemas/Precio"
                      - type: "null"
                  enlace_enviado:
                    type: boolean
                    description: |
                      `true` if the email with the link has gone out. If it is `false`, the order
                      exists but your customer has no way in: retry with
                      `POST /api/partner/v1/orders/{id}/hosted-link`.
                  aviso_entrega:
                    type: string
                    description: Only when `enlace_enviado` is `false`.
                  sandbox:
                    type: boolean
                    description: Only on test orders.
                  aviso_sandbox:
                    type: string
                    description: Only on test orders.
                  reintento:
                    type: boolean
                    description: Only when the response comes from the idempotency path.
                  aviso: { type: string }
        "400":
          description: |
            Malformed body, or a procedure outside your catalogue. Codes
            `idempotency_key_requerida`, `json_invalido`, `servicio_fuera_de_su_catalogo`,
            `idioma_no_soportado`, `datos_requeridos`, `campos_desconocidos`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "Procedure not available. Code `servicio_no_disponible`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: |
            Codes `idempotency_key_reutilizada`, `servicio_a_medida`, `servicio_fusionado`,
            `cliente_no_elegible`, `referencia_duplicada`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "410":
          description: "Procedure withdrawn. Code `servicio_retirado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "413":
          description: "The body is over 256 KB. Code `cuerpo_demasiado_grande`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422":
          description: |
            The data is not acceptable. Codes `datos_invalidos`, `datos_incompletos`,
            `email_cliente_requerido`. `error.detalles` carries the `campo`, the `etiqueta`, the
            `motivo` and, where they exist, the accepted `opciones`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
              examples:
                valor_no_admitido:
                  value:
                    error:
                      codigo: datos_invalidos
                      mensaje: Hay campos con valores que no admitimos.
                      campo: datos
                      detalles:
                        - campo: titulo_solicitado
                          etiqueta: "Título"
                          motivo: '"PER" no es uno de los valores admitidos.'
                          opciones: [pnb_patron_navegacion_basica, per_patron_embarcaciones_recreo, py_patron_yate, cy_capitan_yate]
        "501":
          description: "Charging model not available. Code `modo_cobro_no_disponible`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

    get:
      tags: [Orders]
      operationId: listarPedidos
      summary: List your orders
      description: |
        Cursor-paginated, newest first. The `estado` filter is applied to the page once read, so
        a page can come back shorter and still carry a cursor.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 25, default: 25 }
        - name: cursor
          in: query
          description: The `siguiente_cursor` of the previous page.
          schema: { type: string }
        - name: referencia
          in: query
          description: Your own reference, exact match.
          schema: { type: string }
        - name: estado
          in: query
          schema: { $ref: "#/components/schemas/EstadoPedido" }
        - name: desde
          in: query
          description: Creation date, ISO 8601.
          schema: { type: string, examples: ["2026-09-01"] }
        - name: hasta
          in: query
          description: Creation date, ISO 8601.
          schema: { type: string, examples: ["2026-09-30T23:59:59Z"] }
      responses:
        "200":
          description: One page of orders.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  pedidos:
                    type: array
                    items: { $ref: "#/components/schemas/Pedido" }
                  siguiente_cursor:
                    type: [string, "null"]
                  aviso_filtro:
                    type: string
                    description: Only when `estado` has been used.
        "400":
          description: "Codes `estado_desconocido`, `fecha_invalida`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/orders/{id}:
    get:
      tags: [Orders]
      operationId: obtenerPedido
      summary: State of an order
      description: |
        Where it is, what is missing and whose move it is. No link: to resend it, use
        `/hosted-link`. An order that is not yours answers 404, not 403.
      parameters:
        - $ref: "#/components/parameters/PedidoId"
      responses:
        "200":
          description: The order.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  pedido: { $ref: "#/components/schemas/Pedido" }
        "404":
          description: "Code `pedido_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/orders/{id}/hosted-link:
    post:
      tags: [Orders]
      operationId: reenviarEnlace
      summary: Resend the link to your customer
      description: |
        The previous link is single-use and expires, so the customer who opened it and closed the
        tab, or the one who received it four days ago, needs another one. It ALWAYS goes by email
        to the address on the case file and is never returned in the response.

        There is no delivery parameter. Sending `entrega` in the body returns 400.
      parameters:
        - $ref: "#/components/parameters/PedidoId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                ttl_horas:
                  type: integer
                  minimum: 24
                  maximum: 168
                  default: 24
                  description: Hours the link will live for. 24 by default.
            examples:
              tres_dias:
                value: { ttl_horas: 72 }
              por_defecto:
                summary: Empty body
                value: {}
      responses:
        "200":
          description: The email has gone out.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  entregado:
                    type: string
                    examples: ["email"]
                  destinatario: { type: string, format: email }
                  caduca_en_horas: { type: integer }
                  caduca_en: { type: string, format: date-time }
                  proveedor: { type: string }
        "400":
          description: "Codes `entrega_no_configurable`, `ttl_invalido`, `ttl_fuera_de_rango`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "Code `pedido_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "The order no longer accepts a link. Code `pedido_terminado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "502":
          description: "The email provider rejected it. Code `no_se_pudo_entregar`. Retryable."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/orders/{id}/cancel:
    post:
      tags: [Orders]
      operationId: cancelarPedido
      summary: Cancel an order
      description: |
        For the order created by mistake, or the customer who backs out. Cancelling KILLS your
        customer's link and it cannot be reissued. It is idempotent: cancelling twice returns 200
        with `ya_estaba_cancelado: true`.

        What has already been paid or already been filed cannot be cancelled. Once money is
        involved the decision is a refund with its credit note, and that is agreed with us.
      parameters:
        - $ref: "#/components/parameters/PedidoId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                motivo:
                  type: string
                  maxLength: 300
            examples:
              con_motivo:
                value: { motivo: "The customer has backed out" }
      responses:
        "200":
          description: Cancelled, or already was.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  pedido: { $ref: "#/components/schemas/Pedido" }
                  ya_estaba_cancelado: { type: boolean }
                  aviso: { type: string }
        "404":
          description: "Code `pedido_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "Codes `pedido_ya_pagado`, `pedido_ya_presentado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/webhooks:
    get:
      tags: [Webhook endpoints]
      operationId: listarEndpoints
      summary: Your notification endpoints
      description: The signing secret does not travel here. If you lose it, rotate it.
      responses:
        "200":
          description: Your endpoints.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  endpoints:
                    type: array
                    items: { $ref: "#/components/schemas/Endpoint" }
                  eventos_disponibles:
                    type: array
                    items: { $ref: "#/components/schemas/NombreEvento" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

    post:
      tags: [Webhook endpoints]
      operationId: crearEndpoint
      summary: Register an endpoint
      description: |
        Returns the SECRET once and only once. Store it there and then: it is never shown again.

        The URL must be `https`, on a public host, with no username or password. An internal IP
        or a name such as `localhost` is rejected with 400.

        An empty or absent `eventos` means ALL of them.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
                eventos:
                  type: array
                  items: { $ref: "#/components/schemas/NombreEvento" }
            examples:
              todos_los_eventos:
                value:
                  url: https://api.ejemplo.com/managora/webhooks
              solo_dos:
                value:
                  url: https://api.ejemplo.com/managora/webhooks
                  eventos: [order.paid, order.submitted]
      responses:
        "201":
          description: Endpoint created. The `secreto` is only visible here.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  endpoint: { $ref: "#/components/schemas/Endpoint" }
                  secreto:
                    type: string
                    description: Starts with `whsec_`. Use it exactly as given, prefix included.
                    examples: ["whsec_3f9a1c7e4b2d8065a1f3c7e9b4d2086f5a1c3e7b"]
                  aviso: { type: string }
                  siguiente_paso: { type: string }
        "400":
          description: "Codes `url_no_valida`, `evento_desconocido`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "You already have an endpoint on that URL. Code `endpoint_duplicado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/webhooks/{id}:
    patch:
      tags: [Webhook endpoints]
      operationId: modificarEndpoint
      summary: Change the URL or the events
      description: Moving the URL does not change the signing secret.
      parameters:
        - $ref: "#/components/parameters/EndpointId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                eventos:
                  type: array
                  items: { $ref: "#/components/schemas/NombreEvento" }
            examples:
              mover_url:
                value: { url: "https://api.ejemplo.com/managora/webhooks/v2" }
              cambiar_eventos:
                value: { eventos: [order.paid, order.documents_complete, order.submitted, order.completed] }
      responses:
        "200":
          description: Endpoint updated.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  endpoint: { $ref: "#/components/schemas/Endpoint" }
                  aviso: { type: string }
        "400":
          description: "Codes `json_invalido`, `url_no_valida`, `evento_desconocido`, `nada_que_cambiar`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "Code `endpoint_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "Code `endpoint_duplicado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

    delete:
      tags: [Webhook endpoints]
      operationId: bajaEndpoint
      summary: Remove an endpoint
      description: |
        It is switched off and whatever was queued for it is cancelled. The delivery history is
        kept and remains available through `GET /api/partner/v1/events`.
      parameters:
        - $ref: "#/components/parameters/EndpointId"
      responses:
        "200":
          description: Removed.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  baja: { type: boolean }
                  id: { type: string }
                  url: { type: string }
        "404":
          description: "Code `endpoint_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/webhooks/{id}/test:
    post:
      tags: [Webhook endpoints]
      operationId: probarEndpoint
      summary: Send a signed ping
      description: |
        Sends a signed `ping` notification to your endpoint RIGHT NOW and returns what your server
        answered, plus the exact body we signed. Compare that body, byte for byte, with the one
        you received: the number one mistake when validating an HMAC is signing the re-serialised
        JSON instead of the raw payload.

        A switched-off endpoint can be tested. It does not consume the retry schedule.
      parameters:
        - $ref: "#/components/parameters/EndpointId"
      responses:
        "200":
          description: The ping was attempted. Look at `entregado`.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  entregado: { type: boolean }
                  url: { type: string }
                  endpoint_activo: { type: boolean }
                  respuesta_de_su_servidor:
                    type: object
                    properties:
                      status: { type: [integer, "null"] }
                      error: { type: [string, "null"] }
                  cuerpo_firmado:
                    type: string
                    description: The exact JSON the signature was computed over.
                  ayuda: { type: string }
                  aviso: { type: string }
        "404":
          description: "Code `endpoint_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/webhooks/{id}/rotate:
    post:
      tags: [Webhook endpoints]
      operationId: rotarSecreto
      summary: Rotate the signing secret
      description: |
        Changes the secret and returns the new one, once and only once. During the grace window
        every notification carries TWO `v1` signatures in the same header and validating either
        one is enough, so you can deploy the change whenever it suits you.
      parameters:
        - $ref: "#/components/parameters/EndpointId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                horas_gracia:
                  type: integer
                  minimum: 1
                  maximum: 168
                  default: 48
            examples:
              dos_dias:
                value: { horas_gracia: 48 }
      responses:
        "200":
          description: Secret rotated.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  secreto: { type: string }
                  valido_el_anterior_hasta: { type: string, format: date-time }
                  aviso: { type: string }
        "400":
          description: "Code `gracia_fuera_de_rango`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "Code `endpoint_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/webhooks/{id}/reactivate:
    post:
      tags: [Webhook endpoints]
      operationId: reactivarEndpoint
      summary: Reactivate a switched-off endpoint
      description: |
        An endpoint that fails 20 times in a row switches itself off and we email your technical
        contact. This route turns it back on and puts back in the queue whatever was left
        undelivered.

        Before turning it on we send a ping. If your server still does not answer 2xx, the
        response is 409 and the endpoint stays off.
      parameters:
        - $ref: "#/components/parameters/EndpointId"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reencolar:
                  type: boolean
                  default: true
                  description: "`false` to receive only the NEW notifications."
            examples:
              con_reencolado:
                value: { reencolar: true }
      responses:
        "200":
          description: Endpoint reactivated.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  activo: { type: boolean }
                  url: { type: string }
                  avisos_reencolados: { type: integer }
                  aviso: { type: string }
        "404":
          description: "Code `endpoint_no_encontrado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "Your server still does not answer. Code `endpoint_sigue_sin_responder`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

  /api/partner/v1/events:
    get:
      tags: [Deliveries]
      operationId: listarAvisos
      summary: Notification history
      description: |
        Everything we have sent you or tried to send you, with the EXACT body we signed and the
        result of each delivery. Use it to reconcile, to recover what ran out of retries and to
        see what happened before you registered your endpoint.
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 50 }
        - name: cursor
          in: query
          schema: { type: string }
        - name: evento
          in: query
          description: One of the five events, or `ping`.
          schema: { type: string }
        - name: estado
          in: query
          schema:
            type: string
            enum: [pendiente, entregando, entregado, fallido, cancelado]
        - name: pedido
          in: query
          description: Identifier of one of your orders.
          schema: { type: string }
        - name: desde
          in: query
          schema: { type: string }
        - name: hasta
          in: query
          schema: { type: string }
      responses:
        "200":
          description: One page of notifications.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  eventos:
                    type: array
                    items: { $ref: "#/components/schemas/Entrega" }
                  siguiente_cursor:
                    type: [string, "null"]
        "400":
          description: "Codes `evento_desconocido`, `estado_desconocido`, `fecha_invalida`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

    post:
      tags: [Deliveries]
      operationId: reencolarAviso
      summary: Queue a notification again
      description: |
        Puts one specific notification back in the queue. It goes out through the same path as
        every other one, with its retry schedule, not immediately.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entrega_id]
              properties:
                entrega_id:
                  type: string
                  description: The `entrega_id` from `GET /api/partner/v1/events`, which is the same value that travelled in the `Managora-Entrega-Id` header.
            examples:
              reencolar:
                value: { entrega_id: "whd_9c1f4a7b2e6d" }
      responses:
        "200":
          description: Queued again.
          headers:
            X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                properties:
                  reencolado: { type: boolean }
                  entrega_id: { type: string }
                  aviso: { type: string }
        "400":
          description: "Code `entrega_id_requerida`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "404":
          description: "Code `entrega_no_encontrada`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: "Codes `entrega_ya_en_cola`, `endpoint_desactivado`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "403": { $ref: "#/components/responses/Prohibido" }
        "429": { $ref: "#/components/responses/CuotaExcedida" }
        "500": { $ref: "#/components/responses/ErrorInterno" }

webhooks:

  ping:
    post:
      summary: Test notification
      description: Fired by `POST /api/partner/v1/webhooks/{id}/test` and also by the check that precedes a reactivation.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventoWebhook" }
            examples:
              ping:
                value:
                  evento: ping
                  pedido_id: null
                  referencia: null
                  ocurrido_en: "2026-09-15T09:12:44.118Z"
                  datos:
                    mensaje: "Aviso de prueba de Managora. Si valida esta firma, su integración está lista."
      responses:
        "200":
          description: Answer 2xx within 10 seconds.

  order.signed:
    post:
      summary: The end customer signed the mandate
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventoWebhook" }
            examples:
              firmado:
                value:
                  evento: order.signed
                  pedido_id: "ord_7b3e9a1c4d25"
                  referencia: "PED-2026-4417"
                  ocurrido_en: "2026-09-15T10:04:11.902Z"
                  datos:
                    firmado_en: "2026-09-15T10:04:09.511Z"
      responses:
        "200":
          description: Answer 2xx within 10 seconds.

  order.paid:
    post:
      summary: The payment came in
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventoWebhook" }
            examples:
              pagado:
                value:
                  evento: order.paid
                  pedido_id: "ord_7b3e9a1c4d25"
                  referencia: "PED-2026-4417"
                  ocurrido_en: "2026-09-15T10:07:33.240Z"
                  datos:
                    pagado_en: "2026-09-15T10:07:31.088Z"
                    importe_cents: 14600
      responses:
        "200":
          description: Answer 2xx within 10 seconds.

  order.documents_complete:
    post:
      summary: The customer uploaded the last mandatory document
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventoWebhook" }
            examples:
              documentacion_completa:
                value:
                  evento: order.documents_complete
                  pedido_id: "ord_7b3e9a1c4d25"
                  referencia: "PED-2026-4417"
                  ocurrido_en: "2026-09-16T08:41:02.775Z"
                  datos: {}
      responses:
        "200":
          description: Answer 2xx within 10 seconds.

  order.submitted:
    post:
      summary: A Managora professional filed it
      description: It carries the REAL reference the authority returned. Never an invented number.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventoWebhook" }
            examples:
              presentado:
                value:
                  evento: order.submitted
                  pedido_id: "ord_7b3e9a1c4d25"
                  referencia: "PED-2026-4417"
                  ocurrido_en: "2026-09-18T12:20:55.401Z"
                  datos:
                    presentado_en: "2026-09-18T12:19:40.000Z"
                    referencia_oficial: "REG-2026-0098431"
      responses:
        "200":
          description: Answer 2xx within 10 seconds.

  order.completed:
    post:
      summary: The public administration resolved it and the case file was closed
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EventoWebhook" }
            examples:
              finalizado:
                value:
                  evento: order.completed
                  pedido_id: "ord_7b3e9a1c4d25"
                  referencia: "PED-2026-4417"
                  ocurrido_en: "2026-10-09T16:02:18.663Z"
                  datos:
                    finalizado_en: "2026-10-09T16:02:17.220Z"
                    referencia_oficial: "REG-2026-0098431"
      responses:
        "200":
          description: Answer 2xx within 10 seconds.

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: mgk
      description: |
        `Authorization: Bearer mgk_<environment>_<prefix>_<secret>`.

        `<environment>` is `live` or `sandbox`. The `<prefix>` is 8 hexadecimal characters and is
        not secret: it is the part you can show in a log. The `<secret>` is handed over once and
        only once.

        The key is checked against the database on every request, so a revocation takes effect on
        the very next call.

  parameters:
    PedidoId:
      name: id
      in: path
      required: true
      description: Identifier of the order, the `pedido.id` from the 201.
      schema: { type: string }
    EndpointId:
      name: id
      in: path
      required: true
      description: Identifier of the notification endpoint.
      schema: { type: string }

  headers:
    ApiVersion:
      description: Version of the contract. `1` for now.
      schema: { type: string, examples: ["1"] }
    RateLimitLimit:
      description: Requests allowed per minute and per key.
      schema: { type: integer, examples: [120] }
    RateLimitRemaining:
      description: How many you have left in the window.
      schema: { type: integer, examples: [117] }
    RateLimitReset:
      description: |
        Epoch in seconds at which, at the latest, there is room again. The window is a sliding
        60 seconds.
      schema: { type: integer, examples: [1789050180] }
    RetryAfter:
      description: Seconds to wait before retrying.
      schema: { type: integer, examples: [60] }
    Allow:
      description: Methods that route does serve.
      schema: { type: string, examples: ["GET, POST"] }

  responses:
    NoAutenticado:
      description: |
        The credential is missing or not valid. Codes `sin_credencial`, `formato_invalido`,
        `clave_desconocida`, `clave_revocada`, `clave_caducada`.
      headers:
        X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            sin_credencial:
              value:
                error:
                  codigo: sin_credencial
                  mensaje: "Falta la cabecera Authorization: Bearer <clave>."
    Prohibido:
      description: |
        Authenticated, but without permission. Codes `scope_insuficiente` and
        `partner_suspendido`.
      headers:
        X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    CuotaExcedida:
      description: More than 120 requests in the last minute with that key.
      headers:
        X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
        Retry-After: { $ref: "#/components/headers/RetryAfter" }
        X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
        X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
        X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            cuota:
              value:
                error:
                  codigo: cuota_excedida
                  mensaje: "Máximo 120 peticiones por minuto y clave. Reintente en un minuto."
    ErrorInterno:
      description: Something failed on our side. Code `error_interno`. Retryable.
      headers:
        X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    CanalNoConfigurado:
      description: The channel is not available right now. Code `canal_no_configurado`.
      headers:
        X-Managora-Api-Version: { $ref: "#/components/headers/ApiVersion" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:

    Error:
      type: object
      description: |
        The error envelope for the whole channel, including route 404s and method 405s. Branch on
        `error.codigo`, which is stable, and never on the text of `error.mensaje`.
      required: [error]
      properties:
        error:
          type: object
          required: [codigo, mensaje]
          properties:
            codigo:
              type: string
              description: Stable code.
              examples: ["datos_invalidos"]
            mensaje:
              type: string
              description: |
                Plain-language explanation, written for a human to read. **It always comes back
                in Spanish**, whatever `lang` or `idioma` you used: this documentation is
                translated, the API's messages are not. Branch on `codigo`, which is the stable
                part of the contract, and use this text only to show it to someone or to log it.
                The same applies to `etiqueta` and `motivo` inside `detalles`: they come from the
                Spanish service definition.
            campo:
              type: string
              description: The field or parameter that caused it, where that applies.
            detalles:
              description: |
                A structure that depends on the code. In the 422s about data it is a list of
                `{ campo, etiqueta, motivo, opciones }`.
      examples:
        - error:
            codigo: campos_desconocidos
            mensaje: 'Estos campos no existen en "titulo_nautico_per": patron. Consulte GET /services/titulo_nautico_per para el contrato vivo.'
            campo: datos
            detalles: [patron]

    Precio:
      type: object
      description: |
        Amounts in CENTS of a euro. Official fees are disbursements: they carry no VAT and fall
        outside the taxable base.
      required: [honorario_neto_cents, iva_pct, honorario_con_iva_cents, tasa_oficial_cents, total_cents, desde, tasa_variable, nota_tasa]
      properties:
        honorario_neto_cents:
          type: integer
          description: Our fee, excluding VAT.
          examples: [8182]
        iva_pct:
          type: integer
          examples: [21]
        honorario_con_iva_cents:
          type: integer
          examples: [9900]
        tasa_oficial_cents:
          type: [integer, "null"]
          description: Official fee we pay upfront. `null` when it is worked out case by case.
          examples: [4668]
        total_cents:
          type: integer
          description: |
            WHAT THE PAYMENT GATEWAY CHARGES on your customer's link. Never `null`. When
            `tasa_variable` is `true`, this carries only the fee including VAT, and the official
            fee is communicated in writing and charged separately.
          examples: [14600]
        desde:
          type: boolean
          description: |
            `true` if our fee depends on what the customer answers, in which case the amount is a
            minimum. On the price of an order already created it is always `false`.
        tasa_variable:
          type: boolean
          description: "`true` if the official fee is worked out case by case and charged separately."
        nota_tasa:
          type: [string, "null"]
          description: How the official fee works for that procedure, in plain text.

    Campo:
      type: object
      description: |
        One field of the `datos` contract. The `opciones` are the LITERAL values you have to
        send, including when you request the catalogue with `lang=en`.
      required: [id, etiqueta, tipo, obligatorio]
      properties:
        id:
          type: string
          description: The key to use inside `datos`.
          examples: ["titulo_solicitado"]
        etiqueta:
          type: string
          description: What to call it in your own interface.
        tipo:
          type: string
          description: "`text`, `email`, `phone`, `date`, `number`, `select`, `multi_select`, `text_area`, `dni`, `cif`, `lista`."
          examples: ["select"]
        obligatorio:
          type: boolean
        ayuda:
          type: string
        opciones:
          type: array
          items: { type: string }
          description: Accepted values, literal and in Spanish.
        opciones_etiquetas:
          type: object
          additionalProperties: { type: string }
          description: Only with `lang=en`. How each option reads. Never sent back.
        subida:
          type: boolean
          description: |
            The value of this field is a file. It is NOT sent through the channel in `datos`:
            your customer uploads it from their screen after paying.
        visible_si:
          description: |
            Visibility condition. The field is only asked for, and only required, if it is met.
            A list of rules of the form `{ "field": "...", "equals": ["..."] }`.
        lo_fijamos_nosotros:
          type: boolean
          description: |
            We fill this in ourselves if it does not arrive. Do not ask your customer for it.

    DocumentoPostPago:
      type: object
      description: What your end customer will be asked for AFTER paying, from their screen.
      required: [campo, etiqueta, opcional]
      properties:
        campo: { type: string }
        etiqueta: { type: string }
        opcional: { type: boolean }
        visible_si:
          description: Only asked for if the condition is met.

    NoElegible:
      type: object
      description: |
        When the procedure does NOT apply. Published so that you can filter before selling
        instead of running into a 409 `cliente_no_elegible`.
      required: [cuando, motivo, alternativa]
      properties:
        cuando:
          description: Same shape of condition as `visible_si`.
        motivo: { type: string }
        alternativa:
          type: [string, "null"]
          description: The procedure that does apply, where there is one.

    Servicio:
      type: object
      required: [id, titulo, descripcion, organismo, plazo, precio, campos, documentos_post_pago, no_elegible_si, presentacion]
      properties:
        id:
          type: string
          examples: ["titulo_nautico_per"]
        titulo: { type: string }
        descripcion: { type: string }
        organismo:
          type: [string, "null"]
          description: The authority it is filed with.
        plazo:
          type: [string, "null"]
        precio: { $ref: "#/components/schemas/Precio" }
        campos:
          type: array
          description: Empty in the listing; filled in `GET /services/{tramiteId}`.
          items: { $ref: "#/components/schemas/Campo" }
        documentos_post_pago:
          type: array
          items: { $ref: "#/components/schemas/DocumentoPostPago" }
        no_elegible_si:
          type: array
          items: { $ref: "#/components/schemas/NoElegible" }
        presentacion:
          type: string
          const: presentacion_por_gestor
          description: |
            Filing with the public administration is carried out by a Managora professional. The
            API automates the preparation of the case file, not the filing.

    EstadoPedido:
      type: string
      description: |
        The channel's own stable vocabulary.

        - `pendiente_cliente`: your customer has not done anything yet.
        - `firmado_pendiente_pago`: they have signed the mandate and payment is outstanding.
        - `pagado_pendiente_firma`: they have paid and the mandate signature is outstanding.
        - `pendiente_documentos`: a mandatory document is missing.
        - `listo_para_presentar`: complete and in our queue.
        - `presentado`: filed with the public administration, with an official reference.
        - `finalizado`: resolved and closed.
        - `cancelado`: cancelled.
        - `en_curso`: in progress, with nothing outstanding on your side.
      enum:
        - pendiente_cliente
        - firmado_pendiente_pago
        - pagado_pendiente_firma
        - pendiente_documentos
        - listo_para_presentar
        - presentado
        - finalizado
        - cancelado
        - en_curso

    SiguientePaso:
      type: string
      description: |
        Whose move it is, and what for.

        - `abrir_enlace`, `firmar_mandato`, `pagar`, `subir_documentos`: it is your customer's
          turn.
        - `presentacion_por_gestor`: it is our turn. Here `sla_habiles` carries the deadline.
        - `ninguno`: there is nothing outstanding.
      enum:
        - abrir_enlace
        - firmar_mandato
        - pagar
        - subir_documentos
        - presentacion_por_gestor
        - ninguno

    Pedido:
      type: object
      required:
        - id
        - referencia
        - tramite
        - estado
        - siguiente_paso
        - sla_habiles
        - sandbox
        - modo_pago
        - cliente_email
        - importe_cobrado_cents
        - firmado_en
        - pagado_en
        - presentado_en
        - referencia_oficial
        - finalizado_en
        - documentos_pendientes
        - creado_en
        - enlace_enviado_en
        - enlace_caduca_en
        - enlace_abierto_en
        - precio
        - cancelado_en
        - motivo_cancelacion
        - nota_presentacion
      properties:
        id:
          type: string
          description: Our identifier for the order.
        referencia:
          type: [string, "null"]
          description: Yours, exactly as you sent it when creating the order.
        tramite:
          type: string
          examples: ["titulo_nautico_per"]
        estado: { $ref: "#/components/schemas/EstadoPedido" }
        siguiente_paso: { $ref: "#/components/schemas/SiguientePaso" }
        sla_habiles:
          type: [integer, "null"]
          description: Working days we take to file. Only when the next move is ours.
          examples: [3]
        sandbox:
          type: boolean
          description: Order created with a test key.
        modo_pago:
          type: string
          examples: ["cliente_final_paga"]
        cliente_email:
          type: string
          format: email
          description: Where the link went, and where the invoice and the documents will go.
        importe_cobrado_cents:
          type: [integer, "null"]
          description: What was actually charged. `null` until there is a payment.
        firmado_en:
          type: [string, "null"]
          format: date-time
        pagado_en:
          type: [string, "null"]
          format: date-time
        presentado_en:
          type: [string, "null"]
          format: date-time
        referencia_oficial:
          type: [string, "null"]
          description: The REAL number the authority returned. Never an invented one.
        finalizado_en:
          type: [string, "null"]
          format: date-time
        documentos_pendientes:
          type: array
          description: Ids of the mandatory documents your customer has not uploaded yet.
          items: { type: string }
        creado_en:
          type: string
          format: date-time
        enlace_enviado_en:
          type: [string, "null"]
          format: date-time
          description: When the email with the link went out. `null` if it never did.
        enlace_caduca_en:
          type: [string, "null"]
          format: date-time
        enlace_abierto_en:
          type: [string, "null"]
          format: date-time
          description: When your customer redeemed it. It is burnt on opening.
        precio:
          oneOf:
            - $ref: "#/components/schemas/Precio"
            - type: "null"
          description: The price of THIS order, with the answers already in hand.
        cancelado_en:
          type: [string, "null"]
          format: date-time
        motivo_cancelacion:
          type: [string, "null"]
        nota_presentacion:
          type: string
          description: It travels in every response, not only in the documentation.
          const: "La presentación ante la Administración la realiza un profesional de Managora. La API automatiza la preparación del expediente, no la presentación."

    NuevoPedido:
      type: object
      required: [tramite, datos]
      properties:
        tramite:
          type: string
          description: One of the ids from `GET /api/partner/v1/services`.
          examples: ["titulo_nautico_per"]
        referencia:
          type: string
          maxLength: 120
          description: |
            Your own reference. Unique within your account: repeating it returns 409
            `referencia_duplicada`. It travels back in every webhook.
        idioma:
          type: string
          enum: [es, en]
          default: es
          description: |
            The language in which your customer will read the email, the screen and their
            invoice. It is frozen when the order is created.
        datos:
          type: object
          additionalProperties: true
          description: |
            The customer's answers, under the EXACT keys from
            `GET /api/partner/v1/services/{tramiteId}`. A key that does not exist in the field
            definition returns 400 `campos_desconocidos`: it is never dropped silently.

            Fields marked `subida: true` do NOT go here. Neither do fields marked
            `lo_fijamos_nosotros: true`: we fill those in ourselves.

            For `select` fields we accept the literal option, the same option without accents or
            capitals, or its lower-case form with underscores. For a `Sí`/`No` field we also
            accept `true` and `false`. Anything else is a 422, with the list of valid values in
            `error.detalles`.

    Endpoint:
      type: object
      required: [id, url, eventos, activo]
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        eventos:
          type: array
          items: { $ref: "#/components/schemas/NombreEvento" }
          description: All five if it was registered without a list.
        activo:
          type: boolean
          description: An endpoint that fails 20 times in a row switches itself off.
        fallos_seguidos: { type: integer }
        desactivado_en:
          type: [string, "null"]
          format: date-time
        rotacion_hasta:
          type: [string, "null"]
          format: date-time
          description: How long the previous secret remains valid for.
        creado_en:
          type: string
          format: date-time

    NombreEvento:
      type: string
      enum:
        - order.signed
        - order.paid
        - order.documents_complete
        - order.submitted
        - order.completed

    EventoWebhook:
      type: object
      description: |
        The body of every notification. Always these five keys, always in this order. `datos`
        changes with the event.

        Delivery is AT-LEAST-ONCE. Deduplicate on the `Managora-Entrega-Id` header.
      required: [evento, pedido_id, referencia, ocurrido_en, datos]
      properties:
        evento:
          type: string
          description: One of the five, or `ping`.
          examples: ["order.paid"]
        pedido_id:
          type: [string, "null"]
          description: "`null` only on the `ping`."
        referencia:
          type: [string, "null"]
          description: Yours.
        ocurrido_en:
          type: string
          format: date-time
          description: |
            When the event HAPPENED, not when it is delivered. With retries running into hours,
            the difference matters for reconciliation.
        datos:
          type: object
          description: |
            - `order.signed`: `firmado_en`.
            - `order.paid`: `pagado_en`, `importe_cents`.
            - `order.documents_complete`: empty.
            - `order.submitted`: `presentado_en`, `referencia_oficial`.
            - `order.completed`: `finalizado_en`, `referencia_oficial`.
            - `ping`: `mensaje`.

    Entrega:
      type: object
      description: One delivery attempt of a notification, with its result.
      required: [entrega_id, evento, pedido_id, endpoint, estado, intentos, cuerpo]
      properties:
        entrega_id:
          type: string
          description: The same value that travelled in the `Managora-Entrega-Id` header.
        evento: { type: string }
        pedido_id:
          type: [string, "null"]
        endpoint:
          type: object
          properties:
            id: { type: string }
            url: { type: string }
        estado:
          type: string
          enum: [pendiente, entregando, entregado, fallido, cancelado]
        intentos: { type: integer }
        ultimo_status:
          type: [integer, "null"]
        ultimo_error:
          type: [string, "null"]
        entregado_en:
          type: [string, "null"]
          format: date-time
        proximo_intento:
          type: [string, "null"]
          format: date-time
        creado_en:
          type: string
          format: date-time
        cuerpo:
          type: string
          description: |
            The EXACT body we signed, as a STRING and not as an object. Re-serialising it would
            give a different signature.
