Webhooks
Webhooks push a signed JSON message to a URL you choose the moment something happens in PepoSmart — a booking is created, cancelled, rescheduled or paid, or the AI notetaker finishes a meeting. They are the raw form of the Zapier and Make integrations: use them whenever you own the receiving system and want retries, signatures and a delivery log.
Availability
Webhooks are included on the Professional plan and above (the same tier as Zapier and Make). Each account can register up to 10 endpoints. Prefer to react to events with a no-code tool? Use Zapier or Make instead. Prefer to pull data on demand? See the REST API.
Registering an endpoint
- In the app, open Integrations → Webhooks and click Add endpoint
- Enter your HTTPS URL and an optional description
- Choose a scope: every event type on your account, or only bookings of one event type
- Tick the events you want, or keep Subscribe to all events
- Copy the signing secret shown after saving (it can be revealed or rotated later from the endpoint card) and use it to verify every request
- Click Send test to deliver a sample event and see the response your endpoint returned
Try it without writing code
Paste a URL from webhook.site as the endpoint, click Send test, and inspect the exact headers and body PepoSmart sends. Use ngrok to receive events on a server running on your laptop.
Events
| Event | Sent when | Group |
|---|---|---|
booking.created | Someone books a meeting (also sent for the new booking of a reschedule) | Bookings |
booking.cancelled | A booking is cancelled by the invitee or a host | Bookings |
booking.rescheduled | A booking moves to a new time — sent for the replacement booking, with the original attached | Bookings |
booking.paid | A paid event's Stripe or PayPal payment is confirmed and the booking is created | Payments |
meeting-notes.completed | The AI transcript, summary, analysis and action items are ready | Meeting notes |
action-item.created | AI extracts an action item from a meeting | Meeting notes |
followup-draft.generated | AI drafts a follow-up email after a meeting | Follow-ups |
followup-draft.sent | A follow-up email draft is sent to the invitee | Follow-ups |
A reschedule produces two messages: booking.created for the new booking, then booking.rescheduled for that same booking with a rescheduledFrom block pointing at the original. Cancellations fire whether the invitee cancels from their email link or a host cancels from the Meetings page. Team events are delivered to the event owner's endpoints.
The request
Every delivery is a POST with a JSON body. The envelope is the same for every event; only data changes.
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": { … }
}idis the delivery id. It stays the same across retries — store it and ignore a message you have already processed.typeis the event name; it is also sent as theX-PepoSmart-Eventheader so you can route before parsing the body.- Respond with any 2xx status within 10 seconds. Do the real work after responding if it is slow.
Booking payload (data)
Every booking.* event carries the same object. Field names match the Zapier and Make payloads (bookingId, attendee, eventDetails…), plus host, payment, cancellation and, on a reschedule, rescheduledFrom.
{
"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": "Jamie Rivera",
"firstName": "Jamie",
"lastName": "Rivera",
"email": "[email protected]",
"phone": "+1 555 010 0100",
"timezone": "America/New_York"
},
"additionalGuests": [],
"customAnswers": { "What would you like to discuss?": "Onboarding" },
"eventDetails": { "id": "event456", "title": "Discovery call", "description": null, "duration": 30, "type": "one-on-one" },
"host": { "id": "user789", "name": "Jane Host", "email": "[email protected]" },
"payment": null,
"cancellation": null,
"createdAt": "2026-09-01T10:00:00.000Z"
}booking.cancelledfillscancellation:{ "cancelledAt", "cancelledBy", "reason" }and setsstatustocancelled.booking.rescheduledaddsrescheduledFrom:{ "bookingId", "startTime", "endTime", "timezone" }of the original booking.booking.paidfillspayment:{ "status": "paid", "amount": 5000, "currency": "usd", "sessionId": "…" }(amount in the smallest currency unit).
Meeting-notes and follow-up payloads
meeting-notes.completed, action-item.created, followup-draft.generated and followup-draft.sent use the same data shapes as the Zapier integration — see the Zapier payload reference. One difference: transcripts longer than 100,000 characters are cut and the payload carries "transcriptTruncated": true.
Verifying the signature
Each request carries X-PepoSmart-Signature: t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256 of the string <t>.<raw request body>, keyed with the endpoint's secret. Verify it before trusting a message:
- Read the raw body exactly as received — do not re-serialise parsed JSON
- 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 expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"));
}# Python
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts["v1"])Keep the secret private
Anyone holding the secret can forge events. Store it like a password, never in client-side code, and rotate it from the endpoint card if it leaks — deliveries after a rotation are signed with the new secret immediately.
Retries, failures and the delivery log
- A non-2xx response, a redirect, or no response within 10 seconds counts as a failure and is retried with exponential backoff, up to 7 attempts. Redirects are never followed.
- After the last attempt the delivery is marked Failed. You can resend it from Recent deliveries 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, then re-enable it with the toggle; test sends never count toward this.
- Recent deliveries lists every delivery for 30 days with its attempt count, the payload sent and the first 1 KB of your response. Click a row for details.
Design for at-least-once delivery
A retry can arrive after your endpoint processed the first attempt but failed to answer in time. Treat the delivery id as an idempotency key and make your handler safe to run twice.
Limits and rules
- Up to 10 endpoints per account; each has its own secret, scope and event list
- Endpoint URLs must be
https://on a public hostname — localhost, private network addresses, IPv6 literals and PepoSmart's own domains are refused - Test sends and resends are rate-limited per account (20 and 60 per minute)
- Endpoints keep their configuration if you downgrade below Professional, but deliveries pause until the plan is restored