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

# Create Endpoint

POST https://api.captain.dev/v2/webhooks/endpoints
Content-Type: application/json

Register an HTTPS URL that receives a signed request when any indexing job in the organization finishes, in any environment. The payload's `data.environment` field says where the job ran.

The response includes `secret`, the signing secret the receiver uses to verify requests. It is returned only in this response and cannot be read back, so store it right away. If it is lost, rotate it.

Endpoints belong to the organization, and an API key from any of its environments sees and manages the same endpoints. An organization can have up to 20 endpoints. Filters combine with AND across `collection_ids`, `sync_ids` and `sources`, and OR within each list; the product of the non-empty list sizes must be at most 10. See the [Webhooks](/guides/webhooks) guide.

Reference: https://docs.captain.dev/reference/webhooks/endpoints/create

## Request

### Body (application/json)

This endpoint expects a WebhookEndpointCreate.

- `url` (string, required) — HTTPS URL that receives events. Must start with `https://`, be reachable from the public internet, and be at most 2,048 characters.
- `collection_ids` (list of string, optional, nullable) — Only jobs that index into these collections, by collection id (up to 10). Empty or omitted means every collection.
- `description` (string, optional, nullable) — Free-text label, up to 200 characters.
- `disabled` (boolean, optional, default: false) — Create the endpoint without sending to it yet. Defaults to `false`.
- `event_types` (list of string, optional, nullable) — Events to receive: any of `job.completed`, `job.completed_with_errors`, `job.failed`, `job.timed_out`, `job.cancelled`. Empty or omitted means all five.
- `include_collection_name` (boolean, optional, default: true) — Send `collection_name` in payloads. When any endpoint receiving an event turns this off, that event carries `collection_name: null`.
- `sources` (list of string, optional, nullable) — Only jobs from these sources: `api` (started by a call to an index endpoint) or `sync` (started by a storage sync). Empty or omitted means both.
- `sync_ids` (list of string, optional, nullable) — Only jobs started by these syncs (up to 10). Requires `sources` to include `sync` or be empty.

## Response

### 201

Successful Response

- `endpoint_id` (string, required) — The endpoint's id (`whe_...`).
- `url` (string, required) — HTTPS URL that receives events.
- `description` (string, required) — Free-text label. Empty string when not set.
- `event_types` (list of string, required) — Events this endpoint receives. Empty means all five.
- `collection_ids` (list of string, required) — Collection filter. Empty means every collection.
- `sync_ids` (list of string, required) — Sync filter. Empty means no sync filter.
- `sources` (list of string, required) — Source filter (`api`, `sync`). Empty means both.
- `channels` (list of string, required) — Filters set in Captain Studio's Webhooks page that cannot be expressed as `collection_ids`, `sync_ids` and `sources`. Empty for endpoints whose filters were set through the API. Setting `collection_ids`, `sync_ids` or `sources` with `PATCH` replaces them.
- `include_collection_name` (boolean, required) — Whether payloads sent to this endpoint may carry `collection_name`.
- `disabled` (boolean, required) — `true` when no events are being sent to this endpoint.
- `disabled_reason` (string, required, nullable) — `delivery_failures` when Captain disabled the endpoint after sustained failed deliveries. `null` when it is enabled or was paused through the API.
- `last_delivery` (WebhookLastDelivery, required, nullable) — The most recent delivery, or `null` before the first one.
- `created_at` (string, required)
- `updated_at` (string, required)
- `secret` (string, required) — The signing secret (`whsec_...`). Returned only in this response.

## Errors

### 401 Unauthorized Error

Missing or invalid API key.

- `error` (string, required) — Error kind. `HTTP_ERROR` for errors raised by the route.
- `message` (string, required) — What went wrong.
- `path` (string, required) — The request path.
- `status_code` (integer, optional) — The HTTP status code.

### 403 Forbidden Error

The connection is read-only and cannot change webhooks (MCP connections without write access).

- `error` (string, required) — Error kind. `HTTP_ERROR` for errors raised by the route.
- `message` (string, required) — What went wrong.
- `path` (string, required) — The request path.
- `status_code` (integer, optional) — The HTTP status code.

### 409 Conflict Error

Conflict with the endpoint's current state, or the organization already has 20 endpoints.

- `error` (string, required) — Error kind. `HTTP_ERROR` for errors raised by the route.
- `message` (string, required) — What went wrong.
- `path` (string, required) — The request path.
- `status_code` (integer, optional) — The HTTP status code.

### 422 Unprocessable Entity Error

