> ## Documentation Index
> Fetch the complete documentation index at: https://opentrain.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Platform Webhooks

> Register webhook endpoints for platform lifecycle events, verify signatures, understand the retry schedule and auto-disable behavior, and redeliver failed events.

Platform webhooks push [lifecycle events](/developers/annotation-platforms/lifecycle-events) to your platform as signed HTTPS POSTs. All endpoint management requires the `webhooks:manage` scope.

## Registering an Endpoint

```bash theme={null}
curl -sS -X POST https://app.opentrain.ai/api/partner/v1/webhook-endpoints \
  -H "Authorization: Bearer $OT_PARTNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-platform.example.com/hooks/opentrain",
    "eventTypes": ["contract.started", "contract.ended", "project_link.removed", "install.revoked"]
  }'
```

* `url` must be **https** (plain `http` is allowed only for `localhost` during development). Redirects are not followed — deliveries to a redirecting URL fail.
* `eventTypes` is a non-empty subset of the eight-event catalog: `contract.started`, `contract.ended`, `project_link.created`, `project_link.removed`, `install.revoked`, `milestone.funded`, `milestone.budget_low`, `milestone.budget_depleted`.
* **The signing `secret` is returned once, in the creation response only.** It cannot be retrieved later — if lost, delete the endpoint and create a new one (which mints a new secret).
* **There is no backfill.** A new endpoint starts at the current event high-water mark and only receives events created after registration. Use [`GET /contracts`](/developers/annotation-platforms/api-reference/contracts/list) to reconcile anything earlier.

## Anatomy of a Delivery

Each event is POSTed to your URL with `Content-Type: application/json` and:

| Header                  | Contents                                                         |
| ----------------------- | ---------------------------------------------------------------- |
| `X-OpenTrain-Event`     | The event type (e.g. `contract.started`)                         |
| `X-OpenTrain-Delivery`  | Unique delivery ID — dedupe on it; retries reuse the same ID     |
| `X-OpenTrain-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<rawBody>")>` |
| `User-Agent`            | `OpenTrain-Partner-Webhooks/1.0`                                 |

The body is one [event record](/developers/annotation-platforms/lifecycle-events#the-event-record). Verify the signature against the **raw request bytes** before parsing — the scheme is identical to the Public API's; the [signature verification guide](/developers/guides/verify-webhook-signatures) has tested Node and Python implementations.

Respond with any `2xx` within **10 seconds**. Do the actual work asynchronously if it might run long; a timeout counts as a failure.

## Retries

A failed delivery (non-2xx, timeout, or connection error) is retried up to **5 total attempts** with backoff delays of **1, 5, 30, and 120 minutes** after the first failure. A delivery that exhausts all 5 attempts is marked `FAILED` and can be requeued manually with [redeliver](#redelivery).

Because retries reuse the same `X-OpenTrain-Delivery` ID, idempotent handling is simple: record the IDs you have processed and return `200` immediately for repeats.

## Auto-Disable and Recovery

After **10 consecutive failed deliveries**, the endpoint is auto-disabled: its `status` flips to `DISABLED` (with `disabledAt` and `disabledReason` set), all its `PENDING` deliveries are marked `FAILED`, and no new deliveries are attempted. A successful delivery at any point resets `consecutiveFailures` to 0.

To recover:

```bash theme={null}
# 1. Re-enable — resets the failure counter
curl -sS -X PATCH https://app.opentrain.ai/api/partner/v1/webhook-endpoints/<ENDPOINT_ID> \
  -H "Authorization: Bearer $OT_PARTNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "ACTIVE"}'

# 2. Requeue everything that failed while you were down
curl -sS -X POST https://app.opentrain.ai/api/partner/v1/webhook-endpoints/<ENDPOINT_ID>/redeliver \
  -H "Authorization: Bearer $OT_PARTNER_TOKEN"
```

Then run a pull-side reconciliation against [`GET /contracts`](/developers/annotation-platforms/api-reference/contracts/list) for anything emitted before the endpoint existed or after deliveries were discarded.

## Redelivery

[`POST /webhook-endpoints/{endpointId}/redeliver`](/developers/annotation-platforms/api-reference/webhook-endpoints/redeliver):

* **With** `{"deliveryId": "..."}` — requeues that specific delivery regardless of its status (useful for replaying one event into a fixed handler).
* **Without** a body — requeues **all `FAILED` deliveries** for the endpoint.

Requeued deliveries are only attempted while the endpoint is `ACTIVE`.

## Managing Endpoints

```bash theme={null}
# List (newest first)
curl -sS https://app.opentrain.ai/api/partner/v1/webhook-endpoints \
  -H "Authorization: Bearer $OT_PARTNER_TOKEN"

# Inspect one — includes delivery health (status, consecutiveFailures, disabledReason)
curl -sS https://app.opentrain.ai/api/partner/v1/webhook-endpoints/<ENDPOINT_ID> \
  -H "Authorization: Bearer $OT_PARTNER_TOKEN"

# Change URL or subscribed events
curl -sS -X PATCH https://app.opentrain.ai/api/partner/v1/webhook-endpoints/<ENDPOINT_ID> \
  -H "Authorization: Bearer $OT_PARTNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"eventTypes": ["contract.started", "contract.ended", "install.revoked"]}'

# Delete — pending deliveries are discarded
curl -sS -X DELETE https://app.opentrain.ai/api/partner/v1/webhook-endpoints/<ENDPOINT_ID> \
  -H "Authorization: Bearer $OT_PARTNER_TOKEN"
```

The secret is never returned by `GET` or `PATCH`. Changing the URL via `PATCH` keeps the existing secret; only delete + create mints a new one.

## Operations Checklist

* [ ] Verify the signature on **raw bytes** with a constant-time compare, and reject stale timestamps ([guide](/developers/guides/verify-webhook-signatures))
* [ ] Dedupe by `X-OpenTrain-Delivery`
* [ ] Return `2xx` within 10 seconds; queue slow work
* [ ] Return `5xx` (or any non-2xx) when processing genuinely fails, so OpenTrain retries
* [ ] Monitor `consecutiveFailures` via [`GET /webhook-endpoints/{endpointId}`](/developers/annotation-platforms/api-reference/webhook-endpoints/get) and alert before it reaches 10
* [ ] On recovery: `PATCH {"status": "ACTIVE"}` → redeliver → pull-side reconcile
* [ ] Store the secret in a secrets manager at creation time — it is shown exactly once
