Skip to content

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

How webhooks work

  1. 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.
  2. When a record changes, Nextora writes the event down in the same transaction and delivers it within seconds — whichever way the change was made.
  3. Your endpoint checks the signature, answers 2xx within 10 seconds, and does the work afterwards.
  4. 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.

Event catalog

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

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"
      }
    }
  }
}

The event object

  • 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 *.updated and deal.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 old stage (id, name, type), and the old pipeline (id, name) when the pipeline changed too.

The delivery request

Each delivery is a POST with a JSON body and these headers:

Request
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"
      }
    }
  }
}
HeaderMeaning
Nextora-Signaturet= the signing time in Unix seconds, v1= the signature. See verify signatures.
Nextora-EventThe event type, the same as type in the body.
Nextora-DeliveryThe delivery ID, as in the deliveries log.
Nextora-AttemptThe attempt number: 1 to 6 for automatic attempts; a manual retry continues the count — 7, 8, …
User-AgentNextora-Webhooks/1.0.

Verify signatures

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:

  1. Read t and v1 from the Nextora-Signature header.
  2. Reject the request if t is more than 300 seconds from your clock — this stops replays of old events.
  3. Compute HMAC-SHA256 of t, a period, and the raw request body, keyed with the subscription’s secret — the whole whsec_… string — as hexadecimal.
  4. Compare it with v1 in constant time. Reject the request when they differ.
server.mjs
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:

Shell
t=1791623702
printf '%s.%s' "$t" "$(cat body.json)" \
  | openssl dgst -sha256 -hmac "$NEXTORA_WEBHOOK_SECRET" -hex

Rotating 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.

Responses and retries

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:

AttemptWhen
1Within seconds of the change
21 minute after attempt 1 fails
35 minutes after attempt 2
430 minutes after attempt 3
52 hours after attempt 4
68 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.

Ordering and duplicates

  • Events can arrive out of order — a retried deal.updated may land after a later one. Compare created_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 id of 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_changed and deal.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.