Part 8 — Webhook on every status change · both sites

One CRM, one webhook for both sites. On every lead status change — one POST to our URL.

What
POST with JSON on every status change, including creation — leads from both sites
Where
Your CRM's status-change hook
Done when
You send — we see the events on our side. Nothing for you to test

Endpoint

http
POST https://prod-observix-integrations.orangeplant-bcc99803.eastus.azurecontainerapps.io/api/crm/hooks/hk_9U2-kZVbLTdSx7yfMPuowVsd_aJuKXhTpMNzUtHVB-Y
Content-Type: application/json

Payload

json
{
  // ── Required ─────────────────────────────────────────────────────────────
  "entity_id":   "L-48213",                // your lead / deal id — stable for its whole life
  "status":      "Estimate Sent",          // the status name exactly as it is in your CRM
  "occurred_at": "2026-09-22T14:19:00Z",   // when the status changed in the CRM, UTC — not the send time

  // ── Recommended — revenue in reports, and matching a customer across devices ──
  "value":       8400.00,                  // estimate / contract amount, if known
  "currency":    "USD",
  "email":       "jane@example.com",
  "phone":       "+19165550123",
  "external_id": "C-1092",                 // your customer / contact id

  "properties": {
    "observix_visitor_id": "01a0c8ac-2a12-7e9c-b0a2-059ecef85246",   // from Part 2 — required

    // + any lead fields you have: site, service, address, source, owner, notes…
    "site":     "abdteam.com",               // which website the lead came from
    "service":  "James Hardie siding",
    "city":     "Sacramento",

    // + any customer fields you have
    "first_name": "Jane",
    "last_name":  "Doe",

    // + the marks HandL already stores: gclid, fbclid, utm_* — send what you have
    "gclid":      "Cj0KCQ…",
    "utm_source": "google"
  }
}

Steps

  1. 01

    From the CRM's status-change hook, after the change is saved

    3 s timeout, errors to the log. Analytics must never break a lead update.

    reference implementation · for your review
    // wherever a lead's status changes in your CRM — after the change is saved
    function sendToObservix(lead, newStatus) {
      const payload = {
        entity_id:   String(lead.id),
        status:      newStatus,
        occurred_at: lead.statusChangedAt,          // the CRM's own change time — identical on every retry
        value:       lead.amount ?? undefined,
        currency:    "USD",
        email:       lead.email,
        phone:       lead.phone,
        external_id: lead.contactId ? String(lead.contactId) : undefined,
        properties:  { observix_visitor_id: lead.observix_visitor_id, site: lead.site, ...lead.customFields },   // + whatever you have
      };
    
      fetch("https://prod-observix-integrations.orangeplant-bcc99803.eastus.azurecontainerapps.io/api/crm/hooks/hk_9U2-kZVbLTdSx7yfMPuowVsd_aJuKXhTpMNzUtHVB-Y", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(3000),          // never hold up the CRM
      }).catch((e) => console.warn("observix:", e.message));   // log and move on
    }
  2. 02

    One entity_id per lead, unique across both sites

    Both sites land in the same CRM, so the lead id must not collide between them. Add asite property so we can split reports per website.

  3. 03

    One call per change, no dedup on your side

    Deduplication is on us: the same entity_id, status and occurred_at count once. That only works when occurred_at is the time the status changed (in Airtable, a “Last modified time” field on the status), never the time of sending.

Verify

  • Create a test lead and change its status twice in the CRM
  • Nothing in your error log; the lead updated as usual
  • Tell us the lead id — we confirm from our side that we see three events