Polling a scheduling API every minute to find out whether anyone booked is a poor way to spend compute and a worse way to find out late. Webhooks flip the direction: the scheduling tool calls you, at the moment something happens, with a signed message describing what it was. This guide is for developers wiring a scheduling tool into their own systems. It covers the events worth subscribing to, the exact request shape, signature verification in Node and Python, how to design for retries, and the automations teams actually build once the plumbing works. The examples use PepoSmart's webhooks, and the patterns apply to any well-designed sender.
Quick answer: Register an HTTPS endpoint, subscribe to the events you need, and verify every request by recomputing HMAC-SHA256 over the timestamp and raw body with the endpoint's secret. Respond with a 2xx within the sender's timeout and do slow work afterwards. Expect at-least-once delivery, so key your handler on the delivery id. PepoSmart sends booking created, cancelled, rescheduled and paid events plus meeting-notes and follow-up-draft events, retries up to 7 times with exponential backoff, and logs every delivery for 30 days.
Key takeaways
- Webhooks are for systems you own. For no-code connections use Zapier or Make; for on-demand reads use the REST API.
- Verify the signature over the raw body, never over re-serialised JSON.
- Respond fast and process later. A 10-second timeout is generous for an acknowledgement and tight for a database transaction.
- At-least-once delivery means duplicates. Idempotency is not optional.
- The interesting automations start after the booking: payment reconciliation, CRM enrichment, and reacting to AI meeting notes.
What a scheduling webhook is
A webhook is an HTTP POST from the scheduling tool to a URL you registered, carrying a JSON body that describes one event. The tool sends it when the event happens, so your system learns about a new booking within seconds rather than on the next poll.
The shape that has become standard across payment processors and SaaS APIs, and that PepoSmart follows, has four parts: an envelope with an id, a type and a timestamp; a data object specific to the event type; headers naming the event and the delivery; and a signature header so you can prove the message came from the sender and was not altered in transit. If you have worked with Stripe's webhooks, the model will feel familiar, and the Standard Webhooks specification documents the same conventions vendor-neutrally.
Which events are worth subscribing to
PepoSmart exposes eight subscribable events, listed on the product-facts page. They fall into three groups.
| Event | Sent when | What you usually do with it |
|---|---|---|
booking.created | A booking is confirmed | Create a record in your system, provision access, notify a channel |
booking.cancelled | An invitee or host cancels | Release resources, update a record, trigger a win-back |
booking.rescheduled | A booking moves to a new time | Update the record; the payload carries a rescheduledFrom block |
booking.paid | Payment succeeds on a paid event | Reconcile with accounting, issue a receipt in your own system |
meeting-notes.completed | The AI notetaker finishes processing | Store the summary and transcript, kick off internal review |
action-item.created | An action item is extracted from a meeting | Create tasks in a system PepoSmart does not integrate with natively |
followup-draft.generated | A follow-up email draft is ready for review | Notify the rep, route to a review queue |
followup-draft.sent | A reviewed draft is sent | Log the outreach in your own timeline |
Two details matter for the booking group. A reschedule produces two messages: booking.created for the new booking, then booking.rescheduled for that same booking with rescheduledFrom pointing at the original, so handle both without double-counting. And cancellations fire whether the invitee cancels from their email link or a host cancels from the Meetings page, so you do not need to watch two paths.
Each endpoint subscribes to every event or a chosen subset, and can be scoped to a single event type, for example only bookings of your "Enterprise demo" event. That scoping is often simpler than filtering in code.
The request, exactly
Every delivery is a POST with a JSON body. The envelope never changes; only data does.
POST https://example.com/webhooks/peposmart
Content-Type: application/json
User-Agent: PepoSmart-Webhooks/1.0
X-PepoSmart-Event: booking.created
X-PepoSmart-Delivery: 6f1c3c1e-2c2b-4a4e-9d3a-0f6c1d5b1a2e
X-PepoSmart-Signature: t=1756720800,v1=9f2c8a7d...e41b
{
"id": "6f1c3c1e-2c2b-4a4e-9d3a-0f6c1d5b1a2e",
"type": "booking.created",
"createdAt": "2026-09-01T10:00:00.000Z",
"data": { ... }
}
Three points to design around:
idis the delivery id. It stays the same across retries, so store it and ignore a message you have already processed.typeis also sent as theX-PepoSmart-Eventheader, so you can route before parsing the body.- You must respond with any 2xx status within 10 seconds. Do the real work after responding if it is slow.
The booking payload
Every booking.* event carries the same object. The field names match the Zapier and Make payloads, which means an automation written for one can be moved to the other without renaming anything.
{
"bookingId": "abc123",
"eventId": "event456",
"status": "confirmed",
"startTime": "2026-09-20T14:00:00.000Z",
"endTime": "2026-09-20T14:30:00.000Z",
"timezone": "America/New_York",
"location": "google-meet",
"meetingUrl": "https://meet.google.com/abc-defg-hij",
"attendee": { "name": "...", "email": "..." },
"additionalGuests": [],
"customAnswers": { "...": "..." },
"eventDetails": { "...": "..." },
"host": { "...": "..." },
"payment": null,
"cancellation": null,
"createdAt": "2026-09-01T10:00:00.000Z"
}
booking.cancelled fills cancellation and sets status to cancelled. booking.rescheduled adds rescheduledFrom with the original booking's details. booking.paid fills payment, with the amount in the smallest currency unit, so 4900 means 49.00 in a two-decimal currency.
The meeting-notes and follow-up events use the same data shapes as the Zapier integration. One difference: transcripts longer than 100,000 characters are cut, and the payload carries transcriptTruncated: true, so check that flag before assuming you have the whole conversation. The full reference is in the webhooks documentation.
Verifying the signature
The signature header has the form t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of the string <t>.<raw request body>, keyed with the endpoint's secret. HMAC is a keyed hash: only a holder of the secret can produce a valid value, and any change to the body changes it.
Verify in four steps:
- Read the raw body exactly as received. Do not parse and re-serialise it first.
- Recompute the HMAC over
t + "." + bodywith your secret. - Compare with
v1using a constant-time comparison. - Reject timestamps older than a few minutes to block replays.
Node.js
import crypto from "node:crypto";
export function verifyPepoSmartWebhook(secret, signatureHeader, rawBody) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("="))
);
const payload = parts.t + "." + rawBody;
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 || "", "hex");
return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}
If you use Express, register the route with a raw body parser (express.raw({ type: "application/json" })) so req.body is the bytes, not a parsed object. That single line is the difference between a handler that works and one that fails only in production.
Python
import hashlib, hmac, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
payload = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts.get("v1", ""))
Treat the secret like a password. Keep it out of client-side code and out of version control, and rotate it from the endpoint card if it leaks; deliveries after a rotation are signed with the new secret immediately.
Designing the handler
Acknowledge first, work second. Validate the signature, write the delivery to a queue or a table, return 200, and process from there. A handler that calls a CRM API, sends an email and updates three tables before responding will time out under load and be retried, which multiplies the load.
Make it idempotent. Delivery is at-least-once. Store processed delivery ids with a unique constraint and let a duplicate insert fail harmlessly. This also protects you when you resend a delivery from the dashboard while debugging.
Route on the header. X-PepoSmart-Event tells you the type before you parse the body. A small switch on that header keeps handlers focused.
Log the raw body on failure. When a handler throws, the raw request is the only evidence of what was sent. The sender's delivery log helps, but yours is faster to reach.
Return non-2xx only when you want a retry. A permanently unprocessable message, such as an event type you do not handle, should get a 200 and be ignored, or the retries will continue until they exhaust and the endpoint's failure counter climbs.
Retries, failures and the delivery log
PepoSmart's delivery behaviour, from the webhooks documentation:
- A non-2xx response, a redirect, or no response within 10 seconds counts as a failure. Redirects are never followed.
- Failures are retried with exponential backoff, up to 7 attempts.
- After the last attempt the delivery is marked failed and can be resent from the dashboard once it has settled.
- An endpoint whose deliveries fail 25 attempts in a row, about four events' worth of retries, is switched off automatically. Fix the receiver and re-enable it with the toggle. Test sends never count toward this.
- Every delivery is listed for 30 days with its attempt count, the payload sent and the first 1 KB of your response.
Design implication: 7 attempts with backoff spans a meaningful window, so a short outage will be papered over by retries, but an endpoint that is misconfigured for a day will be disabled and needs a human to turn it back on. Alert on the disabled state, not just on individual failures.
A short security checklist
Webhook endpoints are public URLs that accept POSTs from the internet, so treat them with the same care as any other unauthenticated route.
- Verify before you parse. Signature first, JSON second. A malformed body should never reach your business logic unsigned.
- Reject stale timestamps. Five minutes is a common tolerance. Without it, a captured request can be replayed indefinitely.
- Store the secret like a password. Environment variables or a secrets manager, never source control, never the browser.
- Rotate on suspicion. A rotated secret takes effect immediately for new deliveries, so there is no reason to delay.
- Minimise what you log. Booking payloads contain names, emails and intake answers. Log the delivery id and event type by default and the full body only on failure, with the same retention rules as the rest of your personal data.
- Return 200 for events you ignore. Otherwise the sender keeps retrying and your endpoint's failure counter climbs toward the automatic disable.
- Use a dedicated path with no other behaviour. A webhook route that also serves a page or accepts form posts widens what an attacker can probe.
Testing without deploying anything
Two tools make the first hour painless. Paste a URL from webhook.site as the endpoint and click Send test to see the exact headers and body. Then run your real handler locally behind ngrok, register the tunnel URL, and iterate. Test sends are rate-limited per account, 20 per minute, and resends at 60 per minute, which is plenty for development.
Endpoint rules to know before you build
- Up to 10 endpoints per account, each with its own secret, scope and event list.
- URLs must be
https://on a public hostname. Localhost, private network addresses, IPv6 literals and PepoSmart's own domains are refused. This is a server-side request forgery guard, and it is why ngrok exists in the previous section. - Endpoints keep their configuration if you downgrade below Professional, but deliveries pause until the plan is restored.
- Team events are delivered to the event owner's endpoints, not the assigned host's.
Automations teams actually build
The booking event is the obvious one, but the value tends to come from the events after it.
Provision on booking. A booking.created for a paid consultation creates the client in your practice-management system and sends an intake form from your own domain. If you sell trials, it creates the trial account before the onboarding call so the call starts with a working login.
Reconcile on payment. booking.paid carries the amount and currency. Post it to your accounting system so the ledger matches the payment provider without a monthly export.
Text-message reminders. PepoSmart does not send SMS. A booking.created handler that schedules a text through an SMS provider for 24 hours and 1 hour before startTime adds the channel, and a booking.cancelled or booking.rescheduled handler cancels or moves it. The no-show reduction guide covers the timing.
Enrich the CRM you don't integrate natively. meeting-notes.completed and action-item.created carry the summary, the transcript and the extracted tasks. If your CRM is not one of the ten PepoSmart connects to directly, this is how you write the same data to it. The CRM sync guide describes what native integrations write, which is a useful template for your own handler.
Route follow-ups for review. followup-draft.generated can post to a team channel so a manager sees drafts before reps send them, and followup-draft.sent can stamp the outreach on an internal timeline.
Release resources on cancellation. Rooms, loaner equipment, interpreter bookings: anything reserved on booking.created should be released on booking.cancelled.
Webhooks, Zapier or the REST API?
| Need | Use |
|---|---|
| Push events into a system you build or host | Webhooks |
| Connect to other apps without code | Zapier or Make |
| Read availability or create bookings from your backend | REST API (Business plan and above) |
| Show the booking page inside your product | Embedding |
The three are complementary. A common stack is a webhook for the systems you own, Zapier for the long tail of SaaS tools, and the embed for the booking UI, which the embedding guide covers. Availability and plan details for each are on the pricing page.
Summary
Scheduling webhooks let your systems react to bookings, payments and meeting notes the moment they happen. Verify every request over the raw body with HMAC-SHA256, acknowledge quickly and process asynchronously, key your handler on the delivery id, and alert on a disabled endpoint rather than on individual failures. PepoSmart's webhooks are available on the Professional plan and above, with eight events, signed deliveries, 7-attempt retries and a 30-day delivery log. The full reference, including the meeting-notes payload shapes, is in the developer documentation.