Event stream
The pull feed lets your own job read a project's analytics and purchase events. Webhook and S3 destinations, exports and feed tokens are managed in the dashboard. See the event stream guide.
Availability: paid grovs Cloud plans and the self-hosted Enterprise image. Other instances get 402.
Auth: a feed token (est_…). An instance admin creates it in Developers › Data Export › Pull feed tokens › Create feed token. The token is shown once. It reads one project environment and cannot change anything.
Delivery is at least once, so deduplicate on the event id. Order is kept per device. Captured events are kept for 7 days. Capture starts when a project gets its first destination or feed token. Earlier events are only reachable through an export in the dashboard.
Find the project ID
The feed path takes the numeric ID of the project environment where you created the token. Production and test have different IDs.
It is not the project.id hashid inside events. The dashboard does not show the numeric ID yet. To find it:
- Open Developers › Data Export › Pull feed tokens.
- Open your browser's network panel.
- Click Refresh tokens.
- Read the number in the request path
/api/v1/projects/PROJECT_ID/event_stream_tokens.
A token used with another project's ID gets 403.
/api/v1/projects/123/event_stream
^^^
project ID (integer)Read the pull feed
/api /v1 /projects /{project_id} /event_streamReturns a page of captured analytics and purchase events, merged in arrival order. grovs keeps no position for you. Save next_after after you process the page, then send it back as after. Events become readable about 15 seconds after they arrive. The event stream guide walks through a full polling loop.
Send a feed token as Authorization: Bearer est_….
project_idintegerRequiredafterstringOptionalnext_after of your last page, unchanged. Omit it to start at the oldest captured event (up to 7 days back).limitintegerOptional100. Caps analytics and purchases together.event_types[]stringOptionalevent_types[]=install&event_types[]=purchase. Unknown values are ignored. Default: every type.Returns schema_version, events (an array of event envelopes, without geo.city or geo.ip) and next_after. An empty events array means you have caught up.
In production, each feed token can make 120 requests a minute. Over that, the API answers 503.
Errors
curl -X GET "https://api.sqd.link/api/v1/projects/{project_id}/event_stream?limit=500&event_types%5B%5D=install&event_types%5B%5D=purchase" \
-H "Authorization: Bearer $GROVS_STREAM_KEY"{
"schema_version": 1,
"events": [
{
"schema_version": 1,
"id": "3f9a1c2e7b5d4e0a9c8b7a6f5e4d3c2b",
"type": "install",
"name": null,
"occurred_at": "2026-10-22T09:03:11.412Z",
"project": { "id": "pQx7", "test": false },
"link": null,
"attribution": { "link_visitor_id": null, "inviter_id": null, "sdk_generated": false },
"device": {
"id": "D8mz",
"platform": "android",
"os": "Android",
"os_version": "15",
"app_version": "4.2.0",
"build": "4210",
"model": "Pixel 9",
"timezone": "Europe/Berlin",
"language": "de"
},
"user": { "id": null, "attributes": {} },
"geo": { "country": "DE" },
"session_id": "s_8c21f0",
"screen": null,
"engagement_time": 0,
"properties": {},
"caused_by": null,
"purchase": null
},
{
"schema_version": 1,
"id": "b71e04c9d2a35f8e6c1b0a9d8e7f6a5b",
"type": "purchase",
"name": "buy",
"occurred_at": "2026-10-22T09:05:40.218Z",
"project": { "id": "pQx7", "test": false },
"link": null,
"attribution": { "link_visitor_id": null, "inviter_id": null, "sdk_generated": false },
"device": { "id": "D8mz", "platform": "android" },
"user": { "id": "member-4471", "attributes": {} },
"geo": {},
"session_id": "s_8c21f0",
"screen": null,
"engagement_time": 0,
"properties": {},
"caused_by": null,
"purchase": {
"transaction_id": "GPA.3312-8841-2290-11873",
"original_transaction_id": null,
"product_id": "medly_plus_yearly",
"purchase_type": "subscription",
"store": "google",
"quantity": 1,
"currency": "EUR",
"usd_price_cents": 5399,
"purchase_date": "2026-10-22T09:05:38.000Z",
"version": "2026-10-22T09:05:41.006Z"
}
}
],
"next_after": "eyJlIjpbIjIwMjYtMTAtMjIgMDk6MDM6MTEuNDEyIiwiM2Y5YSJdLCJwIjpbIjIw…"
}{ "error": "Invalid cursor" }The event envelope
Every event has the same JSON shape on every transport: pull feed, webhook and S3. This is envelope version 1. New keys may appear within a version. A rename or removal comes as a new schema_version.
schema_versioninteger1 for this shape.idstringtypestringview, open, install, reinstall, app_open, time_spent, reactivation, user_referred, screen_view, custom, purchase, or test for a test event sent from the dashboard.namestring | nullbuy, cancel, refund, refund_reversed. null otherwise.occurred_atstringprojectobjectid (hashid) and test (whether it is the test environment).linkobject | nullid and campaign_id (integers), path, source, medium, campaign, ads_platform, tags. null when the event has no link.attributionobjectlink_visitor_id and inviter_id (hashids or null), sdk_generated.deviceobjectid (hashid), platform, os, os_version, app_version, build, model, timezone, language. Purchases carry id and platform only.userobjectid is the identifier your app set through the SDK, attributes its attributes. Use user.id to join with a CRM.geoobjectcountry. Destinations with include_ip also get city and ip. The pull feed never includes them.session_idstring | nullscreenstring | nullengagement_timeinteger0 for purchases.propertiesobjectcaused_bystring | nulluser_referred, the id of the install that produced it.purchaseobject | nulltransaction_id, original_transaction_id, product_id, purchase_type, store, quantity, currency, usd_price_cents, purchase_date, version.Stores correct purchases after the fact. Each correction arrives as a new event with a newer purchase.version. Within a project, upsert on (purchase.transaction_id, name) and keep the newest purchase.version.
{
"schema_version": 1,
"id": "3f9a1c2e7b5d4e0a9c8b7a6f5e4d3c2b",
"type": "install",
"name": null,
"occurred_at": "2026-10-22T09:03:11.412Z",
"project": { "id": "pQx7", "test": false },
"link": {
"id": 8812,
"path": "flu-shot",
"campaign_id": 41,
"source": "email",
"medium": "newsletter",
"campaign": "autumn_checkup",
"ads_platform": null,
"tags": ["q4"]
},
"attribution": { "link_visitor_id": "V1k9", "inviter_id": null, "sdk_generated": false },
"device": {
"id": "D8mz",
"platform": "ios",
"os": "iOS",
"os_version": "18.6",
"app_version": "4.2.0",
"build": "4210",
"model": "iPhone16,2",
"timezone": "Europe/Berlin",
"language": "de"
},
"user": { "id": "member-4471", "attributes": { "plan": "plus" } },
"geo": { "country": "DE" },
"session_id": "s_8c21f0",
"screen": "Home",
"engagement_time": 0,
"properties": {},
"caused_by": null,
"purchase": null
}