Webhooks

Receive realtime events from NextX via HTTP push instead of polling. Each event sends a POST with JSON to your URL, signed with HMAC-SHA256 so you can verify the source.

1. Overview

Webhooks work like Meta's: each workspace has exactly one webhook (one URL) per service — CRM, Sales, Accounts & Branches, Office, Omnichannel chat. You pick a service, give it an HTTPS URL, then switch on the events of that service one by one. When an event happens in the workspace, NextX sends a signed request to the URL. Your endpoint must return 2xx within 15 seconds; otherwise the system redelivers — up to 8 delivery attempts (the first + 7 retries over ~24 hours), with increasing backoff (see Delivery & retry).

Webhooks are managed at the workspace level with a user session: Authorization: Bearer <USER_ACCESS_TOKEN> of a user holding webhook.endpoints.manage (listing needs webhook.endpoints.view, delivery logs webhook.deliveries.view / webhook.deliveries.manage). An application's OAuth token (client_credentials) and API keys cannot call these APIs: the webhook module is not granted to applications. An app/agent can guide the customer, or the customer sets it up in the app.

2. Set up a webhook per service

In the app: Settings › Apps & API › Webhooks → pick a service → paste the URL → Verify and save → switch on each event you need. The steps below are the same flow over the API.

Step 1 — List services and their events

curl https://api.nextx.vn/api/v1/webhook/services \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>"

Returns services[]: each service has a service code (CRM, SALES, IAM, OFFICE, COMM), a displayName, its events[] (eventType, displayName, description) and endpoint — the current webhook, or null if not configured yet. legacy[] lists old-style endpoints (created without a service) that have not been deleted.

Step 2 — Verify the URL (recommended)

curl -X POST https://api.nextx.vn/api/v1/webhook/endpoints/verify \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-app.com/webhooks/nextx/crm", "verifyToken": "my-random-token" }'

NextX sends a POST with type: "endpoint.verification", a random challenge and your verify_token. Your server must reply 2xx with the challenge string somewhere in the response body. Nothing is saved by this call; the result is success, responseStatus, durationMs, challenge, responseBody, errorMessage and failureReason (0 OK · 1 unreachable — DNS/TLS/timeout/private IP · 2 HTTP error · 3 returned 2xx but did not echo the challenge).

// Node.js (Express) — answer the verification ping BEFORE checking the signature
app.post("/webhooks/nextx/crm", express.raw({ type: "application/json" }), (req, res) => {
  const event = JSON.parse(req.body.toString("utf8"));
  if (event.type === "endpoint.verification") {
    // optional: compare event.verify_token with the verifyToken you sent
    return res.status(200).json({ challenge: event.challenge });
  }
  // ...verify X-NextX-Signature-256 (section 6), then process the event
  res.sendStatus(200);
});

The verification request is signed with a one-time secret (there is no real secret yet), so it will not pass your signature check — handle endpoint.verification first, as above. Echoing the challenge reveals nothing secret.

Step 3 — Set the service's URL (create or update)

curl -X PUT https://api.nextx.vn/api/v1/webhook/services/CRM \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/nextx/crm",
    "subscribedEvents": ["contact.created", "contact.updated"]
  }'

PUT /api/v1/webhook/services/{service} with body { url, subscribedEvents?, customHeaders? }. The first call creates the webhook (201, created: true) and returns signingSecret (whk_sec_…) only this once — store it to verify signatures:

HTTP/1.1 201 Created
{
  "id": "<endpoint_id>",
  "service": "CRM",
  "url": "https://your-app.com/webhooks/nextx/crm",
  "status": "Active",
  "subscribedEvents": ["contact.created", "contact.updated"],
  "created": true,
  "signingSecret": "whk_sec_3f9a...c21",
  "createdAt": "2026-10-08T03:12:56Z",
  "updatedAt": "2026-10-08T03:12:56Z"
}

