# Add a webhook

POST /v1/webhooks

Source: https://cherami.to/docs/api/webhooks/create-webhook



{/* Generated from apps/web/openapi. Edit the contract, not this file. */}

`POST /v1/webhooks`

Requires an [API key](https://cherami.to/docs/api#authentication): `Authorization: Bearer YOUR_CREDENTIAL`.

Adds a webhook: a destination URL subscribed to one or both event types, for every inbox of the account (including inboxes created later) or for at most 10 named inboxes. Returns `201` with the webhook and its `secret`; this response and explicit retrieval are the only places the secret appears.

`message.received` fires when received mail becomes ready to read. `message.sent` fires when the provider's acceptance of an outgoing message is persisted, which is not delivery. Payloads carry identifiers only (`id`, `type`, `timestamp`, `data.inbox_id`, `data.message_id`); fetch content through the authenticated API and deduplicate on the payload `id`.

The URL is validated for shape (https, public hostname, no credentials or fragment) and is not fetched by Cherami; the delivery provider refuses private or unroutable destinations at delivery time. There is no URL edit: add a new webhook and delete the old one.

No creation key exists. If the response is lost, [list webhooks](https://cherami.to/docs/api/webhooks/list-webhooks), match the URL and retrieve the secret explicitly; do not add again. A `503 webhook_creation_uncertain` names a pending webhook ID to delete before retrying. Verify signatures on the raw body and acknowledge with a 2xx within 15 seconds once you have durably accepted the event: see [Receive webhook notifications](https://cherami.to/docs/guides/webhooks#verify-and-acknowledge).

## Request body [#request-body]

JSON object. Total UTF-8 request body limit: 4096 bytes.

### Schema: CreateWebhook [#schema-createwebhook]

There is no URL edit: to change the destination, add a webhook and delete the old one.

| Field         | Required | Type                                                  | Meaning and constraints                                                                                                                                                                           |
| ------------- | -------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`         | Yes      | string                                                | https URL with a public hostname, no credentials or fragment, at most 2048 characters. Cherami does not fetch it; the provider applies private-network and redirect protections at delivery time. |
| `event_types` | Yes      | array of [WebhookEventType](#schema-webhookeventtype) | `minItems`: `1` `maxItems`: `2`                                                                                                                                                                   |
| `inbox_ids`   | No       | array of string                                       | Live inboxes this account owns, checked when the change is made. Empty or omitted means every inbox, including inboxes created later. `maxItems`: `10`                                            |

Unknown fields are rejected.

## curl example [#curl-example]

Replace resource-ID placeholders with returned IDs. Supply `CHERAMI_API_KEY` through your private shell environment.

Save the intended payload as a private `request.json` file and replace illustrative values. For an uncertain response, follow the [operation-specific recovery rules](https://cherami.to/docs/api/errors#retry-by-operation).

```json
{
  "url": "https://example.com/hooks/cherami",
  "event_types": [
    "message.received"
  ],
  "inbox_ids": [
    "22222222-2222-4222-8222-222222222222"
  ]
}
```

```sh
curl --silent --show-error --include --request POST \
  "https://cherami.to/v1/webhooks" \
  --header "Authorization: Bearer $CHERAMI_API_KEY" \
  --header 'Content-Type: application/json' \
  --data-binary @request.json
```

## Responses [#responses]

### HTTP 201 [#http-201]

Successful operation; inspect resource state and outcome fields.

* `X-Request-ID`: Support correlation ID, not an idempotency key.
* `Location`: Relative URL of the resulting resource.

Content type: `application/json`.

[CreatedWebhook](#schema-createdwebhook)

```json
{
  "id": "55555555-5555-4555-8555-555555555555",
  "created_at": "2026-10-01T00:00:00.000Z",
  "url": "https://example.com/hooks/cherami",
  "event_types": [
    "message.received"
  ],
  "inbox_ids": [
    "11111111-1111-4111-8111-111111111111"
  ],
  "deleted_inbox_ids": [],
  "enabled": true,
  "updated_at": "2026-10-01T00:00:00.000Z",
  "status": "active",
  "pending_operation": null,
  "secret_rotated_at": null,
  "previous_secret_expires_at": null,
  "secret": "whsec_EXAMPLE_NOT_A_REAL_SECRET"
}
```

### HTTP 400 [#http-400]

`invalid_json`: Send a valid UTF-8 JSON object, not an array or scalar.

`invalid_webhook`: Use only url, event\_types and inbox\_ids when adding a webhook.

`invalid_webhook_url`: Use an https URL with a public hostname, without credentials or a fragment. The provider also rejects private or unroutable destinations.

`invalid_webhook_event_types`: Supply a nonempty array containing message.received and/or message.sent.

`invalid_webhook_inbox_ids`: Supply inbox\_ids as an array of at most 10 inbox IDs, or an empty array for every inbox.

`unknown_webhook_inbox`: Every inbox\_id must be a live inbox of this account. error.details.inbox\_ids lists the rejected IDs.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 401 [#http-401]

`unauthorized`: Provide a valid bearer credential. Use [human-approved recovery](https://cherami.to/docs/guides/recovery) if access is lost.

* `X-Request-ID`: Support correlation ID, not an idempotency key.
* `WWW-Authenticate`: `"Bearer"`

Content type: `application/json`.

[Error](#schema-error)

### HTTP 413 [#http-413]

`body_too_large`: Reduce the JSON request to the endpoint's body limit.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 415 [#http-415]

`unsupported_media_type`: Send `Content-Type: application/json`.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

### HTTP 503 [#http-503]

`webhook_provider_unavailable`: The webhook provider refused or could not take the request and nothing was applied. Retry later.

`webhook_creation_uncertain`: The provider did not confirm the new webhook. error.details.webhook\_id is listed as pending: delete it and add again rather than adding a duplicate.

`webhook_secret_unavailable`: The signing secret could not be read. If error.details.webhook\_id is present the webhook was added; retrieve its secret explicitly.

`webhooks_unavailable`: Webhook management is unavailable or the operation's outcome is unknown. Read or list webhooks before repeating a change; adding again can create a duplicate.

* `X-Request-ID`: Support correlation ID, not an idempotency key.

Content type: `application/json`.

[Error](#schema-error)

## Schema: CreatedWebhook [#schema-createdwebhook]

status is the provider's live state: active delivers, disabled does not (set by you or by the provider after persistent failures), pending means creation or removal is unconfirmed, unknown means the provider could not be read or no longer has the endpoint. pending\_operation is distinct from status and names unconfirmed work. Secrets never appear here.

| Field                        | Required | Type                                                          | Meaning and constraints                                                                                                                          |
| ---------------------------- | -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                         | Yes      | string                                                        | Cherami resource ID, distinct from the RFC Message-ID. Use the returned value.                                                                   |
| `created_at`                 | Yes      | string                                                        | UTC service instant with milliseconds and Z suffix. `format`: `date-time` `pattern`: `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$`             |
| `url`                        | Yes      | string or null                                                |                                                                                                                                                  |
| `event_types`                | Yes      | array of [WebhookEventType](#schema-webhookeventtype) or null |                                                                                                                                                  |
| `inbox_ids`                  | Yes      | array of string or null                                       |                                                                                                                                                  |
| `deleted_inbox_ids`          | Yes      | array of string                                               | Scoped inbox IDs that are no longer live inboxes. They match nothing; re-scope to remove them.                                                   |
| `enabled`                    | Yes      | boolean or null                                               |                                                                                                                                                  |
| `updated_at`                 | Yes      | string or null                                                |                                                                                                                                                  |
| `status`                     | Yes      | `"active"` or `"disabled"` or `"pending"` or `"unknown"`      |                                                                                                                                                  |
| `pending_operation`          | Yes      | object or null                                                |                                                                                                                                                  |
| `secret_rotated_at`          | Yes      | string or null                                                |                                                                                                                                                  |
| `previous_secret_expires_at` | Yes      | string or null                                                |                                                                                                                                                  |
| `secret`                     | Yes      | string                                                        | Signing secret for Standard Webhooks verification (whsec\_ prefix). Returned on creation, explicit retrieval and rotation only. Store privately. |

## Schema: WebhookEventType [#schema-webhookeventtype]

message.received: received mail became ready to read. message.sent: provider acceptance of a send was persisted; not delivery.

`"message.received"` or `"message.sent"`

## Schema: Error [#schema-error]

| Field   | Required | Type   | Meaning and constraints |
| ------- | -------- | ------ | ----------------------- |
| `error` | Yes      | object |                         |

### `error` fields [#error-fields]

| Field     | Required | Type   | Meaning and constraints                                                                       |
| --------- | -------- | ------ | --------------------------------------------------------------------------------------------- |
| `code`    | Yes      | string | Programmatic error code. Handle unrecognized codes by status and operation-specific recovery. |
| `message` | Yes      | string | Human-readable context, not a stable string to match.                                         |

[HTTP conventions, errors and pagination](https://cherami.to/docs/api/errors) · [Download OpenAPI 3.1](https://cherami.to/openapi.json)
