Back to Content Management API

events

Returns the store's change journal, oldest first, paginated by a cursor. Events cover content changes plus lifecycle events for content releases, schedules, and A/B tests. See Polling for changes below for how to consume it.

Arguments

  • Name
    first
    Type
    Int
    Description

    Returns up to the first n events. Defaults to 50, maximum 100.

  • Name
    after
    Type
    String
    Description

    Returns the events that come after the specified cursor.

  • Name
    filter
    Type
    EventFilterInput
    Description

    An object.

    • types: [String!] Full event types, such as content.page.published.
    • subjectTypes: [EventSubjectType!] page, section, article, blog, product, collection, template, siteSettings, contentRelease, schedule, or test.
    • actions: [EventAction!] created, updated, deleted, restored, published, unpublished, started, paused, ended, archived, scheduled, unscheduled, succeeded, or failed.
    • since: DateTime A starting point for a new consumer with no cursor. Treat it as a hint: older events can be included.
    • excludeSystemActor: Boolean Drop events recorded by Pack's system actor.

Returns

  • Name
    EventConnection.edges
    Type
    [EventEdge!]!
    Description

    A list of edges. Each edge has a cursor and a node.

  • Name
    EventConnection.nodes
    Type
    [Event!]!
    Description

    A list of events. Each has id: ID!, storeId: ID!, contentEnvironmentId: ID, releaseId: ID, type: String!, subjectType: String!, subjectId: ID!, action: String!, actorUserId: ID, occurredAt: DateTime!, and data: JSON. See the Event object.

  • Name
    EventConnection.pageInfo
    Type
    PageInfo!
    Description

    Information to aid in pagination.

  • Name
    EventConnection.recommendedPollAfterMs
    Type
    Int!
    Description

    How long to wait, in milliseconds, before the next poll once you have caught up.

Request

import { PackClient } from '@pack/client'
const packClient = new PackClient({
  token: 'YOUR-PACK-TOKEN',
})

const query = `
  query PollEvents($first: Int!, $after: String, $filter: EventFilterInput) {
    events(first: $first, after: $after, filter: $filter) {
      edges {
        cursor
        node {
          id
          type
          subjectType
          subjectId
          action
          releaseId
          occurredAt
          data
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      recommendedPollAfterMs
    }
  }
`
const variables = {
  first: 100,
  after: 'saved-cursor',
  filter: {
    subjectTypes: [
      'page',
      'section',
    ],
  },
}
const response = await packClient.fetch(query, { variables })
console.log(response.data)

Response

{
  "data": {
    "events": {
      "edges": [
        {
          "cursor": "next-cursor",
          "node": {
            "id": "event-id",
            "type": "content.page.published",
            "subjectType": "page",
            "subjectId": "page-id",
            "action": "published",
            "releaseId": null,
            "occurredAt": "2026-09-20T15:04:12.000Z",
            "data": null
          }
        }
      ],
      "pageInfo": {
        "hasNextPage": false,
        "endCursor": "next-cursor"
      },
      "recommendedPollAfterMs": 5000
    }
  }
}

Polling for changes

Use events as a pull-based alternative to webhooks: store a cursor, ask for everything after it, and process events in order.

Event types

Each event's type is <subject>.<action>. subjectType and action are also returned separately.

SubjectsubjectTypetype prefixActions
Contentpage, section, article, blog, product, collection, template, siteSettingscontent.<subjectType> (content.site_settings for site settings)created, updated, deleted, restored, published, unpublished
Content releasecontentReleasecontent.releasecreated, updated, scheduled, unscheduled, published, archived, failed
Schedulescheduleschedulecreated, updated, scheduled, unscheduled, deleted, succeeded, failed
A/B testtestexperimentcreated, updated, scheduled, unscheduled, started, paused, ended, deleted, failed