Calling it again updates the URL (200, created: false, no secret). On update, omitting subscribedEvents / customHeaders keeps the current value; on create, an empty or missing list means no events yet — the webhook receives nothing until you switch events on.

Step 4 — Switch events on/off, or remove the webhook

# Turn one event on (or off with "subscribed": false)
curl -X PUT https://api.nextx.vn/api/v1/webhook/services/CRM/events/deal.won \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "subscribed": true }'

# Remove the service's webhook (it can be created again right away with PUT)
curl -X DELETE https://api.nextx.vn/api/v1/webhook/services/CRM \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>"
  • Service codes are upper-case and matched exactly (CRM, not crm). A code with no active events → 404 webhook.service.not_found.
  • An event that belongs to another service → 422 webhook.event.not_in_service (e.g. order.completed on CRM); an unknown event → 422 as well.
  • Switching an event or deleting when the service has no webhook yet → 404 webhook.service.not_configured.
  • The old POST /api/v1/webhook/endpoints still works but now requires service; a service that already has a webhook → 409 webhook.endpoint.service_exists. A body mixing CRM and Sales events with no service is rejected — use one PUT services/{service} per service instead.

By-id operations still apply to a service's webhook (its id is in endpoint.id): GET /endpoints/{id}, POST /endpoints/{id}/disable / /enable, POST /endpoints/{id}/rotate-secret, POST /endpoints/{id}/test, and the delivery log at /endpoints/{id}/deliveries.

3. Event catalog

33 events across 8 services. The Source column is the service code — a service's webhook can only subscribe to that service's events.

CRM · CRMSALES · SalesIAM · Accounts & BranchesOFFICE · OfficeCOMM · Omnichannel chatHRM · HRPAYMENT · PaymentsEINVOICE · E-invoices
contact.identifiedCOMMPROA customer just sent a phone number in chat, with the conversation source (ad / UTM) and the assigned staff member
FieldType
data.object.emailstring (email)
data.object.phonestring
data.object.agentIdstring (uuid)
data.object.attributionobject
data.object.channelTypestring
data.object.displayNamestring
data.object.workspaceIdstring (uuid)
data.object.crmContactIdstring (uuid)
data.object.identifiedAtstring (date-time)
data.object.conversationIdstring (uuid)
data.object.channelAccountIdstring (uuid)
data.object.channelContactIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "contact.identified",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "email": "khach@example.com",
      "phone": "string",
      "agentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "attribution": {
        "adId": "string",
        "adName": null,
        "adsetId": null,
        "utmTerm": null,
        "adsetName": null,
        "utmMedium": null,
        "utmSource": null,
        "adPlatform": "string",
        "landingUrl": null,
        "utmContent": null,
        "referrerUrl": null,
        "utmCampaign": null,
        "adCampaignId": null,
        "adCampaignName": null,
        "attributionMethod": "string"
      },
      "channelType": "string",
      "displayName": "string",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "identifiedAt": "2026-10-27T09:00:00+07:00",
      "conversationId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "channelAccountId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "channelContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
conversation.attribution_resolvedCOMMPROThe conversation's ad tree (ad set, campaign) was just fetched from Meta, usually a few minutes after the event above; same conversationId — an overwrite, not a new record
FieldType
data.object.resolvedAtstring (date-time)
data.object.attributionobject
data.object.workspaceIdstring (uuid)
data.object.conversationIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "conversation.attribution_resolved",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "resolvedAt": "2026-10-27T09:00:00+07:00",
      "attribution": {
        "adId": "string",
        "adName": "string",
        "adsetId": "string",
        "utmTerm": null,
        "adsetName": "string",
        "utmMedium": null,
        "utmSource": null,
        "adPlatform": "string",
        "landingUrl": null,
        "utmContent": null,
        "referrerUrl": null,
        "utmCampaign": null,
        "adCampaignId": "string",
        "adCampaignName": "string",
        "attributionMethod": "string"
      },
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "conversationId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
contact.createdCRMPROA new contact was created in the CRM — manually, by file import, or automatically from chat / web forms
FieldType
data.object.codestring
data.object.namestring
data.object.typestring
data.object.emailstring
data.object.phonestring
data.object.sourcestring
data.object.branchIdstring (uuid)
data.object.contactIdstring (uuid)
data.object.createdAtstring (date-time)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "contact.created",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "code": "string",
      "name": "string",
      "type": "string",
      "email": "string",
      "phone": "string",
      "source": "string",
      "branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "contactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "createdAt": "2026-10-27T09:00:00+07:00"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
