Skip to content

Event Feed

Overview

The event feed is a read-only API that stores a chronological log of all domain events emitted by your tenant. Unlike push webhooks, which require your endpoint to be available at the moment an event occurs, the event feed retains events for a configurable period (30 days by default) so you can poll at your own pace.

Use the event feed when you need to:

  • Reconcile - catch up on events missed during consumer downtime
  • Audit - build a complete history of entity changes for compliance
  • Batch process - consume events in bulk on a schedule rather than reacting in real-time
  • Debug - inspect the exact sequence and content of events during development

Polling the Event Feed

List Events

Returns events in chronological order using cursor-based pagination. Each event has a ULID identifier that serves as the cursor.

Endpoint: GET /api/v1/webhooks/events

Authentication: Required (Bearer token)

Query Parameters:

ParameterTypeDescription
afterstringULID cursor; returns events created after this ID (exclusive)
event_typestringFilter by event type pattern; supports wildcards (product.*, *)
entity_typestringFilter by entity type (product, customer, order, invoice)
per_pageintegerResults per page (default: 50, max: 100)

Example Request:

bash
GET /api/v1/webhooks/events?after=01jevt100abc000def000ghi0&event_type=product.*&per_page=25

Response (200 OK):

json
{
  "success": true,
  "data": [
    {
      "id": "01jevt101abc001def001ghi1",
      "event_type": "product.created",
      "entity_type": "product",
      "entity_id": "01jprod789abc012def345ghi6",
      "occurred_at": "2026-03-15T10:30:00Z",
      "payload": {
        "id": "01jprod789abc012def345ghi6",
        "name": "Wireless Keyboard",
        "sku": "KB-WIRELESS-001",
        "status": "active",
        "selling_price": 49.99,
        "currency": "USD"
      }
    },
    {
      "id": "01jevt102abc002def002ghi2",
      "event_type": "product.price_changed",
      "entity_type": "product",
      "entity_id": "01jprod789abc012def345ghi6",
      "occurred_at": "2026-03-15T11:00:00Z",
      "payload": {
        "id": "01jprod789abc012def345ghi6",
        "name": "Wireless Keyboard",
        "sku": "KB-WIRELESS-001",
        "status": "active",
        "selling_price": 44.99,
        "currency": "USD"
      }
    }
  ],
  "meta": {
    "per_page": 25,
    "has_more": true,
    "cursor": "01jevt102abc002def002ghi2"
  },
  "links": {
    "next": "/api/v1/webhooks/events?after=01jevt102abc002def002ghi2&event_type=product.*&per_page=25"
  }
}

Cursor-Based Pagination

The event feed uses ULID-based cursors rather than page numbers. ULIDs are time-ordered, which means the cursor also represents a point in time. This approach has several advantages:

  • No skipped or duplicated events - new events inserted between polls do not shift page boundaries
  • Efficient queries - the database uses an index range scan starting from the cursor
  • Stateless consumers - store only the last cursor value to resume from where you left off

To paginate:

  1. Make an initial request without the after parameter to start from the oldest available event
  2. Process the returned events
  3. Store the cursor value from the meta object
  4. On the next poll, pass after={cursor} to fetch events newer than your last position
  5. Repeat until has_more is false, then wait before polling again

Empty Responses

When no new events are available, the response returns an empty data array with has_more: false. This is not an error - it means you are caught up.


Filtering

By Event Type

The event_type parameter supports the same wildcard patterns as subscription event types:

bash
# All product events
GET /api/v1/webhooks/events?event_type=product.*

# Only customer creation events
GET /api/v1/webhooks/events?event_type=customer.created

# All events (no filter or explicit wildcard)
GET /api/v1/webhooks/events?event_type=*

By Entity Type

The entity_type parameter filters by the entity that emitted the event. This is useful when you need all events for a particular domain object regardless of the specific event type:

bash
# All order-related events
GET /api/v1/webhooks/events?entity_type=order

# Combine with event_type for precise filtering
GET /api/v1/webhooks/events?entity_type=product&event_type=product.price_changed

Example Polling Loop

The following pseudocode demonstrates a reliable polling consumer that persists its cursor position between runs:

python
import time
import requests

API_BASE = "https://api.synthesq.com/api/v1"
TOKEN = "your-bearer-token"
CURSOR_FILE = "/var/data/webhook-cursor.txt"
POLL_INTERVAL = 30  # seconds

def load_cursor():
    try:
        with open(CURSOR_FILE, "r") as f:
            return f.read().strip() or None
    except FileNotFoundError:
        return None

def save_cursor(cursor):
    with open(CURSOR_FILE, "w") as f:
        f.write(cursor)

def poll_events():
    cursor = load_cursor()

    while True:
        params = {"per_page": 100}
        if cursor:
            params["after"] = cursor

        response = requests.get(
            f"{API_BASE}/webhooks/events",
            headers={"Authorization": f"Bearer {TOKEN}"},
            params=params,
        )
        data = response.json()

        for event in data["data"]:
            process_event(event)

        if data["meta"]["has_more"]:
            cursor = data["meta"]["cursor"]
            save_cursor(cursor)
            # Continue immediately - more events available
        else:
            if data["data"]:
                cursor = data["meta"]["cursor"]
                save_cursor(cursor)
            # Caught up - wait before next poll
            time.sleep(POLL_INTERVAL)

def process_event(event):
    print(f"Processing {event['event_type']}: {event['entity_id']}")
    # Your business logic here

poll_events()

Choosing a Poll Interval

A 30-second interval works well for most use cases. For near-real-time requirements, poll every 5-10 seconds. For batch processing, a longer interval (5-15 minutes) reduces API calls without losing events, since the feed retains data for 30 days.


Retention Policy

Events in the feed are retained for 30 days by default. After the retention period, events are permanently deleted and can no longer be retrieved through the API.

The retention period is measured from the occurred_at timestamp of each event. Events are purged in background cleanup jobs that run daily.

Plan for Retention

If your consumer is offline for longer than the retention period, events emitted during the gap are lost. For critical integrations, combine push webhooks with event feed polling to minimize the risk of data loss.


Documentation for SynthesQ CRM/ERP Platform