Events
Webhooks
Webhooks tell your systems the moment something changes in Nextora. Subscribe a URL to the events you care about; Nextora sends a signed JSON POST for every change — made in the app, through the API or MCP, by a workflow or by an import.
- Events
- 20
- Signature
- HMAC-SHA256
- Automatic attempts
- 6
- Manage
- Subscriptions API
- Create a subscription in Settings → Developers → Webhooks, or with the subscriptions API: a URL and the events to send. You get a signing secret,
whsec_…, once. - When a record changes, Nextora writes the event down in the same transaction and delivers it within seconds — whichever way the change was made.
- Your endpoint checks the signature, answers
2xxwithin 10 seconds, and does the work afterwards. - If it does not answer in time, Nextora retries: six automatic attempts over about ten and a half hours, and you can retry by hand after that. Every attempt is in the deliveries log, in the app and in the API.
Choose an event to see its payload. data.object has exactly the shape the REST API returns for the record — see the event object for deletes.
Contacts
Companies
Deals
- deal.stage_changed
{ "id": "evt_4f7a1c9f41d48f0a3c5e7b9d", "type": "deal.stage_changed", "created_at": "2026-10-10T09:15:02Z", "workspace": "acme", "data": { "object": { "id": "8f2d6c1a-4b7e-4f0a-9c3d-2e1b5a6f7c80", "title": "Northwind Analytics — annual plan", "value": 24000, "currency": "EUR", "status": "in_progress", "pipeline": { "id": "9d4b2f7a-3c8e-4b1d-a6f2-5e7c9b0d1a34", "name": "New business" }, "stage": { "id": "7c3f5a9e-4d1b-4e8c-a2f6-9b0d3e5a7c18", "name": "Negotiation", "type": "open" }, "pipeline_id": "9d4b2f7a-3c8e-4b1d-a6f2-5e7c9b0d1a34", "stage_id": "7c3f5a9e-4d1b-4e8c-a2f6-9b0d3e5a7c18", "probability": 60, "expected_close_date": "2026-11-27", "closed_at": null, "forecast_category": "best_case", "loss_reason": null, "competitor": null, "contact_id": "3f1c9a7e-2b4d-4c8e-9f60-1a2b3c4d5e6f", "company_id": "2b7e4c9a-5d1f-4a8e-9b3c-7f0a1d2e4c56", "owner_id": "0c4f8e2a-7b3d-4a1f-9e6c-5d2b8a1f3e47", "custom_fields": { "seats": 40 }, "created_at": "2026-10-01T10:12:00Z", "updated_at": "2026-10-10T09:15:02Z" }, "previous": { "stage_id": "5b1e9f3c-2a7d-4c6e-8f1b-3d9a0c4e7b21", "stage": { "id": "5b1e9f3c-2a7d-4c6e-8f1b-3d9a0c4e7b21", "name": "Proposal", "type": "open" } } } }
Tasks
Leads
Notes
{
"id": "evt_4f7a1c9f41d48f0a3c5e7b9d",
"type": "deal.stage_changed",
"created_at": "2026-10-10T09:15:02Z",
"workspace": "acme",
"data": {
"object": {
"id": "8f2d6c1a-4b7e-4f0a-9c3d-2e1b5a6f7c80",
"title": "Northwind Analytics — annual plan",
"value": 24000,
"currency": "EUR",
"status": "in_progress",
"pipeline": {
"id": "9d4b2f7a-3c8e-4b1d-a6f2-5e7c9b0d1a34",
"name": "New business"
},
"stage": {
"id": "7c3f5a9e-4d1b-4e8c-a2f6-9b0d3e5a7c18",
"name": "Negotiation",
"type": "open"
},
"pipeline_id": "9d4b2f7a-3c8e-4b1d-a6f2-5e7c9b0d1a34",
"stage_id": "7c3f5a9e-4d1b-4e8c-a2f6-9b0d3e5a7c18",
"probability": 60,
"expected_close_date": "2026-11-27",
"closed_at": null,
"forecast_category": "best_case",
"loss_reason": null,
"competitor": null,
"contact_id": "3f1c9a7e-2b4d-4c8e-9f60-1a2b3c4d5e6f",
"company_id": "2b7e4c9a-5d1f-4a8e-9b3c-7f0a1d2e4c56",
"owner_id": "0c4f8e2a-7b3d-4a1f-9e6c-5d2b8a1f3e47",
"custom_fields": {
"seats": 40
},
"created_at": "2026-10-01T10:12:00Z",
"updated_at": "2026-10-10T09:15:02Z"
},
"previous": {
"stage_id": "5b1e9f3c-2a7d-4c6e-8f1b-3d9a0c4e7b21",
"stage": {
"id": "5b1e9f3c-2a7d-4c6e-8f1b-3d9a0c4e7b21",
"name": "Proposal",
"type": "open"
}
}
}
}- idstring
Unique event ID,
evt_…. The same on every attempt — use it to drop duplicates. - typestring
The event type, such as
deal.stage_changed. - created_atdate-time
When the change happened (UTC).
- workspacestring
The workspace the event comes from, such as
acme. - dataobject
What changed.
- objectobject
The record after the change, as the REST API returns it. For
*.deleted, the record as it was when it was deleted — or only{ "id": "…", "deleted": true }when the record had already been purged by the time the delete was processed. - previousobject
Only on
*.updatedanddeal.stage_changed: the old values of the fields that changed; fields that did not change are absent. For a deal whose stage changed it also holds the oldstage(id,name,type), and the oldpipeline(id,name) when the pipeline changed too.
Each delivery is a POST with a JSON body and these headers:
POST /webhooks/nextora HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: Nextora-Webhooks/1.0
Nextora-Event: deal.stage_changed
Nextora-Delivery: d8e0f2a4-6b8c-4d1e-9f3a-7c9e1b3d5f82
Nextora-Attempt: 1
Nextora-Signature: t=1791623702,v1=6d0f1e8c4b2a9d7e5f3c1a8b6d4e2f0a9c7b5d3e1f8a6c4b2d0e9f7a5c3b1d8e
{
"id": "evt_4f7a1c9f41d48f0a3c5e7b9d",
"type": "deal.stage_changed",
"created_at": "2026-10-10T09:15:02Z",
"workspace": "acme",
"data": {
"object": {
"id": "8f2d6c1a-4b7e-4f0a-9c3d-2e1b5a6f7c80",
"title": "Northwind Analytics — annual plan",
"value": 24000,
"currency": "EUR",
"status": "in_progress",
"pipeline": {
"id": "9d4b2f7a-3c8e-4b1d-a6f2-5e7c9b0d1a34",
"name": "New business"
},
"stage": {
"id": "7c3f5a9e-4d1b-4e8c-a2f6-9b0d3e5a7c18",
"name": "Negotiation",
"type": "open"
},
"pipeline_id": "9d4b2f7a-3c8e-4b1d-a6f2-5e7c9b0d1a34",
"stage_id": "7c3f5a9e-4d1b-4e8c-a2f6-9b0d3e5a7c18",
"probability": 60,
"expected_close_date": "2026-11-27",
"closed_at": null,
"forecast_category": "best_case",
"loss_reason": null,
"competitor": null,
"contact_id": "3f1c9a7e-2b4d-4c8e-9f60-1a2b3c4d5e6f",
"company_id": "2b7e4c9a-5d1f-4a8e-9b3c-7f0a1d2e4c56",
"owner_id": "0c4f8e2a-7b3d-4a1f-9e6c-5d2b8a1f3e47",
"custom_fields": {
"seats": 40
},
"created_at": "2026-10-01T10:12:00Z",
"updated_at": "2026-10-10T09:15:02Z"
},
"previous": {
"stage_id": "5b1e9f3c-2a7d-4c6e-8f1b-3d9a0c4e7b21",
"stage": {
"id": "5b1e9f3c-2a7d-4c6e-8f1b-3d9a0c4e7b21",
"name": "Proposal",
"type": "open"
}
}
}
}| Header | Meaning |
|---|---|
Nextora-Signature | t= the signing time in Unix seconds, v1= the signature. See verify signatures. |
Nextora-Event | The event type, the same as type in the body. |
Nextora-Delivery | The delivery ID, as in the deliveries log. |
Nextora-Attempt | The attempt number: 1 to 6 for automatic attempts; a manual retry continues the count — 7, 8, … |
User-Agent | Nextora-Webhooks/1.0. |
Anyone can send a request to your URL. The signature proves an event came from Nextora and was not changed on the way. Check it on every request, before you parse the body:
- Read
tandv1from theNextora-Signatureheader. - Reject the request if
tis more than 300 seconds from your clock — this stops replays of old events. - Compute HMAC-SHA256 of
t, a period, and the raw request body, keyed with the subscription’s secret — the wholewhsec_…string — as hexadecimal. - Compare it with
v1in constant time. Reject the request when they differ.
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.NEXTORA_WEBHOOK_SECRET; // the whole whsec_… string
const TOLERANCE_SECONDS = 300;
function verify(rawBody, header) {
const parts = Object.fromEntries(
(header || '').split(',').map(part => part.trim().split('=')),
);
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac('sha256', SECRET)
.update(`${timestamp}.`)
.update(rawBody) // the exact bytes received, not re-serialized JSON
.digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
const app = express();
app.post('/webhooks/nextora', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, req.get('Nextora-Signature'))) return res.sendStatus(400);
const event = JSON.parse(req.body);
res.sendStatus(200); // answer first…
queue.push(event); // …then do the work
});
app.listen(3000);To check a signature by hand, for example while debugging:
t=1791623702
printf '%s.%s' "$t" "$(cat body.json)" \
| openssl dgst -sha256 -hmac "$NEXTORA_WEBHOOK_SECRET" -hexRotating the secret takes effect at once: the old secret stops working immediately, and every later delivery carries a single v1 made with the new secret — there is no overlap window. Deploy the new secret straight away; deliveries your endpoint rejects in the meantime are retried on the usual schedule.
A delivery succeeds when your endpoint answers any 2xx status within 10 seconds. Anything else — another status, a timeout, a connection error — is retried on this schedule:
| Attempt | When |
|---|---|
| 1 | Within seconds of the change |
| 2 | 1 minute after attempt 1 fails |
| 3 | 5 minutes after attempt 2 |
| 4 | 30 minutes after attempt 3 |
| 5 | 2 hours after attempt 4 |
| 6 | 8 hours after attempt 5 — the last |
After the sixth attempt the delivery is failed. You can still retry it once your endpoint is fixed: a manual retry is the delivery’s next attempt, so it arrives with Nextora-Attempt: 7 — and attempts in the deliveries log says 7. Count attempts; do not assume six is the most you will see.
A subscription whose deliveries have failed for three days in a row is paused — is_active becomes false, paused_reason becomes failing — and its owner is notified. Resume it by setting is_active to true.
- Events can arrive out of order — a retried
deal.updatedmay land after a later one. Comparecreated_at, or fetch the record from the API, before you overwrite newer data. - An event can arrive more than once, for example when your endpoint processed it but answered too late. Store the
idof handled events and skip ones you have seen. - One change can produce more than one event — moving a deal to its Won stage, for example, can send both
deal.stage_changedanddeal.won. Subscribe to the ones you need.
Send a test event — type webhook.test — from Settings → Developers → Webhooks, or with Send a test event: it is signed and delivered like a real one, and the response shows what your endpoint answered. While developing, expose a local server through any HTTPS tunnel and subscribe its URL.