contact.deletedCRMPROA contact was deleted from the CRM
FieldType
data.object.contactIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "contact.deleted",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "contactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
contact.updatedCRMPROA contact was edited; the payload is the state AFTER the edit, not a diff
FieldType
data.object.citystring
data.object.codestring
data.object.wardstring
data.object.emailstring
data.object.phonestring
data.object.addressstring
data.object.taxCodestring
data.object.branchIdstring (uuid)
data.object.fullNamestring
data.object.sourceIdstring (uuid)
data.object.contactIdstring (uuid)
data.object.assigneeIdstring (uuid)
data.object.sourceNamestring
data.object.changedFieldsstring[]
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "contact.updated",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "city": "string",
      "code": "string",
      "ward": "string",
      "email": "string",
      "phone": "string",
      "address": "string",
      "taxCode": "string",
      "branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "fullName": "string",
      "sourceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "contactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "assigneeId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "sourceName": "string",
      "changedFields": [
        "string"
      ]
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
deal.wonCRMPROAn opportunity moved to won (does not fire for lost or cancelled opportunities)

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "deal.won",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
invoice.cancelledEINVOICEPROAn e-invoice was cancelled — on NextX or detected as cancelled at the provider during sync. Payload includes the cancellation reason.
FieldType
data.object.statusstring
data.object.cqtCodenull
data.object.issuedAtnull
data.object.invoiceIdstring (uuid)
data.object.referenceIdstring (uuid)
data.object.cancelReasonstring
data.object.errorMessagenull
data.object.invoiceNumbernull
data.object.referenceCodestring
data.object.referenceTypestring
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "invoice.cancelled",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "status": "string",
      "cqtCode": null,
      "issuedAt": null,
      "invoiceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "cancelReason": "string",
      "errorMessage": null,
      "invoiceNumber": null,
      "referenceCode": "string",
      "referenceType": "string"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
invoice.failedEINVOICEPROIssuing an e-invoice failed. Payload includes the source document and the error message returned by the e-invoice provider.

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "invoice.failed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
invoice.issuedEINVOICEPROAn e-invoice was issued successfully. Payload includes the source document (referenceType / referenceId / referenceCode), invoice number, tax authority code (if any) and issue time.
FieldType
data.object.statusstring
data.object.cqtCodestring
data.object.issuedAtstring (date-time)
data.object.invoiceIdstring (uuid)
data.object.referenceIdstring (uuid)
data.object.cancelReasonnull
data.object.errorMessagenull
data.object.invoiceNumberstring
data.object.referenceCodestring
data.object.referenceTypestring
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "invoice.issued",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "status": "string",
      "cqtCode": "string",
      "issuedAt": "2026-10-27T09:00:00+07:00",
      "invoiceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "cancelReason": null,
      "errorMessage": null,
      "invoiceNumber": "string",
      "referenceCode": "string",
      "referenceType": "string"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
employee.createdHRMPROA new employee was created in HRM. Payload includes code, full name, employee type, start date, department, job title and the first contract type.
FieldType
data.object.codestring
data.object.fullNamestring
data.object.joinDatestring (date)
data.object.orgUnitIdstring (uuid)
data.object.employeeIdstring (uuid)
data.object.positionIdstring (uuid)
data.object.employeeTypestring
data.object.initialContractTypestring
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "employee.created",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "code": "string",
      "fullName": "string",
      "joinDate": "2026-10-27",
      "orgUnitId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "employeeId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "positionId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "employeeType": "string",
      "initialContractType": "string"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
