> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Crea una suscripción

> Crea una suscripción de entrega para el tenant autenticado. El tenant se toma únicamente de los claims validados del JWT — un `tenant_id` en el cuerpo se ignora.

Una suscripción `webhook` nace `active` y genera un secreto de firma que se devuelve exactamente una vez en esta respuesta (ver `signingSecret`). Una suscripción de cola (`sqs`, `rabbitmq`, `eventbridge`) nace `pending_verification` y no genera ningún secreto; permanece no entregable hasta que se suministra una credencial de salida y se prueba vía `PUT /v1/subscriptions/{id}/credential`. Un `sink_config` o `credential` en línea al crear se rechaza — las credenciales de cola llegan solo en el PUT de credential.



## OpenAPI

````yaml es/openapi/v3-current/streaming-hub.yaml post /v1/subscriptions
openapi: 3.1.0
info:
  title: Lerian Streaming Hub API
  version: v1.0.0
  contact:
    email: contact@lerian.studio
    name: Lerian Studio
    url: https://lerian.studio
  license:
    name: Lerian Studio General License
  description: >-
    La API de control-plane de Streaming Hub. Streaming Hub es el borde
    gestionado de entrega de eventos de Lerian: consume CloudEvents del backbone
    de streaming interno de la plataforma y los distribuye a los destinos
    externos propios de cada tenant — webhooks, Amazon SQS, RabbitMQ, Amazon
    EventBridge o una bandeja de entrada de tipo pull. Esta API permite a un
    tenant explorar el catálogo de eventos alimentado por el manifest, crear y
    gestionar suscripciones de entrega, verificar y rotar sus credenciales, leer
    la salud de entrega y hacer pull de los eventos a los que tiene derecho.


    Los errores usan un envelope plano `{"error":"<token>"}` (un token de baja
    cardinalidad y legible por máquina — nunca RFC 9457 problem+json). Las
    operaciones de mutación requieren un header `X-Idempotency` para semántica
    at-most-once; una petición reproducida (replay) devuelve la respuesta
    original byte a byte con `X-Idempotency-Replayed: true`. El catálogo y la
    superficie de eventos pull están acotados por tenant a través del JWT
    bearer; los endpoints operacionales de sonda (`/healthz`, `/readyz`,
    `/version`, `/runtime`, `/metrics`) no requieren autenticación. Streaming
    Hub es de código cerrado bajo la Lerian Studio General License.
servers:
  - url: https://streaming-hub.sandbox.lerian.net
security:
  - BearerAuth: []
tags:
  - name: Catalog
    description: >-
      Explora el catálogo de tipos de evento disponibles para suscripción,
      alimentado por el manifest.
  - name: Subscriptions
    description: >-
      Crea, lee, actualiza y elimina suscripciones de entrega, y gestiona el
      ciclo de vida de verificación del destino (ping, verify, credential,
      delegated grant, rotación de secreto, health).
  - name: Event Delivery
    description: >-
      Haz pull de los eventos a los que tienes derecho para una suscripción de
      tipo pull (lectura cursor-as-acknowledgment).
  - name: Admin
    description: >-
      Análisis forense de operador entre tenants. Requiere un scope de
      autorización de operador.
  - name: Operational
    description: >-
      Sondas de liveness, readiness, build, runtime y métricas sin
      autenticación.
paths:
  /v1/subscriptions:
    post:
      tags:
        - Subscriptions
      summary: Crea una suscripción
      description: >-
        Crea una suscripción de entrega para el tenant autenticado. El tenant se
        toma únicamente de los claims validados del JWT — un `tenant_id` en el
        cuerpo se ignora.


        Una suscripción `webhook` nace `active` y genera un secreto de firma que
        se devuelve exactamente una vez en esta respuesta (ver `signingSecret`).
        Una suscripción de cola (`sqs`, `rabbitmq`, `eventbridge`) nace
        `pending_verification` y no genera ningún secreto; permanece no
        entregable hasta que se suministra una credencial de salida y se prueba
        vía `PUT /v1/subscriptions/{id}/credential`. Un `sink_config` o
        `credential` en línea al crear se rechaza — las credenciales de cola
        llegan solo en el PUT de credential.
      operationId: createSubscription
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
      responses:
        '201':
          description: >-
            La suscripción se creó. `signingSecret` está presente solo para un
            sink `webhook` y se muestra exactamente una vez — guárdalo al
            recibirlo, pues no puede recuperarse después, solo rotarse.
          headers:
            X-Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSubscriptionResponse'
        '400':
          $ref: '#/components/responses/BadRequestOrMissingIdempotency'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '422':
          description: >-
            La petición es corregible por el llamador. `error` es uno de
            `validation_error` (`sink_kind` inválido, endpoint / schema /
            event_types inválidos), `inline_sink_config_forbidden` (se envió un
            `sink_config` / `credential` en línea) o `endpoint_blocked` (el
            endpoint del webhook resolvió a una dirección bloqueada, privada o
            de metadatos).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - BearerAuth: []
