Webhooks
Create Webhook
Subscribe an HTTPS endpoint to account events. The HMAC signing secret is returned once — store it immediately.
POST
Subscribes a URL to platform events — push delivery of the same events
GET /updates serves. Each delivery is signed with HMAC-SHA256 via the X-OpenTrain-Signature header; verify it with the signature guide.
The secret is returned only in this response. It cannot be retrieved later — if lost, delete the subscription and create a new one. There is no backfill: a new subscription starts at the current event high-water mark and only receives events created after it; older events remain reachable via GET /updates.
Accounts can hold at most 10 subscriptions. For the polling-vs-webhooks decision framework and the full delivery lifecycle, see stay in sync.
Requirements: webhooks:manage scope + the public_api_webhooks feature + the matching read scope for every subscribed event type (e.g. proposal.received needs proposals:read).
Request
Absolute
https URL that will receive deliveries (max 2048 chars). Plain http is allowed only for localhost during development.Non-empty array of event types from the event catalog:
proposal.received, proposal.status_changed, message.received, contract.created, milestone.status_changed, payment.pending, approval.confirmed, contract.budget_state_changed. Duplicates are de-duplicated.Response
The subscription:
{id, url, eventTypes, status: "ACTIVE", createdAt, disabledAt, disabledReason} — same shape as GET /webhooks/{id}.The
whsec_… signing secret. Shown once — store it now.Reminds you to store the secret.
Deliveries
Each event is aPOST to your URL with Content-Type: application/json, User-Agent: OpenTrain-Webhooks/1.0, and headers X-OpenTrain-Event (the event type), X-OpenTrain-Delivery (unique delivery ID — dedupe on it), and X-OpenTrain-Signature (t=<unix seconds>,v1=<hex HMAC>). The body is exactly one /updates event record — IDs only, never content.
Respond with any 2xx within 10 seconds. Failures retry up to 5 attempts with backoff (1m, 5m, 30m, 120m); after 10 consecutive exhausted deliveries the subscription is auto-disabled — recover by deleting and re-creating it.
Errors
| Status | code | Meaning |
|---|---|---|
400 | BAD_REQUEST | Body not valid JSON; url missing/invalid/not https (details.field: "url"); eventTypes empty or contains an unsupported type (details.supportedEventTypes) |
401 | UNAUTHORIZED | Missing or invalid token |
403 | FORBIDDEN | Missing webhooks:manage, missing a subscribed event type’s read scope, or public_api_webhooks disabled |
409 | CONFLICT | Subscription limit reached (details: {limit: 10}) — delete an existing webhook first |