employee.terminatedHRMPROAn employee was terminated. Payload includes code, full name, last working date and decision number; the reason is not included.

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "employee.terminated",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
employee.updatedHRMPROAn employee profile was edited. The payload is the state AFTER the edit (code, full name, email, department, job title).

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "employee.updated",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
labor_contract.evaluatedHRMPROA labor contract was evaluated. Payload includes contract type, decision, total / average score and the next contract if any.

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "labor_contract.evaluated",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
leave.approvedHRMPROA leave request was approved at the final step. Payload includes employee, leave type, code and date range; approval notes are not included.
FieldType
data.object.toDatestring (date)
data.object.leaveIdstring (uuid)
data.object.fromDatestring (date)
data.object.leaveTypestring
data.object.approvedAtstring (date-time)
data.object.employeeIdstring (uuid)
data.object.leaveSymbolstring
data.object.workspaceIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "leave.approved",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "toDate": "2026-10-27",
      "leaveId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "fromDate": "2026-10-27",
      "leaveType": "string",
      "approvedAt": "2026-10-27T09:00:00+07:00",
      "employeeId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "leaveSymbol": "string",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
leave.rejectedHRMPROA leave request was rejected. Payload includes employee, leave type and date range; the rejection reason is not included.

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "leave.rejected",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
payroll.lockedHRMPROA monthly payroll was locked. Payload includes year, month, lock time and who locked it; no amounts.

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "payroll.locked",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
payslip.acknowledgedHRMPROAn employee signed to acknowledge a payslip. Payload includes payslip, employee, month / year and signing time.

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "payslip.acknowledged",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
recruitment.hiredHRMPROA candidate was hired. Payload includes application, candidate, job posting, hire date and the created employee (if any).

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "recruitment.hired",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
branch.createdIAMPROA new branch was created
FieldType
data.object.codestring
data.object.namestring
data.object.addressstring
data.object.branchIdstring (uuid)
data.object.parentIdnull
data.object.managerIdnull
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "branch.created",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "code": "string",
      "name": "string",
      "address": "string",
      "branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "parentId": null,
      "managerId": null
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
branch.updatedIAMPROA branch was edited
FieldType
data.object.branchIdstring (uuid)
data.object.changedFieldsstring[]
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "branch.updated",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "changedFields": [
        "string"
      ]
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
user.createdIAMPROA new user was added to the workspace
FieldType
data.object.emailstring
data.object.userIdstring (uuid)
data.object.fullNamestring
data.object.createdAtstring (date-time)
data.object.workspaceIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "user.created",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "email": "string",
      "userId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "fullName": "string",
      "createdAt": "2026-10-27T09:00:00+07:00",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
workspace.createdIAMPROA new workspace was created
FieldType
data.object.codestring
data.object.namestring
data.object.localestring
data.object.ownerIdstring (uuid)
data.object.timezonestring
data.object.createdAtstring (date-time)
data.object.ownerEmailstring (email)
data.object.workspaceIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "workspace.created",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "code": "string",
      "name": "string",
      "locale": "string",
      "ownerId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "timezone": "string",
      "createdAt": "2026-10-27T09:00:00+07:00",
      "ownerEmail": "khach@example.com",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
