®Docs

Outbound webhooks

Send new Superpractice contacts and booked meetings to your own systems the moment they happen — Zapier, Slack, a CRM, or any URL that accepts an HTTPS request.

Overview

Outbound webhooks are the mirror image of the inbound contact webhook: instead of pushing leads into Superpractice, they push events out to URLs you configure. Superpractice sends a signed POST request to each of your active endpoints for the events it subscribes to:

  • contact.created — a new contact is created, from your website pixel, a booked meeting, a phone call, Meta Lead Ads, a landing page, the AI voice agent, an inbound webhook, or manual entry.
  • meeting.booked — a meeting is booked through your Superpractice booking widget.

Use them to forward leads to Zapier or Make, mirror contacts into another CRM, trigger internal alerting, or feed any downstream automation. You can register up to 10 endpoints per firm, each with its own signing secret, event selection, and optional filters.

Setup

  1. Open Settings > Integrations and find the Outbound Webhooks section.
  2. Click Add Webhook, give it a name, and paste the destination URL. The URL must be a public https:// (or http://) address.
  3. Pick the events the endpoint should receive — contacts, booked meetings, or both.
  4. Optionally set filters (see below) so the endpoint only receives events from certain ad channels, campaigns, landing pages, or Meta lead forms.
  5. Click Test to send a signed sample of each subscribed event and confirm your receiver responds with a 2xx status.
  6. Create a real contact (or book a meeting) and watch the delivered count on the endpoint card tick up.

The request your endpoint receives

Every delivery is an HTTPS POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
X-Superpractice-EventThe event type: contact.created or meeting.booked.
X-Superpractice-DeliveryA unique ID for this delivery attempt. Useful for logging and support.
X-Superpractice-Signaturet=<unix seconds>,v1=<hex HMAC> — see Verifying signatures.

Your endpoint should respond with any 2xx status within 10 seconds. The response body is ignored. Redirects are not followed — point the webhook at the final URL.

contact.created payload
{
  "event": "contact.created",
  "eventId": "contact.created:6f9d2c1e-…",
  "createdAt": "2026-07-27T15:04:05.000Z",
  "data": {
    "contact": {
      "id": "6f9d2c1e-…",
      "firstName": "Jane",
      "lastName": "Public",
      "email": "jane@example.com",
      "phone": "+15550100000",
      "source": "pixel",
      "status": "touchpoint",
      "leadStatus": "new_lead",
      "practiceArea": "Estate Planning",
      "practiceAreaNormalized": "Estate Planning",
      "serviceType": null,
      "matterType": null,
      "caseValue": null,
      "tags": null,
      "notes": "Interested in a consultation.",
      "context": null,
      "address": { "street": null, "street2": null, "city": null, "state": null, "zip": null, "country": "US" },
      "attribution": {
        "firstTouch": {
          "source": "google-ads", "medium": "cpc", "campaign": "Brand",
          "content": null, "term": "estate planning attorney",
          "landingPage": "https://example-firm.com/estate-planning",
          "referrer": null, "gclid": "Cj0K…", "fbclid": null, "msclkid": null,
          "timestamp": "2026-07-27T15:03:58.000Z"
        },
        "lastTouch": { "…": "same shape as firstTouch" }
      },
      "createdAt": "2026-07-27T15:04:04.000Z"
    }
  }
}
meeting.booked payload
{
  "event": "meeting.booked",
  "eventId": "meeting.booked:9a1b3c5d-…",
  "createdAt": "2026-07-31T15:04:05.000Z",
  "data": {
    "meeting": {
      "id": "9a1b3c5d-…",
      "status": "confirmed",
      "startTime": "2026-08-02T14:00:00.000Z",
      "endTime": "2026-08-02T14:30:00.000Z",
      "timezone": "America/New_York",
      "durationMinutes": 30,
      "eventType": { "id": "…", "name": "Initial Meeting", "slug": "initial-meeting" },
      "invitee": {
        "name": "Jane Public",
        "firstName": "Jane",
        "lastName": "Public",
        "email": "jane@example.com",
        "phone": "+15550100000",
        "notes": "Looking for help with a trust."
      },
      "meetingLink": null,
      "location": {
        "mode": "in_person",
        "label": "Main Office",
        "locationId": "3b7d4e21-…",
        "inPersonAddress": "123 Main Street, Springfield, IL 62701",
        "inPersonNotes": "Please check in at reception.",
        "phoneCallDirection": null,
        "phoneCallNumber": null
      },
      "host": { "id": "…", "name": "Alex Attorney", "email": "alex@example-firm.com" },
      "bookedAt": "2026-07-31T15:04:03.000Z"
    },
    "contact": { "…": "the same shape as data.contact in contact.created, or null" }
  }
}

