Docs

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

1

Open Pull feed tokens

Select your project and environment in the dashboard, then go to Developers → Event Stream → Pull feed tokens.

2

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.

3

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:

Bash
export GROVS_API_URL='https://api.sqd.link'
export GROVS_PROJECT_ID='YOUR_PROJECT_ID'
export GROVS_FEED_TOKEN='YOUR_FEED_TOKEN'
VariableWhat to use
GROVS_API_URLThe Grovs backend URL. For self-hosted deployments, replace the hosted URL with your API origin.
GROVS_PROJECT_IDThe 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_TOKENThe 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.

GET/api/v1/projects/:project_id/event_stream

Read a page of captured analytics and purchase events.

Run your first request without an after parameter:

Bash
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:

JSON
{
  "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"
}
FieldHow to use it
schema_versionThe payload format version, currently 1. Allow additional fields within this version.
eventsThe events to process. An empty array means no events are available for this request right now.
next_afterYour 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:

Bash
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 again

Reads 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:

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

ParameterDefaultDescription
afterOmittedExact next_after from the last processed page. Omit on first use to read available events from the last seven days.
limit100Integer from 1 to 1000. Limits analytics and purchases together.
event_types[]All typesRepeatable 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.

FieldMeaning
idDeduplication key for this event.
type, nameEvent type and, when applicable, a custom event name or purchase action.
occurred_atWhen the event occurred.
project.id, project.testOriginating project and whether it is a test environment.
device, userDevice details and the user identity or attributes recorded through the SDK.
link, attributionAssociated link and attribution information, when available.
geoCountry-level location. Pull feed tokens responses do not include IP addresses or cities.
purchaseTransaction, 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:

  1. Create a replacement token for the same project and environment.
  2. Update the integration's secret and verify it can read using its saved cursor.
  3. 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

StatusWhat to check
400Restore 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.
401Check the Bearer token. An invalid or revoked token must be replaced.
402Event Stream requires active Enterprise access. Check your subscription or self-hosted entitlement.
403Confirm the token belongs to the project in the URL, including the selected environment.
503The 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.

Edit this page on GitHubLast updated 2026-09-16