Skip to main content
POST
/
api
/
public
/
v1
/
webhooks
curl -sS -X POST https://app.opentrain.ai/api/public/v1/webhooks \
  -H "Authorization: Bearer $OT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/opentrain",
    "eventTypes": ["proposal.received", "message.received", "approval.confirmed"]
  }'
{
  "webhook": {
    "id": "<WEBHOOK_ID>",
    "url": "https://example.com/hooks/opentrain",
    "eventTypes": ["proposal.received", "message.received", "approval.confirmed"],
    "status": "ACTIVE",
    "createdAt": "2026-06-12T10:00:00.000Z",
    "disabledAt": null,
    "disabledReason": null
  },
  "secret": "whsec_<SECRET>",
  "message": "Store the secret now — it is only returned once. Use it to verify the X-OpenTrain-Signature header on deliveries."
}
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

url
string
required
Absolute https URL that will receive deliveries (max 2048 chars). Plain http is allowed only for localhost during development.
eventTypes
string[]
required
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

webhook
object
The subscription: {id, url, eventTypes, status: "ACTIVE", createdAt, disabledAt, disabledReason} — same shape as GET /webhooks/{id}.
secret
string
The whsec_… signing secret. Shown once — store it now.
message
string
Reminds you to store the secret.

Deliveries

Each event is a POST 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

StatuscodeMeaning
400BAD_REQUESTBody not valid JSON; url missing/invalid/not https (details.field: "url"); eventTypes empty or contains an unsupported type (details.supportedEventTypes)
401UNAUTHORIZEDMissing or invalid token
403FORBIDDENMissing webhooks:manage, missing a subscribed event type’s read scope, or public_api_webhooks disabled
409CONFLICTSubscription limit reached (details: {limit: 10}) — delete an existing webhook first
curl -sS -X POST https://app.opentrain.ai/api/public/v1/webhooks \
  -H "Authorization: Bearer $OT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/opentrain",
    "eventTypes": ["proposal.received", "message.received", "approval.confirmed"]
  }'
{
  "webhook": {
    "id": "<WEBHOOK_ID>",
    "url": "https://example.com/hooks/opentrain",
    "eventTypes": ["proposal.received", "message.received", "approval.confirmed"],
    "status": "ACTIVE",
    "createdAt": "2026-06-12T10:00:00.000Z",
    "disabledAt": null,
    "disabledReason": null
  },
  "secret": "whsec_<SECRET>",
  "message": "Store the secret now — it is only returned once. Use it to verify the X-OpenTrain-Signature header on deliveries."
}