Exposure note: The webhook endpoints (
/webhooks/zoom and /webhooks/microsoft) are the only MeetCost surfaces that must be reachable from the public internet. Everything else can sit behind your internal network. Both webhook endpoints verify signatures before taking any action.The core security property
A webhook body supplies only a meeting ID. MeetCost uses that ID to fetch attendance from the provider’s API using its own pre-configured service credentials. No cost figure, attendance count, or duration value is ever read from the webhook body itself. This means a forged or replayed webhook results in exactly one authenticated read against your own tenant — the same read that would have happened legitimately. There is no injection vector, and there is nothing in the payload that could inflate a cost figure.POST /webhooks/zoom
Receives Zoommeeting.ended events and handles the URL-validation handshake that Zoom sends when you first configure a webhook subscription. This endpoint is called by Zoom’s servers, not by your users. No Authorization header is required or expected.
Required configuration: MEETCOST_ZOOM_WEBHOOK_SECRET
Signature verification
Zoom signs every delivery with an HMAC-SHA256 signature over the stringv0:<timestamp>:<body>. The result is sent in the x-zm-signature header as v0=<hex-digest>, and the timestamp is in x-zm-request-timestamp.
MeetCost verifies this signature before doing anything else. A request with a missing or mismatched signature receives 401 Unauthorized. A missing MEETCOST_ZOOM_WEBHOOK_SECRET returns 503 Service Unavailable.
URL-validation handshake
When you register a new webhook endpoint in the Zoom marketplace, Zoom sends a validation request withevent: "endpoint.url_validation". MeetCost handles this automatically by computing the expected HMAC over the plainToken and returning:
meeting.ended events
When Zoom sendsevent: "meeting.ended", MeetCost extracts the meeting UUID from payload.object.uuid (falling back to payload.object.id), queues an attendance-collection task in the background, and immediately returns:
MEETCOST_ZOOM_CLIENT_ID and MEETCOST_ZOOM_CLIENT_SECRET, ingests the meeting, computes costs, and dispatches the post-meeting summary notification.
Any other event type is acknowledged and ignored:
Headers Zoom sends
Example — forged webhook impact
An attacker who sends a valid-looking body but without the correct HMAC receives401. If somehow a request passed signature verification with a fake meeting ID that does not exist in your Zoom account, the attendance fetch would return an empty result or a 404 from Zoom’s API, and MeetCost would record nothing. No cost is computed from the webhook body itself.
POST /webhooks/microsoft
Receives Microsoft GraphcallRecords change notifications, and handles the subscription validation challenge. Called by Microsoft’s servers; no Authorization header is expected.
Required configuration: MEETCOST_GRAPH_WEBHOOK_CLIENT_STATE
Subscription validation
When you create a Graph change-notification subscription, Microsoft sends a POST request to your endpoint with avalidationToken query parameter. MeetCost echoes this value back as plain text with Content-Type: text/plain, within the 10-second window Graph requires.
Change notifications
ForcallRecords notifications, Graph sends a POST with a JSON body containing a value array of notification objects. MeetCost validates each notification’s clientState against MEETCOST_GRAPH_WEBHOOK_CLIENT_STATE using a constant-time comparison. Notifications with a mismatched clientState are silently discarded (as Graph’s documentation requires).
For accepted notifications, MeetCost extracts resourceData.id and queues an attendance-collection background task. The response is always 202 Accepted.
clientState validation
clientState field). MeetCost checks it on every notification. A notification without the correct state is dropped without logging an error (to avoid log spam from legitimate Graph retries during subscription rotation).
Why forged notifications are harmless
A notification body contains only aresourceData.id. If a forged id is sent, MeetCost will make one authenticated call to https://graph.microsoft.com/v1.0/communications/callRecords/<id> using your tenant’s credentials. If the record does not exist in your tenant, Graph returns 404 and no data is ingested.
POST /webhooks/google/poll
Google Meet does not publish end-of-meeting webhooks. Instead, attendance data becomes available in the Google Workspace Reports API some time after the meeting ends. MeetCost exposes this authenticated trigger endpoint so the sweep can be driven by your own scheduler — a Kubernetes CronJob, a cloud scheduler, or the MeetCost CLI — rather than an in-process timer that might silently fail. This is not a webhook. It is a normal authenticated API endpoint that happens to sit on the/webhooks/ path for operational grouping.
Requires: Admin role or a service token.
Query parameters
integer
default:"24"
Look back this many hours in the Reports API sweep. Use a window that slightly overlaps your cron interval to avoid gaps. The recommended cron invocation uses
--hours 3 on a 2-hour schedule.