Event Connectors

Webhooks

Receive an HTTP callback whenever a TRCItem is created, updated, or deleted, with signature verification and at-least-once delivery

A webhook is an HTTP POST that the Event Connectors API sends to a URL you register, whenever a TRCItem is created, updated, or deleted. Instead of polling the API for changes, you register a callback URL once and the API notifies you as changes happen.

The webhook payload is a notification, not the data. It tells you what changed and gives you a resourceUri to fetch the current state — always re-fetch that URI for the authoritative version rather than trusting the payload as a snapshot.

Registering a webhook

A webhook URL is registered per API token, in the token's callBackUrl field. Create a token with a callback URL via:

POST /accounts/{accountId}/tokens
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "name": "My integration",
  "roles": ["ROLE_EXTERNAL_USER"],
  "callBackUrl": "https://example.com/event-connectors/webhook"
}

The response includes a server-generated signingSecret. It is shown only once — store it securely. You will not be able to read it again; if you lose it, rotate the secret.

{
  "name": "My integration",
  "callBackUrl": "https://example.com/event-connectors/webhook",
  "signingSecret": "whsec_8f3a...e21c",
  "created": "2026-06-17T10:00:00Z"
}

Webhooks are scoped to the token's organisation: you only receive events for TRCItems matching the token's userorganisation filter.

Changing or removing the callback URL

The callBackUrl is editable after creation via PUT /accounts/{accountId}/tokens/{tokenId}. Changing the URL keeps the existing signingSecret (so your receiver keeps verifying); sending an empty callBackUrl removes the webhook and its secret. Adding a callback URL to a token that never had one mints a signingSecret, returned once in that update response.

Rotating the signing secret

Because the secret is never readable after it is issued, you obtain a new one by rotating:

POST /accounts/{accountId}/tokens/{tokenId}/rotate-secret
Authorization: Bearer <admin-token>

The response returns a fresh signingSecret once. The previous secret stops verifying immediately, so update your receiver as part of the rotation. (Rotation requires the token to have a callBackUrl; it is also how tokens created before signing existed obtain their first secret.)

See the API Reference for the full POST /accounts/{accountId}/tokens request, response schema, and the documented callbacks.

What triggers a webhook

A webhook fires for create/update and delete operations across five resource types, giving ten event types:

Resource typeOn create / updateOn delete
EventEventUpdatedEventDeleted
LocationLocationUpdatedLocationDeleted
RouteRouteUpdatedRouteDeleted
EventGroupEventGroupUpdatedEventGroupDeleted
VenueVenueUpdatedVenueDeleted

Creates and updates both map to UPDATED — re-fetch the resourceUri to see the current state regardless of which it was.

The payload

Changes are batched: a single delivery can carry multiple items in one HTTP POST. The body is a CallbackBody:

{
  "items": [
    {
      "id": "507f1f77bcf86cd799439011_EventUpdated",
      "trcid": "trc-12345",
      "resourceUri": "https://app.eventconnectors.nl/api/events/507f1f77bcf86cd799439011",
      "resourceType": "EVENT",
      "actionType": "UPDATED"
    },
    {
      "id": "507f1f77bcf86cd799439012_LocationDeleted",
      "trcid": "trc-12346",
      "resourceUri": "https://app.eventconnectors.nl/api/locations/507f1f77bcf86cd799439012",
      "resourceType": "LOCATION",
      "actionType": "DELETED"
    }
  ]
}
FieldDescription
idIdentifier for the changed item within this delivery.
trcidThe TRCItem's trcid.
resourceUriThe API URL to fetch the item's current state. For a DELETED item this URI will return 404.
resourceTypeOne of EVENT, LOCATION, ROUTE, EVENTGROUP, VENUE.
actionTypeUPDATED or DELETED.

Assets are not covered. Uploading, editing, or deleting an asset fires no webhook. To keep a media library in step, poll GET /assets?lastupdated=... — see Syncing the media library itself.

Verifying a webhook

Every delivery carries two headers so you can confirm it genuinely came from Event Connectors and has not been tampered with or replayed:

HeaderDescription
X-EventConnectors-TimestampUnix epoch seconds when the delivery was signed.
X-EventConnectors-Signaturesha256=<hex>, an HMAC-SHA256 over the signed payload, keyed with your signingSecret.

The signed payload is the timestamp, a literal ., and the exact raw request body:

signed_payload = X-EventConnectors-Timestamp + "." + <raw request body>
expected       = "sha256=" + hex( HMAC_SHA256(signingSecret, signed_payload) )

To verify:

  1. Read X-EventConnectors-Timestamp and reject the request if it is more than 300 seconds from your current time (replay protection).
  2. Compute expected from the raw body — before any JSON parsing or reserialisation, which would change the bytes.
  3. Compare it to X-EventConnectors-Signature using a constant-time comparison.
import crypto from 'node:crypto';

function verify(req, signingSecret) {
  const timestamp = req.headers['x-eventconnectors-timestamp'];
  const signature = req.headers['x-eventconnectors-signature'];

  // req.rawBody must be the exact bytes received, not JSON.stringify(req.body)
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!timestamp || age > 300) return false;

  const expected =
    'sha256=' +
    crypto
      .createHmac('sha256', signingSecret)
      .update(`${timestamp}.${req.rawBody}`)
      .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Sign over the raw bytes you received. Parsing the JSON and re-stringifying it can reorder keys or change whitespace, producing a different signature.

Responding and reliability

Acknowledge fast. Return any 2xx within 5 seconds. Do your real processing asynchronously — enqueue the delivery and ack immediately.

Delivery is at-least-once. A 5xx, a timeout, or a connection error is treated as a transient failure: the API retries with exponential backoff for up to roughly 24 hours, then gives up and drops the delivery. The token itself stays active. A 4xx is treated as permanent — the receiver explicitly rejected the delivery — so it is not retried. Because of retries, the same delivery can arrive more than once.

Deduplicate on the delivery id. Every delivery carries:

HeaderDescription
X-EventConnectors-DeliveryA UUID that is stable across all retries of the same delivery.

Record the IDs you have processed and skip any you have already handled. Re-fetching each resourceUri to apply current state is itself idempotent, so a duplicate that slips through is harmless.

There is no ordering guarantee across deliveries. Two updates to the same item may arrive out of order — relying on resourceUri for the current state (rather than the payload) makes ordering irrelevant.

Putting it together

A robust receiver:

  1. Verifies the signature and timestamp; rejects anything that fails.
  2. Checks X-EventConnectors-Delivery against already-processed IDs.
  3. Returns 2xx immediately.
  4. Asynchronously, for each item: if actionType is UPDATED, fetches resourceUri for the current state; if DELETED, removes its local copy.

On this page