Guide

Poll the OneLifeVibe event log with a cursor in Node.js

Page the OneLifeVibe event log in Node.js 18+ with GET /api/platform/v1/events, following the after cursor and saving it between runs.

Updated November 6, 2026

Stack
Node.js 18+ (fetch)
API
Platform API v1 · Connectors

The code

// Read the event log from a cursor, and keep the cursor between runs (a file, a row, a KV key).
const BASE = 'https://onelifevibe.com/api/platform/v1';

export async function pollEvents(key, after) {
  const events = [];
  for (;;) {
    const url = new URL(BASE + '/events');
    url.searchParams.set('limit', '100');
    if (after != null) url.searchParams.set('after', String(after));
    const res = await fetch(url, { headers: { authorization: `Bearer ${key}` } });
    if (res.status === 429) {
      const wait = Number(res.headers.get('retry-after') || 60);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error(`events: ${res.status} ${(await res.json()).error?.code}`);
    const page = await res.json();
    events.push(...page.events);
    after = page.cursor;
    if (!page.has_more) return { events, cursor: after };
  }
}

The facts it relies on

Related guides

What you are building

A polling job that reads the OneLifeVibe event log one page at a time and remembers where it stopped. Each run starts from a stored sequence cursor, calls GET /api/platform/v1/events, and keeps following the cursor until the API reports there is nothing more. What comes back is the records for that window plus the cursor to store for the next run.

The log carries tool.invoked, client.approved, client.suspended, subscription.changed and world.event records. Polling and webhooks return the same records, so this approach fits when you cannot host a receiving endpoint, when you want to backfill history, or when you want a second path that catches anything a missed delivery would leave behind.

The job stays small on purpose: no queue and no scheduler state beyond one number per poller.

Before you start

You need a key with the events:read scope, sent as a Bearer token. Node.js 18 or later gives you fetch, URL and the response headers the loop reads, so there is nothing to install.

Decide where the cursor lives before you write the code around the function: a file next to the process, a row in your database, or a KV key. It is a single number, and it has to survive restarts.

Polling is available on every plan, including the free Sandbox, which allows 1,000 calls a day at 10 requests a minute. Set limit to 100, the maximum the endpoint accepts, so each call carries as much of the log as it can.

How the code works

pollEvents takes your key and the last cursor. Inside the loop it builds the URL from the API base, always sets limit to 100, and sets after only when after is not null. Omitting after means the request starts at the beginning of the log, which is what a first run wants. The fetch sends the key in the authorization header.

Two branches matter. On a 429 the function reads the retry-after header in seconds, falls back to 60 when the header is missing, waits, and continues. It does not touch the cursor before retrying, so the retry asks for the same page. On any other non-OK response it throws with the HTTP status and the error code from the response body, so a bad key or a wrong scope names itself instead of looking like an empty log.

On success it pushes page.events into the running array, reassigns after to page.cursor, and returns the events together with that cursor once has_more is false. Hand the returned cursor to the next run as after and the loop resumes exactly where it stopped. The endpoint also accepts world and type to narrow a page when you do not want the whole log.

Testing it

Call the function against a Sandbox key with after left undefined. The first call returns the earliest page. Log page.has_more and page.cursor, save the cursor somewhere real, then call again and confirm the second batch does not repeat records from the first.

To reach the rate-limit path, call the function repeatedly in a tight loop. With 10 requests a minute on Sandbox you will hit 429 rate_limited quickly; confirm the process waits out the retry-after interval rather than throwing.

Check the failure path too. Any non-429 error response should surface the status and the API error code, not an empty array that looks like a quiet log.

Going to production

Save the cursor only after the events are handled. Writing it first risks skipping records if the process dies mid-batch; writing it afterwards means a crash replays the batch, so make your handling idempotent and a replay stays harmless.

Run one poller per cursor value. Two processes sharing the same after number will each read part of the log, and neither ends up with all of it.

Choose an interval that fits your plan's per-minute allowance, and keep limit at 100 to spend fewer calls per page. If you also run webhook receivers, polling and webhooks return the same records, so you can treat polling as the catch-up path and webhooks as the fast one.

Questions

What scope do I need to read the OneLifeVibe event log?

events:read. Send the key as a Bearer token in the authorization header on GET /api/platform/v1/events.

How do I read the event log from the beginning?

Leave after out of the query string. The endpoint starts at the beginning when after is omitted, and you pass the returned cursor back as after on later calls.

What happens when I poll faster than my plan allows?

The API answers 429 rate_limited and includes a Retry-After header. Wait the stated number of seconds and repeat the same request with the same cursor.

Try it against the real API. A free Sandbox key reads the catalogue and the event log at once, no card.

Get a free key Read the docs

Written from the OneLifeVibe tool registry and platform configuration, and checked automatically before publishing. Something wrong? Tell us.