DEVELOPER DOCUMENTATION
Platform Webhooks
Register platform webhooks, verify signatures, understand retries and automatic disabling, and redeliver failed lifecycle events.
Platform webhooks push lifecycle events to your platform as signed HTTPS POSTs. All endpoint management requires the webhooks:manage scope.
Registering an Endpoint
Section titled “Registering an Endpoint”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"] }'urlmust be https (plainhttpis allowed only forlocalhostduring development). Redirects are not followed — deliveries to a redirecting URL fail.eventTypesis 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
secretis 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 /contractsto reconcile anything earlier.
Anatomy of a Delivery
Section titled “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. Verify the signature against the raw request bytes before parsing — the scheme is identical to the Public API’s; the signature verification guide 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
Section titled “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.
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
Section titled “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:
# 1. Re-enable — resets the failure countercurl -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 downcurl -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 for anything emitted before the endpoint existed or after deliveries were discarded.
Redelivery
Section titled “Redelivery”POST /webhook-endpoints/{endpointId}/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
FAILEDdeliveries for the endpoint.
Requeued deliveries are only attempted while the endpoint is ACTIVE.
Managing Endpoints
Section titled “Managing Endpoints”# 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 eventscurl -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 discardedcurl -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
Section titled “Operations Checklist”- Verify the signature on raw bytes with a constant-time compare, and reject stale timestamps (guide)
- Dedupe by
X-OpenTrain-Delivery - Return
2xxwithin 10 seconds; queue slow work - Return
5xx(or any non-2xx) when processing genuinely fails, so OpenTrain retries - Monitor
consecutiveFailuresviaGET /webhook-endpoints/{endpointId}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