# Events feed (/docs/events) Read your account's events in order with a cursor or a named consumer, wait for new ones with a long-poll, and recover after a gap `GET /v1/events` lists what happened on your account, oldest first: a discover run that completed, paused or found new rows on a schedule. Each event appears once, in the order it was written, and you page through the list with a cursor. Reading the feed is free. ```bash title="cURL" curl -G 'https://www.socialcrawl.dev/v1/events' \ -H 'x-api-key: YOUR_API_KEY' \ --data-urlencode 'types=discover.*' \ --data-urlencode 'limit=2' ``` ```json title="Response" { "success": true, "platform": "events", "endpoint": "/v1/events", "data": { "items": [ { "id": "evt_k3m7q2xwcbvd4nph6trz5ycgda", "type": "discover.completed", "api_version": "2026-10-01", "created_at": "2026-10-19T10:15:04.212Z", "cursor": "c_MS4xMi4xNzkyNDA0OTA0", "subject": { "kind": "discover_run", "id": "run_7Qk2R9xLpV" }, "data": { "run_id": "run_7Qk2R9xLpV", "status": "completed", "stopped_because": null, "counts": { "legs": { "planned": 24, "done": 22, "empty": 2, "failed": 0, "skipped": 0, "pending": 0 }, "rows_kept": 148 }, "credits": { "spent": 212 }, "version": 9, "schedule_id": null }, "links": { "run": "/v1/prism/discover/runs/run_7Qk2R9xLpV", "results": "/v1/prism/discover/runs/run_7Qk2R9xLpV/results.jsonl", "changes": "/v1/prism/discover/runs/run_7Qk2R9xLpV?changes_since=0" } } ], "retained_from": "2026-09-20T08:02:11.540Z", "next_step": "Process these events, save pagination.next_cursor, then call data.next.poll_url to wait for new ones.", "next": { "poll_url": "https://www.socialcrawl.dev/v1/events?cursor=c_MS4xMi4xNzkyNDA0OTA0&types=discover.*&limit=2&wait_s=25" }, "dropped": 0 }, "pagination": { "next_cursor": "c_MS4xMi4xNzkyNDA0OTA0", "has_more": false }, "credits_used": 0, "request_id": "req_9f1c2a7d", "cached": false } ``` The first call without a cursor starts at the oldest event still kept (the feed keeps 30 days). `pagination.next_cursor` is your position after the last event on the page: save it, and send it as `cursor` on the next call. `data.next.poll_url` is that next call, ready to copy. ## How do I wait for new events? Add `wait_s` (up to 25 seconds). When nothing is new, the call holds open and answers as soon as an event arrives, or with an empty page at the deadline. Set your HTTP timeout to `wait_s` plus 10 seconds. ```bash title="cURL" curl -G 'https://www.socialcrawl.dev/v1/events' \ -H 'x-api-key: YOUR_API_KEY' \ --data-urlencode 'cursor=c_MS4xMi4xNzkyNDA0OTA0' \ --data-urlencode 'wait_s=25' ``` ```python title="Python" import requests cursor = "oldest" while True: page = requests.get( "https://www.socialcrawl.dev/v1/events", headers={"x-api-key": "YOUR_API_KEY"}, params={"cursor": cursor, "types": "discover.*", "wait_s": 25}, timeout=35, ).json() for event in page["data"]["items"]: handle(event) # deduplicate on event["id"] cursor = page["pagination"]["next_cursor"] # save it after processing ``` A key holds at most 5 long-polls at once. A sixth answers at once with what it has and an `X-Poll-Interval` header: wait that many seconds before the next call. A page also carries an `ETag`; send it back as `If-None-Match` and an unchanged page answers `304` with no body. ## Named consumers: let the server keep your position If your code cannot store a cursor (an agent that starts fresh each session, a cron script), read as a named consumer instead. The server keeps the position for your API key, and only an acknowledgement moves it. ```bash title="cURL" curl -G 'https://www.socialcrawl.dev/v1/events' \ -H 'x-api-key: YOUR_API_KEY' \ --data-urlencode 'consumer=crm-sync' \ --data-urlencode 'types=discover.*' ``` The answer carries `data.unread` (events after the committed position) and `data.next.ack_url`. Process the page, then acknowledge its `pagination.next_cursor`, an empty page's too: ```bash title="cURL" curl -X POST 'https://www.socialcrawl.dev/v1/events/consumers/crm-sync/ack' \ -H 'x-api-key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"cursor": "c_MS4xMi4xNzkyNDA0OTA0"}' ``` - **A read commits nothing.** Reading again before you acknowledge returns the same page, so a crash or a lost context loses nothing. - **An acknowledgement never moves backwards**, and acknowledging past the last page the consumer was served is a free `400`. - **Filters are bound at the first read.** A later read with different `types` or `subject_id` is a `400` with `reason: "conflicting_params"`; use a second consumer name for a second filter. A read that omits them uses the bound ones. - **A new consumer starts at the oldest kept event.** Send `start=now` on its first read to begin with events from now on. - Consumer names are 1 to 64 characters of `a-z`, `0-9`, `_` and `-`, and belong to the API key that created them. Run one process per consumer name. ## Parameters | Parameter | What it does | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cursor` | Where to read from: `oldest` (the default), `now`, a `pagination.next_cursor`, or an event id (`evt_…`), which reads the events after that one | | `consumer` | Read as a named consumer instead of a cursor. Send one of `cursor` and `consumer`, never both | | `start` | With `consumer`, on its first read only: `oldest` (the default) or `now` | | `types` | Comma-separated event types; `discover.*` selects every discover type and `*` every type | | `subject_id` | Comma-separated ids of the object an event is about, such as a `run_id` or a schedule id (at most 100). A schedule id also selects its runs' events | | `limit` | Events per page, 1 to 500 (default 100) | | `wait_s` | Seconds to hold an empty read open for new events, 0 to 25 (default 0) | The feed is cursor only: it takes no `since` or `until`. A cursor is a position in your whole log, so it stays valid when you change `types` or `subject_id` between calls. A page that is not cut short (`has_more: false`, an empty page too) moves `next_cursor` to the end of your log, past events your key or filters do not select, so a reader that keeps polling never falls behind the 30-day retention. ## Event types | Type | When | | ------------------------------------------------------------ | ----------------------------------------------------------------------- | | `discover.progress` | A background or scheduled run crosses each 10% of its planned searches | | `discover.accounts_ready` | A `view=accounts` run finished enriching and judging its accounts | | `discover.completed`, `discover.partial`, `discover.paused` | A background or scheduled run settles in that status | | `discover.failed` | A background or scheduled run settles `failed` (a cancel sends nothing) | | `discover.new_rows`, `discover.new_accounts`, `discover.new_items` | A scheduled run settles with new rows for its view | | `discover.run_skipped` | A schedule occurrence was skipped | | `discover.run_failed` | A scheduled run failed, or three skips in a row paused the schedule | Discover events carry counts and ids, never your keywords, questions or row text. Read the rows through the URLs in `links`. ## What an event looks like Every event has the same fields in the feed and in a [webhook batch](/docs/webhook-endpoints.md): `id`, `type`, `api_version`, `created_at`, `cursor`, `subject` (`kind`, `id` and sometimes `name`), `data` and `links`. `id` is stable: the same fact never produces a second event, so deduplicate on it. `cursor` is the position right after that event, so a webhook receiver that missed deliveries can read the feed from the last event it processed by sending that event's `cursor`. Store `pagination.next_cursor`, not event ids: an event id cannot tell an event that aged out from a mistyped one, so an unknown id answers `400`, while a cursor carries its position and time and answers an exact `410` with the missed window. The feed is in the order events were written. Webhooks are delivered at least once and can arrive out of order; use the feed to fill a gap. ## Which events does a key see? An API key sees the events of the objects it created (its runs and its schedules) and events about the whole account. Events about the whole account appear in every key's feed and go to every key's [webhook endpoints](/docs/webhook-endpoints.md) that subscribe to them. Another key's runs do not appear, and consumers belong to the key that created them. A cursor is a position in the account's whole log, so its position also counts other keys' events; their content stays with them. ## Retention and gaps Events are kept for 30 days. `data.retained_from` is the time of the oldest event still kept. A cursor older than that answers `410` [`CURSOR_EXPIRED`](/docs/errors.md#cursor-expired) with `oldest_cursor`, `missed_from` and `missed_until`: restart from `cursor=oldest` and tell the user which window was missed. An event id sent as the cursor that is not a kept event of your key answers `400` with `retained_from` and `oldest_cursor`, since a mistyped id and one older than 30 days look the same. A consumer idle for more than 30 days answers the same `410` once; its next read restarts at the oldest kept event and carries `restarted_from` once. ## Errors | Status | `error.type` and `details.reason` | What to do | | ------ | ------------------------------------------ | ---------------------------------------------------------------------------------------------- | | 400 | `INVALID_REQUEST`, `invalid_param` | Change the field named in `details.param`; `details.did_you_mean` suggests a value when it can | | 400 | `INVALID_REQUEST`, `conflicting_params` | Send `cursor` or `consumer`, not both; or use another consumer name for another filter | | 404 | `RESOURCE_NOT_FOUND`, `unknown_consumer` | Read `GET /v1/events?consumer=` first: a consumer is created by its first read | | 410 | `CURSOR_EXPIRED`, `retention_passed` | Restart from `cursor=oldest` and report the missed window | | 503 | `SERVICE_UNAVAILABLE`, `store_unavailable` | Retry after `Retry-After` seconds | Every error is free and carries `details.next_step`, one sentence naming the call to make next. See [Errors](/docs/errors.md) for the envelope. - [Event webhooks](/docs/webhook-endpoints.md): Have the same events pushed to your HTTPS endpoint, signed. - [Errors](/docs/errors.md): Every error code, its status and whether to retry. - [Rate limits](/docs/rate-limits.md): Per-key request limits that also apply to the feed.