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 type | On create / update | On delete |
|---|---|---|
| Event | EventUpdated | EventDeleted |
| Location | LocationUpdated | LocationDeleted |
| Route | RouteUpdated | RouteDeleted |
| EventGroup | EventGroupUpdated | EventGroupDeleted |
| Venue | VenueUpdated | VenueDeleted |
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"
}
]
}| Field | Description |
|---|---|
id | Identifier for the changed item within this delivery. |
trcid | The TRCItem's trcid. |
resourceUri | The API URL to fetch the item's current state. For a DELETED item this URI will return 404. |
resourceType | One of EVENT, LOCATION, ROUTE, EVENTGROUP, VENUE. |
actionType | UPDATED 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:
| Header | Description |
|---|---|
X-EventConnectors-Timestamp | Unix epoch seconds when the delivery was signed. |
X-EventConnectors-Signature | sha256=<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:
- Read
X-EventConnectors-Timestampand reject the request if it is more than 300 seconds from your current time (replay protection). - Compute
expectedfrom the raw body — before any JSON parsing or reserialisation, which would change the bytes. - Compare it to
X-EventConnectors-Signatureusing 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:
| Header | Description |
|---|---|
X-EventConnectors-Delivery | A 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:
- Verifies the signature and timestamp; rejects anything that fails.
- Checks
X-EventConnectors-Deliveryagainst already-processed IDs. - Returns
2xximmediately. - Asynchronously, for each item: if
actionTypeisUPDATED, fetchesresourceUrifor the current state; ifDELETED, removes its local copy.