A value was rejected. A route check returns `HTTP_ERROR` and names the value in `message`, for example `url must start with https://`. A malformed body returns `VALIDATION_ERROR` with one entry per field in `details`.

- `error` (string, required) — `HTTP_ERROR` when a route check rejected a value, `VALIDATION_ERROR` when the body is malformed.
- `message` (string, required) — What went wrong. For `HTTP_ERROR`, names the rejected value.
- `path` (string, required) — The request path.
- `status_code` (integer, optional) — The HTTP status code. Present only on `HTTP_ERROR`.
- `details` (list of V2WebhooksEndpointsPostResponsesContentApplicationJsonSchemaDetailsItems, optional) — Present only on `VALIDATION_ERROR`: one entry per invalid field, with `type`, `loc` and `msg`.

### 429 Too Many Requests Error

`Too many webhook management requests. Try again shortly.` Retry after the `Retry-After` header.

- `error` (string, required) — Error kind. `HTTP_ERROR` for errors raised by the route.
- `message` (string, required) — What went wrong.
- `path` (string, required) — The request path.
- `status_code` (integer, optional) — The HTTP status code.

### 503 Service Unavailable Error

Webhooks are temporarily unavailable. Retry shortly.

- `error` (string, required) — Error kind. `HTTP_ERROR` for errors raised by the route.
- `message` (string, required) — What went wrong.
- `path` (string, required) — The request path.
- `status_code` (integer, optional) — The HTTP status code.

## Types

### WebhookLastDelivery

- `at` (string, required) — When the most recent delivery attempt finished.
- `status` (enum, required) — Outcome of that delivery.
  - Allowed values: `succeeded`, `failed`

### V2WebhooksEndpointsPostResponsesContentApplicationJsonSchemaDetailsItems

## Examples

**Request**

```json
{
  "url": "https://example.com/webhooks/captain",
  "description": "Indexing notifications",
  "event_types": [
    "job.completed",
    "job.completed_with_errors",
    "job.failed"
  ]
}
```

**Response**

```json
{
  "endpoint_id": "whe_3f9a1c7e5b2d4a6c8e0f1a2b",
  "url": "https://example.com/webhooks/captain",
  "description": "Indexing notifications",
  "event_types": [
    "job.completed",
    "job.completed_with_errors",
    "job.failed"
  ],
  "collection_ids": [],
  "sync_ids": [],
  "sources": [],
  "channels": [],
  "include_collection_name": true,
  "disabled": false,
  "disabled_reason": null,
  "last_delivery": null,
  "created_at": "2026-09-20T16:42:08Z",
  "updated_at": "2026-09-20T16:42:08Z",
  "secret": "whsec_kR2v9XoJ4mQ8sT1wY6zB3nC7fH0dL5gP2aE9uI4xV8c="
}
```

**SDK Code**

```python Python
import os

import requests

BASE_URL = "https://api.captain.dev"
API_KEY = "your_api_key"
SECRET_FILE = "captain-webhook.env"


def save_webhook_secret(secret):
    """Write the secret to an env file only this user can read. The receiver loads it as CAPTAIN_WEBHOOK_SECRET."""
    fd = os.open(SECRET_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    os.fchmod(fd, 0o600)  # also tightens a file that already existed
    with os.fdopen(fd, "w") as secret_file:
        secret_file.write(f"CAPTAIN_WEBHOOK_SECRET={secret}\n")


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()
data = response.json()
print(f"Endpoint: {data['endpoint_id']}")
save_webhook_secret(data["secret"])
print(f"Saved the signing secret to {SECRET_FILE}")

```

```typescript TypeScript
import { closeSync, fchmodSync, openSync, writeSync } from "node:fs";

const BASE_URL = "https://api.captain.dev";
const API_KEY = "your_api_key";
// The receiver loads this file and reads CAPTAIN_WEBHOOK_SECRET. Only this user can read it.
const SECRET_FILE = "captain-webhook.env";

function saveWebhookSecret(secret: string) {
  const fd = openSync(SECRET_FILE, "w", 0o600);
  fchmodSync(fd, 0o600); // also tightens a file that already existed
  writeSync(fd, `CAPTAIN_WEBHOOK_SECRET=${secret}\n`);
  closeSync(fd);
}

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(`Request failed: ${response.status} ${await response.text()}`);
const data = await response.json();
console.log(`Endpoint: ${data.endpoint_id}`);
saveWebhookSecret(data.secret);
console.log(`Saved the signing secret to ${SECRET_FILE}`);

```