Webhooks

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. 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.

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']}")

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.

The Endpoint management 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.

EventWhen it is sent
job.completedThe job finished and every file was indexed or skipped.
job.completed_with_errorsThe job finished and at least one file failed. The files that succeeded are searchable.
job.failedThe job failed. data.error_code says why.
job.timed_outThe job ran past its time limit.
job.cancelledThe job was cancelled with DELETE /v2/jobs/{job_id}.

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.

{
"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"
}
}
FieldTypeDescription
idstringevt_ followed by the job id. The same for every delivery of this job’s event.
typestringOne of the five event names.
timestampstringWhen the job finished, in ISO 8601 UTC.
data.job_idstringThe indexing job’s id, as returned when the job was started.
data.statusstringThe job’s final status: completed, completed_with_errors, failed, timed_out or cancelled.
data.sourcestringapi when the job was started by a call to an index endpoint, sync when a storage sync started it.
data.job_typestring or nullThe kind of indexing job, for example index_s3_directory.
data.collection_idstring or nullThe id of the collection the job indexed into.
data.collection_namestring or nullThe collection’s name. null when an endpoint receiving the event has include_collection_name set to false.
data.environmentstringThe environment the job ran in: development, staging or production.
data.sync_idstring or nullThe sync that started the job, or null for jobs started through the API.
data.created_atstring or nullWhen the job was created, in ISO 8601 UTC.
data.completed_atstring or nullWhen the job finished, in ISO 8601 UTC.
data.files.totalintegerFiles in the job.
data.files.indexedintegerFiles indexed.
data.files.failedintegerFiles that failed.
data.files.skippedintegerFiles skipped, for example because skip_existing matched an existing document.
data.error_codestring or nullWhy 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.testbooleantrue for a test event, false for a real job.
data.urlstringThe 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 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:

FieldMatches
event_typesOnly these events.
collection_idsOnly jobs that index into these collections. Collection ids are returned by GET /v2/collections.
sync_idsOnly jobs started by these syncs.
sourcesOnly 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.

{
"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.

{
"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:

{
"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:

{
"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

LimitValue
Endpoints per organization20
Values per filter list10
Filter combinations per endpoint10
description length200 characters
url length2,048 characters
Recovery window7 days
Old secret validity after rotation24 hours
Webhook management requests60 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 covers the 12 routes that create, update, test and recover endpoints. Webhook events 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.

© 2026 Captain