Lawfficient API

Webhooks

Receive signed, real-time events when your firm's data changes.

Webhooks are the outbound mirror of the read API: instead of polling, you give us a URL and we POST a signed event to it whenever something changes in your firm. Add and manage endpoints in Settings → Integrations → Developer access.

Events

Event types are named resource.action. Today the lead and consultation lifecycles are covered:

EventFires when
lead.createdA lead is created (via the API or the app).
lead.updatedA lead's core or data fields change (a restore counts as this).
lead.status_changedA lead moves to a different pipeline status.
lead.assignedA lead's assignee changes.
lead.archivedA lead is archived.
consultation.bookedA consultation is booked.
consultation.rescheduledA consultation is moved to a new slot.
consultation.canceledA consultation is canceled.

Each endpoint subscribes to specific events, or to * for all of them. The catalog grows as more of the app is exposed.

Payload

A POST with a JSON envelope. data is the resource in the same shape the API returns — a lead.* event carries the lead object verbatim, and a consultation.* event carries the consultation object.

{
  "id": "f1e2d3c4-...unique-per-event...",
  "type": "lead.created",
  "created_at": "2026-06-24T10:00:00.000Z",
  "data": {
    "id": "3f8c1e2a-1b2c-4d5e-8f90-abcdef012345",
    "first_name": "Ada",
    "status": { "key": "new", "name": "New" }
  }
}

Deliveries can, rarely, repeat — dedupe on the event id.

Verifying the signature

Every delivery carries a Lawfficient-Signature header:

Lawfficient-Signature: t=1750766400,v1=3a4b5c...

v1 is HMAC-SHA256(secret, "{t}.{rawBody}") — the unix-second timestamp t joined to the raw request body with a dot, so the timestamp is covered by the signature. The signing secret (prefix whsec_) is shown once when you create the endpoint.

To verify a delivery: recompute the HMAC with your stored secret over "{t}.{body}", compare it to v1 in constant time, and reject if t is too old (a 5-minute tolerance bounds replay).

import crypto from "node:crypto"

// `body` MUST be the raw request bytes — verify before any JSON parse/re-serialize.
function verify(secret, body, header, toleranceSec = 300) {
  if (typeof header !== "string") return false
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=").map((s) => s.trim())))
  const t = Number(parts.t)
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
  if (!parts.v1) return false
  const expected = crypto.createHmac("sha256", secret).update(t + "." + body).digest("hex")
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Delivery

  • A 2xx response is success. Anything else — including a 3xx (redirects are not followed) — is a failure and is logged.
  • Each attempt has a short (~5s) timeout, so one slow endpoint can't hold up others.
  • Acknowledge fast: return 200 as soon as you've accepted the event, then do your work asynchronously.

Delivery is best-effort today — a single attempt per event, with every attempt logged for you to see. Durable retry with backoff is a planned follow-up.

On this page