For example, content.page.published, content.release.scheduled, or experiment.started.

  • updated events include data.changedFields, the fields that changed.
  • Content events are recorded against the release they were made in, so releaseId is set for changes staged in a release.
  • Release, schedule, and test events always have a null releaseId. Poll without the X-Pack-Release-Id header to receive them.
  • Schedule events are only returned to stores whose plan includes content scheduling.
  • A pending scheduled run ends with unscheduled (including when archiving a release cancels its schedule, with data.reason set to archived), the run itself (such as content.release.published or schedule.succeeded), failed, or the subject's deleted event.

Treat subjectType and action as open-ended strings so a new value added later does not break your consumer.

Filtering

  • filter.types takes full type strings. An unknown type, such as schedule.paused, is rejected with an invalid_input error.
  • filter.subjectTypes and filter.actions take the EventSubjectType and EventAction enums.
  • Each list accepts up to 50 values.
  • filter.excludeSystemActor drops events recorded by Pack's system actor.

Cursors and checkpoints

Events are scoped to the store, the X-Environment content environment, and the optional X-Pack-Release-Id release. Keep a separate checkpoint for each combination and each filter you use.

  1. Request up to 100 events per page.
  2. Process each event, then save its edge cursor. Only advance the checkpoint after an event is processed successfully.
  3. Make your handler idempotent using the event id.
  4. When a poll returns no new events, pageInfo.endCursor echoes the after cursor you sent. An initial empty poll returns an empty string.

Adaptive polling

  1. While pageInfo.hasNextPage is true, fetch the next page immediately.
  2. Once caught up, wait at least recommendedPollAfterMs (currently 5 seconds), plus some jitter.
  3. Back off after consecutive empty polls, for example 5, 10, 20, then 30 seconds. Reset when an event arrives.
  4. On transient errors, keep the previous cursor and retry with exponential backoff. A poll can return a transient api_error while an earlier write is still committing, so retry rather than skipping ahead.

Rate limits

Each credential gets a short burst allowance that refills at one request per second, and a separate store-wide budget applies across all credentials. When a request is limited, the error code is RATE_LIMITED. Wait for retryAfterSeconds in the GraphQL error, or the HTTP Retry-After header, before retrying. Following recommendedPollAfterMs keeps you well under the limits.

Retention and initial sync

Events are kept for 30 days. If a consumer has not polled successfully in that window, run a full content resync. An expired cursor does not return a distinct error.

To bootstrap a new consumer:

  1. Record the current time.
  2. Read the content you need, such as with pages and sections.
  3. Poll events with filter.since set to the recorded time and reconcile the results.
  4. Continue with the cursor loop above.

since is a lower-bound hint. The first page can include slightly older events, so process them idempotently.

Polling loop

import { PackClient } from '@pack/client'

const packClient = new PackClient({ token: 'YOUR-PACK-TOKEN' })

const query = `
  query PollEvents($first: Int!, $after: String, $filter: EventFilterInput) {
    events(first: $first, after: $after, filter: $filter) {
      edges {
        cursor
        node {
          id
          type
          subjectId
          occurredAt
          data
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      recommendedPollAfterMs
    }
  }
`

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

async function poll(cursor, handleEvent, saveCursor) {
  const idleDelays = [5000, 10000, 20000, 30000]
  let emptyPolls = 0

  while (true) {
    const response = await packClient.fetch(query, {
      variables: { first: 100, after: cursor },
    })
    const { edges, pageInfo, recommendedPollAfterMs } = response.data.events

    for (const edge of edges) {
      await handleEvent(edge.node) // idempotent on edge.node.id
      cursor = edge.cursor
      await saveCursor(cursor)
    }

    if (pageInfo.hasNextPage) continue

    emptyPolls = edges.length ? 0 : emptyPolls + 1
    const backoff = emptyPolls
      ? idleDelays[Math.min(emptyPolls - 1, idleDelays.length - 1)]
      : 0
    await sleep(Math.max(recommendedPollAfterMs, backoff) + Math.random() * 1000)
  }
}

Was this page helpful?