NexoCRM webhooks send a real-time HTTP POST to your server the moment something changes in your CRM. When a contact is created, a deal is won, or a task is completed, NexoCRM delivers a signed JSON payload describing exactly what happened to whatever URL you choose. You pick which of the 17 event types each endpoint receives. Every request carries an HMAC-SHA256 signature so you can prove it came from NexoCRM, failed deliveries retry automatically with exponential backoff, and every attempt is logged and replayable. No polling, no scheduled imports — your app hears about the change within seconds.
NexoCRM is an AI-guided CRM for small business owners. Webhooks are available on every plan; the number of endpoints you can create depends on your plan.
NexoCRM sends 17 event types across contacts, companies, deals, tasks, notes, and activities — including contact.created, deal.won, deal.stage_changed, and task.completed. You choose which types each endpoint receives, so an endpoint only ever gets the events you subscribed it to.
Events are grouped by the object they describe. Subscribing an endpoint to
deal.won alone means it hears about closed deals and nothing else —
you are never forced to receive traffic you do not want.
Full payload examples for every type are in the event catalogue below.
Compute HMAC-SHA256 over the string "{timestamp}.{raw request body}" using your endpoint's signing secret, hex-encode it, and compare it in constant time against the X-NexoCRM-Signature header (which is formatted as v1=<hex>). You must hash the raw request body exactly as received — re-serializing the parsed JSON changes the bytes and the signature will not match.
signed_payload = "{X-NexoCRM-Timestamp}" + "." + "{raw request body}"
signature = hex( HMAC_SHA256(signed_payload, your_endpoint_secret) )
header = "X-NexoCRM-Signature: v1=" + signature
Hash the raw body, not a re-serialized copy. This is the single most common cause of signature mismatches: a framework parses the JSON for you, you re-encode it to hash it, and the key order or whitespace shifts by one byte. Capture the raw request body before anything parses it.
<?php
$secret = getenv('NEXOCRM_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // RAW bytes — do not json_decode first
$ts = $_SERVER['HTTP_X_NEXOCRM_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_NEXOCRM_SIGNATURE'] ?? '';
// Reject stale requests (replay protection)
if (!$ts || abs(time() - (int)$ts) > 300) { http_response_code(400); exit('stale'); }
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit('bad signature'); }
$event = json_decode($raw, true);
// $event['type'], $event['id'], $event['data'], $event['previous'] ?? null
http_response_code(200);
echo 'ok';POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: NexoCRM-Webhooks/1.0
X-NexoCRM-Event: deal.won
X-NexoCRM-Event-Id: e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91
X-NexoCRM-Delivery-Id: 4471
X-NexoCRM-Timestamp: 1784301067
X-NexoCRM-Signature: v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Nothing is lost. NexoCRM retries automatically with exponential backoff over roughly 36 hours, and every attempt is recorded in a delivery log you can inspect. After 20 consecutive failures the endpoint is disabled to stop pointless traffic, and you can re-enable it and replay missed events once your receiver is healthy again.
| Your response | What NexoCRM does |
|---|---|
2xx | Success. The failure counter resets to zero. |
410 Gone | Treated as permanent. No retry, and the endpoint is disabled. |
Other 4xx | Retried — a misconfigured receiver may be fixed. |
5xx, timeout, connection error | Retried on the schedule below. |
Up to 6 attempts by default (configurable from 1 to 10). Retries are sent after 1 minute, 5 minutes, 30 minutes, 2 hours, 10 hours, and 24 hours, each with plus-or-minus 20 percent random jitter so a recovering receiver is not hit by a stampede. A 410 Gone response is treated as final and is never retried.
| Attempt | Sent |
|---|---|
| 1 | immediately |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 10 hours |
| 7 | 24 hours |
Jitter is applied per delivery, not per batch. If a receiver goes down and comes back with a hundred queued events, they return spread across a window rather than all at once.
Yes. Every delivery — including ones that permanently failed — can be replayed from the endpoint's detail page, the REST API, or an MCP tool. A replay creates a fresh delivery of the same original event with the retry counter reset, and leaves the original record intact for auditing.
Use the "Send test event" button on the endpoint page, or the equivalent API and MCP calls. It sends a synthetic ping.test event to that one endpoint only. No contact, deal, or task is created, and no other endpoint receives it — so you can verify reachability and your signature check safely.
The test event looks exactly like a real one — same headers, same signature scheme — so it is a complete end-to-end check of your verification code.
Every event body has the same envelope: a unique id, the event type,
an api_version, an ISO-8601 UTC created_at, your company_id,
and a data object. Update and stage-change events add a previous object
holding the prior state. Money is always an integer number of cents plus a currency code — never
a float.
| Event | Fires when |
|---|---|
| Contacts | |
contact.created |
A contact was created, on any surface (dashboard, REST API, MCP, or an automation). |
contact.updated |
A contact's fields changed. Includes a top-level `previous` object with the prior state. |
contact.deleted |
A contact was permanently deleted. `data` is a snapshot taken immediately before deletion. |
| Companies | |
company.created |
A company was created — either directly, or implicitly by creating a contact with a new company name. |
company.updated |
A company's fields changed. Includes `previous`. |
company.deleted not yet fired |
A company was deleted. |
| Deals | |
deal.created |
A deal was created. |
deal.updated |
A deal's fields changed. Includes `previous`. Fires alongside `deal.stage_changed` when the stage moved. |
deal.stage_changed |
A deal moved to a different pipeline stage. `previous.stage` holds the stage it came from. |
deal.won |
A deal reached the `won` stage. Fires IN ADDITION to `deal.stage_changed` — subscribe to one or the other, not both, unless you want two notifications. |
deal.lost |
A deal reached the `lost` stage. Also fires in addition to `deal.stage_changed`. |
deal.deleted |
A deal was permanently deleted. `data` is a pre-delete snapshot. |
| Tasks | |
task.created |
A task was created — by a person, or by an automation (check `actor_source`). |
task.completed |
A task transitioned to done. Only a genuine transition fires this — re-saving an already-done task does not. |
task.overdue |
A task passed its due date without being completed. Emitted once per task, ever — guarded by a database flag so it never repeats. |
| Notes & activity | |
note.created |
A note was added to a contact or a deal. |
activity.logged |
A call, email, meeting, or other activity was logged against a contact or deal. |
contact.created
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "contact.created",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 87,
"object": "contact",
"name": "Jane Doe",
"email": "jane@acme.example",
"phone": "+1 555 0142",
"company": "Acme Inc",
"company_id": 12,
"job_title": "VP Operations",
"status": "prospect",
"source": "website",
"tags": "inbound,priority",
"last_contacted": "2026-07-18",
"last_activity_at": "2026-07-20T14:05:44Z",
"created_at": "2026-06-02T09:14:00Z"
}
}
contact.updated
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "contact.updated",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 87,
"object": "contact",
"name": "Jane Doe",
"email": "jane@acme.example",
"phone": "+1 555 0142",
"company": "Acme Inc",
"company_id": 12,
"job_title": "VP Operations",
"status": "customer",
"source": "website",
"tags": "inbound,priority",
"last_contacted": "2026-07-18",
"last_activity_at": "2026-07-20T14:05:44Z",
"created_at": "2026-06-02T09:14:00Z"
},
"previous": {
"status": "prospect"
}
}
contact.deleted
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "contact.deleted",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 87,
"object": "contact",
"name": "Jane Doe",
"email": "jane@acme.example",
"phone": "+1 555 0142",
"company": "Acme Inc",
"company_id": 12,
"job_title": "VP Operations",
"status": "prospect",
"source": "website",
"tags": "inbound,priority",
"last_contacted": "2026-07-18",
"last_activity_at": "2026-07-20T14:05:44Z",
"created_at": "2026-06-02T09:14:00Z"
}
}
company.created
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "company.created",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 12,
"object": "company",
"name": "Acme Inc",
"domain": "acme.example",
"industry": "Logistics",
"website": "https://acme.example",
"phone": "+1 555 0100",
"address": "400 Harbor Way, Portland, OR",
"created_at": "2026-05-30T11:02:00Z",
"updated_at": "2026-07-20T14:05:44Z"
}
}
company.updated
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "company.updated",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 12,
"object": "company",
"name": "Acme Inc",
"domain": "acme.example",
"industry": "Manufacturing",
"website": "https://acme.example",
"phone": "+1 555 0100",
"address": "400 Harbor Way, Portland, OR",
"created_at": "2026-05-30T11:02:00Z",
"updated_at": "2026-07-20T14:05:44Z"
},
"previous": {
"status": "prospect"
}
}
company.deleted
— catalogued, not currently fired CATALOGUED BUT NOT CURRENTLY FIREABLE. NexoCRM has no code path that deletes a company — there is no delete action in the dashboard and no DELETE route on the API. The event type is reserved and documented so the contract is stable if company deletion is added later. You may subscribe to it, but it will never be delivered today.
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "company.deleted",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 12,
"object": "company",
"name": "Acme Inc",
"domain": "acme.example",
"industry": "Logistics",
"website": "https://acme.example",
"phone": "+1 555 0100",
"address": "400 Harbor Way, Portland, OR",
"created_at": "2026-05-30T11:02:00Z",
"updated_at": "2026-07-20T14:05:44Z"
}
}
deal.created
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "deal.created",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 24,
"object": "deal",
"title": "Acme — annual contract",
"value_cents": 1250000,
"currency": "USD",
"stage": "proposal",
"priority": "high",
"contact_id": 87,
"company_id": 12,
"expected_close": "2026-08-15",
"created_at": "2026-06-11T16:40:00Z"
}
}
deal.updated
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "deal.updated",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 24,
"object": "deal",
"title": "Acme — annual contract",
"value_cents": 1450000,
"currency": "USD",
"stage": "proposal",
"priority": "high",
"contact_id": 87,
"company_id": 12,
"expected_close": "2026-08-15",
"created_at": "2026-06-11T16:40:00Z"
},
"previous": {
"status": "prospect"
}
}
deal.stage_changed
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "deal.stage_changed",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 24,
"object": "deal",
"title": "Acme — annual contract",
"value_cents": 1250000,
"currency": "USD",
"stage": "won",
"priority": "high",
"contact_id": 87,
"company_id": 12,
"expected_close": "2026-08-15",
"created_at": "2026-06-11T16:40:00Z"
},
"previous": {
"stage": "negotiation"
}
}
deal.won
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "deal.won",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 24,
"object": "deal",
"title": "Acme — annual contract",
"value_cents": 1250000,
"currency": "USD",
"stage": "won",
"priority": "high",
"contact_id": 87,
"company_id": 12,
"expected_close": "2026-08-15",
"created_at": "2026-06-11T16:40:00Z"
}
}
deal.lost
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "deal.lost",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 24,
"object": "deal",
"title": "Acme — annual contract",
"value_cents": 1250000,
"currency": "USD",
"stage": "lost",
"priority": "high",
"contact_id": 87,
"company_id": 12,
"expected_close": "2026-08-15",
"created_at": "2026-06-11T16:40:00Z"
}
}
deal.deleted
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "deal.deleted",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 24,
"object": "deal",
"title": "Acme — annual contract",
"value_cents": 1250000,
"currency": "USD",
"stage": "proposal",
"priority": "high",
"contact_id": 87,
"company_id": 12,
"expected_close": "2026-08-15",
"created_at": "2026-06-11T16:40:00Z"
}
}
task.created
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "task.created",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 903,
"object": "task",
"title": "Send the revised proposal",
"description": "Include the 3-year pricing option.",
"due_date": "2026-07-22",
"priority": "high",
"status": "pending",
"contact_id": 87,
"deal_id": 24,
"completed_at": null,
"created_at": "2026-07-20T14:31:00Z"
}
}
task.completed
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "task.completed",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 903,
"object": "task",
"title": "Send the revised proposal",
"description": "Include the 3-year pricing option.",
"due_date": "2026-07-22",
"priority": "high",
"status": "done",
"contact_id": 87,
"deal_id": 24,
"completed_at": "2026-07-20T14:31:07Z",
"created_at": "2026-07-20T14:31:00Z"
}
}
task.overdue
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "task.overdue",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 903,
"object": "task",
"title": "Send the revised proposal",
"description": "Include the 3-year pricing option.",
"due_date": "2026-07-18",
"priority": "high",
"status": "pending",
"contact_id": 87,
"deal_id": 24,
"completed_at": null,
"created_at": "2026-07-20T14:31:00Z"
}
}
note.created
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "note.created",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 512,
"object": "note",
"content": "Left a voicemail. Trying again Thursday.",
"contact_id": 87,
"deal_id": null,
"created_at": "2026-07-20T14:22:10Z"
}
}
activity.logged
{
"id": "e6c1f2a4-9d3b-4f8e-b7a1-2c5d8e0f3a91",
"type": "activity.logged",
"api_version": "v1",
"created_at": "2026-07-20T14:31:07Z",
"company_id": 5,
"data": {
"id": 341,
"object": "activity",
"type": "call",
"subject": "Discovery call",
"body": "Walked through pricing. Sending a proposal Monday.",
"contact_id": 87,
"deal_id": 24,
"occurred_at": "2026-07-20T13:00:00Z",
"created_at": "2026-07-20T14:05:44Z"
}
}
2xx first and do slow work afterwards.id twice; de-duplicate on it.