Event Connectors

Sync Events to Your Own Database

Keep a WordPress site or other datastore in step with Event Connectors using an incremental poll, with a webhook alternative

Many integrators keep their own datastore — a WordPress site, a headless CMS, a search index — in step with Event Connectors. This recipe covers the recommended approach: a periodic incremental sync that polls for changes, upserts new and updated events, and removes the ones that went away. It then shows how to do the same with webhooks.

The worked example uses GET /events, but locations, routes, venues, and event groups follow the identical pattern via their own list endpoints — each exposes lastupdated and wfstatus.

Polling is the recommended baseline. It needs no public endpoint, it is self-healing (a missed run is caught by the next one), and it is simple to operate. Use webhooks when you need lower latency — ideally alongside a periodic poll as a backstop (see Webhooks as an alternative).

How it works

Every TRCItem carries a lastupdated timestamp and a wfstatus (workflow status). An incremental sync:

  1. Remembers a high-water mark — the largest lastupdated it has seen so far.
  2. Each run, asks the API for every item changed since that mark, in lastupdated order.
  3. For each item, acts on its wfstatus:
    • approved → the live, published state → upsert into your datastore.
    • archived or deletedremove it from your datastore.
  4. Advances the high-water mark and waits for the next run (e.g. every 15 minutes).

approved is what published=true means; there is no published status. draft, readyforvalidation, and rejected are work-in-progress states and are not synced.

Prerequisites

Your API token must be scoped to see approved, archived, and deleted. A token provisioned for live data only (approved) will never return archived or deleted items, so removals would be invisible to you. If you cannot see archived/deleted events, ask support@eventconnectors.nl to widen your token's status scope.

The query

A single request per page fetches both upserts and removals:

curl -H "Authorization: Bearer YOUR_API_TOKEN" \
  "https://app.eventconnectors.nl/api/events?\
wfstatus=approved,archived,deleted&\
lastupdated=2026-06-17T09:00:00Z&\
sortField=lastupdated&sortOrder=ASC&size=500"
  • lastupdated=<watermark> returns only items changed at or after the watermark (inclusive).
  • wfstatus=approved,archived,deleted returns live items and the ones to remove in one pass.
  • sortField=lastupdated&sortOrder=ASC lets you advance the watermark safely and resume after a crash.

The response is a JSON array of events; each has top-level trcid, lastupdated, and wfstatus.

Worked example

const BASE = "https://app.eventconnectors.nl/api";
const TOKEN = process.env.EVENTCONNECTORS_TOKEN;
const PAGE_SIZE = 500;

async function getEvents(params) {
  const url = `${BASE}/events?${new URLSearchParams(params)}`;
  const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
  if (!res.ok) throw new Error(`GET /events failed: ${res.status}`);
  return res.json(); // bare array
}

async function runIncrementalSync() {
  // Load the watermark persisted by the previous successful run (null on first run).
  let cursor = await loadWatermark();
  let maxSeen = cursor;

  while (true) {
    const params = {
      wfstatus: "approved,archived,deleted",
      sortField: "lastupdated",
      sortOrder: "ASC",
      size: String(PAGE_SIZE),
    };
    if (cursor) params.lastupdated = cursor;

    const items = await getEvents(params);
    if (items.length === 0) break;

    for (const item of items) {
      if (item.wfstatus === "approved") {
        await upsertEvent(item);       // idempotent, keyed by item.trcid
      } else {
        await removeEvent(item.trcid); // archived or deleted
      }
      if (!maxSeen || item.lastupdated > maxSeen) maxSeen = item.lastupdated;
    }

    if (items.length < PAGE_SIZE) break;
    cursor = items[items.length - 1].lastupdated; // keyset: advance within this run
  }

  // Persist only after the whole run succeeds, so a crash re-runs from the old mark.
  if (maxSeen) await saveWatermark(maxSeen);
}

