> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Webhooks

> Receive a signed HTTPS request when an indexing job finishes, instead of polling the job status endpoint.

A webhook is an HTTPS request that Captain sends to a URL of your choice when an indexing job reaches its final status. Register the URL once as a webhook endpoint. From then on, every indexing job in the organization reports its outcome to it: which job finished, how it finished, and how many files were indexed, failed or skipped.

With webhooks you don't need to poll `GET /v2/jobs/{job_id}` in a loop to find out when a job has finished. That endpoint still has the per-file detail, such as which files failed and why. The webhook tells you that a job finished and how, and `data.url` in the payload points at the job so you can fetch the rest.

## Set up an endpoint

Set up an endpoint in Captain Studio or with the API. Both manage the same endpoints: the API returns endpoints you added in Captain Studio, and Captain Studio shows endpoints you created with the API.

Endpoints belong to the organization. Each endpoint receives events for every indexing job in the organization, whichever environment the job ran in, and the payload's `data.environment` field says which one it was. Any API key in the organization lists and manages the same endpoints. An organization can have up to 20 endpoints.

The URL must start with `https://` and be reachable from the public internet.

### In Captain Studio

Open the **Webhooks** page in [Captain Studio](https://captain.dev/studio). From that page you can:

* Add an endpoint and choose which events it receives.
* Set filters that limit the endpoint to certain collections, syncs or sources.
* Send a test event to an endpoint.
* Read the delivery log for an endpoint, including the response your receiver returned to each attempt.
* Resend a single delivery, or recover every delivery that failed since a given time.
* View and rotate an endpoint's signing secret.

### With the API

Create an endpoint with `POST /v2/webhooks/endpoints`. This example subscribes to three of the five events.

#### Python

```python
import os

import requests

BASE_URL = "https://api.captain.dev"
API_KEY = "your_api_key"

response = requests.post(
    f"{BASE_URL}/v2/webhooks/endpoints",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "url": "https://example.com/webhooks/captain",
        "description": "Indexing notifications",
        "event_types": ["job.completed", "job.completed_with_errors", "job.failed"],
    },
    timeout=30.0,
)
response.raise_for_status()
endpoint = response.json()
print(f"Endpoint: {endpoint['endpoint_id']}")
```

#### TypeScript

```typescript
const BASE_URL = "https://api.captain.dev";
const API_KEY = "your_api_key";

const response = await fetch(`${BASE_URL}/v2/webhooks/endpoints`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com/webhooks/captain",
    description: "Indexing notifications",
    event_types: ["job.completed", "job.completed_with_errors", "job.failed"],
  }),
});
if (!response.ok) throw new Error(`Create failed: ${response.status}`);
const endpoint = await response.json();
console.log(`Endpoint: ${endpoint.endpoint_id}`);
```

The response includes `secret`, the signing secret that the receiver uses to verify requests. The API returns the secret only in this response. The **Webhooks** page in Captain Studio also shows the secret. To replace a secret, rotate it as described in [Rotate the signing secret](#rotate-the-signing-secret).

The [Endpoint management](/reference/webhooks/endpoints/create) section of the API reference documents each of the 12 routes.

## Events

Every indexing job sends exactly one of these five events, when it reaches its final status. The event name matches the job's final `status`.

| Event                                                                               | When it is sent                                                                         |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| [`job.completed`](/reference/webhooks/events/job-completed)                         | The job finished and every file was indexed or skipped.                                 |
| [`job.completed_with_errors`](/reference/webhooks/events/job-completed-with-errors) | The job finished and at least one file failed. The files that succeeded are searchable. |
| [`job.failed`](/reference/webhooks/events/job-failed)                               | The job failed. `data.error_code` says why.                                             |
| [`job.timed_out`](/reference/webhooks/events/job-timed-out)                         | The job ran past its time limit.                                                        |
| [`job.cancelled`](/reference/webhooks/events/job-cancelled)                         | The job was cancelled with `DELETE /v2/jobs/{job_id}`.                                  |
| [`parse.completed`](/reference/webhooks/events/parse-completed)                     | A parse job finished. Fetch `data.url` for its chunks.                                  |
| [`parse.failed`](/reference/webhooks/events/parse-failed)                           | A parse job failed. `data.error_code` says why. It is not billed.                       |
| [`parse.cancelled`](/reference/webhooks/events/parse-cancelled)                     | A parse job was cancelled with `DELETE /v3/parse/jobs/{job_id}`. It is not billed.      |

Each event's page in the API reference shows its payload schema, the request headers and a complete example.

`job.completed_with_errors` and `job.failed` differ in what landed. After `job.completed_with_errors`, the collection holds every file that succeeded, and `files.failed` counts the rest. After `job.failed` with `error_code` set to `all_files_failed` or `embedding_stalled`, nothing from the job was indexed. After `job.failed` with `execution_failed`, the job stopped with an error, and `files.indexed` counts any files indexed before it stopped. In every case, check the per-file errors on `GET /v2/jobs/{job_id}`, then retry the failed files or the whole job.

A job that includes video or audio sends its event after embedding finishes, not when the files are first processed. So a `job.completed` event for a media job means the media is searchable.

The event reflects the job's status when it finished. If Captain refines that status later, it doesn't send a second event. `GET /v2/jobs/{job_id}` always returns the current status.

Captain sends events only for jobs that finish while an endpoint is enabled. A new endpoint doesn't receive events for jobs that finished before it existed.

`GET /v2/webhooks/event-types` returns the five events with a JSON Schema and an example payload for each.

## Many jobs at once

One endpoint handles every job in the organization, however many run at once. Each job sends its own event, so 500 jobs send 500 separate requests to the same URL, and several can arrive at the same moment. Your endpoint needs to handle concurrent requests, as any web server does.

Every indexing request returns a `job_id`, and the event for that job carries the same value in `data.job_id`. Use it to match each event to the job you started. Events arrive in the order jobs finish, not the order they started, and a retried event can arrive after newer ones.

## Payload

Each request body is a JSON object with the event and a `data` object describing the job. This example is a `job.completed_with_errors` event for a job that a sync started.

```json
{
  "id": "evt_4f7c2a91-8d3e-4b6a-9f12-7e5d8c0b3a64",
  "type": "job.completed_with_errors",
  "timestamp": "2026-09-23T10:05:12Z",
  "data": {
    "job_id": "4f7c2a91-8d3e-4b6a-9f12-7e5d8c0b3a64",
    "status": "completed_with_errors",
    "source": "sync",
    "job_type": "index_s3_directory",
    "collection_id": "2b8e6f10-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
    "collection_name": "contracts",
    "environment": "production",
    "sync_id": "sync_7d2c91e0a4b3",
    "created_at": "2026-09-23T10:00:03Z",
    "completed_at": "2026-09-23T10:05:12Z",
    "files": {
      "total": 25,
      "indexed": 23,
      "failed": 2,
      "skipped": 0
    },
    "error_code": null,
    "test": false,
    "url": "https://api.captain.dev/v2/jobs/4f7c2a91-8d3e-4b6a-9f12-7e5d8c0b3a64"
  }
}
```

| Field                  | Type           | Description                                                                                                                                                                                                                       |
| ---------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                   | string         | `evt_` followed by the job id. The same for every delivery of this job's event.                                                                                                                                                   |
| `type`                 | string         | One of the five event names.                                                                                                                                                                                                      |
| `timestamp`            | string         | When the job finished, in ISO 8601 UTC.                                                                                                                                                                                           |
| `data.job_id`          | string         | The indexing job's id, as returned when the job was started.                                                                                                                                                                      |
| `data.status`          | string         | The job's final status: `completed`, `completed_with_errors`, `failed`, `timed_out` or `cancelled`.                                                                                                                               |
| `data.source`          | string         | `api` when the job was started by a call to an index endpoint, `sync` when a storage sync started it.                                                                                                                             |
| `data.job_type`        | string or null | The kind of indexing job, for example `index_s3_directory`.                                                                                                                                                                       |
| `data.collection_id`   | string or null | The id of the collection the job indexed into.                                                                                                                                                                                    |
| `data.collection_name` | string or null | The collection's name. `null` when an endpoint receiving the event has `include_collection_name` set to `false`.                                                                                                                  |
| `data.environment`     | string         | The environment the job ran in: `development`, `staging` or `production`.                                                                                                                                                         |
| `data.sync_id`         | string or null | The sync that started the job, or `null` for jobs started through the API.                                                                                                                                                        |
| `data.created_at`      | string or null | When the job was created, in ISO 8601 UTC.                                                                                                                                                                                        |
| `data.completed_at`    | string or null | When the job finished, in ISO 8601 UTC.                                                                                                                                                                                           |
| `data.files.total`     | integer        | Files in the job.                                                                                                                                                                                                                 |
| `data.files.indexed`   | integer        | Files indexed.                                                                                                                                                                                                                    |
| `data.files.failed`    | integer        | Files that failed.                                                                                                                                                                                                                |
| `data.files.skipped`   | integer        | Files skipped, for example because `skip_existing` matched an existing document.                                                                                                                                                  |
| `data.error_code`      | string or null | Why the job did not fully succeed. Known values are `all_files_failed`, `execution_failed`, `timed_out` and `embedding_stalled`. New values may be added, so treat an unknown value as a failure rather than rejecting the event. |
| `data.test`            | boolean        | `true` for a test event, `false` for a real job.                                                                                                                                                                                  |
| `data.url`             | string         | The job's status URL on the Captain API.                                                                                                                                                                                          |

The payload carries ids, counts and timestamps only. It never includes file names, file paths, error messages or `custom_metadata`. The only name it can include is `collection_name`. To leave that out, create the endpoint with `include_collection_name` set to `false`. Every endpoint that receives an event gets the same body, so when any of them turns the name off, the event carries `collection_name: null` for all of them.

Payloads are versioned. New fields may be added to an event without notice, so parse the body without rejecting unknown fields. A change that removes or renames a field is released as a new event name.

## Receive events

Captain sends each event as an HTTPS `POST` with the JSON payload above. Return any `2xx` status within a few seconds to confirm you received it. Any other status, or no response, counts as a failed delivery, and Captain sends the event again later.

Every request is signed with the endpoint's signing secret, following the [Standard Webhooks](https://www.standardwebhooks.com) specification, so you can confirm it came from Captain.

## Retries

When a delivery fails, Captain retries it for about 28 hours. The same event can occasionally arrive twice, so if your handler does something per job, key it on `data.job_id`. Each job sends exactly one event.

When every delivery to an endpoint keeps failing for five days, Captain disables it. Re-enable it on the **Webhooks** page in Captain Studio after fixing the receiver.

## Filters

By default an endpoint receives all five events for every job in the organization. Four optional fields narrow that:

| Field            | Matches                                                                                            |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| `event_types`    | Only these events.                                                                                 |
| `collection_ids` | Only jobs that index into these collections. Collection ids are returned by `GET /v2/collections`. |
| `sync_ids`       | Only jobs started by these syncs.                                                                  |
| `sources`        | Only jobs from these sources: `api` or `sync`.                                                     |

Values within one field are alternatives (OR). Different fields must all match (AND). For example, `collection_ids: ["c1", "c2"]` with `sources: ["sync"]` receives sync jobs in `c1` or `c2`, and nothing started through the API.

Each list holds up to 10 values. The collection, sync and source lists together allow up to 10 combinations, counted as the product of the sizes of the non-empty lists: 2 collections and 2 sources make 4, while 5 collections and 3 syncs make 15 and are rejected. Because `sync_ids` only match sync jobs, `sources` must include `sync`, or be empty, when `sync_ids` is set.

Change filters in Captain Studio or with `PATCH /v2/webhooks/endpoints/{endpoint_id}`. A `PATCH` changes only the fields in the request. Send an empty list to remove a filter.

Captain Studio can also set filters that do not reduce to these three lists. The API returns those in the endpoint's `channels` field and leaves `collection_ids`, `sync_ids` and `sources` empty. Setting any of the three lists with `PATCH` replaces them.

## Send a test event

A test event checks the receiver end to end without starting a job. Send one from the **Webhooks** page in Captain Studio, or with `POST /v2/webhooks/endpoints/{endpoint_id}/test`. The test goes to that endpoint only. It is signed like a real event, carries the example payload of the chosen event with placeholder ids, and has `data.test` set to `true`. Choose the event with `event_type`, which defaults to `job.completed`.

```json
{
  "event_type": "job.failed"
}
```

The response includes the test message's `message_id`. Pass it to the attempts endpoint described below to see what the receiver returned.

Have your receiver check for `data.test: true` and skip any work that acts on the job, since the job id doesn't exist.

## Check deliveries

The **Webhooks** page in Captain Studio shows each endpoint's delivery log. The same information is available from the API.

`GET /v2/webhooks/endpoints/{endpoint_id}/deliveries` lists the messages sent to an endpoint, newest first, with each one's status (`succeeded`, `failed`, `pending` or `sending`) and the time of the next retry. `GET /v2/webhooks/endpoints/{endpoint_id}/deliveries/{message_id}/attempts` lists every attempt for one message, with the HTTP status the receiver returned and how long it took. The `message_id` in these paths is either the delivery's `message_id` or the event's `id` (`evt_` followed by the job id).

`GET /v2/webhooks/endpoints` and `GET /v2/webhooks/endpoints/{endpoint_id}` also return `last_delivery`, the time and status of the most recent delivery.

## Recover missed deliveries

To send one message again, resend it from the delivery log in Captain Studio, or call `POST /v2/webhooks/endpoints/{endpoint_id}/deliveries/{message_id}/resend`. The resent request has the same `webhook-id` and body as the original.

To send everything that failed during an outage, recover the endpoint from the time the problem started. In Captain Studio, use the endpoint's recover action. With the API, call `POST /v2/webhooks/endpoints/{endpoint_id}/recover` with the start time. Either way, every message that failed to reach the endpoint since then is sent again. The time must be within the last 7 days.

```json
{
  "since": "2026-09-22T08:00:00Z"
}
```

Recovery runs in the background, and the request returns `202` immediately. Follow progress in the delivery log.

## Rotate the signing secret

Rotate the secret from the **Webhooks** page in Captain Studio, or with `POST /v2/webhooks/endpoints/{endpoint_id}/rotate-secret`. The API creates a new secret and returns it once, in the `secret` field.

For the next 24 hours, each request carries two signatures, one made with the old secret and one with the new one, so the receiver keeps working whichever secret it holds. Deploy the new secret to the receiver within those 24 hours. After that, only the new secret is used.

Rotate the secret when it may have been exposed, or when the original was lost.

## Pause or delete an endpoint

To stop deliveries without losing the endpoint's settings and history, disable the endpoint in Captain Studio, or send `PATCH /v2/webhooks/endpoints/{endpoint_id}` with `{"disabled": true}`. Send `{"disabled": false}` to resume. A disabled endpoint doesn't receive events for jobs that finish while it's disabled.

Deleting an endpoint in Captain Studio or with `DELETE /v2/webhooks/endpoints/{endpoint_id}` removes the endpoint and its delivery history, and you can't undo it.

## Set up endpoints from an agent

The Captain MCP server manages webhooks with two tools.

`captain_webhook_setup` manages endpoints. Its `action` argument is one of `create`, `list`, `get`, `update`, `delete`, `rotate_secret` or `test`. This call creates an endpoint for two events and returns the endpoint with its signing secret:

```json
{
  "action": "create",
  "url": "https://example.com/webhooks/captain",
  "event_types": [
    "job.completed",
    "job.failed"
  ]
}
```

`captain_webhook_events` reads event types and delivery history and sends deliveries again. Its `action` argument is one of `event_types`, `deliveries`, `attempts`, `resend` or `recover`. This call lists the deliveries to one endpoint with each delivery's status:

```json
{
  "action": "deliveries",
  "endpoint_id": "whe_3f9a1c7e5b2d4a6c8e0f1a2b"
}
```

To verify a receiver, call `captain_webhook_setup` with `{"action": "test", "endpoint_id": "whe_3f9a1c7e5b2d4a6c8e0f1a2b"}`, then `captain_webhook_events` with the `attempts` action and the returned `message_id` to read the status code the receiver returned.

## Limits

| Limit                              | Value                          |
| ---------------------------------- | ------------------------------ |
| Endpoints per organization         | 20                             |
| Values per filter list             | 10                             |
| Filter combinations per endpoint   | 10                             |
| `description` length               | 200 characters                 |
| `url` length                       | 2,048 characters               |
| Recovery window                    | 7 days                         |
| Old secret validity after rotation | 24 hours                       |
| Webhook management requests        | 60 per minute per organization |

Requests over the management limit return `429` with a `Retry-After` header. The limit applies to the `/v2/webhooks` endpoints only, not to deliveries.

## API reference

The API reference documents every route and event in full. [Endpoint management](/reference/webhooks/endpoints/create) covers the 12 routes that create, update, test and recover endpoints. [Webhook events](/reference/webhooks/events/job-completed) shows the payload of each of the five events Captain sends.

On the Captain MCP server, `captain_webhook_setup` covers the endpoint routes and `captain_webhook_events` covers event types, deliveries, resends and recovery. No tool reads a secret back.