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

# POST /v1/subscriptions

> Create a new webhook subscription with a destination URL and event list. Returns the full resource including the signing secret, which is only revealed once.

Use this endpoint to create a new subscription that delivers webhook events to your HTTPS endpoint. Once created, Off the Hook immediately begins matching on-chain activity against the subscription's address filters and queues deliveries signed with the subscription's secret. The secret is returned only in this response — store it somewhere safe before the request completes.

## Request

**`POST /v1/subscriptions`**

### Headers

| Header            | Required | Description                                                                                                                                  |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization`   | Yes      | `Bearer oth_YOUR_API_KEY`                                                                                                                    |
| `Content-Type`    | Yes      | `application/json`                                                                                                                           |
| `Idempotency-Key` | No       | Unique string to make this request idempotent. Re-sending the same key returns the original response without creating a second subscription. |

### Body

<ParamField body="destination" type="object" required>
  Webhook delivery target. Only HTTPS is supported today; additional destination types (SQS, Pub/Sub) are planned.

  <Expandable title="destination properties">
    <ParamField body="destination.type" type="string" required>
      Must be `"https"`. This is the only supported destination type in the current release.
    </ParamField>

    <ParamField body="destination.url" type="string" required>
      The HTTPS URL Off the Hook will POST webhook events to. The URL must be publicly reachable and must not resolve to a private IP range (loopback, RFC 1918, link-local, etc.). Validation is performed at registration time and again on each delivery attempt.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="events" type="string[]" required>
  One or more event kinds to subscribe to. Must be non-empty. Example: `["wallet.transfer.broadcasted"]`. See [Supported events](/api/subscriptions/create#supported-events) for valid values.
</ParamField>

<ParamField body="status" type="string" default="enabled">
  Whether the subscription should immediately receive events. Accepted values: `"enabled"` or `"disabled"`. Defaults to `"enabled"` when omitted.
</ParamField>

<ParamField body="description" type="string">
  A human-readable label for your own reference. Not interpreted by Off the Hook.
</ParamField>

### Supported events

| Event kind                    | Status                       |
| ----------------------------- | ---------------------------- |
| `wallet.transfer.broadcasted` | Available                    |
| `service.ping`                | Available (fired by `/test`) |
| `wallet.transfer.confirmed`   | Planned                      |
| `wallet.transfer.failed`      | Planned                      |

### Example request

```bash theme={null}
curl -X POST https://api.offthehook.dev/v1/subscriptions \
  -H "Authorization: Bearer oth_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-unique-key-123" \
  -d '{
    "destination": {
      "type": "https",
      "url": "https://your-server.example.com/webhooks"
    },
    "events": ["wallet.transfer.broadcasted"],
    "status": "enabled",
    "description": "Production wallet monitor"
  }'
```

## Response

**`201 Created`**

```json theme={null}
{
  "id": "sub_2QkP9aB7xN...",
  "destination": {
    "type": "https",
    "url": "https://your-server.example.com/webhooks",
    "secret": "whsec_..."
  },
  "events": ["wallet.transfer.broadcasted"],
  "filters": {
    "addresses": [],
    "hasMore": false
  },
  "status": "enabled",
  "description": "Production wallet monitor",
  "dateCreated": "2026-05-08T12:00:00Z",
  "dateUpdated": "2026-05-08T12:00:00Z"
}
```

<Warning>
  The `secret` field is only returned when creating a subscription or rotating the secret. Store it immediately — subsequent GET requests will not include it.
</Warning>

### Response fields

<ResponseField name="id" type="string" required>
  Unique subscription identifier. Always prefixed with `sub_`.
</ResponseField>

<ResponseField name="destination" type="object" required>
  Webhook delivery target.

  <Expandable title="destination properties">
    <ResponseField name="destination.type" type="string" required>
      Destination type. Currently always `"https"`.
    </ResponseField>

    <ResponseField name="destination.url" type="string" required>
      The HTTPS URL registered for this subscription.
    </ResponseField>

    <ResponseField name="destination.secret" type="string">
      The `whsec_`-prefixed signing secret used to produce `webhook-signature` headers. **Only present in the create and secret-rotation responses.** Pass this to a Standard Webhooks-compatible verification library.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="events" type="string[]" required>
  The event kinds this subscription will receive.
</ResponseField>

<ResponseField name="filters" type="object" required>
  Address filters attached to this subscription.

  <Expandable title="filters properties">
    <ResponseField name="filters.addresses" type="object[]">
      Up to 200 inlined address filters. Each entry has `chainId` (CAIP-2 identifier, e.g. `"tron:mainnet"`) and `address` (canonical base58check).
    </ResponseField>

    <ResponseField name="filters.hasMore" type="boolean">
      `true` when more than 200 address filters exist. Use `GET /v1/subscriptions/:id/filters/addresses` to paginate the full list.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="status" type="string" required>
  `"enabled"` or `"disabled"`.
</ResponseField>

<ResponseField name="description" type="string" required>
  The human-readable label supplied at creation time. Empty string if none was provided.
</ResponseField>

<ResponseField name="dateCreated" type="string" required>
  ISO 8601 timestamp of when the subscription was created.
</ResponseField>

<ResponseField name="dateUpdated" type="string" required>
  ISO 8601 timestamp of the most recent update.
</ResponseField>

## Errors

| Code                    | Description                                                                                                          |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`       | The request body is missing a required field, contains an invalid value, or the JSON is malformed.                   |
| `ssrf_blocked`          | The `destination.url` resolved to a private or reserved IP range. The error detail includes the blocked IP and CIDR. |
| `unsupported_event`     | One or more entries in `events` are not recognized. The error detail names the unsupported value.                    |
| `description_too_long`  | The `description` field exceeds the maximum allowed length.                                                          |
| `idempotency_in_flight` | Another request with the same `Idempotency-Key` is still being processed. Wait and retry.                            |
| `idempotency_conflict`  | A completed request with the same `Idempotency-Key` exists but had a different request body. Use a new key.          |
| `unauthorized`          | The `Authorization` header is missing or the API key is invalid.                                                     |
