> ## Documentation Index
> Fetch the complete documentation index at: https://opentrain.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Poll Updates

> The authenticated delta feed: fetch every account event since a cursor in one call — proposals, messages, contracts, milestones, payments, approvals.

Fetches everything that happened on your account since your last poll in one cheap call, instead of re-reading every resource. Events are returned oldest first; pass the returned `nextCursor` on the next poll to receive only newer events. Payloads carry **IDs only** — fetch details with the matching read endpoint.

[Webhooks](/docs/developers/api-reference/webhooks/create) push these same events; the recommended architecture uses the webhook as the trigger and this feed as the source of truth — see [stay in sync](/docs/developers/guides/stay-in-sync). For tokenless public marketplace changes, use [`GET /jobs/changes`](/docs/developers/api-reference/jobs/changes) instead.

**Requirements:** at least one of `proposals:read`, `messages:read`, or `payments:read` — each event type is visible only with its scope (below). Works pre-claim.

## Event types

| `type`                          | Required scope   | Key `data` fields                                                                                                              |
| ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `proposal.received`             | `proposals:read` | `proposalId`, `jobId`                                                                                                          |
| `proposal.status_changed`       | `proposals:read` | `proposalId`, `status`                                                                                                         |
| `message.received`              | `messages:read`  | `conversationId`, `messageId`                                                                                                  |
| `contract.created`              | `payments:read`  | `contractId`, `jobId`, `proposalId`                                                                                            |
| `milestone.status_changed`      | `payments:read`  | `milestoneId`, `contractId`, `status`                                                                                          |
| `payment.pending`               | `payments:read`  | `invoiceId`, `milestoneId`, `contractId`                                                                                       |
| `approval.confirmed`            | `payments:read`  | `approvalId`, `status`                                                                                                         |
| `contract.budget_state_changed` | `payments:read`  | `contractId`, `previousState`, `state`, `paymentType`, `fundedVolume`, `consumedVolume`, `remainingVolume`, `consumedFraction` |

Event types your token cannot read are silently filtered out of the feed; they are not an error. A token with none of the three scopes gets `403`.

## Request

<ParamField query="cursor" type="string">
  The `nextCursor` from your previous poll (a numeric event ID). Omit on the first poll to start from the beginning of your account's event history.
</ParamField>

<ParamField query="limit" type="integer" default="50">
  Max events to return, 1–200.
</ParamField>

## Response

<ResponseField name="events" type="object[]">
  <Expandable title="event">
    <ResponseField name="id" type="string">
      Monotonically increasing numeric event ID — this is what cursors point at.
    </ResponseField>

    <ResponseField name="type" type="string">
      One of the eight event types above.
    </ResponseField>

    <ResponseField name="apiVersion" type="string">
      `v1`.
    </ResponseField>

    <ResponseField name="createdAt" type="string">
      ISO timestamp of the event.
    </ResponseField>

    <ResponseField name="resourceId" type="string">
      ID of the primary affected resource (e.g. the proposal ID for `proposal.received`).
    </ResponseField>

    <ResponseField name="jobId" type="string | null">
      The related job, when the event is job-linked.
    </ResponseField>

    <ResponseField name="data" type="object">
      IDs-only payload (see the table above) — never message bodies or other content.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Persist this and pass it as `cursor` on the next poll. When the page is empty it echoes your input cursor (`null` on a first poll with no events).
</ResponseField>

<ResponseField name="hasMore" type="boolean">
  `true` when more events already exist beyond this page — poll again immediately with `nextCursor`.
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                                  |
| ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `400`  | `BAD_REQUEST`  | `cursor` is not a numeric event ID (`details.field: "cursor"`), or `limit` outside 1–200 (`details.field: "limit"`)      |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                                 |
| `403`  | `FORBIDDEN`    | Token has none of `proposals:read`, `messages:read`, `payments:read` (`details.requiredScopes`, `details.grantedScopes`) |

<RequestExample>
  ```bash curl theme={null}
  curl -sS "https://app.opentrain.ai/api/public/v1/updates?cursor=$LAST_CURSOR&limit=50" \
    -H "Authorization: Bearer $OT_API_TOKEN"
  ```

  ```bash CLI theme={null}
  opentrain updates poll --cursor <LAST_CURSOR> --json
  ```

  ```json MCP: opentrain_poll_updates theme={null}
  {
    "cursor": "<LAST_CURSOR>"
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "events": [
      {
        "id": "1042",
        "type": "proposal.received",
        "apiVersion": "v1",
        "createdAt": "2026-06-12T10:00:00.000Z",
        "resourceId": "<PROPOSAL_ID>",
        "jobId": "<JOB_ID>",
        "data": {
          "proposalId": "<PROPOSAL_ID>",
          "jobId": "<JOB_ID>"
        }
      },
      {
        "id": "1043",
        "type": "approval.confirmed",
        "apiVersion": "v1",
        "createdAt": "2026-06-12T10:05:00.000Z",
        "resourceId": "<APPROVAL_ID>",
        "jobId": "<JOB_ID>",
        "data": {
          "approvalId": "<APPROVAL_ID>",
          "status": "confirmed"
        }
      }
    ],
    "nextCursor": "1043",
    "hasMore": false
  }
  ```
</ResponseExample>
