Smartify

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 POST to 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

  1. An owner or admin opens Settings → Integrations (the webhooks section) in Smartify Pro.
  2. They choose Add endpoint, enter an HTTPS URL, and pick either all events or the event groups to receive.
  3. The signing secret (whsec_…) is shown once. Store it next to your API key.
  4. They choose Send test to receive a ping event, 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: changes names 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.
  • 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, origin is {"type":"integration","key_id":…}. Skip events whose key_id is 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.unshared and 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 False

The scheme is Standard Webhooks, so its open-source verifier libraries work too.

Responding and retries

  • Any 2xx within 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.
RequestWhat it does
GET /webhooksThe endpoints this key created.
POST /webhooksCreate 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-secretReturns { "data": { "secret": "whsec_…" } }. The old secret keeps signing alongside the new one for 7 days.
POST /webhooks/{endpointId}/testSends 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:

StatuscodeMeaning
400invalid_urlThe URL cannot be used for webhooks.
400invalid_eventsenabled_events must be ["*"] or a list of event types.
400invalid_statusstatus must be active or disabled.
400invalid_jsonThe body is not valid JSON.
403insufficient_scopeThe key lacks webhooks:manage.
404endpoint_not_foundNo such endpoint, or it was not created by this key.
409endpoint_limit_reachedThe organization already has 25 endpoints.
429rate_limitedOver 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.

  1. 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.
  2. 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.
  3. Build the rest of the Zap or scenario from the event fields: type, data.lead_id, data.changes, data.actor and data.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_id is 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

EventWhen
lead.createdA lead your organization owns was created
lead.shared / lead.unsharedA lead was shared with your organization, or stopped being shared
lead.unlockedYour organization unlocked a shared lead
lead.updatedProfile fields changed (changes names them)
lead.stage_changedThe stage changed (from/to)
lead.note.created / .updated / .deletedA note on the lead
lead.activity.created / .updated / .deletedA call, visit or other activity
lead.task.created / .updated / .completed / .deletedOne of your organization's tasks
lead.reminder.created / .updated / .completed / .deletedOne of your organization's reminders
lead.attachment.created / .deletedA file on the lead
pingSent by Send test