Skip to content
NEXTOREx

Guides

Webhooks

Outgoing webhooks: events, signature verification, retries, pausing, secret rotation and replay.

Webhooks let NEXTOREx call your server when something happens, so you do not have to poll. Each call is an HTTPS POST with a JSON body and a signature you can verify.

Supported events

EventWhen it is sent
crm.lead.createdA CRM lead was created.
crm.lead.updatedA real change was made to a lead's public fields.
education.student.createdAn Education student was created.
invoice.paidAn invoice became fully paid.
payment.failedAn online payment for one of your customer invoices failed.
subscription.updatedYour NEXTOREx subscription changed (also sent when it is first created).

Events are sent only after the underlying change is saved, and only for a real change. No other events exist in version 1.

Setting up an endpoint

A webhook endpoint belongs to one company and one place in your organisation tree, subscribes to the event types you choose, and receives events for that place and everything beneath it. subscription.updated is business-wide and goes to every active subscribed endpoint of the business.

  • Webhook endpoints are managed by an authorised NEXTOREx user (the same permission as API keys) on an account with Developer API access. You cannot manage them with an API key, and a dashboard screen for managing endpoints is not available yet. While it is being built, ask NEXTOREx to register or change an endpoint for you.
  • The URL must be HTTPS on port 443 in production, with a publicly resolvable host name. Addresses that point at private, loopback or internal networks are refused, and redirects are never followed.
  • An account can have up to 20 endpoints.
  • When an endpoint is created, its signing secret (it starts with whsec_) is shown once. Store it like an API key.

What a delivery looks like

POST /your/webhook/path HTTP/1.1
Content-Type: application/json
User-Agent: Nextorex-Webhooks/1.0
Nextorex-Delivery-Id: whd_0123456789abcdef01234567
Nextorex-Event-Id: evt_0123456789abcdef01234567
Nextorex-Event-Type: crm.lead.created
Nextorex-Timestamp: 1792000000
Nextorex-Signature: v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
  "id": "evt_0123456789abcdef01234567",
  "type": "crm.lead.created",
  "api_version": "v1",
  "created_at": "2026-10-19T09:00:00+00:00",
  "data": {
    "id": "ld_0123456789abcdef01234567",
    "source": "Website",
    "status": "New",
    "lifecycle": "active",
    "priority": "medium",
    "is_hot": false,
    "node": "nd_0123456789abcdef01234567",
    "created_at": "2026-10-19T09:00:00+00:00",
    "updated_at": "2026-10-19T09:00:00+00:00"
  }
}
HeaderMeaning
Nextorex-Event-IdIdentifies the event. It is the same on every retry and replay. Use it to de-duplicate.
Nextorex-Delivery-IdIdentifies one delivery of the event. Constant across automatic retries; a manual replay gets a new one.
Nextorex-Event-TypeThe event type.
Nextorex-TimestampUnix time in seconds when this attempt was signed. Fresh on every attempt.
Nextorex-SignatureOne or more v1=<hex> signatures, comma separated.

Event data

data is a short, hand-picked list of fields with public ids only. Lead and student events carry no name, email or phone number: fetch the full record with a normal GET using the id. A crm.lead.updated event also lists changed_fields. subscription.updated carries the same object as GET /billing/subscription plus changed_fields.

Eventdata fields
crm.lead.created, crm.lead.updatedid, source, status, lifecycle, priority, is_hot, node, created_at, updated_at (and changed_fields on update)
education.student.createdid, code, status, is_active, node, created_at
invoice.paidid, number, currency, total, amount_paid, paid_at, customer_id, node
payment.failedid (the payment), invoice_id, amount, currency, gateway, node, failed_at
subscription.updatedthe subscription object, plus changed_fields

Verifying the signature

Always verify before trusting a delivery. The signature is an HMAC-SHA256 of the string "<timestamp>.<raw request body>", using your signing secret (the whole string, including whsec_) as the key, written as lowercase hexadecimal.

  1. Read the raw request body exactly as received. Do not parse and re-serialise it.
  2. Read Nextorex-Timestamp. Reject the request if it differs from your clock by more than 300 seconds (replay protection).
  3. Compute HMAC_SHA256(secret, timestamp + "." + body) as hex.
  4. Compare it to every v1= value in Nextorex-Signature using a constant-time comparison. Accept if any matches. (During a secret rotation two signatures are sent.)
  5. Look up Nextorex-Event-Id. If you have already processed it, return 200 and do nothing.
  6. Process the event, and reply with a 2xx status quickly. Do the slow work afterwards.

Node.js

import crypto from 'node:crypto'

export function verifyNextorexWebhook(rawBody, headers, secret) {
  const timestamp = headers['nextorex-timestamp']
  const header = headers['nextorex-signature'] || ''
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  const expectedBuf = Buffer.from(expected)

  return header.split(',').some((part) => {
    const [version, value] = part.trim().split('=')
    if (version !== 'v1' || !value) return false
    const given = Buffer.from(value)
    return given.length === expectedBuf.length && crypto.timingSafeEqual(given, expectedBuf)
  })
}

PHP

function verifyNextorexWebhook(string $rawBody, array $headers, string $secret): bool
{
    $timestamp = $headers['Nextorex-Timestamp'] ?? '';
    $header = $headers['Nextorex-Signature'] ?? '';
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
        return false;
    }
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    foreach (explode(',', $header) as $part) {
        [$version, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($version === 'v1' && $value !== '' && hash_equals($expected, $value)) {
            return true;
        }
    }
    return false;
}

In PHP use file_get_contents('php://input') for the raw body. In Node frameworks, capture the raw body before any JSON parser (for example express.raw({ type: 'application/json' })).

Retries

A delivery succeeds on any 2xx. NEXTOREx retries when your server times out, cannot be reached, or answers 408, 425, 429 or 5xx. It waits 1, 5, 15, 30, 60, 120, 240 and 480 minutes and then 7 hours between attempts: 10 attempts over about 23 hours, after which the delivery is marked failed.

A redirect (3xx), 410 or any other 4xx fails the delivery immediately, because retrying cannot help. Respond within a few seconds: connection and response timeouts are short.

Because retries and replays can arrive more than once, and occasionally out of order, make your handler idempotent by de-duplicating on Nextorex-Event-Id.

Endpoint pause

If an endpoint has 10 deliveries in a row that fail permanently, NEXTOREx pauses it so it stops hammering a broken server. A later success resets the counter. A paused endpoint receives nothing until an authorised user re-enables it, which re-checks the URL.

Rotating the signing secret

Rotation creates a new secret that is shown once. For an overlap window (24 hours by default, at most 72) the old secret keeps working, and every delivery carries two v1= signatures. Because your verifier accepts any matching signature, you can deploy the new secret without dropping a single event. After the overlap only the new secret signs.

Manual replay

An authorised user can replay a finished delivery. A replay re-sends the same stored event (same Nextorex-Event-Id and identical body) as a new delivery with a new Nextorex-Delivery-Id and a fresh timestamp and signature. Your de-duplication on the event id is what keeps a replay harmless.

Good to know

  • Delivery history (status, attempts, HTTP status, timing) is kept for 30 days.
  • Webhooks are a convenience on top of the business: a webhook problem never blocks or rolls back a sale, lead or payment.
  • Webhooks can be paused for the whole platform in an emergency; events then stop and resume safely when it is lifted.