NexoCRM Webhooks

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.

What events can NexoCRM send?

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.

How do I verify a NexoCRM webhook signature?

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.

The recipe

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.

Verification examples

<?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';

What a request looks like

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

What happens if my endpoint is down?

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.

Response handling

Your responseWhat NexoCRM does
2xxSuccess. The failure counter resets to zero.
410 GoneTreated as permanent. No retry, and the endpoint is disabled.
Other 4xxRetried — a misconfigured receiver may be fixed.
5xx, timeout, connection errorRetried on the schedule below.

How many times does NexoCRM retry a failed webhook?

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.

AttemptSent
1immediately
21 minute
35 minutes
430 minutes
52 hours
610 hours
724 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.

Can I replay a webhook?

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.

How do I test a webhook without creating real data?

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.

Full event catalogue

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.

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

Example payloads

Contacts

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

Companies

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.deletedcatalogued, 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"
    }
}

Deals

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

Tasks

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

Notes & activity

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

Getting started

  1. Sign in and open Webhooks in the sidebar.
  2. Add an endpoint: give it a name, an HTTPS URL, and tick the events you want.
  3. Copy the signing secret. It is shown once, at creation, and is encrypted at rest — if you lose it you must rotate to get a new one.
  4. Click Send test event and confirm your receiver verifies the signature.
  5. Watch the delivery log on the endpoint page for status, response code, and timing.

Requirements for your endpoint

Create a free account API access