Outbound webhooks
With an outbound webhook, exoCortex sends a POST request to your system every time an event you subscribed to happens: a ticket created, a status change, an SLA about to expire. This way your system reacts straight away, without polling the API at intervals.
Creating a subscription
Section titled “Creating a subscription”- In exoCortex Core, open Integrations → Webhooks. You need an administrative role.
- Enter the URL that will receive the events. It must be
https://. - Choose the events.
*means all events. - Choose the format:
json: the standard exoCortex format, described on this page;datadog,pagerduty,slack-block: the native format of those services, to use when the URL is theirs.
- If your gateway requires it, add fixed headers, for example an authentication header.
- Save. If you did not enter a signing secret, exoCortex generates one (
whsec_…) and shows it only once: save it, you need it to verify signatures.
With the test action you send the webhook.test event to the subscription and check that your system receives it.
Events
Section titled “Events”| Event | When it arrives |
|---|---|
ticket.created |
A ticket was created |
ticket.comment_added |
A comment was added |
ticket.field_changed |
A field changed |
ticket.assignee_changed |
The assignee changed |
ticket.status_changed |
The status changed |
ticket.priority_changed |
The priority changed |
ticket.sla_warning |
An SLA is about to expire |
ticket.sla_breach |
An SLA has expired |
ticket.resolved |
The ticket was resolved |
ticket.closed |
The ticket was closed |
ticket.reopened |
The ticket was reopened |
ticket.link_created |
A link between tickets was created |
ticket.link_deleted |
A link between tickets was removed |
routing.no_match |
No routing rule found a queue for the ticket |
routing.manual_routed |
An agent routed the ticket by hand |
webhook.test |
You sent a test event to the subscription |
In addition, you can subscribe by name to the events of your tenant’s notification rules, for example approval_requested.
exoCortex may add new event types: your system must ignore the ones it does not know.
Event format
Section titled “Event format”Every event in json format has the same structure:
{ "id": "evt_8a1f4c2e9b7d4e3f8a6b5c4d3e2f1a00", "type": "ticket.status_changed", "created_at": "2026-11-01T09:31:47.120Z", "api_version": "1", "tenant_id": "ACME", "data": { "ticket": { "key": "ACME-1042", "summary": "Payment gateway returns HTTP 502", "status": "In Progress", "priority": "High", "issuetype": "IT-INC", "assignee": "jane.doe", "created": "2026-11-01T09:14:05.000Z", "updated": "2026-11-01T09:31:47.000Z" }, "actor": "jane.doe", "changes": { "from": "Open", "to": "In Progress" } }}ididentifies the event and is the same for all the subscriptions that receive it.actoris the user who caused the event, orsystem.changescontains the event details: for example the previous and new status, or the comment text.
Until 30 June 2027 the body also contains the deprecated fields event, timestamp and source and a copy of the data fields at the top level. A new system must read only the fields described above.
Headers
Section titled “Headers”| Header | Example | Meaning |
|---|---|---|
X-Exo-Timestamp |
1793491200 |
Time of signing, in Unix seconds |
X-Exo-Signature |
v1=5257a869… |
Event signature |
X-Exo-Event |
ticket.created |
Event type, same as type |
X-Exo-Delivery |
dlv_0b9d… |
Unique identifier of the delivery attempt |
Verifying the signature
Section titled “Verifying the signature”Before you trust an event, verify its signature:
- Read the body exactly as it arrived, without parsing and rewriting the JSON: that would change the bytes.
- Discard the request if
X-Exo-Timestampis missing or differs by more than 300 seconds from your clock. This blocks replayed requests. Keep your clock synchronised with NTP. - Calculate the HMAC-SHA256 of
timestamp + "." + bodywith the subscription secret, in lower-case hex. - Compare the result, in constant time, with each
v1=value inX-Exo-Signature. During secret rotation the header can contain more than one, separated by commas.
Example in Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyExoWebhook(rawBody, headers, secret, toleranceSec = 300) { const ts = headers['x-exo-timestamp']; const sigHeader = headers['x-exo-signature'] || ''; if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false; const expected = Buffer.from(createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')); return sigHeader .split(',') .map((s) => s.trim()) .filter((s) => s.startsWith('v1=')) .some((s) => { const got = Buffer.from(s.slice(3)); return got.length === expected.length && timingSafeEqual(got, expected); });}Example in Python:
import hashlib, hmac, time
def verify_exo_webhook(raw_body: bytes, headers, secret: str, tolerance_sec: int = 300) -> bool: ts = headers.get('X-Exo-Timestamp') if not ts or abs(time.time() - int(ts)) > tolerance_sec: return False expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest() sigs = [s.strip()[3:] for s in headers.get('X-Exo-Signature', '').split(',') if s.strip().startswith('v1=')] return any(hmac.compare_digest(s, expected) for s in sigs)The X-NH-Signature header, which signs only the body without a timestamp, is deprecated and will be removed on 30 June 2027.
Responding and handling duplicates
Section titled “Responding and handling duplicates”- Respond with any
2xxcode within 5 seconds. If you have long work to do, queue it and respond straight away. - Delivery is guaranteed at least once: the same event can arrive more than once. Use
idorX-Exo-Deliveryto recognise duplicates, for example by keeping the identifiers seen in the last 5 minutes. - Events can arrive in a different order from the one in which they happened: sort them by
created_at.