Event Stream
Read live events from Grovs with a feed token. Set up your first request, paginate safely, and keep your integration running.
The pull feed lets your server read analytics and purchase events from a Grovs project over HTTP. Create a read-only feed token, fetch a page, process its events, and save the returned cursor to continue where you left off.
This guide takes you from creating a token to a working polling integration. Event Stream requires Enterprise access and an instance admin to create or revoke tokens.
1. Create a feed token
Open Pull feed tokens
Select your project and environment in the dashboard, then go to Developers → Event Stream → Pull feed tokens.
Name your integration
Click Create feed token and give it a recognisable name, such as Production warehouse. Use a separate token for each integration so you can revoke access independently.
Copy the token
The token starts with est_ and is shown only once. Store it in your server's secret store or environment. If you lose it, create a replacement.
Each token reads one project and cannot change its settings. Keep it out of browser and mobile app code.
2. Fetch your first page
Set these variables in your terminal, replacing the placeholder values:
export GROVS_API_URL='https://api.sqd.link'
export GROVS_PROJECT_ID='YOUR_PROJECT_ID'
export GROVS_FEED_TOKEN='YOUR_FEED_TOKEN'| Variable | What to use |
|---|---|
GROVS_API_URL | The Grovs backend URL. For self-hosted deployments, replace the hosted URL with your API origin. |
GROVS_PROJECT_ID | The numeric project ID for the environment where you created the token. Do not use the hashid from an event’s project.id. To find it, open your browser's Network panel on Pull feed tokens, click Refresh tokens, and inspect the request path: /api/v1/projects/PROJECT_ID/event_stream_tokens. |
GROVS_FEED_TOKEN | The complete est_… token copied in the previous step. |
The project ID in the URL selects the environment. This endpoint uses Bearer authentication; SDK project keys and the SDK's ENVIRONMENT header do not select a feed.
/api/v1/projects/:project_id/event_streamRead a page of captured analytics and purchase events.
Run your first request without an after parameter:
curl --fail-with-body --get \
"$GROVS_API_URL/api/v1/projects/$GROVS_PROJECT_ID/event_stream" \
--header "Authorization: Bearer $GROVS_FEED_TOKEN" \
--data-urlencode 'limit=500'This reads up to 500 available events from the last seven days. Analytics and purchases share the same page limit.
Capture starts when a project gets its first destination or feed token. Creating a token does not backfill earlier analytics into the feed. For earlier history, use Event Stream → Exports within your plan's retention window.
3. Process the page and save your cursor
A successful response has three top-level fields. This example shortens the event payload and uses illustrative IDs:
{
"schema_version": 1,
"events": [
{
"schema_version": 1,
"id": "example_event_id",
"type": "install",
"occurred_at": "2026-09-16T10:03:11.412Z",
"project": { "id": "PROJECT_HASHID", "test": false }
}
],
"next_after": "OPAQUE_CURSOR_FROM_THE_API"
}| Field | How to use it |
|---|---|
schema_version | The payload format version, currently 1. Allow additional fields within this version. |
events | The events to process. An empty array means no events are available for this request right now. |
next_after | Your continuation cursor. Save it after processing the whole page, then send it as after on the next request. |
Save the cursor only after every event on the page has been processed successfully. If your process stops partway through a page, retry using the previous saved cursor. Deduplicate by each event's id so repeating a page does not repeat its effects.
Keep the cursor unchanged: do not decode, edit, or build it yourself. Grovs does not save your position for you. Store a separate cursor for each project, integration, and set of event filters.
4. Continue reading
Replace the placeholder with the actual next_after value from your response:
export GROVS_FEED_CURSOR='NEXT_AFTER_FROM_YOUR_RESPONSE'
curl --fail-with-body --get \
"$GROVS_API_URL/api/v1/projects/$GROVS_PROJECT_ID/event_stream" \
--header "Authorization: Bearer $GROVS_FEED_TOKEN" \
--data-urlencode 'limit=500' \
--data-urlencode "after=$GROVS_FEED_CURSOR"Repeat this cycle for each page. When events is empty, save the returned cursor, wait briefly, and poll again. New events may take a short time to become available.
The following pseudocode shows where processing, checkpointing, and retries belong. Implement process_idempotently and cursor storage for your destination:
cursor = load_saved_cursor() // absent on the first run
repeat:
wait until below the request limit
response = fetch_feed(after=cursor, limit=500)
if network failure or response is 503:
wait with exponential backoff and jitter
continue // keep the last saved cursor
if response is not successful:
stop and resolve the error
for event in response.events:
process_idempotently(event.id, event)
// If processing fails, retry this page before advancing.
save_cursor_durably(response.next_after)
cursor = response.next_after
if response.events is empty:
wait before polling againReads are limited to 120 requests per minute per token. Pace requests even while draining a backlog. On 503, back off and retry without advancing the cursor.
Filter event types
Omitting event_types[] returns all supported types. To read only installs and purchases, add both flags to your request:
--data-urlencode 'event_types[]=install' \
--data-urlencode 'event_types[]=purchase'Keep the same filters on subsequent pages. If you expand your filters, start a separate cursor to read retained events for the new selection; an existing cursor may already be past them.
Query parameters
| Parameter | Default | Description |
|---|---|---|
after | Omitted | Exact next_after from the last processed page. Omit on first use to read available events from the last seven days. |
limit | 100 | Integer from 1 to 1000. Limits analytics and purchases together. |
event_types[] | All types | Repeatable filter, for example install and purchase. |
Work with event data
Events include their ID, type, occurrence time, and project, plus device, user, attribution, and purchase data when applicable. Events are merged in arrival order; occurred_at describes when an event happened and is not a pagination cursor.
| Field | Meaning |
|---|---|
id | Deduplication key for this event. |
type, name | Event type and, when applicable, a custom event name or purchase action. |
occurred_at | When the event occurred. |
project.id, project.test | Originating project and whether it is a test environment. |
device, user | Device details and the user identity or attributes recorded through the SDK. |
link, attribution | Associated link and attribution information, when available. |
geo | Country-level location. Pull feed tokens responses do not include IP addresses or cities. |
purchase | Transaction, product, store, amount, and version details for a purchase. |
Purchase corrections arrive with a new event id and a newer purchase.version. Within each project, upsert on (purchase.transaction_id, name), keeping the record with the newest purchase.version. The envelope’s name identifies the store action, so different actions for the same transaction remain separate records. Deduplicate repeated deliveries by the envelope id.
Rotate or revoke a token
To rotate credentials without interrupting your integration:
- Create a replacement token for the same project and environment.
- Update the integration's secret and verify it can read using its saved cursor.
- Return to Pull feed tokens, click Revoke on the old token, and confirm.
Revocation takes effect immediately. The old token can no longer read events, and the dashboard removes it from the active token list. Last used is updated to the minute, not on every request.
Revoking the last feed token when there are no destinations also removes the project's captured feed data. Retained analytics history is unaffected. Create a replacement before revoking the old token if capture needs to continue.
Troubleshooting
The response has no events
Check that the token and URL refer to the intended project and environment. If capture was just enabled, generate a new event and allow a short delay. If you added event filters, check their values. An empty page can also mean your integration has caught up; continue polling with the returned cursor.
The feed retains seven days of captured events. If your integration has been offline longer than that, use Exports to recover the missing range from retained analytics history, then resume polling.
A request fails
| Status | What to check |
|---|---|
400 | Restore the original saved cursor and use an integer limit. If the cursor is lost, omit after to reread available retained events and deduplicate by id. |
401 | Check the Bearer token. An invalid or revoked token must be replaced. |
402 | Event Stream requires active Enterprise access. Check your subscription or self-hosted entitlement. |
403 | Confirm the token belongs to the project in the URL, including the selected environment. |
503 | The token may have exceeded 120 requests per minute, or the service may be unavailable. Back off and retry from the last saved cursor. |
Never advance your cursor after a failed request or partially processed page.