---
title: Webhooks Reference
path: reference/webhooks
status: published
---

# Webhooks Reference

Endpoints for managing webhook endpoints and the Event Webhook settings. For the guide, see [Webhooks](../tutorials/webhooks).

**Base paths:**
- `/v3/user/webhooks` — endpoint CRUD (multiple URLs, per-URL event set)
- `/v3/user/webhooks/event/settings` — single-URL model with per-type booleans

**Required permission:** `webhooks.read` for reads, `webhooks.write` for writes.

## GET /v3/user/webhooks

List webhook endpoints.

**Query parameters:**

| Parameter | Notes |
|-----------|-------|
| `page` | 1-indexed (default 1) |
| `page_size` | 1–100 (default 20) |
| `is_active` | Filter by enabled state |

**Response (200):**

```json
{
  "webhooks": [
    {
      "id": "wh_01HXYZ",
      "url": "https://api.example.com/webhooks/scaisend",
      "enabled_events": ["delivered", "bounce"],
      "enabled": true,
      "created_at": "2026-04-23T10:00:00Z",
      "last_success_at": "2026-04-23T10:15:30Z",
      "last_failure_at": null,
      "failure_count": 0,
      "disabled_at": null
    }
  ],
  "total": 1
}
```

## POST /v3/user/webhooks

Create a webhook endpoint.

**Request body:**

| Field | Type | Required | Notes |
|-------|------|---------|-------|
| `url` | string | Yes | HTTPS URL; HTTP is accepted for local development but refused in production |
| `enabled_events` | array | Yes | Event types to subscribe to, or `["*"]` for all |
| `oauth_client_id` | string | No | If set, ScaiSend does OAuth2 before delivering |
| `oauth_client_secret` | string | No | Paired with `oauth_client_id` |

**Response (201):**

```json
{
  "id": "wh_01HXYZ",
  "url": "https://api.example.com/webhooks/scaisend",
  "enabled_events": ["delivered", "bounce"],
  "signing_secret": "whsec_abc123...",
  "enabled": true,
  "created_at": "2026-04-23T10:00:00Z"
}
```

**The `signing_secret` is returned exactly once.** Save it.

## GET /v3/user/webhooks/{webhook_id}

Get a single endpoint.

**Response (200):** endpoint object (without `signing_secret`).

## PATCH /v3/user/webhooks/{webhook_id}

Update an endpoint.

**Request body:** any subset of:

| Field | Notes |
|-------|-------|
| `url` | New URL |
| `enabled_events` | New event set |
| `enabled` | Pause/resume delivery |

**Response (200):** updated endpoint.

## DELETE /v3/user/webhooks/{webhook_id}

Delete an endpoint. Past delivery records are also removed.

**Response (204):** no body.

## POST /v3/user/webhooks/{webhook_id}/signing_secret

Rotate the signing secret.

**Response (200):**

```json
{"webhook_id": "wh_01HXYZ", "signing_secret": "whsec_newSecret..."}
```

The old secret stops validating immediately. Update your verifier before rotating, or accept both briefly during rollout.

## GET /v3/user/webhooks/event/settings

Get the Event Webhook settings (single-URL model).

**Response (200):**

```json
{
  "enabled": true,
  "url": "https://api.example.com/webhooks/scaisend",
  "processed": true,
  "deferred": false,
  "delivered": true,
  "bounce": true,
  "dropped": true,
  "open": true,
  "click": true,
  "spam_report": true,
  "unsubscribe": true,
  "group_unsubscribe": true,
  "group_resubscribe": false
}
```

## PATCH /v3/user/webhooks/event/settings

Update Event Webhook settings. Partial update — unspecified fields retain their value.

**Request body:** any subset of the response fields above.

**Response (200):** updated settings.

## Webhook request format

Every outbound request from ScaiSend carries:

| Header | Value |
|--------|-------|
| `Content-Type` | `application/json` |
| `User-Agent` | `ScaiSend-Webhook/1.0` |
| `X-ScaiSend-Event` | Event type |
| `X-ScaiSend-Timestamp` | Unix timestamp |
| `X-ScaiSend-Signature` | HMAC-SHA256 of `{timestamp}.{body}` |

**Payload:**

```json
{
  "event_id": "evt_01HXYZ",
  "event_type": "delivered",
  "timestamp": 1713888000,
  "message_id": "msg_01ABC",
  "recipient_email": "user@example.com",
  "tenant_id": "tnt_acme",
  "metadata": {
    "smtp_response": "250 2.0.0 OK"
  }
}
```

See [Events and Webhooks](../concepts/events-and-webhooks) for the per-event `metadata` fields.

## Signature verification

```python
import hmac, hashlib, time

def verify(body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), f"{timestamp}.{body.decode()}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

Reject requests with a timestamp more than 5 minutes off from wall-clock. That prevents replay attacks.

## Retry policy

ScaiSend retries failed deliveries (any non-2xx response or timeout > 30 seconds) with exponential backoff:

| Attempt | Delay after previous failure |
|---------|------------------------------|
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 2 hours |

After 6 failed attempts, the delivery is marked failed and not retried. After 10 consecutive failures across deliveries, the endpoint is auto-disabled (`disabled_at` set). Re-enable with `PATCH {"enabled": true}`.

See [Webhooks Deep Dive](../tutorials/webhooks-deep-dive) for the details.

## Event types

Full list of `X-ScaiSend-Event` values (and the `event_type` field):

- `processed`
- `deferred`
- `delivered`
- `bounce`
- `blocked`
- `dropped`
- `open`
- `click`
- `spam_report`
- `unsubscribe`
- `group_unsubscribe`
- `group_resubscribe`

## Related

- [Webhooks (guide)](../tutorials/webhooks)
- [Webhooks Deep Dive](../tutorials/webhooks-deep-dive)
- [Events and Webhooks (concept)](../concepts/events-and-webhooks)