Why this is safe

  • Inclusive boundary, idempotent writes. lastupdated is a >= filter, so the item(s) exactly on the watermark are returned again next run. Because upsertEvent is keyed by trcid (insert-or-update) and removeEvent is a no-op when the item is already gone, reprocessing them changes nothing.
  • No dependency on your clock. The watermark is a value the API gave you, not your own now(), so clock skew between your server and ours can never make you skip an item.
  • Resumable. The watermark is only saved after a successful run. If a run dies halfway, the next run simply starts again from the last good mark.

First run (bootstrap)

On the first run there is no watermark, so the same loop performs a full backfill: it pages through every approved/archived/deleted event from the beginning, then stores the high-water mark for incremental runs thereafter. For a large catalogue this first pass may take many pages — that is expected.

Event payloads reference images on the Event Connectors asset server (for example https://app.eventconnectors.nl/api/assets/{id}/{filename}). We recommend downloading those images during sync and serving them from your own server or CDN, rather than linking to the asset-server URL directly in your pages.

Self-hosting the images:

  • keeps your site working even if the asset server is briefly unavailable or an asset URL changes;
  • lets you control caching, sizing, and delivery (your own CDN, your own cache headers);
  • avoids putting page-render traffic on our asset server.

When you upsert an event, fetch each referenced image once, store it locally (in WordPress, the media library), and point your content at the local copy. The asset endpoint supports on-the-fly resizing via the w, h, and mode query parameters, so you can download an appropriately sized variant. Re-download only when the event's lastupdated (or the asset reference) changes.

Periodic reconciliation

Incremental removal only catches items whose archived/deleted transition was visible to your token and bumped lastupdated. To guard against anything that slips through — a missed transition, a long outage, an item removed in a way that didn't move its timestamp — run an occasional full reconciliation (for example, nightly):

  1. Fetch the trcid of every currently approved event (published=true, paged).
  2. Build the set of live trcids.
  3. Delete locally any synced event whose trcid is not in that set.

This is heavier than an incremental run, so run it far less often. Together, the fast incremental poll keeps you fresh and the periodic reconciliation keeps you correct.

Mapping to WordPress

The mechanics are datastore-agnostic; on WordPress a typical mapping is:

  • Custom post type (e.g. event) holds each synced event.
  • trcid as the idempotency key — store it in post meta (e.g. _ec_trcid) and look up by it on upsert, so re-processing updates the same post instead of creating duplicates.
  • wp_insert_post / wp_update_post for approved items; wp_trash_post (or delete) for archived/deleted.
  • Images go through media_sideload_image into the media library (see above).
  • WP-Cron (or a real system cron hitting WP-Cron) runs the incremental sync on your interval and the reconciliation nightly.

Other stacks (a headless CMS, a custom database, a search index) follow the same shape: an upsert keyed by trcid, a delete on removal, and a persisted high-water mark.

Webhooks as an alternative

Webhooks push a notification the moment an event changes, instead of waiting for the next poll. The same wfstatus logic applies: on a webhook delivery, fetch the item's resourceUri, then upsert if approved or remove if archived/deleted.

Incremental pollWebhooks
LatencyUp to the poll interval (e.g. 15m)Near real-time
InfrastructureNone — outbound calls onlyA public HTTPS endpoint to receive POSTs
Missed changesSelf-healing — next run catches upDelivery is at-least-once and can be dropped
EffortLowVerify signatures, dedupe, ack fast

Recommended: combine them. Use webhooks for low latency and keep the periodic reconciliation poll as a backstop, since webhook delivery is at-least-once and a delivery can be missed. The reconciliation pass guarantees you converge even if a webhook never arrives.

Choosing a poll interval

The interval is yours to set. Around 5–15 minutes suits most catalogues: shorter means fresher data but more requests; longer means less load but more lag. The reconciliation pass can run far less often (nightly is typical).

See the Paginate a Result Set recipe for paging mechanics, the Webhooks guide for the push model, and the API Reference for the full GET /events parameter list.

On this page