> ## 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. 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 /pt/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 `title` and
    `status`. An error the hub raises adds `type`, `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`. 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.
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. 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. `idempotency_outcome_unrecorded`: an earlier
            request under this key may have committed with no recorded outcome;
            read the resource before you send anything again.
          headers:
            X-Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/NameTakenError'
        '422':
          description: >-
            The request is caller-correctable. `code` names the fault, for
            example `validation_error` (a field failed validation),
            `inline_sink_config_forbidden` (an inline `sink_config` /
            `credential` was sent), `endpoint_blocked` (the webhook endpoint
            resolved to a blocked, private, or metadata address),
            `unknown_event_type` (an `event_types` value has no catalog entry at
            the schema major the subscription receives it at; each `errors`
            entry names one refused type and its major), or
            `event_type_outside_nature` (the delegated credential never receives
            an `event_types` value).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          description: >-
            The request could not be served now. `code` names the cause, for
            example `AUT-0033` (Access Manager did not decide the request) or
            `dns_unavailable` (the DNS lookup of the endpoint host failed;
            nothing was written, and the same `X-Idempotency` key can retry).
            Wait the `Retry-After` seconds, then retry.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          description: >-
            A lookup did not finish in time. `code` names the cause, for example
            `AUT-1021` (Access Manager did not answer in time) or `dns_timeout`
            (the DNS lookup of the endpoint host timed out; nothing was written,
            and the same `X-Idempotency` key can retry).
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Error'
      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.
            A value the catalog does not list at the schema major the
            subscription receives it at, from the `origin` producer when one is
            set, is refused with `422 unknown_event_type`. When the catalog
            cannot be read or lists no producer event yet, the values are
            accepted unchecked. Omit the field to receive every event type.
          items:
            type: string
          examples:
            - - transaction.posted
        origin:
          type: string
          description: The `ce-source` of a producing application.
          examples:
            - ledger
        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. Omit it to follow major 1. `event_majors`
            overrides it per event type.
          examples:
            - 1
        event_majors:
          type: object
          additionalProperties:
            type: integer
          description: >-
            Optional schema major per event type, overriding `schema_major` for
            the types it names. When `event_types` lists types, each key must be
            one of them. Each value is from `1` to `2147483647`. It cannot be
            patched: create a new subscription to change it.
        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:
            - AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
      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`.
      properties:
        type:
          type: string
          format: uri
          description: >-
            A stable, versioned URI identifying the problem type
            (`https://errors.lerian.studio/v1/<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 human explanation of this specific occurrence.
          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
        instance:
          type: string
          description: The trace id of the request, when the hub has one.
          examples:
            - 4bf92f3577b34da6a3ce929d0e0e4736
        errors:
          type: array
          description: >-
            When present, the entries name what was refused. On
            `unknown_event_type`, each entry names one refused event type and
            its schema major.
          items:
            type: object
            properties:
              location:
                type: string
                description: The refused field, with its position in a list.
                examples:
                  - event_types/2
              message:
                type: string
                description: Why the hub refused the value.
                examples:
                  - '"transaction.creatd" has no catalog entry at schema major 1'
      required:
        - 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.
          examples:
            - 1
        event_majors:
          type: object
          additionalProperties:
            type: integer
          description: >-
            The schema major per event type that overrides `schema_major`.
            Omitted when none is set.
        origin:
          type: string
          description: >-
            The `ce-source` of a producing application. Omitted when none is
            set.
          examples:
            - ledger
        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: >-
            Whether delivery is enabled: `false` once auto-disable trips; `POST
            /v1/subscriptions/{id}/verify` re-enables it. 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'
    RetryAfter:
      description: The whole seconds to wait before a retry. Always at least `1`.
      schema:
        type: string
        examples:
          - '1'
  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. `code` names the cause, for example `AUT-0006`
        (no token), `AUT-0007` (an expired, invalid, or malformed token), or
        `unauthorized` (no trusted tenant context).
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: >-
        The caller is authenticated but is not allowed to make this request.
        `code` names the cause, for example `AUT-1020` (the credential does not
        have the permission; `detail` names the refused `resource:action`) or
        `forbidden`.
      content:
        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). On a mutation that requires `X-Idempotency`, `code` can
        instead be `idempotency_outcome_unrecorded`: the mutation may have
        committed, and its outcome was not recorded. Read the resource before
        you send anything again.
      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.