Webhooks
Webhooks tell your system the moment something happens in a SmartContact workspace: a company added, a contact changed, a deal won, a task completed. Every change in SmartContact is recorded together with its event, so events are never lost, and they reach you within about a minute.
Subscribe
Admins can add a webhook in Settings › Webhooks. Integrations subscribe through the API with their key:
POST https://app.smartcontact.online/v1/webhooks
Authorization: Bearer sc_…
Content-Type: application/json
{ "url": "https://example.com/smartcontact-events", "events": ["company.created", "deal.updated"] }
The answer includes a secret, shown only once. Remove the webhook with DELETE /v1/webhooks/{id}, or answer any delivery with
410 Gone. A key only sees and removes its own webhooks, and can only subscribe to events about data its role can read. Plugins declare a webhook URL and events
on their version in the developer portal; installing the plugin subscribes them.
What you receive
POST https://example.com/smartcontact-events
Content-Type: application/json
X-SmartContact-Event: deal.updated
X-SmartContact-Delivery: 6a1c… (the same on every retry: use it to ignore repeats)
X-SmartContact-Signature: t=1792569600,v1=5f2b…
{
"id": "evt_10423",
"type": "deal.updated",
"created_at": "2026-10-21T09:41:07.000Z",
"workspace_id": "…",
"data": { "id": "…", "changed": ["stage_id"] },
"actor_type": "user",
"api_url": "https://app.smartcontact.online/v1/deals/…"
}
Events carry ids and the names of changed fields, never names, emails or notes. Fetch api_url with your key to get the record as your key is allowed to see
it. When you receive contact.erased or company.erased, delete everything you hold about that record: the person asked to be forgotten.
Check the signature
v1 is the hex HMAC-SHA256 of <t>.<raw body> with your secret (a plugin's events use the plugin's signing secret). Refuse it if it
doesn't match or t is more than 5 minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
function isFromSmartContact(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const want = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
const got = Buffer.from(parts.v1 ?? "", "hex");
return got.length === want.length && timingSafeEqual(got, want);
} Retries and switching off
- Answer with any 2xx within 10 seconds. Redirects are not followed.
- Anything else is retried after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours, then given up. Admins can see every delivery in Settings › Webhooks.
- After 20 failures in a row the webhook is switched off. It is also switched off if the key that made it is revoked, the plugin is uninstalled or the person leaves.
- Events may arrive more than once, and in rare cases out of order: use
X-SmartContact-Deliveryandcreated_at.
Events (33)
| Event | When | Needs |
|---|---|---|
company.created | A company was added | Companies (read) |
company.updated | A company changed (data.changed lists the fields) | Companies (read) |
company.deleted | A company went to the recycle bin | Companies (read) |
company.restored | A company came back from the recycle bin | Companies (read) |
company.merged | A duplicate company was merged into another | Companies (read) |
company.erased | A company was erased: delete any copy you hold | Companies (read) |
company.contact_added | A contact was added to a company | Companies (read) |
company.deal_added | A deal was added to a company | Companies (read) |
contact.created | A contact was added | Contacts (read) |
contact.updated | A contact changed (data.changed lists the fields) | Contacts (read) |
contact.deleted | A contact went to the recycle bin | Contacts (read) |
contact.restored | A contact came back from the recycle bin | Contacts (read) |
contact.merged | A duplicate contact was merged into another | Contacts (read) |
contact.erased | A person was erased: delete any copy you hold | Contacts (read) |
contact.consent_changed | Marketing consent was given or withdrawn | Contacts (read) |
deal.created | A deal was added | Companies (read) |
deal.updated | A deal changed, including its stage, won or lost | Companies (read) |
deal.deleted | A deal went to the recycle bin | Companies (read) |
deal.restored | A deal came back from the recycle bin | Companies (read) |
task.created | A task was added | Tasks (read) |
task.updated | A task changed | Tasks (read) |
task.completed | A task was completed | Tasks (read) |
meeting.created | A meeting was booked | Companies (read) |
activity.created | A note, call or email was added to a timeline | Companies (read) |
quote.created | A draft quote was started on a deal | Companies (read) |
quote.sent | A quote was sent to the customer (its accept link now works) | Companies (read) |
quote.viewed | The customer opened a quote for the first time | Companies (read) |
quote.accepted | The customer accepted a quote (the deal is marked won) | Companies (read) |
quote.declined | The customer declined a quote | Companies (read) |
quote.revised | A sent quote was reopened for changes (its old link stopped working) | Companies (read) |
quote.voided | A quote was withdrawn | Companies (read) |
quote.invoiced | A draft invoice was created in Xero for an accepted quote | Companies (read) |
quote.paid | The quote's Xero invoice was paid | Companies (read) |