Meeting webhooks

Meeting webhooks for bookings, calls and AI summaries

Meeto webhooks send a signed HTTPS POST to your server when a booking is created, rescheduled or cancelled, when the booked call ends and when its AI summary is ready. Personal webhooks come with Pro or a paid Team seat, up to 10 endpoints; a Team workspace adds up to 5 shared endpoints. Failed deliveries are retried for about 33 hours, 45 for workspaces, with a delivery log and manual redelivery.

Which events can I receive?

Five events cover a booking from start to finish. Pick any of them per endpoint; each delivery names its event in the Meeto-Event header and the event field.

Webhook events
EventSent when
booking.createdA guest books a time on your booking page
booking.rescheduledThe booking moves; carries previousBookingUid and previousStartsAt
booking.cancelledThe host or guest cancels; carries cancelledBy (host or guest)
meeting.endedA call session in the booked room ends; one booking can have several sessions
recap.readyThe AI summary of the booked call is ready, with links to finished recordings

Subscribe at least one endpoint to booking.created. Later events of a booking are sent only if its creation went out to some endpoint. They go to the endpoints that received booking.created; when none of your endpoints holds that delivery, for example after the log was cleaned up, they go to every endpoint subscribed to the event. A booking made before you added any endpoint stays silent, also when it is cancelled, ends or gets a summary. Summaries need a host on Pro or Team; see AI meeting summaries.

What does a webhook payload look like?

Every event has the same shape, so one parser handles all five. Times are UTC in RFC 3339; timezone is the host's working time zone for display.

POST /hooks/meeto HTTP/1.1
Content-Type: application/json
User-Agent: Meeto-Webhooks/1.0
Meeto-Event: booking.created
Meeto-Webhook-Id: <endpoint id>
Meeto-Delivery-Id: 1421
Meeto-Signature: t=1805702400,v1=5f0c…e9a1
{
  "event": "booking.created",
  "createdAt": "2027-03-20T09:14:03Z",
  "occurredAt": "2027-03-20T09:14:03Z",
  "bookingUid": "bk_4f1c9a2b7e3d8054a1cb62d7f9",
  "ref": "deal-4821",
  "tracking": { "ref": "deal-4821", "utm_source": "newsletter" },
  "workspace": null,
  "host": { "userId": "<user id>", "handle": "anna", "name": "Anna Example", "email": "anna@example.com" },
  "guest": { "name": "Sam Guest", "email": "sam@example.com", "company": "Acme" },
  "eventType": { "id": "<type id>", "title": "Intro call", "slug": "intro", "durationMin": 30 },
  "startsAt": "2027-03-22T10:00:00Z",
  "endsAt": "2027-03-22T10:30:00Z",
  "timezone": "Europe/Berlin",
  "meetingUrl": "https://meeto.me/<room>?k=<room access key>",
  "status": "confirmed",
  "answers": [{ "label": "Company", "answer": "Acme" }]
}
// meeting.ended adds:
"meeting": {
  "sessionId": "<call session id>",
  "startedAt": "2027-03-22T10:01:12Z",
  "endedAt": "2027-03-22T10:34:40Z",
  "durationSec": 2008,
  "peakParticipants": 2
}

// recap.ready adds:
"recap": {
  "url": "https://meeto.me/m/<page token>",
  "title": "Intro call",
  "recordings": [{ "url": "https://meeto.me/api/m/<page token>/file/<id>/video", "durationSec": 1990 }]
}
Fields worth knowing
FieldMeaning
bookingUidbk_ plus 26 hex characters; a reschedule issues a new one and sends the old one as previousBookingUid
ref / trackingYour own markers from the booking link, for example ?ref=deal-4821&utm_source=newsletter; up to 10 pairs, always present ({} when empty)
workspacenull for a personal endpoint; { id, slug } for a workspace endpoint
answers / guest.companyThe booking form answers; a company question is also copied into guest.company
cancelledBy / reason / messageWho cancelled, rebook_requested when the host asked the guest to pick another time, and the note written with the change
recap.url / recordingsThe summary page and finished video files; they open according to the owner's sharing settings
truncatedtrue when the body was cut to fit 64 KiB: answers shortened to 500 characters and guests to the first 20, and if still too large the answers are removed; ids and times are never cut

How do I verify the signature?

Compute HMAC-SHA256 with your endpoint secret (whsec_…) over "<t>.<raw body>" and compare it with v1 from Meeto-Signature in constant time. Use the raw request bytes, not re-serialized JSON, and reject a t more than five minutes away from your clock to block replays.

import crypto from "node:crypto";
import express from "express";

const secret = process.env.MEETO_WEBHOOK_SECRET; // whsec_…

