Skip to content
For developers
View as Markdown

Webhooks

Event-driven integration: catalog, signatures, retries.

Webhooks turn your integration from pull to push. Instead of polling the API on a schedule and diffing results, you register a URL once and ActuallyCare sends an HTTP POST to it the moment something happens. This page covers the concepts — event catalog, payload, signatures, delivery. For a step-by-step walkthrough, see the webhook setup guide.

Webhooks or polling?#

SituationBetter fit
React to changes as they happen (new lead, escrow update)Webhooks
Periodic full sync or reporting snapshotPolling the REST API
You can't host a public HTTPS endpointPolling
Low latency matters (lead routing, notifications)Webhooks

The usual pattern is both: webhooks for the real-time signal, with an occasional polled reconciliation pass as a safety net.

The event catalog#

The catalog is self-documenting. Ask the API what events exist (all /v1/webhooks endpoints take a JWT bearer token — not an API key — and need a broker or system-admin role, see Managing webhooks):

curl "https://api.actuallycare.com/v1/webhooks/events" \
  -H "Authorization: Bearer YOUR_JWT"

The response uses the standard envelope: data lists every event type with its category, description, and the fields its delivery payload carries, and meta totals it up. Trimmed to two entries:

{
  "success": true,
  "data": [
    {
      "event": "escrow.created",
      "category": "escrow",
      "description": "An escrow/transaction was created",
      "payload": ["id", "status", "property_address", "escrow_number"]
    },
    {
      "event": "lead.created",
      "category": "lead",
      "description": "A new lead was created",
      "payload": ["id", "name", "email", "phone", "source", "status"]
    }
  ],
  "meta": {
    "total": 150,
    "categories": ["appointment", "care_status", "client", "..."]
  }
}

There are about 150 event types across roughly 27 categories — escrow.*, lead.*, client.*, listing.*, appointment.*, care_status.*, and more. A few representative examples:

EventFires when
escrow.createdA new escrow is opened
escrow.updatedAn escrow's details change
lead.createdA new lead enters the pipeline
lead.convertedA lead becomes a client
client.createdA new client relationship is created
listing.updatedA listing's details change
appointment.cancelledAn appointment is cancelled
care_status.changedA relationship's follow-up health changes

This table is illustrative — the catalog endpoint is the source of truth. GET /v1/webhooks/events/categories returns the category list if you want to browse by group. When you create a webhook you subscribe it to specific events, or to all of them.

The payload envelope#

Every delivery is a POST with a JSON body in the same envelope:

{
  "event": "escrow.created",
  "team_id": "3f8a1c2e-9d4b-4f6a-8e2d-1b7c5a9e0f3d",
  "timestamp": "2026-06-11T17:32:08.123Z",
  "data": {
    "id": "9c4e7b1a-2f8d-4e3c-a6b5-0d1f2e3a4b5c"
  }
}
FieldMeaning
eventThe event type that fired
team_idThe team the event belongs to (webhooks are team-scoped)
timestampWhen the event occurred
dataThe affected record — its shape depends on the event type

Verifying signatures#

Every delivery is signed so you can confirm it really came from ActuallyCare. Three headers arrive with each request:

HeaderContents
X-Webhook-Signaturesha256= followed by the HMAC-SHA256 hex digest of the JSON body, computed with your webhook secret (e.g. sha256=3f5a…).
X-Webhook-EventThe event type, same as event in the body
X-Webhook-TimestampWhen the delivery was sent

Compute the same HMAC over the raw request body and compare with a timing-safe comparison — never a plain string equality:

const crypto = require('crypto');
const express = require('express');
 
const app = express();
 
function verifySignature(rawBody, signatureHeader, secret) {
  const expectedHex = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
 
  // Header format: "sha256=<hex digest>"
  const receivedHex = (signatureHeader || '').replace(/^sha256=/, '');
  const expected = Buffer.from(expectedHex, 'hex');
  const received = Buffer.from(receivedHex, 'hex');
  return (
    expected.length === received.length &&
    crypto.timingSafeEqual(expected, received)
  );
}
 
app.post(
  '/webhooks/actuallycare',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('X-Webhook-Signature');
    if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send('invalid signature');
    }
 
    // Acknowledge fast, process async
    res.status(200).send('ok');
 
    const payload = JSON.parse(req.body.toString('utf8'));
    // handle payload.event, payload.data ...
  }
);

Two details that bite people: verify against the raw body (a re-serialized JSON object may not match byte-for-byte), and remember to strip the sha256= prefix from the header before comparing digests. Your webhook secret is set when you create the webhook (minimum 10 characters).

Delivery and retries#

  • ActuallyCare waits up to 30 seconds for your endpoint to respond. Return a 2xx quickly and do real work asynchronously.
  • Failed deliveries are retried at 1 minute, 5 minutes, and 15 minutes after the initial attempt (up to 4 attempts total), then given up. A delivery counts as failed on a network error or on a 5xx, 408, or 429 response; other 4xx responses are treated as your endpoint rejecting the event and are not retried.
  • Retries survive server restarts, and deactivating a webhook cancels its pending retries.
  • Delivery is at-least-once: your endpoint can see the same event more than once, so process idempotently. The delivery logs — visible in the app, and available via GET /v1/webhooks/:id/logs — show every attempt.

Deduplicate on the signature header value — a retried delivery carries the same signature as the original, and unlike event + timestamp it can't collide when two records fire the same event in the same instant.

Managing webhooks#

Webhooks are team-scoped and managed by broker and system-admin roles. Two ways to manage them:

  • In the app — Settings, Webhooks tab (app.actuallycare.com/settings?tab=webhooks): create endpoints, choose events, and review delivery logs.
  • Via RESTGET/POST /v1/webhooks to list and create, PUT/DELETE /v1/webhooks/:id to update and remove, GET /v1/webhooks/:id/logs for delivery history.

Ready to wire one up end to end? Follow the webhook setup guide.