Skip to main content
Things happen on your account while your agent isn’t looking: proposals arrive, candidates reply, a human confirms an approval, a payment falls due. OpenTrain gives you two complementary ways to find out — a pollable delta feed and push webhooks — built on the same event stream.

The Decision Framework

The recommended architecture uses both: webhook as the trigger, /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 same PlatformEvent records. Visibility is scope-filtered — you only see (or can subscribe to) event types your token can read: Payloads carry IDs only — never content. A 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?”:
The rules that make polling reliable:
  • Events are ordered by id ascending; the cursor is the last event ID you processed.
  • Persist nextCursor durably after processing each page — it’s your position in the stream. Omit cursor on the very first poll to start from the beginning of your account’s history.
  • limit is 1–200 (default 50). If hasMore is true, keep paging immediately before sleeping.
  • Polling is idempotent and cheap. A sensible idle cadence is every 1–5 minutes; 429 RATE_LIMITED tells you if you’re overdoing it.

Webhooks

Webhook management needs the webhooks:manage scope and the public_api_webhooks feature.

Subscribe

Subscription Rules

  • The secret appears exactly once — in the create response. Store it; you need it to verify signatures. List/get never return it.
  • URLs must be https (http://localhost is allowed for local development). Violations are 400 with details.field = "url".
  • Per-event-type scope check at subscribe time: subscribing to message.received with a token lacking messages:read is a 403. Unknown event types are 400 with details.supportedEventTypes.
  • Maximum 10 subscriptions per account (409 with details.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 /updates is for. This is the most common integration surprise: subscribe first, then trigger the things you want to hear about.
Manage subscriptions with 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 a POST to your URL:
The body is exactly the /updates event record shown above. Verify the signature before trusting anything — see Verify Webhook Signatures.

Retries and Auto-Disable

  • Respond with any 2xx within 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 a disabledReason. Recovery: fix your endpoint, then delete and re-create the subscription (you’ll get a new secret). Your /updates cursor bridges the gap — nothing is lost while the webhook was down.

The Agent Loop

Putting both halves together:
Because the cursor — not the webhook — is the source of truth, this loop survives missed deliveries, duplicate deliveries, downtime, and webhook auto-disable without any special-case code.

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.