Developers
NotusCRM API and webhooks
Connect your systems to the CRM: read and write records with the REST API, and receive deal, person, company and task changes instantly with webhooks.
REST API
- A workspace owner or admin creates the key in Settings → API & webhooks. The key is shown only once, when it is created, and starts with ncrm_live_.
- Send the Authorization: Bearer <key> header with each request. The key carries the workspace, so no other header is needed.
- The key acts with the permissions of the person who created it. It is read-only by default; write access allows creating and updating records (read-only on the free plan).
- Records cannot be deleted with a key; key, connection and webhook management, sending email, export and closing the workspace are closed too. Every write is recorded in the audit log as "API · key name".
- Errors come back as application/problem+json: 400 invalid request, 401 invalid key, 403 not allowed, 404 not found, 409 not a valid operation.
All endpoints and schemas are in the OpenAPI document: https://api.notuscrm.com/openapi/v1.json
curl https://api.notuscrm.com/api/v1/deals \
-H "Authorization: Bearer ncrm_live_…"Webhooks
- Add endpoints in Settings → API & webhooks: an HTTPS URL and the events to subscribe to. The signing secret (whsec_) is shown only when it is created or rotated.
- Each event is sent as a signed POST request. A delivery that returns 2xx within 10 seconds succeeds; others are retried at 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h, 8 attempts in total.
- After 50 consecutive failed deliveries the endpoint turns itself off. The delivery log is kept for 30 days; you can redeliver from it and send a ping event with "Send test".
- The same event may arrive more than once (retries): use the id in the body to drop duplicates. Ordering is not guaranteed; use createdAt.
- data carries the record's fields; delete events carry only the id. Stage changes also include previousStageId. Private network addresses are never called.
Events
- deal.created
- deal.updated
- deal.stage_changed
- deal.deleted
- person.created
- person.updated
- person.deleted
- company.created
- company.updated
- company.deleted
- task.created
- task.updated
- task.completed
- task.deleted
- custom_record.created
- custom_record.updated
- custom_record.deleted
POST /your/endpoint
Content-Type: application/json
X-Notus-Event: deal.stage_changed
X-Notus-Delivery: 0199c3a1-…
X-Notus-Signature: t=1790000000,v1=5f2b…
{
"id": "0199c3a0-…",
"type": "deal.stage_changed",
"createdAt": "2026-09-26T09:12:44Z",
"workspaceId": "01a0c31a-…",
"data": { "id": "…", "title": "…", "stageId": "…", "previousStageId": "…", "amount": 120000, "currency": "TRY" }
}Verifying the signature
The X-Notus-Signature header carries t (Unix time) and v1 (HMAC-SHA256). The signature is computed with your secret over t, a dot and the raw body joined together (t.body). Verify against the body exactly as received and reject requests with old timestamps.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (do not re-serialize JSON)
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}using System.Security.Cryptography;
using System.Text;
static bool Verify(string rawBody, string header, string secret)
{
var parts = header.Split(',').Select(p => p.Split('=', 2)).ToDictionary(p => p[0], p => p[1]);
byte[] hash = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{parts["t"]}.{rawBody}"));
bool fresh = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(parts["t"])) <= 300;
return fresh && CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(Convert.ToHexStringLower(hash)), Encoding.ASCII.GetBytes(parts["v1"]));
}