# Webhooks API

> REST endpoints for webhooks — request and response reference with examples.

<!-- Source: https://docs.actuallycare.com/api/reference/webhooks -->

Manage webhook subscriptions and browse the self-documenting event catalog. Concepts and signature verification live in [Webhooks](/concepts/webhooks); the hands-on walkthrough is [Set up webhooks](/guides/webhook-setup).

Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors).

## List webhook subscriptions

```http
GET /v1/webhooks
```

Returns the webhook subscriptions registered for your account. Requires the broker or system admin role.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 |
| `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 |

### Example request

```bash title="cURL"
curl "https://api.actuallycare.com/v1/webhooks" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/webhooks", {
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.actuallycare.com/v1/webhooks",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": [
    {
      "id": "wh_abc123def456",
      "url": "example url",
      "events": [
        "escrow.created"
      ],
      "secret": "example secret",
      "active": true,
      "last_triggered": "2026-07-15T14:32:10.000Z",
      "created_at": "2026-07-15T14:32:10.000Z"
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Webhook subscriptions |
| `401` | Authentication required |
| `403` | Requires broker or system admin role |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Create a webhook subscription

```http
POST /v1/webhooks
```

Registers an endpoint to receive event notifications. Each delivery is an HTTPS POST with the JSON payload, an `X-Webhook-Event` header naming the event, an `X-Webhook-Timestamp` header, and an `X-Webhook-Signature` header containing `sha256=` followed by the hex-encoded HMAC-SHA256 of the payload computed with your secret. Each event gets 4 delivery attempts total: the initial delivery plus retries after 1, 5, and 15 minutes. Only HTTP 5xx, 408, and 429 responses (and network failures) are retried — any other 4xx response is logged and NOT retried.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | Yes | Endpoint to deliver events to · Format: uri |
| `events` | array of strings | Yes | Event names to subscribe to (see GET /webhooks/events for the catalog) |
| `secret` | string | Yes | Shared secret used to sign every delivery |

### Example request

```bash title="cURL"
curl -X POST "https://api.actuallycare.com/v1/webhooks" \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "your-url",
    "events": [
      "escrow.created"
    ],
    "secret": "your-secret"
  }'
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/webhooks", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_JWT",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "your-url",
    "events": [
      "escrow.created"
    ],
    "secret": "your-secret"
  }),
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.post(
    "https://api.actuallycare.com/v1/webhooks",
    headers={"Authorization": "Bearer YOUR_JWT"},
    json={
        "url": "your-url",
        "events": [
            "escrow.created"
        ],
        "secret": "your-secret"
    },
)
data = resp.json()
```

### Example response (201)

```json
{
  "success": true,
  "data": {
    "id": "wh_abc123def456",
    "url": "example url",
    "events": [
      "escrow.created"
    ],
    "secret": "example secret",
    "active": true,
    "last_triggered": "2026-07-15T14:32:10.000Z",
    "created_at": "2026-07-15T14:32:10.000Z"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Webhook created |
| `400` | Invalid request data |
| `401` | Authentication required |
| `403` | Requires broker or system admin role |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## List available webhook events

```http
GET /v1/webhooks/events
```

Returns the catalog of events you can subscribe to, including each event's category, description, and the field names included in its payload.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `category` | query | string | No | Filter to a single category (e.g. escrow, lead, email) |

### Example request

```bash title="cURL"
curl "https://api.actuallycare.com/v1/webhooks/events" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/webhooks/events", {
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.actuallycare.com/v1/webhooks/events",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": [
    {
      "event": "escrow.created",
      "category": "escrow",
      "description": "example description",
      "payload": [
        "example payload"
      ]
    }
  ],
  "meta": {
    "total": 150,
    "categories": [
      "escrow"
    ]
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Event catalog |
| `401` | Authentication required |
| `403` | Requires broker or system admin role |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## List webhook event categories

```http
GET /v1/webhooks/events/categories
```

Returns every event category with the number of events it contains.

### Example request

```bash title="cURL"
curl "https://api.actuallycare.com/v1/webhooks/events/categories" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/webhooks/events/categories", {
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.actuallycare.com/v1/webhooks/events/categories",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": [
    {
      "category": "escrow",
      "event_count": 3
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Categories with event counts |
| `401` | Authentication required |
| `403` | Requires broker or system admin role |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Update a webhook subscription

```http
PUT /v1/webhooks/{id}
```

Updates a subscription's URL, events, secret, or active flag. Only the fields you send are changed.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes | Webhook ID |

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | No | Endpoint that receives event deliveries · Format: uri |
| `events` | array of strings | No | Subscribed event names (entity.action format) |
| `active` | boolean | No | Whether the subscription is enabled to receive deliveries |
| `secret` | string | No | Shared secret used to sign deliveries (minimum 10 characters) |

### Example request

```bash title="cURL"
curl -X PUT "https://api.actuallycare.com/v1/webhooks/:id" \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "your-url",
    "events": [
      "your-events"
    ],
    "active": true,
    "secret": "your-secret"
  }'
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/webhooks/:id", {
  method: "PUT",
  headers: {
    "Authorization": "Bearer YOUR_JWT",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "url": "your-url",
    "events": [
      "your-events"
    ],
    "active": true,
    "secret": "your-secret"
  }),
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.put(
    "https://api.actuallycare.com/v1/webhooks/:id",
    headers={"Authorization": "Bearer YOUR_JWT"},
    json={
        "url": "your-url",
        "events": [
            "your-events"
        ],
        "active": true,
        "secret": "your-secret"
    },
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": {
    "id": "wh_abc123def456",
    "url": "example url",
    "events": [
      "escrow.created"
    ],
    "secret": "example secret",
    "active": true,
    "last_triggered": "2026-07-15T14:32:10.000Z",
    "created_at": "2026-07-15T14:32:10.000Z"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Webhook updated |
| `400` | Invalid request data |
| `401` | Authentication required |
| `404` | Resource not found |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Deactivate a webhook subscription

```http
DELETE /v1/webhooks/{id}
```

Deactivates a subscription so it stops receiving deliveries. The subscription record is kept and can be re-enabled by setting `active` back to true.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes | Webhook ID |

### Example request

```bash title="cURL"
curl -X DELETE "https://api.actuallycare.com/v1/webhooks/:id" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/webhooks/:id", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.delete(
    "https://api.actuallycare.com/v1/webhooks/:id",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": {
    "id": "wh_abc123def456",
    "url": "example url",
    "events": [
      "escrow.created"
    ],
    "secret": "example secret",
    "active": true,
    "last_triggered": "2026-07-15T14:32:10.000Z",
    "created_at": "2026-07-15T14:32:10.000Z"
  },
  "timestamp": "2026-07-01T22:51:14.902Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Webhook deactivated (response is the updated subscription with active=false) |
| `401` | Authentication required |
| `404` | Resource not found |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |

## Get webhook delivery logs

```http
GET /v1/webhooks/{id}/logs
```

Returns recent delivery attempts for a subscription, including the payload sent, the HTTP status your endpoint returned, and whether delivery succeeded.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | Yes | Webhook ID |
| `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 |
| `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 |

### Example request

```bash title="cURL"
curl "https://api.actuallycare.com/v1/webhooks/:id/logs" \
  -H "Authorization: Bearer YOUR_JWT"
```

```javascript title="JavaScript"
const res = await fetch("https://api.actuallycare.com/v1/webhooks/:id/logs", {
  headers: {
    "Authorization": "Bearer YOUR_JWT",
  },
});
const data = await res.json();
```

```python title="Python"
import requests

resp = requests.get(
    "https://api.actuallycare.com/v1/webhooks/:id/logs",
    headers={"Authorization": "Bearer YOUR_JWT"},
)
data = resp.json()
```

### Example response (200)

```json
{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "webhook_id": "550e8400-e29b-41d4-a716-446655440000",
      "event": "escrow.created",
      "payload": {},
      "response_status": 200,
      "delivered_at": "2026-07-15T14:32:10.000Z",
      "created_at": "2026-07-15T14:32:10.000Z"
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Delivery log entries, most recent first |
| `401` | Authentication required |
| `404` | Resource not found |
| `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. |
| `500` | Internal server error |
