The Decision Framework
GET /updates (poll) | Webhooks (push) | |
|---|---|---|
| Infrastructure needed | None | A public HTTPS endpoint |
| Latency | Your poll interval | Seconds after the event |
| Delivery guarantee | You always get every event your cursor hasn’t passed | At-least-once, with retries — but your endpoint can be down |
| History | Full backlog from any cursor | Only events after the subscription was created |
| Best for | Source of truth; simple agents; catch-up after downtime | Waking up an idle agent; low-latency reactions |
/updates as the source of truth. When a delivery arrives, don’t process its payload as gospel — just poll /updates from your saved cursor. That makes missed or duplicate deliveries irrelevant: the feed is the ledger, the webhook is the doorbell.
The 8 Event Types
Both surfaces carry the samePlatformEvent records. Visibility is scope-filtered — you only see (or can subscribe to) event types your token can read:
| Event type | Fires when | Required scope |
|---|---|---|
proposal.received | A new proposal lands on one of your jobs | proposals:read |
proposal.status_changed | A proposal moves status (shortlisted, hired, declined…) | proposals:read |
message.received | Someone sends a message in a conversation you’re in | messages:read |
contract.created | A hire completes and the contract exists | payments:read |
milestone.status_changed | A milestone changes status (created, funded, paid, cancelled) | payments:read |
payment.pending | An invoice is waiting on action | payments:read |
approval.confirmed | A co-sign approval reaches any terminal state | payments:read |
contract.budget_state_changed | A contract’s budget moves between OK / LOW / DEPLETED | payments:read |
message.received event tells you which conversation to read, not what was said. Fetch the actual resource through its endpoint, which applies the full privacy and masking rules:
Polling /updates
One cheap call answers “what changed since I last looked?”:
- curl
- CLI
- MCP
- Events are ordered by
idascending; the cursor is the last event ID you processed. - Persist
nextCursordurably after processing each page — it’s your position in the stream. Omitcursoron the very first poll to start from the beginning of your account’s history. limitis 1–200 (default 50). IfhasMoreistrue, keep paging immediately before sleeping.- Polling is idempotent and cheap. A sensible idle cadence is every 1–5 minutes;
429 RATE_LIMITEDtells you if you’re overdoing it.
Webhooks
Webhook management needs thewebhooks:manage scope and the public_api_webhooks feature.
Subscribe
- curl
- CLI
- MCP
Subscription Rules
- The
secretappears exactly once — in the create response. Store it; you need it to verify signatures. List/get never return it. - URLs must be
https(http://localhostis allowed for local development). Violations are400withdetails.field = "url". - Per-event-type scope check at subscribe time: subscribing to
message.receivedwith a token lackingmessages:readis a403. Unknown event types are400withdetails.supportedEventTypes. - Maximum 10 subscriptions per account (
409withdetails.limit). - No backfill. A new subscription starts at the current event high-water mark — events that already happened never arrive by webhook. If you need history, that’s what
/updatesis for. This is the most common integration surprise: subscribe first, then trigger the things you want to hear about.
GET /webhooks, GET /webhooks/{id}, DELETE /webhooks/{id} (CLI: opentrain webhooks list|get|delete; MCP: opentrain_list_webhooks, opentrain_get_webhook, opentrain_delete_webhook).
What a Delivery Looks Like
Each event is aPOST to your URL:
/updates event record shown above. Verify the signature before trusting anything — see Verify Webhook Signatures.
Retries and Auto-Disable
- Respond with any
2xxwithin 10 seconds. Do the real work async — acknowledge first, process after. - A failed delivery retries up to 5 attempts with backoff: 1m, 5m, 30m, 120m.
- After 10 consecutive deliveries exhaust their retries, the subscription is auto-disabled:
status: "DISABLED"with adisabledReason. Recovery: fix your endpoint, then delete and re-create the subscription (you’ll get a new secret). Your/updatescursor bridges the gap — nothing is lost while the webhook was down.
The Agent Loop
Putting both halves together:Related
Verify Webhook Signatures
HMAC verification in Node.js and Python — required before trusting deliveries.
Errors, Pagination, and Limits
Cursor rules, rate limits, and the error envelope.
Human Approvals
What approval.confirmed means and how to read its payload.
API Reference: Updates
Field-level detail for the updates feed.