# Create webhook

> Register a webhook.

Source: https://sndr.sh/docs/cranl/webhooks/create-webhook

`POST https://api.sndr.sh/api/cranl/v1/webhooks`

Register a webhook. Returns 201 with the webhook and its signing secret. This is the only time the full secret is shown, so store it now.

Scope: `send`

**Body** (JSON)

- `url` (string, required): Public HTTPS URL. Hosts that resolve to private, loopback or link-local addresses are rejected.
- `events` (string[], required): Events to receive, at least one of: email.delivered, email.bounced, email.failed, email.unsubscribed (email.queued and email.complained are accepted but not sent).

**Request**

```bash
curl -X POST 'https://api.sndr.sh/api/cranl/v1/webhooks' \
  -H 'Authorization: Bearer sndr_your_access_token' \
  -H 'Content-Type: application/json' \
  -d '{
    "url":    "https://your-app.com/webhooks/sndr",
    "events": ["email.delivered", "email.bounced", "email.failed"]
  }'
```

**Response** (201)

```json
{
  "id":              "wh_2a7d4e9c-6b1",
  "organization_id": "org_8d41c7a2-3f6",
  "url":             "https://your-app.com/webhooks/sndr",
  "events":          ["email.delivered", "email.bounced", "email.failed"],
  "secret_prefix":   "whsec_Xk9pQ2",
  "active":          true,
  "created_at":      "2026-06-01T12:00:00Z",
  "secret":          "Xk9pQ2vLr8TzW4mN1bYcE7hJ0aSdFgUi"
}
```

**Errors**

- `unauthenticated` (401): Missing or invalid Bearer token.
- `token_expired` (401): Access token expired — exchange the refresh token for a new pair.
- `token_revoked` (401): Token was explicitly revoked.
- `invalid_request` (400): Body is not valid JSON, url is missing or not a URL, or an event name is not supported.
- `plan_limit_reached` (402): The plan’s webhook limit is reached.
- `insufficient_scope` (403): Token is missing the send scope.
- `validation_failed` (422): url is not https, cannot be resolved, or points at a private address.
