> ## 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.

# Create a subscription

> Creates a delivery subscription for the authenticated tenant. The tenant is taken from the validated JWT claims only — a `tenant_id` in the body is ignored.

A `webhook` subscription is born `pending_verification` and mints a signing secret that is returned in this response (see `signingSecret`); it stays undeliverable until a successful probe via `POST /v1/subscriptions/{id}/ping` moves it to `active`. A queue subscription (`sqs`, `rabbitmq`, `eventbridge`) is also born `pending_verification` and mints no secret; it stays undeliverable until a probe succeeds. Every queue kind activates on an outbound credential supplied and probed via `PUT /v1/subscriptions/{id}/credential`. An AWS kind (`sqs`, `eventbridge`) has a second path that stores no credential: register a delegated grant, then call `POST /v1/subscriptions/{id}/verify`. Inline `sink_config` or `credential` at create is rejected — queue credentials arrive only on the credential PUT.



## OpenAPI

````yaml /es/openapi/v3-current/streaming-hub.yaml post /v1/subscriptions
openapi: 3.1.0
info:
  title: Lerian Streaming Hub API
  version: 1.2.0
  contact:
    email: contact@lerian.studio
    name: Lerian Studio
    url: https://lerian.studio
  license:
    name: Lerian Studio General License
  description: >-
    The Streaming Hub control-plane API. Streaming Hub is Lerian's managed
    event-delivery edge: it consumes CloudEvents from the platform's internal
    streaming backbone and fans them out to a tenant's own external destinations
    — webhooks, Amazon SQS, RabbitMQ, Amazon EventBridge, or a pull inbox. This
    API lets a tenant browse the manifest-fed event catalog, create and manage
    delivery subscriptions, verify and rotate their credentials, read delivery
    health, and pull entitled events.


    An RFC 9457 `application/problem+json` error body carries `type`, `title`,
    and `status`. An error the hub raises adds `detail` and a stable
    low-cardinality `code` you can branch on; a request the framework rejects
    before the operation runs, such as a malformed or schema-invalid one,
    carries no `code`. In a `5xx`, `detail` is the static `"internal error"`.
    Mutating operations require an `X-Idempotency` header for at-most-once
    semantics; a replayed request returns the original response byte-for-byte
    with `X-Idempotency-Replayed: true`. The catalog and the pull events surface
    are tenant-scoped through the bearer JWT; the operational probe endpoints
    (`/healthz`, `/readyz`, `/version`, `/runtime`, `/metrics`) are
    unauthenticated. Streaming Hub is closed source under the Lerian Studio
    General License.
servers:
  - url: https://streaming-hub.sandbox.lerian.net
security:
  - BearerAuth: []
tags:
  - name: Catalog
    description: Browse the manifest-fed catalog of event types available for subscription.
  - name: Subscriptions
    description: >-
      Create, read, update, and delete delivery subscriptions, and drive the
      destination verification lifecycle (ping, verify, credential, delegated
      grant, secret rotation, health).
  - name: Event Delivery
    description: >-
      Pull entitled events for a pull-sink subscription
      (cursor-as-acknowledgment read).
  - name: Dashboard
    description: >-
      The read-only operational dashboard: a consistent snapshot of ingestion,
      push delivery, and pull eligibility, plus the paged list of subscriptions
      that need attention. It never consumes events and never returns a secret.
  - name: Admin
    description: Cross-tenant operator forensics. Requires an operator authorization scope.
  - name: Operational
    description: Unauthenticated liveness, readiness, build, runtime, and metrics probes.