approval.instance.status_changedOFFICEPROAn approval request changed status (pending / approved / rejected / withdrawn)

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "approval.instance.status_changed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
approval.task.completedOFFICEPROAn approver finished their approval step

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "approval.task.completed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
approval.task.createdOFFICEPROAn approval step was assigned to an approver

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "approval.task.created",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
payment.completedPAYMENTPROA buyer paid successfully through an online gateway (QR, card, wallet, pay-later…) or a manual collection was confirmed. Payload includes transaction code, source document (referenceType / referenceId — sales order, gym contract…), gateway, amount and paid time.
FieldType
data.object.amountinteger
data.object.paidAtstring (date-time)
data.object.gatewaystring
data.object.payerIdstring (uuid)
data.object.currencystring
data.object.metadataobject
data.object.netAmountinteger
data.object.payerTypestring
data.object.referenceIdstring (uuid)
data.object.workspaceIdstring (uuid)
data.object.eInvoiceHintnull
data.object.providerCodestring
data.object.referenceTypestring
data.object.transactionIdstring
data.object.dbTransactionIdstring (uuid)
data.object.paymentIntentIdstring (uuid)
data.object.paymentInstrumentstring
data.object.providerApprovalCodestring
data.object.providerTransactionIdstring
data.object.paymentInstrumentDetailstring
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "payment.completed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "amount": 0,
      "paidAt": "2026-10-27T09:00:00+07:00",
      "gateway": "string",
      "payerId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "currency": "string",
      "metadata": {},
      "netAmount": 0,
      "payerType": "string",
      "referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "eInvoiceHint": null,
      "providerCode": "string",
      "referenceType": "string",
      "transactionId": "string",
      "dbTransactionId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "paymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "paymentInstrument": "string",
      "providerApprovalCode": "string",
      "providerTransactionId": "string",
      "paymentInstrumentDetail": "string"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
payment.failedPAYMENTPROAn online payment attempt failed (gateway declined, buyer cancelled, expired). Payload includes source document, gateway and the reason returned by the gateway.
FieldType
data.object.reasonstring
data.object.failedAtstring (date-time)
data.object.referenceIdstring (uuid)
data.object.workspaceIdstring (uuid)
data.object.providerCodestring
data.object.referenceTypestring
data.object.paymentIntentIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "payment.failed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "reason": "string",
      "failedAt": "2026-10-27T09:00:00+07:00",
      "referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "providerCode": "string",
      "referenceType": "string",
      "paymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
payment.recurring_chargedPAYMENTPROThe gateway successfully charged one period of a recurring payment agreement. Payload has the agreement number and period number (periodNum) — the gateway may report the same period more than once, so deduplicate on this pair.
FieldType
data.object.amountinteger
data.object.paidAtstring (date-time)
data.object.payerIdstring (uuid)
data.object.currencystring
data.object.metadataobject
data.object.payerTypestring
data.object.periodNuminteger
data.object.referenceIdstring (uuid)
data.object.workspaceIdstring (uuid)
data.object.providerCodestring
data.object.referenceTypestring
data.object.dbTransactionIdstring (uuid)
data.object.paymentIntentIdstring (uuid)
data.object.paymentInstrumentstring
data.object.recurringContractNostring
data.object.providerApprovalCodenull
data.object.providerTransactionIdstring
data.object.originalPaymentIntentIdstring (uuid)
data.object.paymentInstrumentDetailstring
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "payment.recurring_charged",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "amount": 0,
      "paidAt": "2026-10-27T09:00:00+07:00",
      "payerId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "currency": "string",
      "metadata": {},
      "payerType": "string",
      "periodNum": 0,
      "referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "providerCode": "string",
      "referenceType": "string",
      "dbTransactionId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "paymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "paymentInstrument": "string",
      "recurringContractNo": "string",
      "providerApprovalCode": null,
      "providerTransactionId": "string",
      "originalPaymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "paymentInstrumentDetail": "string"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
payment.recurring_failedPAYMENTPROThe gateway tried to charge one recurring period but failed. Payload includes agreement number, period number, the charge order code and the reason.
FieldType
data.object.reasonstring
data.object.failedAtstring (date-time)
data.object.periodNuminteger
data.object.referenceIdstring (uuid)
data.object.workspaceIdstring (uuid)
data.object.providerCodestring
data.object.referenceTypestring
data.object.providerOrderRefstring
data.object.recurringContractNostring
data.object.originalPaymentIntentIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "payment.recurring_failed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "reason": "string",
      "failedAt": "2026-10-27T09:00:00+07:00",
      "periodNum": 0,
      "referenceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "workspaceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "providerCode": "string",
      "referenceType": "string",
      "providerOrderRef": "string",
      "recurringContractNo": "string",
      "originalPaymentIntentId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
