# POST /v1/webhooks

Create a webhook

Registers a URL to receive events. The answer carries the signing secret once; every delivery is signed with it (HMAC-SHA256 over the body) so the receiver can verify Sendora sent it. A second secret can be created later to roll the first one.

Takes a server key (`sk_…`, or `sk_test_…` on a test server) as a bearer token. TypeScript: `sendora.webhooks.create({ url, events })`. Python: `sendora.webhooks.create(url=..., events=...)`.

## Request body

- `url` (string, required): An https URL on a public host, without credentials or a fragment.
- `events` (array of "delivered" | "bounced" | "deferred" | "spam_complaint" | "unsubscribed" | "cap_warning" | "cap_reached" | "inbound", at least 1): The events to deliver; every event when left out.
- `streamId` (string (uuid)): Limits the message events to one stream of the server; every stream when left out. Cap events concern the server and arrive either way.
- `inboundContent` ("full" | "reference"): How the inbound event carries a received message: full sends the parsed message with the event; reference sends ids and sizes only, for you to fetch. full when left out.

Example:

```json
{
  "url": "https://example.se/hooks/sendora",
  "events": [
    "delivered",
    "bounced",
    "spam_complaint"
  ]
}
```

## Responses

### 201 The new webhook with its signing secret, shown this once.

- `webhookId` (string (uuid), required)
- `url` (string, required): Where the events are posted.
- `events` (array of "delivered" | "bounced" | "deferred" | "spam_complaint" | "unsubscribed" | "cap_warning" | "cap_reached" | "inbound", required)
- `streamId` (string (uuid) or null, required): The one stream whose message events it receives; null for every stream.
- `enabled` (boolean, required): False while switched off in the dashboard; nothing is queued for it then.
- `inboundContent` ("full" | "reference", required): How the inbound event carries a received message: the parsed message, or a reference.
- `createdAt` (string (date-time), required)
- `secrets` (array of object, required): The live secrets, oldest first; every delivery is signed with each of them, the newest first in Sendora-Signature.
  - `secretId` (string (uuid), required)
  - `createdAt` (string (date-time), required)
- `secret` (string, required): The signing secret, shown this once.

Example:

```json
{
  "webhookId": "7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70",
  "url": "https://example.se/hooks/sendora",
  "events": [
    "delivered",
    "bounced",
    "spam_complaint"
  ],
  "streamId": null,
  "enabled": true,
  "inboundContent": "full",
  "createdAt": "2026-09-15T12:00:00.000Z",
  "secrets": [
    {
      "secretId": "4a5b6c7d-8e9f-4a0b-8c1d-2e3f4a5b6c7d",
      "createdAt": "2026-09-15T12:00:00.000Z"
    }
  ],
  "secret": "whsec_9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c"
}
```

## Errors

Every error answers `error`, the code, and `message`, a sentence for a person. A code that adds fields is shown in full below the table.

| Code | Status | Meaning |
| --- | --- | --- |
| `invalid_request` | 400 | The body, the query or a header does not match what the route takes. |
| `unauthorized` | 401 | The key is missing, malformed or revoked. |
| `wrong_token_kind` | 403 | The key is of the other kind: a server key (sk_) where an account key (ak_) is needed, or the reverse. The message names the kind the operation takes. |
| `webhook_exists` | 409 | The server already has a webhook for that URL. |
| `stream_not_found` | 422 | The streamId names no stream of this server. |

### `invalid_request`

- `issues` (array of object): One entry per invalid field; absent when a header is wrong.
  - `path` (string, required): The field, dotted, such as to.0.email; empty when the whole body is wrong.
  - `message` (string, required)

Example:

```json
{
  "error": "invalid_request",
  "message": "The request is invalid: to.0: Invalid email address",
  "issues": [
    {
      "path": "to.0",
      "message": "Invalid email address"
    }
  ]
}
```

### `webhook_exists`

- `webhookId` (string (uuid), required): The id of the webhook the server already has for that URL.

Example:

```json
{
  "error": "webhook_exists",
  "message": "This server already has a webhook for that URL.",
  "webhookId": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
```