paths:
  /v1/subscriptions:
    post:
      tags:
        - Subscriptions
      summary: Create a subscription
      description: >-
        Creates a delivery subscription for the authenticated tenant. The tenant
        is taken from the validated JWT claims only — a `tenant_id` in the body
        is ignored.


        A `webhook` subscription is born `pending_verification` and mints a
        signing secret that is returned in this response (see `signingSecret`);
        it stays undeliverable until a successful probe via `POST
        /v1/subscriptions/{id}/ping` moves it to `active`. A queue subscription
        (`sqs`, `rabbitmq`, `eventbridge`) is also born `pending_verification`
        and mints no secret; it stays undeliverable until a probe succeeds.
        Every queue kind activates on an outbound credential supplied and probed
        via `PUT /v1/subscriptions/{id}/credential`. An AWS kind (`sqs`,
        `eventbridge`) has a second path that stores no credential: register a
        delegated grant, then call `POST /v1/subscriptions/{id}/verify`. Inline
        `sink_config` or `credential` at create is rejected — queue credentials
        arrive only on the credential PUT.
      operationId: createSubscription
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
      responses:
        '201':
          description: >-
            The subscription was created. `signingSecret` carries a secret only
            for a `webhook` sink. No read route returns it; a replay of the same
            `X-Idempotency` key returns this same response.
          headers:
            Cache-Control:
              $ref: '#/components/headers/NoStore'
            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':
          description: >-
            Branch on `code`, not on the status. `subscription_name_taken`: a
            live subscription in your scope already holds this `name`, and
            `existing_id` names it, so you can reuse or delete it. A retry with
            the same name never succeeds. After you delete the holder, send the
            create with a new `X-Idempotency` key: the hub stores this refusal
            as the answer to the key that met it and replays it for that key.
            `idempotency_conflict`: the `X-Idempotency` key refuses this send,
            for example an in-flight duplicate or a key reused with a different
            request; nothing ran.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/NameTakenError'
        '422':
          description: >-
            The request is caller-correctable. `error` is one of
            `validation_error` (a field failed validation),
            `inline_sink_config_forbidden` (an inline `sink_config` /
            `credential` was sent), or `endpoint_blocked` (the webhook endpoint
            resolved to a blocked, private, or metadata address).
          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 `eventKey` values to deliver, as `GET /v1/catalog` serves them.
            Any well-formed key is accepted. A key no producer emits matches
            nothing.
          items:
            type: string
          examples:
            - - transaction.created
        origin:
          type: string
          description: >-
            Pins the subscription to one producing application, by its
            `ce-source`. Omit it to receive the event types from any producer.
          examples:
            - midaz
        timeout_ms:
          type: integer
          description: >-
            The per-attempt delivery timeout in milliseconds, from `1000` to
            `20000`. For `webhook` subscriptions only: the hub refuses a
            non-zero value for any other sink kind. Omit it, or send `0`, to use
            the `10000` default.
          examples:
            - 20000
        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` carries a secret only for a
        `webhook` sink; it is stored only as ciphertext. No read route returns
        it; a replay of the same `X-Idempotency` key returns this same response.
      properties:
        subscription:
          $ref: '#/components/schemas/Subscription'
        signingSecret:
          type: string
          description: >-
            The webhook signing secret, minted server-side. No read route
            returns it; a replay of the same `X-Idempotency` key returns this
            same response. An empty string for any other sink kind.
          examples:
            - whsec_9f8c2b1e4a7d6055c3e2f10987ab4c21
      required:
        - subscription
        - signingSecret
    NameTakenError:
      description: >-
        The `409` body of a create: the `Error` document, plus `existing_id`
        when `code` is `subscription_name_taken`.
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            existing_id:
              type: string
              format: uuid
              description: >-
                The id of the subscription in your scope that held the name when
                the hub refused the create. Absent only when that subscription
                was deleted before the hub looked up its id. A replayed refusal
                can name a subscription deleted since.
              examples:
                - 0192f1a0-0000-7000-8000-0000000000ff
    Error:
      type: object
      description: >-
        RFC 9457 `application/problem+json` document. An error the hub raises
        carries `detail` and the stable `code` a client branches on; a request
        the framework rejects before the operation runs carries no `code`. In a
        `5xx`, `detail` is the static `"internal error"`.
      properties:
        type:
          type: string
          format: uri
          description: >-
            A stable, versioned URI identifying the problem type
            (`https://errors.lerian.studio/v1/<code>`), or `about:blank` for
            problems without a hub-assigned code.
          examples:
            - https://errors.lerian.studio/v1/not_found
        title:
          type: string
          description: A short human-readable summary — the HTTP status text.
          examples:
            - Not Found
        status:
          type: integer
          description: The HTTP status code, mirrored in the body.
          examples:
            - 404
        detail:
          type: string
          description: >-
            A caller-safe human explanation of this specific occurrence. For
            `5xx` responses this is always the static string `"internal error"`.
          examples:
            - subscription not found
        code:
          type: string
          description: >-
            The stable, low-cardinality, machine-readable token a client
            branches on (for example `not_found`, `unauthorized`,
            `idempotency_conflict`, `validation_error`). Absent when the
            framework rejects the request before the operation runs.
          examples:
            - not_found
      required:
        - type
        - title
        - status
    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
        origin:
          type: string
          description: >-
            The producing application (`ce-source`) the subscription accepts
            events from. Omitted when it accepts any producer.
          examples:
            - midaz
        timeout_ms:
          type: integer
          description: >-
            The per-attempt delivery timeout in milliseconds. Omitted when every
            attempt uses the `10000` default.
          examples:
            - 20000
        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 `pull`
        subscription is born `active` — it has no destination to probe. Every
        other kind is born `pending_verification` and reaches `active` on a
        successful probe: `POST /v1/subscriptions/{id}/ping` for a `webhook`,
        and `PUT /v1/subscriptions/{id}/credential` for any queue sink (`sqs`,
        `rabbitmq`, `eventbridge`). An `sqs` or `eventbridge` sink has a second
        path: register a delegated grant — which persists the coordinates and
        leaves this field unchanged — then call `POST
        /v1/subscriptions/{id}/verify`. An `active` subscription whose probe
        later fails becomes `degraded`. The same successful probe returns this
        field to `active`. This field is one half of deliverability: `enabled`
        is the other half, and both must hold. No probe changes `enabled` except
        `POST /v1/subscriptions/{id}/verify`, which on a probe success clears an
        auto-disable in the same transaction as the state move.
      enum:
        - pending_verification
        - active
        - degraded
  headers:
    NoStore:
      description: Always `private, no-store`. Do not cache this response.
      schema:
        type: string
        enum:
          - private, no-store
    IdempotencyReplayed:
      description: >-
        Present and set to `true` when this response is a replay of a previously
        committed request carrying the same `X-Idempotency` key (the handler did
        not run again).
      schema:
        type: string
        enum:
          - 'true'
  responses:
    BadRequestOrMissingIdempotency:
      description: >-
        The request body is malformed (`bad_request`) or the required
        `X-Idempotency` header is missing (`missing_idempotency_key`, rejected
        before any write).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: >-
        Authentication failed, or there is no trusted tenant context. `code` is
        `unauthorized` (uniform body — no reason is leaked).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: >-
        Access Manager denied the request ahead of the hub, with a plain-text
        body. On the `/v1` surface the hub itself can also answer `403` as
        `application/problem+json` with `code` `forbidden`: the delegated scope
        the request presents disagrees with the scope claim on the relayed
        token.
      content:
        text/plain:
          schema:
            type: string
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalError:
      description: >-
        An infrastructure fault. `code` is `internal_error` and `detail` is
        scrubbed to the static `"internal error"` (the cause is logged, never
        returned).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        A bearer JWT issued by Access Manager. The `/v1` surface never reads a
        tenant from the body, path, or query. Machine callers obtain a token
        with the Access Manager client-credentials flow. The `/admin` surface
        authorizes against an operator scope and carries no tenant context.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.