payment.refundedPAYMENTPROA refund was completed. Payload includes refund code, original transaction, source document, amount and refund method.

Fields are being recorded from real events and will appear here after the first delivery. The example below is the standard envelope.

{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "payment.refunded",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {}
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
order.cancelledSALESPROAn order was cancelled
FieldType
data.object.totalinteger
data.object.orderIdstring (uuid)
data.object.orderCodestring
data.object.cancelledAtstring (date-time)
data.object.cancelledBystring (uuid)
data.object.channelCodenull
data.object.cancelReasonstring
data.object.crmContactIdstring (uuid)
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "order.cancelled",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "total": 0,
      "orderId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "orderCode": "string",
      "cancelledAt": "2026-10-27T09:00:00+07:00",
      "cancelledBy": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "channelCode": null,
      "cancelReason": "string",
      "crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
order.completedSALESPROAn order was completed — delivered and fully paid
FieldType
data.object.itemsobject[]
data.object.totalstring
data.object.orderIdstring (uuid)
data.object.vatModestring
data.object.branchIdstring (uuid)
data.object.subtotalstring
data.object.orderCodestring
data.object.orderTypestring
data.object.taxAmountstring
data.object.assignedTostring (uuid)
data.object.paidAmountstring
data.object.vatPercentnull
data.object.channelCodestring
data.object.completedAtstring (date-time)
data.object.shippingFeestring
data.object.vatIncludedboolean
data.object.crmContactIdstring (uuid)
data.object.customerNamestring
data.object.customerTypestring
data.object.customerEmailstring
data.object.customerPhonestring
data.object.paymentMethodstring
data.object.discountAmountstring
data.object.customerAddressstring
data.object.customerTaxCodestring
data.object.customerWardCodenull
data.object.sourceContractIdstring (uuid)
data.object.sourceQuotationIdnull
data.object.customerDistrictCodenull
data.object.customerProvinceCodenull
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "order.completed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "items": [
        {
          "total": "string",
          "barcode": "string",
          "serials": null,
          "taxRate": "string",
          "quantity": "string",
          "subtotal": "string",
          "unitCode": "string",
          "unitName": "string",
          "lotNumber": "string",
          "productId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
          "taxAmount": "string",
          "unitPrice": "string",
          "expiryDate": "2026-10-27",
          "productSku": "string",
          "productName": "string",
          "variantName": null,
          "discountAmount": "string",
          "warrantyMonths": 0
        }
      ],
      "total": "string",
      "orderId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "vatMode": "string",
      "branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "subtotal": "string",
      "orderCode": "string",
      "orderType": "string",
      "taxAmount": "string",
      "assignedTo": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "paidAmount": "string",
      "vatPercent": null,
      "channelCode": "string",
      "completedAt": "2026-10-27T09:00:00+07:00",
      "shippingFee": "string",
      "vatIncluded": false,
      "crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "customerName": "string",
      "customerType": "string",
      "customerEmail": "string",
      "customerPhone": "string",
      "paymentMethod": "string",
      "discountAmount": "string",
      "customerAddress": "string",
      "customerTaxCode": "string",
      "customerWardCode": null,
      "sourceContractId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "sourceQuotationId": null,
      "customerDistrictCode": null,
      "customerProvinceCode": null
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}
order.status_changedSALESPROAn order changed status; fires on EVERY change, including to completed or cancelled
FieldType
data.object.totalinteger
data.object.orderIdstring (uuid)
data.object.branchIdstring (uuid)
data.object.changedAtstring (date-time)
data.object.changedBystring (uuid)
data.object.newStatusstring
data.object.orderCodestring
data.object.orderTypestring
data.object.assignedTostring (uuid)
data.object.channelCodestring
data.object.crmContactIdstring (uuid)
data.object.previousStatusstring
{
  "id": "evt_0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
  "type": "order.status_changed",
  "api_version": "v1",
  "created_at": "2026-10-27T02:00:00+00:00",
  "workspace_id": "0199a1b2-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "object": {
      "total": 0,
      "orderId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "branchId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "changedAt": "2026-10-27T09:00:00+07:00",
      "changedBy": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "newStatus": "string",
      "orderCode": "string",
      "orderType": "string",
      "assignedTo": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "channelCode": "string",
      "crmContactId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
      "previousStatus": "string"
    }
  },
  "metadata": {
    "source_service": "string",
    "correlation_id": "string"
  }
}