A few meeting.booked notes:

  • status is confirmed for instantly confirmed bookings and pending when the event type requires host confirmation. The event fires once, when the meeting is booked — a later host confirmation does not fire a second event.
  • data.contact carries the full contact record (with attribution) that the booking created or matched. In the rare moment before contact integration finishes it can be null.
  • Legacy bookings and event types without client-selectable locations send location: null. Video meeting URLs arrive in meetingLink rather than the location object.
  • Only meetings booked through the Superpractice booking widget fire this event — meetings synced from HubSpot, GoHighLevel, or calendar imports do not.

eventId is deterministic (contact.created:<contact id>, meeting.booked:<meeting id>), so if your receiver ever sees the same event twice you can safely deduplicate on it. Attribution uses the same canonical source values as the rest of Superpractice.

Verifying signatures

Each endpoint has its own signing secret (shown in the endpoint's Configure dialog). The signature header proves the request came from Superpractice and that the body was not modified:

  • t is the Unix timestamp (seconds) when the request was signed.
  • v1 is HMAC-SHA256(secret, "<t>.<raw body>") as lowercase hex.

Verify by recomputing the HMAC over the raw request body — before any JSON parsing or re-serialization — and comparing with a constant-time comparison. Reject requests whose timestamp is too old to block replays.

Node.js verification
import { createHmac, timingSafeEqual } from "crypto"

function verifySuperpracticeSignature(signatureHeader, rawBody, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.split("=")))
  const timestamp = Number(parts.t)
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex")
  if (!parts.v1 || parts.v1.length !== expected.length) return false
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"))
}

Rotating? Update the secret on your receiver first, then contact support — or delete and recreate the endpoint, which issues a fresh secret.

Filters

By default an endpoint receives every subscribed event. Filters narrow that down. All matching uses the contact's first-touch attribution — how the lead originally found the firm. For meeting.booked, filters evaluate against the contact who booked the meeting.

FilterMatches
Ad channelsThe contact's normalized channel: Google Ads, Google LSA, Meta Ads, OpenAI Ads, Bing Ads, Organic SEO, Google Business, Referral, Email, Direct, Social, AI, or Other. Legacy source spellings normalize automatically (adwords still matches Google Ads).
Campaign containsCase-insensitive substring match against the first-touch campaign name. Comma-separate multiple values; any match passes.
Landing page URL containsCase-insensitive substring match against the first-touch landing page URL. Comma-separate multiple values; any match passes.
Meta lead formsThe specific Meta lead form the contact was created from — an exact match, so an endpoint can serve a single form. Only contacts born from a Meta lead form can match; requires the Meta Ads integration with lead form sync enabled (the picker is greyed out until then).

Within one filter the values are OR'd; different filters are AND'd together. So "channels: Google Ads, Meta Ads + campaign contains: brand" means (Google Ads or Meta Ads) and a campaign containing "brand".

Contacts with no attribution at all (for example, manually entered contacts) only match a channel filter when Direct is selected. Similarly, a Meta lead forms filter never matches contacts that came from any other source.

Delivery, retries, and reliability

  • At-least-once delivery. Each event is sent immediately when it happens. If your endpoint is unreachable or returns a non-2xx status, Superpractice retries with exponential backoff (roughly 5 minutes to 6 hours between attempts) for up to 3 days. A background reconciliation pass also re-checks recent contacts and bookings, so a delivery is never silently lost — worst case it arrives a few minutes late.
  • Ordering is not guaranteed. Retries mean events can occasionally arrive out of order. Use data.contact.createdAt or data.meeting.bookedAt if order matters.
  • Duplicates are possible but rare. Dedupe on eventId if your automation is not idempotent.
  • New endpoints start from now. Adding an endpoint does not replay past contacts or meetings.
  • Not sent: bulk CSV imports, calendar imports, and contacts or meetings brought in by the HubSpot/GoHighLevel syncs do not trigger outbound webhooks.

Automatic pause

If an endpoint fails 10 consecutive deliveries — or responds with 410 Gone — Superpractice pauses it so a dead URL is not hammered forever. When that happens:

  • The endpoint card in Settings shows the pause reason.
  • Every firm owner and admin receives an email with the endpoint, the reason, and a re-enable link.

Fix the receiver, then toggle the endpoint back on (or save a corrected URL) — deliveries resume immediately and the failure counter resets. While paused, new contacts are not queued for that endpoint.

Troubleshooting

SymptomCheck
Test succeeds but real contacts never arriveFilters — a channel filter silently skips non-matching and unattributed contacts.
Signature verification failsYou must HMAC the raw body bytes. Frameworks that re-serialize JSON (or middleware that trims whitespace) change the body and break the signature.
Deliveries time outYour receiver must respond within 10 seconds. Accept fast, process async.
Endpoint paused unexpectedlyThe card shows the last error and response status. Ten consecutive failures of any kind trigger the pause.

On this page