Webhooks
Get a signed POST whenever a lead your organization can see changes, and catch up with the event feed.
Smartify Pro tells your system when a lead your organization can see changes. You get the same events two ways:
- Webhooks: a signed
POSTto your HTTPS endpoint within seconds. - Event feed:
GET /v1/events, to catch up after downtime or as a polling alternative.
Both carry the same thin events, kept for 30 days. The REST API itself is described in Partner API.
Set up an endpoint
- An owner or admin opens Settings → Integrations (the webhooks section) in Smartify Pro.
- They choose Add endpoint, enter an HTTPS URL, and pick either all events or the event groups to receive.
- The signing secret (
whsec_…) is shown once. Store it next to your API key. - They choose Send test to receive a
pingevent, and check the Delivery log.
Limits:
- Up to 25 endpoints per organization, counting endpoints made in the app and by API keys together.
- URLs must be HTTPS, on port 443 or 8443, and resolve to a public address. Smartify checks this when you save and again before every delivery.
- Redirects are not followed.
What a delivery looks like
POST /hooks/smartify HTTP/1.1
content-type: application/json
user-agent: SmartifyPro-Webhooks/1.0
webhook-id: evt_17945500031200a1b2c3d4e5f60718
webhook-timestamp: 1794550003
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "lead.stage_changed",
"timestamp": "2026-11-13T10:15:03.120Z",
"data": {
"event_id": "evt_17945500031200a1b2c3d4e5f60718",
"org_id": "org_a49a5663",
"lead_id": "CRM-1794550000000-123",
"occurred_at": "2026-11-13T10:15:02.874Z",
"actor": { "type": "organization", "id": "smartify", "name": "Smartify" },
"origin": null,
"object": { "type": "lead", "id": "CRM-1794550000000-123" },
"changes": [{ "field": "stage", "from": "CONTACTED", "to": "QUOTED" }],
"attempt": 1,
"links": { "lead": "https://api.pro.smartify.in/v1/leads/CRM-1794550000000-123" }
}
}- Thin events:
changesnames the fields that changed.- Values (
from/to) are included only for the stage,effective_value(deal value) and a task's or reminder's status. - Names, phones, emails, addresses and note text are never in an event: fetch the lead (
links.lead), where your organization's access and masking apply.
- Values (
- Actor: your own people and keys appear by name. Anyone outside your organization appears as Smartify, the same rule as lead history.
- Origin: when one of your integration keys made the change,
originis{"type":"integration","key_id":…}. Skip events whosekey_idis your own, so a write you made through the Partner API doesn't echo back. - Who receives what: an event reaches your organization only if your API key could read that lead at that moment.
- A lead shared with you but still locked sends only what its preview shows (stage, city, property type).
- Notes and activities arrive once the lead is unlocked.
- Tasks and reminders reach only the organization that created them.
- If a lead stops being shared with you, you get
lead.unsharedand nothing after it.
Verify the signature
The signature is v1, followed by the base64 of HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<raw body>. The
key is the base64-decoded part of the secret after whsec_. During a secret rotation the header carries two
signatures, separated by a space; accept the request if either matches. Reject requests whose timestamp is more than
5 minutes from your clock.
Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifySmartifyWebhook({ secret, headers, rawBody }) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest();
return String(headers['webhook-signature']).split(' ').some((part) => {
const [version, signature] = part.split(',');
const given = Buffer.from(signature || '', 'base64');
return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected);
});
}Python:
import base64, hashlib, hmac, time
def verify_smartify_webhook(secret: str, headers: dict, raw_body: bytes) -> bool:
msg_id, timestamp = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
expected = hmac.new(key, f"{msg_id}.{timestamp}.".encode() + raw_body, hashlib.sha256).digest()
for part in headers["webhook-signature"].split(" "):
version, _, signature = part.partition(",")
if version == "v1" and hmac.compare_digest(base64.b64decode(signature), expected):
return True
return FalseThe scheme is Standard Webhooks, so its open-source verifier libraries work too.
Responding and retries
- Any
2xxwithin 10 seconds acknowledges the delivery. Do the work asynchronously and respond quickly. - Anything else counts as a failure: a timeout, a connection error or a
3xx. Smartify retries after at least 1 min, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h and 24 h (retries are picked up once a minute, so each can run up to a minute later), about 3 days in all, then stops. Every attempt is listed in the delivery log, and you can resend one from there. - An endpoint with no successful delivery for 3 days, while failing, is disabled. Re-enable it in Settings → Integrations once
it's fixed, then catch up with
GET /v1/events. - More than 20 events for one lead within a minute pauses that lead's deliveries to that endpoint for 10 minutes, to stop a sync loop.
- Order isn't guaranteed, and the same event can arrive twice. Dedupe on
webhook-id, and read current state through the API instead of relying on arrival order.
Manage webhook endpoints with an API key
Instead of adding endpoints by hand in Settings, software can subscribe and unsubscribe itself with a partner API key that has the webhooks:manage scope. This is what integration platforms such as Zapier and Make need: subscribe when a user turns a workflow on, unsubscribe when they turn it off.
- A key sees and manages only the endpoints it created. An endpoint added in Settings is invisible to keys and cannot be changed or deleted by one; asking for it answers
404 endpoint_not_found. - Endpoints made by keys and endpoints made in the app count together toward the limit of 25 endpoints per organization (
409 endpoint_limit_reached). - The delivery log and Resend stay in Smartify Pro.
- Signatures, retries and the event shape are exactly as described above.
| Request | What it does |
|---|---|
GET /webhooks | The endpoints this key created. |
POST /webhooks | Create an endpoint. Returns 201 with the endpoint and its signing secret (shown only here). |
GET /webhooks/{endpointId} | One endpoint. |
PATCH /webhooks/{endpointId} | Change url, enabled_events, description or status (active or disabled). |
DELETE /webhooks/{endpointId} | Delete an endpoint. Returns 204. |
POST /webhooks/{endpointId}/rotate-secret | Returns { "data": { "secret": "whsec_…" } }. The old secret keeps signing alongside the new one for 7 days. |
POST /webhooks/{endpointId}/test | Sends a ping event. Returns 202 with { "data": { "event_id": "…" } }. |
Create an endpoint. url and enabled_events are required; enabled_events is ["*"] or a list of the event types in the catalogue below (not ping). description is optional, up to 200 characters.
curl -s -X POST https://api.pro.smartify.in/v1/webhooks \
-H "Authorization: Bearer $SMARTIFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/smartify/abc123",
"enabled_events": ["lead.stage_changed", "lead.created"],
"description": "Zap 123: new leads"
}'{
"data": {
"endpoint_id": "whe_0123456789abcdef0123456789abcdef",
"org_id": "org_a49a5663",
"url": "https://hooks.example.com/smartify/abc123",
"description": "Zap 123: new leads",
"enabled_events": ["lead.stage_changed", "lead.created"],
"status": "active",
"disabled_reason": null,
"created_at": "2026-11-13T10:00:00.000Z",
"created_by": "int_key_example",
"last_success_at": null,
"secret_rotated_at": null,
"secret": "whsec_…"
}
}Store the secret if you will verify signatures. It cannot be read again; use rotate-secret to get a new one.
Delete it when the workflow is turned off:
curl -s -X DELETE https://api.pro.smartify.in/v1/webhooks/whe_0123456789abcdef0123456789abcdef \
-H "Authorization: Bearer $SMARTIFY_API_KEY"URL rules are the same as in the app: HTTPS, port 443 or 8443, a public address. Errors are application/problem+json:
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_url | The URL cannot be used for webhooks. |
| 400 | invalid_events | enabled_events must be ["*"] or a list of event types. |
| 400 | invalid_status | status must be active or disabled. |
| 400 | invalid_json | The body is not valid JSON. |
| 403 | insufficient_scope | The key lacks webhooks:manage. |
| 404 | endpoint_not_found | No such endpoint, or it was not created by this key. |
| 409 | endpoint_limit_reached | The organization already has 25 endpoints. |
| 429 | rate_limited | Over 120 requests a minute for this key. |
Retrying a subscribe is safe: if your key already has an endpoint for the same URL, POST /webhooks returns that endpoint (200, without the secret) instead of creating a second one.
Event feed
curl -s -H "Authorization: Bearer $SMARTIFY_API_KEY" "https://api.pro.smartify.in/v1/events?limit=100"The response is { "data": [event, …], "next_cursor": "…", "has_more": true }, oldest first. Each item is the
data object of a webhook, plus type and without attempt. Pass next_cursor back as after. The cursor belongs
to your organization's feed only. Requires the leads:read scope. Events are kept for 30 days.
Zapier and Make
There is no listed Smartify app in Zapier or Make yet. Until there is, you connect them with a webhook for the trigger and an HTTP step for the action.
Trigger: a Smartify event starts a Zap or scenario
Software that manages its own hooks can subscribe through the API instead: see Manage webhook endpoints with an API key. The steps below are for building a Zap or scenario by hand.
- In Zapier, create a Zap whose trigger is Webhooks by Zapier → Catch Hook. In Make, add a Webhooks → Custom webhook module. Copy the URL it gives you.
- In Smartify Pro, add that URL as a webhook endpoint (Settings → Integrations) and choose the events you want. Choose Send test to send a
ping, so the tool can learn the shape of the data. - Build the rest of the Zap or scenario from the event fields:
type,data.lead_id,data.changes,data.actoranddata.links.lead.
Things to know:
- Events are thin. They name the fields that changed; values are included only for the stage, the effective (deal) value and a task's or reminder's status. A name, phone number or note text is never in an event.
- To use lead details in a later step, add a step that fetches the lead (see below).
- Zapier and Make do not check the signature for you. Their catch URLs are hard to guess, which is the protection you get; if you need the signature checked, receive the webhook on your own server instead and use the verifier above.
- The same event can arrive twice. If a duplicate would do harm, filter on
data.event_id. - If a Zap or scenario writes back to Smartify with an API key, add a filter that stops when
data.origin.key_idis that key's id.
Action: fetch the lead, or change it
Use Webhooks by Zapier → Custom Request (or Zapier's API Request step), or Make's HTTP → Make a request module. Create a partner API key with only the scopes the step needs, and send it in a header:
Authorization: Bearer spk_live_…To get the lead behind an event, call GET on the event's own link:
GET https://api.pro.smartify.in/v1/leads/{data.lead_id}To change something, call the matching Partner API endpoint. For example, to add a note, POST https://api.pro.smartify.in/v1/leads/{lead_id}/notes with the header Content-Type: application/json, the body { "note": "…" } and an Idempotency-Key set to the Zap run id or scenario execution id, so a replayed run adds the note once.
If you cannot receive webhooks, poll GET /v1/events on a schedule instead (Zapier Schedule, Make Scheduler) and keep the last next_cursor between runs.
Event catalogue
| Event | When |
|---|---|
lead.created | A lead your organization owns was created |
lead.shared / lead.unshared | A lead was shared with your organization, or stopped being shared |
lead.unlocked | Your organization unlocked a shared lead |
lead.updated | Profile fields changed (changes names them) |
lead.stage_changed | The stage changed (from/to) |
lead.note.created / .updated / .deleted | A note on the lead |
lead.activity.created / .updated / .deleted | A call, visit or other activity |
lead.task.created / .updated / .completed / .deleted | One of your organization's tasks |
lead.reminder.created / .updated / .completed / .deleted | One of your organization's reminders |
lead.attachment.created / .deleted | A file on the lead |
ping | Sent by Send test |