components:
  parameters:
    IdempotencyKey:
      name: X-Idempotency
      in: header
      required: true
      description: >-
        A client-chosen unique key that makes this mutation at-most-once. A
        mutation sent without it is rejected before any write with `400
        missing_idempotency_key`. Reusing the same key with an identical request
        replays the original response byte-for-byte (with
        `X-Idempotency-Replayed: true`); reusing it with a different request
        body returns `409 idempotency_conflict`. To re-drive a corrected
        request, mint a new key.
      schema:
        type: string
  schemas:
    CreateSubscriptionRequest:
      type: object
      description: >-
        The create body. It carries no `tenant_id` (the tenant is resolved from
        the JWT). Inline `sink_config` / `credential` fields are rejected with
        `422 inline_sink_config_forbidden` — queue credentials arrive only via
        the credential PUT.
      properties:
        name:
          type: string
          description: >-
            A human label for the subscription. Required and non-blank
            (whitespace-only is rejected).
          examples:
            - orders-webhook
        sink_kind:
          $ref: '#/components/schemas/SinkKind'
        endpoint:
          type: string
          description: >-
            The destination address, interpreted per sink kind. For `webhook`,
            an `https://` URL with no embedded userinfo. For `pull`, omit it —
            the server synthesizes a `pull://<id>` value. For `sqs`, the
            `https://` queue URL. For `rabbitmq`, an `"<exchange>/<routingKey>"`
            string (exchange required, routing key optional). For `eventbridge`,
            the event-bus name / detail-type addressing string. The broker host
            or AWS region for queue kinds lives in the encrypted credential, not
            here.
          examples:
            - https://hooks.example.com/lerian
        event_types:
          type: array
          description: >-
            The event types to deliver. Validated softly against the catalog —
            an unknown type warns but never blocks onboarding.
          items:
            type: string
          examples:
            - - transaction.created
        schema_major:
          type: integer
          description: >-
            Optional schema-major pin. When set, delivery follows the versioned
            topic for that major; when omitted, the subscription follows the
            base topic.
          examples:
            - 1
        plan_tier:
          type: string
          description: The plan tier for the subscription.
          examples:
            - standard
      required:
        - name
        - sink_kind
    CreateSubscriptionResponse:
      type: object
      additionalProperties: false
      description: >-
        The create response. `signingSecret` is present only for a `webhook`
        sink and is shown exactly once; it is stored only as ciphertext and is
        never returned by any read path.
      properties:
        subscription:
          $ref: '#/components/schemas/Subscription'
        signingSecret:
          type: string
          description: >-
            The one-time webhook signing secret (write-only). Minted
            server-side, returned exactly once here, and never retrievable later
            — only rotated. Absent for non-webhook sinks.
          examples:
            - whsec_9f8c2b1e4a7d6055c3e2f10987ab4c21
      required:
        - subscription
    Error:
      type: object
      additionalProperties: false
      description: >-
        The flat error envelope used across the `/v1` and `/admin` surfaces. It
        carries a single low-cardinality, machine-readable token and never leaks
        secret material or internal detail. (A `403` from the authorization
        decision point is the one exception — its body is plain text.)
      properties:
        error:
          type: string
          description: The machine-readable error token.
          examples:
            - not_found
      required:
        - error
    SinkKind:
      type: string
      description: >-
        The delivery destination kind. `webhook` posts signed HTTPS requests;
        `pull` exposes an inbox read over `GET /v1/events`; `sqs`, `rabbitmq`,
        and `eventbridge` fan out to the named queue or bus.
      enum:
        - webhook
        - pull
        - sqs
        - rabbitmq
        - eventbridge
    Subscription:
      type: object
      additionalProperties: false
      description: >-
        The non-secret projection of a subscription. It never carries the
        signing secret or any credential material.
      properties:
        id:
          type: string
          format: uuid
          description: The unique identifier of the subscription (UUIDv7).
          examples:
            - 0192f1a0-0000-7000-8000-00000000c001
        name:
          type: string
          description: The human label supplied at create.
          examples:
            - orders-webhook
        sink_kind:
          $ref: '#/components/schemas/SinkKind'
        endpoint:
          type: string
          description: The destination address (per sink kind).
          examples:
            - https://hooks.example.com/ingest
        event_types:
          type: array
          description: >-
            The event types the subscription delivers. Omitted when none are
            pinned.
          items:
            type: string
          examples:
            - - account.created
              - account.updated
        schema_major:
          type: integer
          description: >-
            The pinned schema major, when set. Omitted when the subscription
            follows the base topic.
          examples:
            - 2
        signature_version:
          type: integer
          description: The webhook signature scheme version.
          examples:
            - 1
        plan_tier:
          type: string
          description: The subscription's plan tier.
          examples:
            - standard
        enabled:
          type: boolean
          description: >-
            The operator on/off flag. Independent of `verification_state`; both
            must hold for delivery.
          examples:
            - true
        verification_state:
          $ref: '#/components/schemas/VerificationState'
        created_at:
          type: string
          format: date-time
          description: Creation timestamp (UTC, RFC 3339).
          examples:
            - '2026-01-15T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last-update timestamp (UTC, RFC 3339).
          examples:
            - '2026-01-15T12:00:00Z'
      required:
        - id
        - name
        - sink_kind
        - endpoint
        - signature_version
        - plan_tier
        - enabled
        - verification_state
        - created_at
        - updated_at
    VerificationState:
      type: string
      description: >-
        The subscription's position in the destination verification state
        machine. Only an `active` subscription is deliverable. A `webhook` sub
        is born `active`; a queue sub is born `pending_verification` and reaches
        `active` on a successful credential probe. `degraded` and `disabled`
        signal an impaired or auto-disabled destination that a successful
        `verify` returns to `active`.
      enum:
        - pending_verification
        - active
        - degraded
        - disabled
  headers:
    IdempotencyReplayed:
      description: >-
        Presente y con valor `true` cuando esta respuesta es un replay de una
        petición previamente confirmada que lleva la misma clave `X-Idempotency`
        (el handler no volvió a ejecutarse).
      schema:
        type: string
        enum:
          - 'true'
  responses:
    BadRequestOrMissingIdempotency:
      description: >-
        El cuerpo de la petición está malformado (`bad_request`) o falta el
        header obligatorio `X-Idempotency` (`missing_idempotency_key`, rechazado
        antes de cualquier escritura).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: >-
        La autenticación falló, o no hay contexto de tenant confiable. `error`
        es `unauthorized` (cuerpo uniforme — no se revela ninguna razón).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: >-
        El punto de decisión de autorización denegó la petición. El cuerpo es
        texto plano (no el envelope de error JSON).
      content:
        text/plain:
          schema:
            type: string
    IdempotencyConflict:
      description: >-
        Una duplicada en curso (in-flight), o la misma clave `X-Idempotency` se
        reutilizó con un fingerprint de petición diferente — `error` es
        `idempotency_conflict`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalError:
      description: >-
        Un fallo de infraestructura. `error` es `internal_error` (sanitizado; el
        detalle se registra, nunca se devuelve).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Un JWT bearer emitido por plugin-auth (lib-auth). La identidad del
        tenant se resuelve a partir de los claims validados del token; la
        superficie `/v1` nunca lee un tenant del cuerpo, del path ni de la
        query. Los llamadores de máquina obtienen un token vía el flujo
        client-credentials de plugin-auth. La superficie `/admin` autoriza
        contra un scope de operador y no lleva contexto de tenant.

````