MeMachineDocs Open the app

Docs  /  Bookings  /  Station 06 · what came back

Sending bookings to your CRM

Updated August 7, 2026

1. What the webhook does

When somebody books a time on your page, MeMachine sends that booking to one URL of your choosing as a JSON POST — their name, email, phone, notes, the time they picked and the thing they booked. There are no per-CRM integrations to connect and nothing to map inside MeMachine: it is one field on the Settings card headed Send bookings to your CRM, and it is pointed at a catch hook in an automation tool (Zapier, Make, n8n) or at a small endpoint of your own. From there the booking goes wherever you already work — GoHighLevel, Jobber, Housecall Pro, HubSpot, a spreadsheet, a text message to yourself. Delivery is signed, retried, and completely separate from the booking itself: an endpoint that is down or misconfigured can never cost you the appointment.

This is your customer's details leaving our systems

The moment it is delivered, that name, email and phone number lives in a third-party tool under your account, governed by that tool's terms, not ours. Only point it somewhere you control.

2. Turning it on

  1. Open Settings and find Send bookings to your CRM.
  2. Paste an https URL into Webhook URL and tap Save.
  3. Your signing secret appears once, on that save. Copy it now — it is never shown again, and the only way to get another is New secret, which retires the old one immediately. If you are only using Zapier you can ignore it; if you are writing your own receiver, you need it (see the contract).
  4. Tap Send test event. That fires a complete sample booking, marked test, so the other end has real data to map against before a real customer ever books.
  5. Leave Sending ticked. Unticking it stops delivery without losing the URL — useful while you are rebuilding the other end.

Only https URLs are accepted, and only ones that resolve to the public internet. A http:// URL, a user:password@ URL, or anything pointing at a private network is refused when you save it.

3. The Zapier path, step by step