function verifyMeetoSignature(rawBody, header, toleranceSec = 300) {
  const parts = Object.fromEntries(
    (header ?? "").split(",").map((part) => part.split("=", 2)),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest();
  const received = Buffer.from(parts.v1, "hex");
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

// express.raw keeps the exact bytes Meeto signed; do not parse JSON first.
app.post("/hooks/meeto", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyMeetoSignature(req.body, req.get("Meeto-Signature"))) {
    return res.sendStatus(401);
  }
  const deliveryId = req.get("Meeto-Delivery-Id"); // the same on every retry
  const event = JSON.parse(req.body.toString("utf8"));
  console.log(deliveryId, event.event, event.bookingUid);
  res.sendStatus(200); // answer fast, do slow work in your own queue
});

app.listen(3000);
import hashlib, hmac, time

def verify_meeto_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), v1.encode())

Before real traffic, press Send test on the endpoint. It sends a signed {"event":"test","createdAt":"…"} with Meeto-Event: test and a Meeto-Delivery-Id starting with test-, and shows your server's answer at once. The button allows five tests, then one a minute.

How do retries and ordering work?

Answer with any 2xx to accept a delivery. Meeto waits 10 seconds to connect and 15 seconds for your answer, does not follow redirects (a 3xx counts as a failure) and retries everything else on a fixed schedule.

Retry schedules
EndpointSchedule
Personal1 min, 5 min, 30 min, 2 h, 6 h, 24 h: 7 attempts over about 33 hours
Workspace1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h: 8 attempts over about 45 hours
422 or 410 answerStops at once: the delivery is parked as failed and can only be redelivered by hand

Retries send the exact same body and the same Meeto-Delivery-Id. Deliveries to one endpoint leave in order, but a delivery that has failed three times stops holding back the ones behind it, so order your own records by occurredAt. Reply 2xx to event names you do not know; new ones may be added.

Delivery log and redelivery

See every attempt

The log on the webhooks screen keeps each delivery for about five weeks: event, status (pending, delivered, failed), attempts, HTTP code and the first 512 bytes of your answer.

Redeliver by hand

A failed delivery has a retry button. It queues the same body once more as a new delivery with a new id; the same row cannot be redelivered twice.

Pause without losing events

Switching an endpoint off pauses it. Events keep queuing and go out when you switch it back on; deliveries still waiting after 30 days are marked failed.

Endpoint URLs must be public https addresses without credentials in them; each endpoint listens to at least one event. A personal secret is shown once, when you create the endpoint. New secret replaces it immediately, and the old one stops verifying.

Personal and workspace webhooks compared

A Team workspace can send events to its own endpoints, managed by the owner and admins on the team webhooks screen. Every paid seat also includes personal webhooks, because a seat gives full Pro.

Personal and workspace endpoints
SettingPersonal / Workspace
PlanPro ($6.99/month) or a paid Team seat / Team workspace ($11.99 per seat per month), after its first payment
EndpointsUp to 10 per account / up to 5 per workspace
Which bookingsAll of your own bookings / bookings of workspace meeting types and team round-robin links, for every member; members' personal types never reach it
SecretShown once / can be revealed again by owners and admins
RotationOld secret stops at once / the new secret signs at once; the previous one stays visible for 7 days
Test eventtest, not logged / test.ping, written to the log
Failing endpointKeeps retrying per delivery / switched off after 3 days of failures, owner and admins get an email

After rotating a workspace secret, put the new one into your receiver promptly: deliveries signed with it fail verification on a receiver that knows only the old secret, and they come back on the retry schedule above. A workspace that stops paying pauses its endpoints; events wait in the queue, up to 30 days, and go out after payment. For the call itself, see how Meeto handles security.

Frequently asked questions

Do webhooks fire for meetings created through the REST API?

No. Every event belongs to a booking made on a Meeto booking page, personal or team round-robin. For meetings your backend creates through /api/v1, read the meeting, its notes and its recordings from the API instead.

Can I receive webhooks on the Free plan?

No. Creating, editing, testing and redelivering answer 403 pro_required, and new events of a Free account are not queued. One exception: the cancellation of a booking an endpoint already received still goes out, so you are not left with a meeting that is off. Endpoints made before a downgrade stay listed with their log and can be deleted.

Why did the same event arrive twice?

Delivery is at least once. Automatic retries repeat the Meeto-Delivery-Id, so store it and skip ids you have seen. A manual redelivery gets a new id, so also make your handler safe to run twice for the same bookingUid and event.

Can I point a webhook at localhost while I develop?

No. Production deliveries go only to public https addresses; localhost, private and link-local addresses are refused when you save the endpoint and again when Meeto connects. Expose your local receiver through an https tunnel for testing.

Put it to work

Open your Meeto account to set it up, or explore the working product first.