Only events that are actually delivered are listed. Retired names (deal.created, deal.updated, deal.stage_changed, member.updated, user.updated, order.created, order.confirmed, product.created, product.updated) can no longer be subscribed to; an endpoint that still has them can switch them off.

4. Payload structure

Each request has a JSON body in the standard envelope:

{
  "id": "evt_018f...d2",          // ID sự kiện (idempotency)
  "type": "contact.created",      // = X-NextX-Event-Type
  "api_version": "v1",
  "created_at": "2026-06-16T03:00:00Z",
  "workspace_id": "<workspace_guid>",
  "livemode": true,               // false nếu là test
  "data": {
    "object": { "id": "...", "...": "..." }   // payload của entity
  }
}

data.object carries the event's object. Test events have livemode: false. There is currently no slim-payload option: every request carries the full data.object.

5. Headers

HeaderMeaning
X-NextX-Event-IdEvent ID (bare GUID) — use it for idempotency. The body's id field is evt_ + the same GUID
X-NextX-Event-TypeEvent type, e.g. contact.created
X-NextX-TimestampUnix seconds at signing time — check it to prevent replay
X-NextX-Signature-256sha256=<hex> — HMAC-SHA256 of `timestamp.body` with the current secret
X-NextX-Signature-256-PreviousOnly present during the 7 days after a secret rotation: sha256=<hex>, same timestamp + body but signed with the OLD secret
X-NextX-Delivery-AttemptDelivery attempt number (1 = first)
X-NextX-Webhook-IdID of the registered webhook (endpoint)
webhook-idStandard Webhooks — message id, stable across redeliveries (= X-NextX-Event-Id). Only with whsec_ secrets
webhook-timestampStandard Webhooks — Unix seconds at signing time (= X-NextX-Timestamp)
webhook-signatureStandard Webhooks — v1,<base64> = HMAC-SHA256 of `id.timestamp.body`, key = base64-decoded part after whsec_. Two space-separated signatures during rotation

User-Agent: NextX-Webhook/1.0. Custom headers set on the webhook (customHeaders) are also sent.

6. Verifying the signature

Fastest — Standard Webhooks: whsec_… secrets follow the open Standard Webhooks spec (standardwebhooks.com); each request also carries webhook-id, webhook-timestamp, webhook-signature. Use the standardwebhooks library (Node, Python, Go, Java, C#, PHP, Ruby…) — it checks the signature, clock skew and the dual signature during rotation for you. Webhooks created before 14/10/2026 (whk_sec_… secret) only have the X-NextX-* headers: rotate the secret once to get the standard headers too.

// Node.js — npm i standardwebhooks
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.NEXTX_WEBHOOK_SECRET);   // "whsec_..."
// Ném lỗi nếu chữ ký sai hoặc timestamp lệch quá 5 phút. headers = header thô của request.
const event = wh.verify(req.rawBody, req.headers);
# Python — pip install standardwebhooks
import os
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["NEXTX_WEBHOOK_SECRET"])   # "whsec_..."
event = wh.verify(raw_body, headers)                # raise nếu signature/timestamp sai

Or verify the HMAC yourself — works with every secret, including whk_sec_…:

