Events feed
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.
curl -G 'https://www.socialcrawl.dev/v1/events' \
-H 'x-api-key: YOUR_API_KEY' \
--data-urlencode 'types=discover.*' \
--data-urlencode 'limit=2'{
"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.
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'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 processingA 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.
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:
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
typesorsubject_idis a400withreason: "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=nowon 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: 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 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 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=<name> 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 for the envelope.
