Meetily
Agent APISample workflows

Receive and verify webhooks

Register a webhook, verify the HMAC signature on delivery, and fetch the summary that triggered it.

Instead of polling for a summary to finish, register a webhook. Meetily pings your server when the event fires, your server verifies the delivery is genuine, then fetches the actual summary over the API.

The webhook payload itself is thin - it just tells you which event happened and which meeting it's about. You still call the API to get the content.

Register the webhook

curl -s -X POST http://127.0.0.1:8420/v1/webhooks \
  -H "Authorization: Bearer $MEETILY_PRO_TOKEN" \
  -H "content-type: application/json" \
  -d '{
    "url": "https://your-public-url/hook",
    "events": ["summary.completed"]
  }'

The response includes the signing secret. It's returned once, at creation time. Store it somewhere durable - you can't fetch it again later, and you'll need it to verify deliveries.

The target URL must be public

The gateway rejects 127.0.0.1 and other private/loopback addresses with 400 by default - this is SSRF protection, not a bug. https://your-public-url/hook above is a placeholder for a real public endpoint. To receive webhooks on your laptop, run a public tunnel (ngrok, Cloudflare Tunnel, or similar) and register the tunnel's https:// URL, not localhost.

Verify the signature

Every delivery carries two headers:

  • X-Meetily-Signature, formatted as sha256=<hex>
  • X-Meetily-Timestamp, a Unix timestamp in seconds

The expected signature is an HMAC-SHA256 over the string timestamp + "." + body, keyed with your secret, rendered as lowercase hex. body is the raw request bytes exactly as received - don't re-serialize JSON before hashing, since re-serialization can reorder keys or change whitespace and break the signature.

Compare the computed signature to the header using a constant-time comparison, not == or .equals(). A timing-sensitive comparison can leak the correct signature one byte at a time.

Receiver

Each example below starts a small HTTP server, reads the raw body, computes the expected signature, and rejects the request with 401 if it doesn't match. The secret comes from an environment variable, never hardcoded.

Standard library only - http.server, hmac, hashlib.

import hmac
import hashlib
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["MEETILY_WEBHOOK_SECRET"].encode()

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)

        timestamp = self.headers.get("X-Meetily-Timestamp", "")
        signature = self.headers.get("X-Meetily-Signature", "")

        signed_payload = timestamp.encode() + b"." + body
        expected = "sha256=" + hmac.new(SECRET, signed_payload, hashlib.sha256).hexdigest()

        if not hmac.compare_digest(expected, signature):
            self.send_response(401)
            self.end_headers()
            return

        print("verified event:", body.decode())
        self.send_response(200)
        self.end_headers()

if __name__ == "__main__":
    HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

Fetch the summary

Once the signature checks out, parse the event body to get the meeting id, then fetch the summary content over the API. The webhook itself never carries the summary text.

curl -s "http://127.0.0.1:8420/v1/meetings/MEETING_ID/summary" -H "Authorization: Bearer $MEETILY_PRO_TOKEN"

From there, do whatever you need with it - post it to Slack, write it to a database, kick off another job. The webhook's only job was to tell you it was time to look.

Last updated on

On this page