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

# Managing subscriptions

> Create webhook and queue subscriptions in Streaming Hub, wire an AWS delegated grant, rotate a signing secret, recover an auto-disabled destination, and re-pin a schema major.

A **subscription** tells Streaming Hub which events go to which destination for your tenant. You manage subscriptions through the `/v1` control-plane API, authenticated with a plugin-auth JWT. This page covers the model and the onboarding flows; the exact request and response shapes are in the API reference (start from the [reference introduction](/en/reference/introduction)).

## The subscription model

***

A subscription records:

* **`name`** — a human label (required, non-blank).
* **`sink_kind`** — one of `webhook`, `pull`, `sqs`, `rabbitmq`, `eventbridge`.
* **`endpoint`** — where deliveries go, in a shape that depends on the sink kind (see below).
* **`event_types`** — the event types to deliver. An unknown type warns but never blocks a create, so a catalog gap never stalls onboarding.
* **`schema_major`** — the payload major version the subscription follows.
* **`plan_tier`** — the delivery tier the subscription runs under.

The endpoint format follows the sink kind:

| Sink kind     | Endpoint                                                                                                                |
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `webhook`     | An `https://` URL, no embedded userinfo.                                                                                |
| `pull`        | Omitted — the server synthesizes `pull://<id>`.                                                                         |
| `sqs`         | The `https://` SQS queue URL.                                                                                           |
| `rabbitmq`    | `<exchange>/<routingKey>` (exchange required, routing key optional). The broker host lives in the encrypted credential. |
| `eventbridge` | The event-bus name / detail-type addressing string. The region lives in the encrypted credential.                       |

### Two orthogonal status fields

***

Every subscription carries two independent status fields. Confusing them is the most common source of "why did delivery stop" questions, so keep them separate:

* **`verification_state`** is **proof** that the destination can actually receive events. It moves through `pending_verification` → `active` → `degraded`, driven by probes. Only an `active` subscription is deliverable.
* **`enabled`** is the **deliverability switch**. It is what auto-disable flips off, and what a re-enable turns back on.

Subscription matching requires **both**: a subscription delivers only when `enabled` **and** `verification_state = active`. Auto-disable lives entirely in `enabled` and never changes `verification_state`, so an auto-disabled subscription reads as `enabled = false, verification_state = active`. See [auto-disabling a broken destination](/en/streaming-hub/how-streaming-hub-works) for how the two interact.

## Creating a webhook subscription

***

A webhook subscription is the simplest to onboard — it is born ready to receive:

1. **Create** — `POST /v1/subscriptions` with `sink_kind: "webhook"` and your `https://` endpoint. The destination URL is validated against private, loopback, and cloud-metadata address ranges before any row is written, so a private or metadata target can never be stored. The response returns the new subscription **already `active`**, and the **signing secret exactly once**.
2. **Save the signing secret** — it is shown only in this response, stored only as ciphertext, and never returned by any read. Save it on receipt; if you lose it, you can only rotate to a new one.
3. **Confirm reachability (optional)** — `POST /v1/subscriptions/:id/ping` sends a synthetic, signed probe through the real delivery path and reports the classified outcome.

<Warning>
  The signing secret is returned only in the create response (and again on rotation). It is never logged, never stored in plaintext, and never returned by a `GET`. Capture it when you create the subscription.
</Warning>

Once created, the webhook subscription is matched and begins receiving events. See [Consuming events](/en/streaming-hub/consuming-events) for how to verify the signature on each delivery.

## Onboarding a queue subscription

***

Queue subscriptions (`sqs`, `rabbitmq`, `eventbridge`) are born **`pending_verification`** and **deliver nothing** until their destination is verified. How you verify depends on the kind:

* **RabbitMQ** — supply a broker credential, verified on write (below).
* **SQS and EventBridge** — [wire an AWS delegated grant](#wiring-an-aws-delegated-grant) (next section) instead of storing a credential.

For RabbitMQ, the credential flow is three steps:

1. **Create** — `POST /v1/subscriptions` with the queue `sink_kind` and its endpoint, and **no** inline credential (an inline `sink_config` or `credential` is rejected). The subscription is stored `pending_verification`; matching excludes it, so it produces no delivery jobs yet.
2. **Supply the credential** — `PUT /v1/subscriptions/:id/credential` with the write-only broker credential. The hub holds it in memory, **probes it immediately** (connect and authenticate against the broker), and persists it as ciphertext **only if the probe succeeds** — a failed probe stores nothing. The broker host is validated against blocked address ranges before it is stored. A successful probe flips the subscription `pending_verification → active` in the same transaction.
3. **Active** — once `active`, matching admits the subscription and it begins receiving events.

The credential is **write-only**: you supply it here and it is never returned, not even masked, on any read path. To change it, `PUT` a new one — the same probe-on-write applies.

## Wiring an AWS delegated grant

***

For AWS sinks (`sqs`, `eventbridge`), the hub delivers by assuming a role **in your AWS account** — it never stores an AWS credential. You wire that trust before supplying the credential:

1. **Fetch the setup artifacts** — `GET /v1/subscriptions/:id/setup-artifacts` returns a cross-account IAM **trust policy**, a CloudFormation **quick-create link**, and a non-secret **`ExternalId`** the hub mints for this subscription.
2. **Apply them in your AWS account** — create the delivery role from the trust policy (the quick-create link scaffolds it). The role trusts the hub's principal *only* under the minted `ExternalId` condition, which closes the confused-deputy gap: a trust policy missing that condition is treated as a verification failure, never as verified.
3. **Register the grant** — `PUT /v1/subscriptions/:id/delegated-grant` with the delivery role ARN, region, and destination. This records non-secret coordinates only; it runs no probe and does not change `verification_state`.
4. **Verify** — `POST /v1/subscriptions/:id/verify` runs the hardened role-assumption probe and, on success, flips the subscription to `active`.

No AWS credential ever crosses these requests or is stored by the hub; the hub assumes your role per delivery, guarded by the `ExternalId`.

## Rotating a signing secret

***

`POST /v1/subscriptions/:id/secret/rotate` mints a **new** webhook signing secret, returns it **once**, and starts a **24-hour dual-sign overlap**. During the overlap the hub signs each delivery with **both** the new and the previous secret and sends **two** signature headers, so your consumer can switch to the new secret at any point in the window without dropping deliveries.

Rotate cleanly like this:

1. Call rotate and save the new secret from the response (alongside the `overlapUntil` timestamp).
2. Deploy the new secret to your consumer within the overlap window. While both are configured, your verifier accepts a delivery if **either** signature validates.
3. After the overlap, retire the old secret.

Rotating a `pull` sink — which has no signing secret — is rejected. See [handling two signatures during rotation](/en/streaming-hub/consuming-events) for the consumer-side verification.

## Recovering an auto-disabled subscription

***

When a destination stays broken long enough, the hub [auto-disables](/en/streaming-hub/how-streaming-hub-works) the subscription by flipping `enabled = false`. To recover it:

1. Fix the destination (the endpoint, the queue, or the grant).
2. Call `POST /v1/subscriptions/:id/verify`. It re-probes the destination and, on a successful probe, re-enables the subscription and clears the auto-disable mark — in place, keeping the same id and signing secret.

A failed re-probe changes nothing, so re-enable is always bound to a real, current probe success. `GET /v1/subscriptions/:id/health` gives you the delivery-health rollup — recent outcomes, dead-letter counts, and the auto-disable verdict — to confirm the destination is healthy before and after.

## Re-pinning the schema major

***

`PATCH /v1/subscriptions/:id` re-pins the subscription's `schema_major` — the **only** mutable field:

* `{"schema_major": 2}` pins the subscription to that major version.
* `{"schema_major": null}` clears the pin to follow the base version.

Any other field, an empty body, or a value below `1` is **rejected** (not silently ignored), so you can never believe a forbidden change took effect. Changing the endpoint, sink kind, or secret is out of scope for `PATCH` by design; create a new subscription for a different destination. A re-pin is not a destination change — it does not reset `verification_state` and echoes no secret.

## Idempotency

***

The mutating routes — create, delete, `PATCH`, secret rotate, and delegated-grant registration — require an idempotency key:

```
X-Idempotency: <your-unique-key>
```

A mutation sent without it is rejected **before any write** with `400 missing_idempotency_key`. The store is **fail-closed**: if the idempotency store is unreachable, the request fails rather than risking a double write — this guarantees a replayed create or rotate re-serves the original once-shown secret instead of minting a new one.

* Replaying a committed key returns the original response byte-for-byte, with `X-Idempotency-Replayed: true`.
* Reusing a key with a *different* request body returns `409 idempotency_conflict` — mint a new key for a corrected request.

The verification routes (`ping`, `verify`, `PUT /credential`, `GET /setup-artifacts`) are naturally idempotent and require no key. See the platform-wide guidance on [retries and idempotency](/en/reference/retries-idempotency).

## Next steps

***

<CardGroup cols={2}>
  <Card title="Consuming events" icon="inbox" href="/en/streaming-hub/consuming-events">
    Verify webhook signatures, deduplicate deliveries, and pull events.
  </Card>

  <Card title="How Streaming Hub works" icon="diagram-project" href="/en/streaming-hub/how-streaming-hub-works">
    Matching, dispatch, retries, and auto-disable in detail.
  </Card>
</CardGroup>