Zapier is the shortest route for almost everybody: every platform below already has a first-party Zapier integration, so one Zap does the job no matter what you run.

  1. In Zapier, tap Create and choose Webhooks by Zapier as the trigger app, event Catch Hook. (Catch Hook is on Zapier's free plan; some of the destination apps below are not.)
  2. Leave Pick off a Child Key empty and continue. Zapier shows you a Custom Webhook URL like https://hooks.zapier.com/hooks/catch/123456/abcdef/. Copy it.
  3. Paste that into MeMachine's Webhook URL field and Save.
  4. Back in Zapier, tap Test trigger so it starts listening — then in MeMachine tap Send test event. Zapier picks up the sample within a second or two.
  5. Add an action step for wherever the booking should land, and map the fields. Zapier lists nested values under their parent, so the person's name arrives as booking name, their number as booking phone, the appointment time as booking starts at, and what they booked as service thing.
  6. Publish the Zap.
Filter the test events out

Every test carries test: true (real bookings carry test: false). Once you are live, add a Zapier Filter step — only continue if test is false — so pressing Send test event later never creates a fake customer in your CRM.

Make, n8n and Pipedream all work the same way: their equivalent of Catch Hook is a plain HTTP-trigger node, and the field names below are identical.

4. Recipes

One action step each. Where a CRM wants a single "name" and we send one too, the mapping is one-to-one; where it wants first and last separately, Zapier's Formatter → Text → Split on a space does it in one extra step.

GoHighLevel

Action: Lead Connector (GoHighLevel) → Create or Update Contact. Map booking name to Full Name, booking email to Email, booking phone to Phone, and booking notes plus service thing into the contact's Notes. Add a static tag like memachine-booking so your workflows can trigger on it. If you run a pipeline, add a second action — Create Opportunity — using the contact from step one and service thing as the opportunity name.

Jobber

Action: Jobber → Create Client. Map booking name to the client name (split it into First/Last with Formatter if your Jobber account is set up that way), booking email to Email and booking phone to Phone. Then add Jobber → Create Request against that client, with booking notes as the request details and booking starts at as the requested date — a Request, not a Job, is the honest match for "somebody asked", and it lands in the same queue as your other inbound work.

Housecall Pro

Action: Housecall Pro → Create Customer with booking name, booking email and booking phone. Follow it with Create Job for that customer, scheduled at booking starts at, with service thing as the job description and booking notes in the notes field. Housecall Pro expects an ISO timestamp, which is exactly what booking starts at is — no Formatter step needed.

HubSpot

Action: HubSpot → Create or Update Contact, matching on booking email. Map booking phone to Phone Number and split booking name into First Name / Last Name with Formatter. Set Lifecycle Stage to Lead and put booking notes into a note or a custom property. Add Create Deal after it if you track jobs as deals — service thing as the deal name, booking starts at as the close date.

Google Sheets

Action: Google Sheets → Create Spreadsheet Row. Make a sheet with a header row — Booked at · Name · Email · Phone · Service · Notes · Timezone — and map booking created at, booking name, booking email, booking phone, service thing, booking notes, booking timezone straight into it. This is the one to build first even if you also run a CRM: it costs nothing, it never errors, and it is the fastest way to see what the payload actually contains.

Text your prospects meeting reminders

MeMachine does this natively now

Text reminders send the day-before and two-hour texts on the same schedule as the reminder emails, through your own Twilio account, and they know when an appointment has been cancelled or moved. Use that instead of the Zap below unless you specifically want the SMS to come out of a CRM you already pay for. The recipe stays here because it still works.

The payload carries a phone number and the appointment time, which is everything a reminder needs. After your catch hook, add Delay by Zapier → Delay Until and set the date to booking starts at with an offset of -24 hours, then an SMS action: Lead Connector (GoHighLevel) → Send SMS to booking phone, or Twilio → Send SMS if you are not on GHL. Body: something like "Hi {{booking name}} — reminder of your {{service thing}} tomorrow. Reply here if you need to move it." Duplicate the Delay + SMS pair with a -2 hours offset for a same-day nudge. Two things to get right: use the customer's own number as the From where the platform allows it, so replies reach you, and keep an opt-out line in the first message — the booking is consent to be contacted about that appointment, not a subscription to marketing texts.

5. When a delivery fails

The Settings card shows the last delivery's result and how many are still waiting. What happens behind it:

  • Anything in the 2xx range counts as delivered. Nothing else does.
  • A timeout, a 5xx, a 408 or a 429 is retried with a widening gap, up to six attempts — roughly two hours of trying.
  • Any other 4xx is dropped immediately. A 401, 404 or 410 means the endpoint is wrong, not busy, and hammering it for two hours helps nobody.
  • A redirect is dropped immediately and never followed. Point the URL at the final address.
  • Turning Sending off drops anything queued. Your last instruction wins.
  • Each delivery has ten seconds to answer. Do the slow work after you have replied.

The booking itself is untouched by all of this — it is already on your Bookings tab and your calendar before the first delivery attempt is made.

6. For developers: the contract

Only needed if you are writing the receiver yourself. Skip this if you are using Zapier.

Headers

HeaderValue
x-memachine-eventbooking.created — the only event that exists today.
x-memachine-deliveryA UUID. Stable across retries of the same delivery — dedupe on it, because delivery is at-least-once.
x-memachine-timestampUnix seconds. The same value that is inside the signature.
x-memachine-signaturet=<unix>,v1=<hex> where v1 is HMAC-SHA256(secret, "<t>.<raw body>").

Content type is application/json; the user agent is MeMachine-Webhook/1.

The body

Versioned on purpose. Fields are only ever added to version 1 — anything that would break a receiver gets a new version number instead, so read the field you want by name and ignore the rest.

{
  "event": "booking.created",
  "version": 1,
  "sent_at": "2026-08-07T19:04:05.000Z",
  "test": false,
  "business": { "id": 12, "name": "Rivera Plumbing" },
  "booking": {
    "id": 481,
    "status": "booked",
    "starts_at": "2026-08-12T16:00:00.000Z",
    "ends_at": "2026-08-12T16:30:00.000Z",
    "timezone": "America/Denver",
    "created_at": "2026-08-07T19:04:04.000Z",
    "name": "Dana Rivera",
    "email": "dana@example.com",
    "phone": "+13035550142",
    "notes": "Kitchen tap dripping for a week."
  },
  "service": {
    "thing": "Free estimate",
    "funnel": { "id": 3, "slug": "kitchen-taps", "mode": "booking", "url": "/f/kitchen-taps" }
  }
}
  • status is one of booked, canceled, completed, no_show. A booking.created event is always booked.
  • mode is booking or lead.
  • test is true only for Send test event.
  • Every field inside booking except id, status, starts_at and ends_at can be null, as can service.thing and the whole of service.funnel for a booking made outside a funnel.
  • No tokens, no secrets, no internal identifiers beyond the booking's own.

Verifying the signature

Sign the raw request body, byte for byte, before any JSON parsing — re-serializing a parsed object changes the bytes and the signature will not match. Reject anything more than five minutes out of date, which is what stops a captured delivery being replayed tomorrow. Compare in constant time.

import crypto from 'node:crypto';

const MAX_SKEW_SECONDS = 300;

/** rawBody: the exact string of bytes received, NOT JSON.parse'd first. */
export function verifyMeMachineWebhook(rawBody, headers, secret) {
  const parts = Object.fromEntries(
    String(headers['x-memachine-signature'] || '')
      .split(',').map((p) => p.trim().split('=')).filter((kv) => kv.length === 2)
  );
  const t = Number(parts.t);
  const v1 = String(parts.v1 || '');
  if (!Number.isFinite(t) || !v1) throw new Error('Malformed webhook signature.');
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > MAX_SKEW_SECONDS) {
    throw new Error('Webhook signature timestamp is outside the accepted window.');
  }

  const expected = crypto.createHmac('sha256', secret)
    .update(`${t}.${rawBody}`, 'utf8').digest();
  const supplied = Buffer.from(v1, 'hex');
  if (supplied.length !== expected.length || !crypto.timingSafeEqual(supplied, expected)) {
    throw new Error('Webhook signature does not match.');
  }

  return JSON.parse(rawBody);           // safe to parse now
}

In Express, get the raw bytes with express.json({ verify: (req, res, buf) => { req.rawBody = buf.toString('utf8'); } }). The same algorithm in any language: HMAC-SHA256 over the concatenation of the timestamp, a literal dot, and the body text.

What your endpoint must do

  • Answer 2xx within 10 seconds. Queue the real work; do not do it inline.
  • Be idempotent on x-memachine-delivery — a retry after a slow success will arrive.
  • Serve https with a valid certificate, on a public address. Loopback, RFC1918, link-local and CGNAT hosts are refused at save time and re-checked at every delivery.
  • Not redirect. Redirects are never followed and are treated as a permanent failure.
  • Reject anything that fails verification, rather than trusting the body.