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.

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