Skip to main content
Platform webhooks push lifecycle events to your platform as signed HTTPS POSTs. All endpoint management requires the webhooks:manage scope.

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"]
  }'
  • 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 to reconcile anything earlier.

Anatomy of a Delivery

Each event is POSTed to your URL with Content-Type: application/json and:
HeaderContents
X-OpenTrain-EventThe event type (e.g. contract.started)
X-OpenTrain-DeliveryUnique delivery ID — dedupe on it; retries reuse the same ID
X-OpenTrain-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<rawBody>")>
User-AgentOpenTrain-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

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

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 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 for anything emitted before the endpoint existed or after deliveries were discarded.

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 FAILED deliveries for the endpoint.
Requeued deliveries are only attempted while the endpoint is ACTIVE.

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 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)
  • 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} 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