# Webhooks: Scarleta API reference

Page: https://www.scarleta.ai/docs/reference/webhooks
Base URL: https://api.scarleta.ai. Authentication: `Authorization: Bearer $SCARLETA_API_KEY`.
Operations: 6.

## GET /v1/webhooks

Operation id: `list_webhooks`

**List registered webhook addresses**

Returns the addresses registered for your account, oldest first. Registered addresses receive every `batch_completed` and `recipe_completed` event of your account, in addition to the `webhook_url` of a single request (an address identical to the request's `webhook_url` is delivered once). Each delivery is signed exactly like a `webhook_url` delivery, with the same 3 attempts (waiting 2 s and 4 s), 10-second timeout and no redirects. You can register at most 5 addresses.

Access: API key required; free and paid accounts. Cost: no token cost.

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `webhooks` | array<object> |   |
| `webhooks[].id` | string | Identifier of the registered address, for example `whk_3f9c1a7e5b2d4c8a6e01b9d3`. |
| `webhooks[].url` | string | The address deliveries are POSTed to. |
| `webhooks[].created_at` | number | When the address was registered, in milliseconds since the Unix epoch. |

### Responses

200: Success

```json
{
  "data": {
    "webhooks": [
      {
        "id": "whk_3f9c1a7e5b2d4c8a6e01b9d3",
        "url": "https://example.com/hooks/scarleta",
        "created_at": 1761650000000
      }
    ]
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl "https://api.scarleta.ai/v1/webhooks" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/webhooks

Operation id: `register_webhook`

**Register a webhook address**

Registers an address that receives your account's webhook events. Registered addresses receive every `batch_completed` and `recipe_completed` event of your account, in addition to the `webhook_url` of a single request (an address identical to the request's `webhook_url` is delivered once). Each delivery is signed exactly like a `webhook_url` delivery, with the same 3 attempts (waiting 2 s and 4 s), 10-second timeout and no redirects. You can register at most 5 addresses. Registering the same address again returns the existing registration and sends no second email. The address must be an https URL that resolves to a public address. The account owner is emailed a notice naming the address's host (never its path or query), because a registered address receives every event; `notice_sent` is false when no email was sent, and never means the registration failed.

Access: API key required; free and paid accounts. Cost: no token cost.

### Request body

| Field | Type | Description |
| --- | --- | --- |
| `url` | string | An https URL that resolves to a public address, at most 4096 bytes. Allowed: 1 to 4096 characters. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Identifier of the registered address, for example `whk_3f9c1a7e5b2d4c8a6e01b9d3`. |
| `url` | string | The address deliveries are POSTed to. |
| `created_at` | number | When the address was registered, in milliseconds since the Unix epoch. |
| `notice_sent` | boolean | True when an email notice was sent to the account owner. False when no email was sent, including when the address was already registered. A false value never means the registration failed. |

### Responses

201: Success

```json
{
  "data": {
    "id": "whk_3f9c1a7e5b2d4c8a6e01b9d3",
    "url": "https://example.com/hooks/scarleta",
    "created_at": 1761650000000,
    "notice_sent": true
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

400: `url` is missing, not a string, or not an https URL that resolves to a public address.

```json
{
  "error": {
    "code": "invalid_webhook_url",
    "message": "URL must use https",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_webhook_url"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

409: The account already has 5 registered addresses.

```json
{
  "error": {
    "code": "webhook_endpoint_limit_reached",
    "message": "You can register at most 5 webhook URLs. Delete one with DELETE /v1/webhooks/{id} first.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#webhook_endpoint_limit_reached"
  }
}
```

413: `url` is too long.

```json
{
  "error": {
    "code": "webhook_url_too_large",
    "message": "webhook_url exceeds 4096 bytes",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#webhook_url_too_large"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_json`](https://www.scarleta.ai/docs/errors#invalid_json), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`invalid_webhook_url`](https://www.scarleta.ai/docs/errors#invalid_webhook_url), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized), [`webhook_endpoint_limit_reached`](https://www.scarleta.ai/docs/errors#webhook_endpoint_limit_reached), [`webhook_url_too_large`](https://www.scarleta.ai/docs/errors#webhook_url_too_large), [`webhook_validation_unavailable`](https://www.scarleta.ai/docs/errors#webhook_validation_unavailable)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/webhooks" \
  -H "Authorization: Bearer $SCARLETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/scarleta"
  }'
```

## DELETE /v1/webhooks/{id}

Operation id: `delete_webhook`

**Delete a registered webhook address**

Permanently deletes a registered address and erases its stored URL; no further event is delivered to it. Only addresses registered by your own account can be deleted; any other id, or an already deleted one, returns 404. The account owner is emailed a notice naming the address's host (never its path or query); `notice_sent` is false when no email was sent, and never means the deletion failed.

Access: API key required; free and paid accounts. Cost: no token cost.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The `id` returned when the address was registered. |

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `id` | string |   |
| `deleted` | true |   |
| `notice_sent` | boolean | True when an email notice was sent to the account owner. A false value never means the deletion failed. |

### Responses

200: Success

```json
{
  "data": {
    "id": "whk_3f9c1a7e5b2d4c8a6e01b9d3",
    "deleted": true,
    "notice_sent": true
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: No active registered address with this id exists for your account.

```json
{
  "error": {
    "code": "webhook_not_found",
    "message": "Webhook not found",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#webhook_not_found"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized), [`webhook_not_found`](https://www.scarleta.ai/docs/errors#webhook_not_found)

### Example

```bash
ID="YOUR_ID"
curl -X DELETE "https://api.scarleta.ai/v1/webhooks/$ID" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## GET /v1/webhooks/signing-secret

Operation id: `get_signing_secret`

**Get the signing secret**

Returns the secret that signs every webhook delivery of your account, creating it on the first call. Every key of one account returns the same secret. Verify a delivery by recomputing `v1=` plus the lowercase hex HMAC-SHA256 of `<X-Scarleta-Timestamp>.<raw body>` keyed with this secret, and reject a timestamp more than 300 seconds from your clock. After a revocation this route answers 409 `signing_secret_revoked` until you rotate.

Access: API key required; free and paid accounts. Cost: no token cost.

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `secret` | string | Your signing secret: 64 lowercase hex characters. Keep it private. |
| `created_at` | number | When this secret was created, in milliseconds since the Unix epoch. |

### Responses

200: Success

```json
{
  "data": {
    "secret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "created_at": 1761650000000
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

409: The secret was revoked; no secret is returned.

```json
{
  "error": {
    "code": "signing_secret_revoked",
    "message": "The signing secret was revoked. Call POST /v1/webhooks/signing-secret/rotate to create a new one.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#signing_secret_revoked"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`signing_secret_revoked`](https://www.scarleta.ai/docs/errors#signing_secret_revoked), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl "https://api.scarleta.ai/v1/webhooks/signing-secret" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/webhooks/signing-secret/revoke

Operation id: `revoke_signing_secret`

**Revoke the signing secret**

Revokes the signing secret. No webhook is delivered, to any address, until you rotate to a new secret. A revoked secret is never accepted as the previous secret. The account owner is emailed a notice. `notice_sent` is false when no email was sent, including when the secret was already revoked: revoking an already revoked secret changes nothing, it answers 200 with the original `revoked_at` and `notice_sent: false`, and sends no second email. Answers 404 `no_signing_secret` when the account has no secret yet.

Access: API key required; free and paid accounts. Cost: no token cost.

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `revoked_at` | number | When the secret was revoked, in milliseconds since the Unix epoch. |
| `notice_sent` | boolean | True when an email notice was sent to the account owner. False when no email was sent, including when the secret was already revoked. A false value never means the revocation failed. |

### Responses

200: Success

```json
{
  "data": {
    "revoked_at": 1761650000000,
    "notice_sent": true
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

404: The account never created a signing secret.

```json
{
  "error": {
    "code": "no_signing_secret",
    "message": "No signing secret exists for this account yet.",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#no_signing_secret"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`no_signing_secret`](https://www.scarleta.ai/docs/errors#no_signing_secret), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/webhooks/signing-secret/revoke" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```

## POST /v1/webhooks/signing-secret/rotate

Operation id: `rotate_signing_secret`

**Rotate the signing secret**

Creates a new signing secret and returns it. The new secret is in this response and can be read again with `GET /v1/webhooks/signing-secret`. For one day after a rotation each delivery also carries `X-Scarleta-Signature-Previous`, signed with the old secret, so a receiver that has not switched yet can verify that header with its old secret and you can switch over without dropping events. A rotation after a revocation restores deliveries. The account owner is emailed a notice that never contains the secret; `notice_sent` is false when no email was sent.

Access: API key required; free and paid accounts. Cost: no token cost.

### Response fields (inside `data`)

| Field | Type | Description |
| --- | --- | --- |
| `secret` | string | Your signing secret: 64 lowercase hex characters. Keep it private. |
| `created_at` | number | When this secret was created, in milliseconds since the Unix epoch. |
| `notice_sent` | boolean | True when an email notice was sent to the account owner. A false value never means the rotation failed. |

### Responses

200: Success

```json
{
  "data": {
    "secret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "created_at": 1761650000000,
    "notice_sent": true
  },
  "meta": {
    "request_id": "req_0b7a3c2e-6f1d-4d9a-8e55-3a4b5c6d7e8f"
  }
}
```

401: Missing, malformed, unknown or revoked API key.

```json
{
  "error": {
    "code": "invalid_key",
    "message": "Invalid or revoked API key",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#invalid_key"
  }
}
```

429: Over the account request budget or the per-minute cap of this route. Wait for the `Retry-After` header, then retry.

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#rate_limit_exceeded"
  }
}
```

503: Temporary platform condition; retry shortly.

```json
{
  "error": {
    "code": "auth_unavailable",
    "message": "Authentication service temporarily unavailable",
    "request_id": "req_5b1c0f3e-2d41-4a8e-9a57-0c1b2d3e4f50",
    "docs_url": "https://www.scarleta.ai/docs/errors#auth_unavailable"
  }
}
```

### Error codes

[`auth_unavailable`](https://www.scarleta.ai/docs/errors#auth_unavailable), [`invalid_auth_format`](https://www.scarleta.ai/docs/errors#invalid_auth_format), [`invalid_key`](https://www.scarleta.ai/docs/errors#invalid_key), [`invalid_key_format`](https://www.scarleta.ai/docs/errors#invalid_key_format), [`missing_api_key`](https://www.scarleta.ai/docs/errors#missing_api_key), [`missing_auth`](https://www.scarleta.ai/docs/errors#missing_auth), [`rate_limit_exceeded`](https://www.scarleta.ai/docs/errors#rate_limit_exceeded), [`rate_limit_unavailable`](https://www.scarleta.ai/docs/errors#rate_limit_unavailable), [`unauthorized`](https://www.scarleta.ai/docs/errors#unauthorized)

### Example

```bash
curl -X POST "https://api.scarleta.ai/v1/webhooks/signing-secret/rotate" \
  -H "Authorization: Bearer $SCARLETA_API_KEY"
```
