Skip to content

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.

  1. In exoCortex Core, open Integrations → Webhooks. You need an administrative role.
  2. Enter the URL that will receive the events. It must be https://.
  3. Choose the events. * means all events.
  4. 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.
  5. If your gateway requires it, add fixed headers, for example an authentication header.
  6. 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.

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.

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" }
}
}
  • id identifies the event and is the same for all the subscriptions that receive it.
  • actor is the user who caused the event, or system.
  • changes contains 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.

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

Before you trust an event, verify its signature:

  1. Read the body exactly as it arrived, without parsing and rewriting the JSON: that would change the bytes.
  2. Discard the request if X-Exo-Timestamp is missing or differs by more than 300 seconds from your clock. This blocks replayed requests. Keep your clock synchronised with NTP.
  3. Calculate the HMAC-SHA256 of timestamp + "." + body with the subscription secret, in lower-case hex.
  4. Compare the result, in constant time, with each v1= value in X-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.

  • Respond with any 2xx code 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 id or X-Exo-Delivery to 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.