SocialCrawl

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
curl -G 'https://www.socialcrawl.dev/v1/events' \
  -H 'x-api-key: YOUR_API_KEY' \
  --data-urlencode 'types=discover.*' \
  --data-urlencode 'limit=2'
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.

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
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.

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:

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

ParameterWhat it does
cursorWhere to read from: oldest (the default), now, a pagination.next_cursor, or an event id (evt_…), which reads the events after that one
consumerRead as a named consumer instead of a cursor. Send one of cursor and consumer, never both
startWith consumer, on its first read only: oldest (the default) or now
typesComma-separated event types; discover.* selects every discover type and * every type
subject_idComma-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
limitEvents per page, 1 to 500 (default 100)
wait_sSeconds 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

TypeWhen
discover.progressA background or scheduled run crosses each 10% of its planned searches
discover.accounts_readyA view=accounts run finished enriching and judging its accounts
discover.completed, discover.partial, discover.pausedA background or scheduled run settles in that status
discover.failedA background or scheduled run settles failed (a cancel sends nothing)
discover.new_rows, discover.new_accounts, discover.new_itemsA scheduled run settles with new rows for its view
discover.run_skippedA schedule occurrence was skipped
discover.run_failedA 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

Statuserror.type and details.reasonWhat to do
400INVALID_REQUEST, invalid_paramChange the field named in details.param; details.did_you_mean suggests a value when it can
400INVALID_REQUEST, conflicting_paramsSend cursor or consumer, not both; or use another consumer name for another filter
404RESOURCE_NOT_FOUND, unknown_consumerRead GET /v1/events?consumer=<name> first: a consumer is created by its first read
410CURSOR_EXPIRED, retention_passedRestart from cursor=oldest and report the missed window
503SERVICE_UNAVAILABLE, store_unavailableRetry 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.