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

# Subscriptions

> Register and manage the endpoints that receive your events

A subscription records where your events go and which ones you want. Every endpoint on this page requires the `webhooks` permission and is scoped to your partner account.

<Warning>
  The signing secret is returned on exactly two endpoints — [create](#create-a-subscription) and [rotate](#rotate-the-signing-secret) — and only once each. Store it when you receive it.
</Warning>

## The subscription object

<ResponseField name="id" type="uuid">Subscription id.</ResponseField>
<ResponseField name="deliveryMode" type="enum">`PUSH` (default) or `PULL`. See [delivery modes](/webhooks/reference#delivery-modes).</ResponseField>
<ResponseField name="target" type="string | null">Your HTTPS receiver URL. Present for `PUSH`, `null` for `PULL`.</ResponseField>
<ResponseField name="eventFilter" type="string[]">The types you receive. `["*"]` means all of them.</ResponseField>
<ResponseField name="schemaVersion" type="string">Version of the delivered payload. Defaults to `"1.0"`.</ResponseField>
<ResponseField name="status" type="enum">`ACTIVE`, `PAUSED`, `SUSPENDED`, or `DELETED`.</ResponseField>
<ResponseField name="secretHint" type="string">A non-sensitive fingerprint of the signing secret.</ResponseField>
<ResponseField name="webhook_secret" type="string">The plaintext signing secret. Returned **only** on create and rotate.</ResponseField>
<ResponseField name="createdOn" type="date-time">When the subscription was created.</ResponseField>

***

## Create a subscription

<ParamField path="POST /api/v0/webhooks/subscriptions" />

Registers a subscription — `PUSH` by default, or `PULL` — and returns your signing secret once.

### Body

<ParamField body="deliveryMode" type="enum" default="PUSH">
  `PUSH` or `PULL`. See [delivery modes](/webhooks/reference#delivery-modes).
</ParamField>

<ParamField body="target" type="string" required>
  Required for `PUSH`; ignored for `PULL`, where it is stored as `null`. Must be an absolute `https://` URL on a public host, using an allowed port, with no embedded credentials. Maximum 512 characters.
</ParamField>

<ParamField body="eventFilter" type="string[]" default="[&#x22;*&#x22;]">
  The event types to receive.
</ParamField>

<ParamField body="schemaVersion" type="string" default="1.0">
  Payload version to deliver.
</ParamField>

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://qa-api.endl.xyz/api/v0/webhooks/subscriptions \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET" \
    -H "Content-Type: application/json" \
    -d '{
      "target": "https://api.acme.com/webhooks/endl",
      "eventFilter": ["payout.completed", "payout.failed"]
    }'
  ```

  ```json 201 Created theme={null}
  {
    "code": 201,
    "message": "Subscription created",
    "status": "SUCCESS",
    "data": {
      "id": "2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34",
      "deliveryMode": "PUSH",
      "target": "https://api.acme.com/webhooks/endl",
      "eventFilter": ["payout.completed", "payout.failed"],
      "schemaVersion": "1.0",
      "status": "ACTIVE",
      "secretHint": "whsec_…5RtBw",
      "webhook_secret": "whsec_k2p9Fa7QxVn3Jh05RtBw",
      "createdOn": "2026-09-02T12:34:56.789Z"
    },
    "errors": []
  }
  ```
</CodeGroup>

<Warning>
  **Store the secret now.** `webhook_secret` is shown only in this response. If you lose it, your only option is to [rotate](#rotate-the-signing-secret).
</Warning>

### Create a PULL subscription

Omit `target` and set `"deliveryMode": "PULL"`. A PULL subscription never delivers anything — it authorizes and configures [the event feed](/webhooks/pull-feed), carrying the `eventFilter` and `schemaVersion` that feed reads.

You may have one live PULL subscription per partner, and it requires the `events` permission. A signing secret is still returned: PULL responses are unsigned, but rotation and the secret hint stay consistent across both modes.

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://qa-api.endl.xyz/api/v0/webhooks/subscriptions \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET" \
    -H "Content-Type: application/json" \
    -d '{
      "deliveryMode": "PULL",
      "eventFilter": ["payout.completed", "payout.failed"]
    }'
  ```

  ```json 201 Created theme={null}
  {
    "code": 201,
    "message": "Subscription created",
    "status": "SUCCESS",
    "data": {
      "id": "7d3a1c22-88fe-4b90-a1c4-2f0e9b6d5a10",
      "deliveryMode": "PULL",
      "target": null,
      "eventFilter": ["payout.completed", "payout.failed"],
      "schemaVersion": "1.0",
      "status": "ACTIVE",
      "secretHint": "whsec_…9QsLm",
      "webhook_secret": "whsec_Xr4Tn8Va2Kd7Bp19QsLm",
      "createdOn": "2026-09-02T12:34:56.789Z"
    },
    "errors": []
  }
  ```
</CodeGroup>

<Accordion title="Status codes">
  `201` created · `400` invalid or blocked target, `target` missing for PUSH, or missing permission · `409` duplicate target, active cap of 5 reached, or a live PULL subscription already exists · `422` SQS or SSE requested · `401` / `503` authentication
</Accordion>

***

## List subscriptions

<ParamField path="GET /api/v0/webhooks/subscriptions" />

Returns all your non-deleted subscriptions, newest first, as `data.subscriptions[]` with `data.totalCount`. Never includes `webhook_secret`. There is no pagination — you get everything.

<CodeGroup>
  ```bash Request theme={null}
  curl https://qa-api.endl.xyz/api/v0/webhooks/subscriptions \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET"
  ```

  ```json 200 OK theme={null}
  {
    "code": 200,
    "message": "Success",
    "status": "SUCCESS",
    "data": {
      "subscriptions": [
        {
          "id": "2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34",
          "deliveryMode": "PUSH",
          "target": "https://api.acme.com/webhooks/endl",
          "eventFilter": ["payout.completed", "payout.failed"],
          "schemaVersion": "1.0",
          "status": "ACTIVE",
          "secretHint": "whsec_…5RtBw",
          "createdOn": "2026-09-02T12:34:56.789Z"
        }
      ],
      "totalCount": 1
    },
    "errors": []
  }
  ```
</CodeGroup>

<Accordion title="Status codes">
  `200` · `400` missing permission · `401` / `503` authentication
</Accordion>

***

## Get a subscription

<ParamField path="GET /api/v0/webhooks/subscriptions/{id}" />

Fetches one subscription by its UUID.

<CodeGroup>
  ```bash Request theme={null}
  curl https://qa-api.endl.xyz/api/v0/webhooks/subscriptions/2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34 \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET"
  ```

  ```json 200 OK theme={null}
  {
    "code": 200,
    "message": "Success",
    "status": "SUCCESS",
    "data": {
      "id": "2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34",
      "deliveryMode": "PUSH",
      "target": "https://api.acme.com/webhooks/endl",
      "eventFilter": ["payout.completed", "payout.failed"],
      "schemaVersion": "1.0",
      "status": "ACTIVE",
      "secretHint": "whsec_…5RtBw",
      "createdOn": "2026-09-02T12:34:56.789Z"
    },
    "errors": []
  }
  ```
</CodeGroup>

<Accordion title="Status codes">
  `200` · `404` not found, not yours, or deleted · `400` non-UUID id or missing permission · `401` / `503` authentication
</Accordion>

***

## Update a subscription

<ParamField path="PATCH /api/v0/webhooks/subscriptions/{id}" />

A partial update: a `null` or omitted field is left unchanged. You can change the status and the event filter.

### Body

<ParamField body="status" type="enum">
  `PAUSED` holds events, `ACTIVE` resumes delivery, `DELETED` soft-deletes. `SUSPENDED` is rejected with a `400` — only Endl sets it.
</ParamField>

<ParamField body="eventFilter" type="string[]">
  Replaces the current filter. An empty list resets it to `["*"]`.
</ParamField>

<Warning>
  **The target URL is immutable.** A request that includes `target` is rejected. To send events somewhere else, delete the subscription and create a new one, which issues a fresh signing secret. This is deliberate: it stops a compromised key from silently repointing a live, signed event stream at an attacker.
</Warning>

<CodeGroup>
  ```bash Request theme={null}
  curl -X PATCH https://qa-api.endl.xyz/api/v0/webhooks/subscriptions/2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34 \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET" \
    -H "Content-Type: application/json" \
    -d '{ "status": "PAUSED", "eventFilter": ["payout.completed"] }'
  ```

  ```json 200 OK theme={null}
  {
    "code": 200,
    "message": "Subscription updated",
    "status": "SUCCESS",
    "data": {
      "id": "2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34",
      "deliveryMode": "PUSH",
      "target": "https://api.acme.com/webhooks/endl",
      "eventFilter": ["payout.completed"],
      "schemaVersion": "1.0",
      "status": "PAUSED",
      "secretHint": "whsec_…5RtBw",
      "createdOn": "2026-09-02T12:34:56.789Z"
    },
    "errors": []
  }
  ```
</CodeGroup>

<Accordion title="Status codes">
  `200` · `400` `SUSPENDED` requested, `target` supplied, or missing permission · `404` not found · `409` resuming would exceed the active cap · `401` / `503` authentication
</Accordion>

***

## Rotate the signing secret

<ParamField path="POST /api/v0/webhooks/subscriptions/{id}/rotate-secret" />

Issues a new signing secret and returns it once. This is the only endpoint besides create that returns `webhook_secret`.

<Warning>
  Endl signs with the new secret **immediately** — there is no overlap window. Accept both the old and the new secret in your verifier until every instance has rolled over. See [replay and rotation](/webhooks/signature-verification#replay-and-rotation).
</Warning>

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://qa-api.endl.xyz/api/v0/webhooks/subscriptions/2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34/rotate-secret \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET"
  ```

  ```json 200 OK theme={null}
  {
    "code": 200,
    "message": "Signing secret rotated",
    "status": "SUCCESS",
    "data": {
      "id": "2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34",
      "deliveryMode": "PUSH",
      "target": "https://api.acme.com/webhooks/endl",
      "eventFilter": ["payout.completed"],
      "schemaVersion": "1.0",
      "status": "ACTIVE",
      "secretHint": "whsec_…9Kd2",
      "webhook_secret": "whsec_NEWvalueStoreThisNow",
      "createdOn": "2026-09-02T12:34:56.789Z"
    },
    "errors": []
  }
  ```
</CodeGroup>

<Accordion title="Status codes">
  `200` rotated · `404` not found · `400` missing permission · `401` / `503` authentication
</Accordion>

***

## Delete a subscription

<ParamField path="DELETE /api/v0/webhooks/subscriptions/{id}" />

Soft-deletes the subscription. It returns `200` with `status: "DELETED"`, not an empty `204`. In-flight deliveries are cancelled, delivery history is kept, and the URL can be registered again later.

<CodeGroup>
  ```bash Request theme={null}
  curl -X DELETE https://qa-api.endl.xyz/api/v0/webhooks/subscriptions/2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34 \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET"
  ```

  ```json 200 OK theme={null}
  {
    "code": 200,
    "message": "Subscription deleted",
    "status": "SUCCESS",
    "data": {
      "id": "2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34",
      "deliveryMode": "PUSH",
      "target": "https://api.acme.com/webhooks/endl",
      "eventFilter": ["payout.completed"],
      "schemaVersion": "1.0",
      "status": "DELETED",
      "secretHint": "whsec_…9Kd2",
      "createdOn": "2026-09-02T12:34:56.789Z"
    },
    "errors": []
  }
  ```
</CodeGroup>

<Accordion title="Status codes">
  `200` · `404` not found or already deleted · `400` missing permission · `401` / `503` authentication
</Accordion>

***

## Send an event to a subscription

<ParamField path="POST /api/v0/webhooks/subscriptions/{id}/send" />

Manually sends an already-recorded event to this subscription. This is useful for a subscription that did not exist when the event first occurred. It queues a delivery rather than sending synchronously, and returns a `deliveryId` you can follow in [Deliveries](/webhooks/deliveries).

### Body

<ParamField body="eventId" type="string" required>
  The event id — the value delivered as `X-WEBHOOK-EVENT-ID`. Maximum 64 characters.
</ParamField>

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://qa-api.endl.xyz/api/v0/webhooks/subscriptions/2b1e8f40-3c9a-4d21-9f7e-6a1c0b5d2e34/send \
    -H "X-API-KEY: $ENDL_API_KEY" -H "X-API-SECRET: $ENDL_API_SECRET" \
    -H "Content-Type: application/json" \
    -d '{ "eventId": "evt_9f1a4c7b2e08" }'
  ```

  ```json 200 OK theme={null}
  {
    "code": 200,
    "message": "Event queued for delivery",
    "status": "SUCCESS",
    "data": {
      "message": "queued for delivery — this does not guarantee immediate receipt at the endpoint",
      "deliveryId": "4d2f6a11-8c73-4b90-9e21-5f0a2c8d1e44"
    },
    "errors": []
  }
  ```
</CodeGroup>

<Note>
  A `200` here means the delivery was **queued**, not received. Track the returned `deliveryId` to find out what actually happened.
</Note>

<Accordion title="Status codes">
  `200` queued · `400` bad `eventId`, a PULL subscription, a send rate limit, or missing permission · `404` subscription or event not found · `409` conflicting delivery · `401` / `503` authentication
</Accordion>