Signature = HMAC-SHA256(secret, `{ts}.{raw_body}`), output as lowercase hex, placed in X-NextX-Signature-256: sha256=<hex>. Recompute using the raw body (before JSON parsing) and X-NextX-Timestamp, then compare; reject if the timestamp drifts more than ~5 minutes (replay protection).

// Node.js (Express, cần raw body)
import crypto from "crypto";

function verify(req, secret) {
  const ts = req.header("X-NextX-Timestamp");
  const sig = req.header("X-NextX-Signature-256");          // "sha256=..."
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(ts + "." + req.rawBody)                          // timestamp.body
    .digest("hex");
  const ok = sig?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return ok && fresh;
}
# Python
import hmac, hashlib, time

def verify(headers, raw_body, secret):
    ts = headers["X-NextX-Timestamp"]
    sig = headers["X-NextX-Signature-256"]          # "sha256=..."
    expected = "sha256=" + hmac.new(
        secret.encode(), f"{ts}.{raw_body}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig, expected) and abs(time.time() - int(ts)) < 300

7. Rotating the secret

Rotating (POST /api/v1/webhook/endpoints/{id}/rotate-secret) returns newSecret and oldSecretValidUntil. The old secret keeps working for a 7-day grace period: until that moment every request carries two signatures over the same timestamp.body — X-NextX-Signature-256 with the new secret and X-NextX-Signature-256-Previous with the old one, same sha256=<hex> format.

curl -X POST https://api.nextx.vn/api/v1/webhook/endpoints/<endpoint_id>/rotate-secret \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>"

{
  "newSecret": "whsec_Mf9KQr8GKYqr...",
  "oldSecretValidUntil": "2026-10-15T03:12:56Z"
}

So you can switch without downtime: keep both secrets on your side, accept a request when either header matches either secret, then drop the old secret once you have deployed the new one (at the latest by oldSecretValidUntil):

// Node.js — accept a request if ANY known secret matches ANY NextX signature header
function verifyDuringRotation(req, secrets /* [newSecret, oldSecret] */) {
  const ts = req.header("X-NextX-Timestamp");
  const sigs = [
    req.header("X-NextX-Signature-256"),             // signed with the NEW secret
    req.header("X-NextX-Signature-256-Previous"),    // signed with the OLD secret (7-day grace only)
  ].filter(Boolean);
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  const ok = secrets.filter(Boolean).some((secret) => {
    const expected = "sha256=" + crypto
      .createHmac("sha256", secret).update(ts + "." + req.rawBody).digest("hex");
    return sigs.some((sig) => sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
  });
  return ok && fresh;
}

After the grace period the -Previous header is no longer sent and only the new secret verifies. Rotating again during a grace period starts a new 7-day window with the secret that was current at that time as the "previous" one — the older secret stops verifying immediately.

8. Delivery & retry

  • Success = the endpoint returns 2xx within 15s.
  • Up to 8 delivery attempts (attempt 1 immediately, then retries after 1 min, 5 min, 30 min, 2 h, 4 h, 8 h, 12 h — about 24 hours in total) on error/timeout; see X-NextX-Delivery-Attempt (1 = first). After 8 failed attempts the delivery stays failed.
  • Idempotency: the same id (evt_…) may arrive multiple times — dedupe by id.
  • Circuit breaker: an endpoint that fails 100 times in a row or more (the default threshold) is switched off automatically. It is not switched back on automatically and no notification is sent yet — re-enable it with POST /endpoints/{id}/enable once you have fixed the problem on your side.
  • SSRF guard: URLs pointing at internal/private IPs are blocked — only public HTTPS is accepted.
  • You can retry one delivery manually: POST /endpoints/{id}/deliveries/{deliveryId}/retry.

9. Test

Send a test event to your endpoint to check the integration:

curl -X POST https://api.nextx.vn/api/v1/webhook/endpoints/<endpoint_id>/test \
  -H "Authorization: Bearer <USER_ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "eventType": "contact.created" }'

The test payload has livemode: false and a placeholder data.object (id: "test_object_id"), not real data. Any event name is accepted.