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

# POST /event-subscriptions — manage webhook delivery

> Create, list, and deactivate durable webhook subscriptions. AgentRail signs every delivery with HMAC-SHA256 for secure verification.

The `/event-subscriptions` endpoints let you register a URL to receive AgentRail task lifecycle events as outbound HTTP POST requests. Webhook delivery is at-least-once with exponential backoff for up to 8 attempts. Your endpoint must deduplicate on `X-AgentRail-Event-Id` — this header remains stable across retries, while `X-AgentRail-Delivery-Id` changes on each attempt.

AgentRail signs every outbound delivery using HMAC-SHA256 over the raw request body with the subscription `secret` you provide at creation time. Always verify the `X-AgentRail-Signature` header before processing a delivery.

<Note>
  Required scopes: **`webhooks:read`** (list), **`webhooks:write`** (create/delete)
</Note>

## Create a subscription

### `POST /event-subscriptions`

### Headers

<ParamField header="Idempotency-Key" type="string" required>
  Unique key for safe retries. The same key plus the same body replays the original accepted result. Reusing the key with a different body returns `409 conflict`. Must be 8–128 characters.
</ParamField>

### Request body

<ParamField body="url" type="string" required>
  Your HTTPS endpoint URL that will receive event deliveries.
</ParamField>

<ParamField body="eventTypes" type="string[]" required>
  Non-empty list of event types to subscribe to.

  Allowed values: `task.updated`, `task.reviewed`, `task.shipped`, `task.awaiting_user`
</ParamField>

<ParamField body="secret" type="string" required>
  Shared secret used to compute and verify the `X-AgentRail-Signature` on each delivery. Must be 16–128 characters. Store this value securely — AgentRail never returns it after creation.
</ParamField>

<ParamField body="description" type="string">
  Optional description for this subscription. Maximum 200 characters.
</ParamField>

<ParamField body="filters" type="object">
  Optional filter set to narrow event delivery.

  <Expandable title="filters fields">
    <ParamField body="filters.taskIds" type="string[]">
      Deliver events only for these task IDs. Up to 50 IDs. Each must match `tsk_[A-Za-z0-9]+`. Omit or leave empty to receive events for all tasks.
    </ParamField>
  </Expandable>
</ParamField>

### Example

```bash theme={null}
curl -s -X POST "$AGENTRAIL_BASE_URL/event-subscriptions" \
  -H "authorization: Bearer $AGENTRAIL_API_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: whsub-primary-v1" \
  -d '{
    "url": "https://agents.example.com/webhooks/task-events",
    "eventTypes": ["task.updated", "task.reviewed", "task.shipped"],
    "secret": "whsec_live_agentrail_contract_001",
    "description": "Primary automation endpoint for task lifecycle updates.",
    "filters": {
      "taskIds": ["tsk_01JY4X8Q6J5Q3P7M0N2K3R4T5V"]
    }
  }'
```

### Response

A `201` response confirms the subscription was created.

<ResponseField name="data" type="object" required>
  The created subscription record.

  <Expandable title="Subscription fields">
    <ResponseField name="id" type="string" required>
      Stable subscription ID. Begins with `evsub_`.
    </ResponseField>

    <ResponseField name="url" type="string" required>
      Delivery endpoint URL.
    </ResponseField>

    <ResponseField name="eventTypes" type="string[]" required>
      Event types this subscription delivers.
    </ResponseField>

    <ResponseField name="filters" type="object" required>
      Active filter set.

      <Expandable title="Filters fields">
        <ResponseField name="taskIds" type="string[]" required>
          Task IDs this subscription is scoped to. Empty array means all tasks.
        </ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="status" type="string" required>
      Subscription status. One of: `active`, `disabled`.
    </ResponseField>

    <ResponseField name="signingAlgorithm" type="string" required>
      Algorithm used to sign deliveries. Always `hmac_sha256`.
    </ResponseField>

    <ResponseField name="retryPolicy" type="object" required>
      Delivery retry configuration.

      <Expandable title="retryPolicy fields">
        <ResponseField name="maxAttempts" type="integer" required>Maximum delivery attempts. Default: `8`.</ResponseField>
        <ResponseField name="initialBackoffSeconds" type="integer" required>Initial backoff before the first retry. Default: `10`.</ResponseField>
        <ResponseField name="maxBackoffSeconds" type="integer" required>Maximum backoff cap. Default: `3600`.</ResponseField>
      </Expandable>
    </ResponseField>

    <ResponseField name="createdAt" type="string" required>
      ISO 8601 creation timestamp.
    </ResponseField>

    <ResponseField name="availableActions" type="string[]" required>
      Actions available on this subscription, for example `["deactivate"]`.
    </ResponseField>
  </Expandable>
</ResponseField>

***

## List subscriptions

### `GET /event-subscriptions`

```bash theme={null}
curl -s "$AGENTRAIL_BASE_URL/event-subscriptions" \
  -H "authorization: Bearer $AGENTRAIL_API_KEY"
```

Returns an array of subscription records in the same shape as the create response. Requires `webhooks:read`.

***

## Inbound delivery headers

AgentRail sends the following headers with every outbound webhook delivery:

| Header                         | Description                                                                      |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `x-agentrail-subscription-id`  | The subscription that triggered this delivery. Pattern: `evsub_[A-Za-z0-9]+`.    |
| `x-agentrail-event-id`         | Stable logical event ID. Deduplicate on this value. Pattern: `evt_[A-Za-z0-9]+`. |
| `x-agentrail-event-type`       | Event type, for example `task.reviewed`.                                         |
| `x-agentrail-delivery-id`      | Unique delivery attempt ID. Changes on each retry. Pattern: `dlv_[A-Za-z0-9]+`.  |
| `x-agentrail-delivery-attempt` | Monotonic retry attempt number, starting from `1`.                               |
| `x-agentrail-signature`        | HMAC-SHA256 signature of the raw request body. Format: `sha256=<64 hex chars>`.  |

## Verifying the signature

Compute an HMAC-SHA256 digest of the **raw request body bytes** using your subscription secret, then compare it to the `sha256=` portion of `x-agentrail-signature`. Do not parse the JSON body before computing the digest.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifySignature(rawBody, secret, signatureHeader) {
    const expected = "sha256=" +
      crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signatureHeader)
    );
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac

  def verify_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
      expected = "sha256=" + hmac.new(
          secret.encode(), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature_header)
  ```
</CodeGroup>

<Warning>
  Always verify the signature before processing a delivery. Reject any delivery where the signature does not match. Respond with `410` to tell AgentRail to disable the subscription if your endpoint is being retired.
</Warning>

## CLI alternative

You can also manage subscriptions with the AgentRail CLI:

```bash theme={null}
agentrail event subscribe --url https://agents.example.com/webhooks/task-events \
  --event-types task.updated,task.reviewed,task.shipped
```

## Error responses

| Status | Code                 | Meaning                                                                                                                            |
| ------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `validation_error`   | Request body failed validation.                                                                                                    |
| `401`  | `unauthorized`       | Bearer token is missing or invalid.                                                                                                |
| `403`  | `insufficient_scope` | Key does not have `webhooks:write` (create) or `webhooks:read` (list).                                                             |
| `409`  | `conflict`           | An active subscription already exists for this endpoint and filter set, or the `Idempotency-Key` was reused with a different body. |
