Guide to Jamie Webhooks: Features and Best Practices
Jamie webhooks deliver your meeting notes to your own systems as soon as each meeting is processed. Every webhook carries the summary, the full transcript, the action items and the attendees, so teams use them to file notes in a CRM, create tickets from tasks, archive transcripts and trigger follow-up workflows without copying anything out of Jamie, the AI meeting note-taker.
This guide covers how Jamie webhooks are created and authenticated, how to verify the signature, the meeting.completed payload, Jamie's retry schedule, and the practices that make a Jamie receiver reliable.
What are Jamie webhooks?
A Jamie webhook sends one event, meeting.completed, to an HTTPS URL as a JSON POST. There are two kinds:
- A personal webhook fires only for its creator's meetings. Any workspace member can create one, up to 3 per user.
- A workspace webhook fires for every meeting in the workspace. Only admins can create one, up to 5 per workspace.
When you create a webhook, you choose how it authenticates, and you can't see the secret again afterwards:
- API Key, which Jamie labels "Recommended", sends a static key (
sk_...) inx-jamie-api-key, or in a header you name. It proves who sent the request but says nothing about the body. - Signature Verification, which Jamie labels "Advanced", signs each request with HMAC-SHA256 and sends the result in
x-jamie-signature. It "verifies both authenticity and payload integrity".
Choose Signature Verification. Besides being the stronger option, it is the only one Hookdeck's Jamie source can verify.
Jamie webhook features
| Feature | Details |
|---|---|
| Configuration | Settings > Integrations > Webhooks > Create Webhook |
| Availability | Paid plans. Jamie's docs say "Plus or higher" in one place and list Pro, Team and Enterprise in another |
| Scopes | Personal (3 per user) or workspace (5 per workspace, admins only) |
| Events | meeting.completed |
| Method and format | POST, Content-Type: application/json |
| Signature header | x-jamie-signature: t=<timestamp>,v0=<signature> |
| Scheme | HMAC-SHA256, lowercase hex |
| Signed content | {t}.{raw body} |
| Secret | Shown once at creation. Regenerate it from the webhook settings if lost |
| Other headers | user-agent: Jamie-Webhooks/1.0, jamie-event, jamie-delivery |
| Replay window | Jamie recommends rejecting timestamps older than 5 minutes |
| Response window | 2xx within 30 seconds |
| Retries | 5 attempts in all: immediately, then after 10 s, 60 s, 10 min and 1 h |
| After the last failure | Dead-lettered, and the owner or workspace admins are emailed (at most once an hour per endpoint) |
| Testing | A Test button sends a mock payload through the normal delivery path |
| Source IPs | Not published |
Common events
Jamie currently sends one event, meeting.completed, when a meeting "has been fully processed". Here is Jamie's example, trimmed:
{
"metadata": {
"id": "7893456789012345678",
"event": "meeting.completed",
"created": 1701234567
},
"data": {
"title": "Q4 Planning Meeting",
"startTime": "2024-11-27T14:00:00.000Z",
"endTime": "2024-11-27T15:00:00.000Z",
"user": { "id": "user-456", "email": "sarah.johnson@example.com" },
"summary": {
"markdown": "# Meeting Summary\n\n## Key Discussion Points\n...",
"html": "<h1>Meeting Summary</h1>...",
"short": "Q4 product planning meeting covering roadmap, mobile redesign, and user research budget allocation."
},
"transcript": [
{ "speakerId": "1", "speakerName": "Sarah Johnson", "text": "Hi everyone! Thanks for joining today's meeting." }
],
"participants": [
{ "id": "1", "name": "Sarah Johnson", "email": "sarah.johnson@example.com" }
],
"event": {
"id": "calendar-event-123",
"externalId": "AAMkAGI1AAAt9AHjAAA=",
"title": "Q4 Planning Meeting",
"scheduledTime": "2024-11-27T14:00:00.000Z",
"endTime": "2024-11-27T15:00:00.000Z",
"attendees": [
{ "name": "Sarah Johnson", "email": "sarah.johnson@example.com", "responseStatus": "accepted", "organizer": true }
]
},
"tasks": [
{ "content": "Schedule follow-up meeting for next week", "completed": false, "assignee": null }
],
"tags": [{ "name": "Planning", "color": "#50C878" }]
}
}
metadata.ididentifies the delivery and matches thejamie-deliveryheader.metadata.eventmatchesjamie-event.metadata.createdis in Unix seconds. The meeting times are ISO 8601 strings.participantsare people who spoke, detected from the transcript.data.event.attendeesare people invited to the calendar event, who may not have attended. Don't treat them as the same list.data.event.externalIdis the calendar provider's event ID, from Microsoft Graph or Google Calendar, so you can match the meeting to your own calendar data. It anddata.event.idcan benull, andattendeesis empty when no calendar event is linked.endTime, participant emails and task assignees can all benull.
Bodies can be large: a long meeting's transcript is in every delivery.
Setting up Jamie webhooks
Have your endpoint ready to receive, because the secret is shown only once.
- In Jamie, go to Settings > Integrations > Webhooks and click Create Webhook.
- Enter your HTTPS URL and, optionally, a Description.
- Under Events, select
meeting.completed. - Choose Personal webhook or Workspace webhook.
- Choose Signature Verification as the authentication method.
- Click Create, then copy the signing secret straight away, for example into
JAMIE_WEBHOOK_SECRET. - Click Test on the webhook to send a mock payload.
To route through Hookdeck, create a source with the Jamie type, use its URL as the webhook URL, and paste the signing secret into Webhook Signing Secret. API key mode isn't supported: a webhook created with API Key sends no signature, so every request would fail verification. If you already have an API key webhook, create a new one with Signature Verification.
Securing Jamie webhooks
Split x-jamie-signature on commas and each part on its first =, take t and v0, compute an HMAC-SHA256 of {t}.{raw body} with the signing secret as a UTF-8 string, hex-encode it, and compare it with v0 in constant time.
Jamie's docs recommend rejecting timestamps older than 5 minutes when Jamie calls you directly. The check is optional in the code below and off by default, as it is in the Jamie skill, where JAMIE_ defaults to 0 and you set 300 to follow Jamie's recommendation. Behind Hookdeck, leave it at 0: Hookdeck's retries and replays resend the original t, and so do Jamie's own retries, the last of which comes more than an hour after the first attempt.
const crypto = require('crypto');
const express = require('express');
function parseJamieHeader(header) {
let t;
const v0 = [];
for (const part of header.split(',')) {
const i = part.indexOf('='); // split on the first '=' only
if (i <= 0) continue;
const key = part.slice(0, i).trim();
const value = part.slice(i + 1).trim();
if (key === 't') t = value;
else if (key === 'v0') v0.push(value);
}
return { t, v0 };
}
function verifyJamieSignature(rawBody, signatureHeader, secret, toleranceSeconds = 0) {
if (!signatureHeader || !secret) return false;
const { t, v0 } = parseJamieHeader(signatureHeader);
if (!t || !/^\d+$/.test(t) || v0.length === 0) return false;
// Optional replay check, off by default. t is in seconds.
if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) {
return false;
}
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
const computed = Buffer.from(expected, 'utf8');
return v0.some((sig) => {
const received = Buffer.from(sig.toLowerCase(), 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
});
}
const app = express();
app.post('/webhooks/jamie', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.JAMIE_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
const tolerance = Number(process.env.JAMIE_TIMESTAMP_TOLERANCE_SECONDS || 0);
if (!verifyJamieSignature(req.body, req.get('x-jamie-signature'), secret, tolerance)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on event.metadata.id, queue the meeting, then respond.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import time
def verify_jamie_signature(raw_body: bytes, signature_header, secret: str, tolerance_seconds: int = 0) -> bool:
if not signature_header or not secret:
return False
t, v0 = None, []
for part in signature_header.split(","):
key, sep, value = part.partition("=") # first '=' only
if not sep:
continue
if key.strip() == "t":
t = value.strip()
elif key.strip() == "v0":
v0.append(value.strip().lower())
if not t or not (t.isascii() and t.isdigit()) or not v0:
return False
# Optional replay check, off by default. t is in seconds.
if tolerance_seconds > 0 and abs(time.time() - int(t)) > tolerance_seconds:
return False
expected = hmac.new(secret.encode("utf-8"), t.encode("utf-8") + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(sig.encode("utf-8"), expected.encode("utf-8")) for sig in v0)
# In FastAPI: raw_body = await request.body()
# verify_jamie_signature(raw_body, request.headers.get("x-jamie-signature"),
# os.environ["JAMIE_WEBHOOK_SECRET"])
Jamie's own Node example verifies JSON.stringify(req.body) after express.json() has parsed the request. Re-serialising the JSON can change the bytes, for example the escaping of characters in a transcript, and the signature then fails. Verify the raw bytes, as above.
To check your verifier, use the signing secret jamie-guide-test-signing-secret, a header timestamp of 1761400000 and this exact body:
{"metadata":{"id":"7893456789012345678","event":"meeting.completed","created":1761400000},"data":{"title":"Q4 Planning Meeting"}}
The v0 value should be:
1bbc7a192fe41d7d65d98642b28921f59cacfc454aba933886d8d257687fd567
Make Jamie webhooks production-ready. Hookdeck Event Gateway verifies the
x-jamie-signatureheader on every meeting, answers Jamie immediately, and keeps each transcript durably until your service has processed it.
Jamie webhook limitations and pain points
The default mode isn't signed
The Problem: Your webhook authenticates the sender, but a modified body still passes, and Hookdeck's Jamie source rejects every request.
Why It Happens: Jamie recommends API Key mode, and it is the default. It sends a static key and no signature. The choice is made when you create the webhook.
Workarounds:
- Create webhooks with Signature Verification.
- To switch an existing webhook, create a new one with Signature Verification, move your receiver over, then delete the old one.
How Hookdeck Can Help: Hookdeck's Jamie source verifies x-jamie-signature. A request without a valid signature still gets a 200, but Hookdeck records it as failed verification and never forwards it, so an API key webhook pointed at it shows up straight away in the request log.
The secret is shown once
The Problem: You lose the signing secret and can't look it up.
Why It Happens: Jamie displays it only in the creation dialog.
Workarounds:
- Copy it into a secrets manager before you close the dialog.
- If you lose it, regenerate it from the webhook settings and update every receiver.
How Hookdeck Can Help: With Hookdeck in front, the secret lives on one source, so regenerating it is one change rather than one per service.
A 5-minute window rejects late deliveries
The Problem: Valid deliveries fail verification after an outage.
Why It Happens: Jamie's last two retries come 10 minutes and an hour after the first attempt, with the original t. A 5-minute window rejects them, and it rejects Hookdeck retries and replays too.
Workarounds:
- Enforce the window only when Jamie calls you directly, and make it configurable.
- Deduplicate on
metadata.id, which protects you against replays whatever the window.
How Hookdeck Can Help: Hookdeck verifies the signature when the request arrives and applies no timestamp window. Deliveries from Hookdeck keep their original headers, so keep your own window off behind Hookdeck.
Five attempts in about an hour
The Problem: Meetings processed during a longer outage never reach your systems.
Why It Happens: Jamie makes 5 attempts over about 1 hour and 11 minutes, then moves the delivery to a dead-letter queue and emails the owner or the workspace admins. A response slower than 30 seconds counts as a failure.
Workarounds:
- Acknowledge within 30 seconds and do the CRM or ticketing work afterwards.
- Act on Jamie's failure emails, which are limited to one an hour per endpoint, so one email can stand for many failures.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives, then delivers to your service with retries you configure. Issues alert you to failing deliveries, and you can replay any meeting.
Best practices
Verify against the raw body
Jamie signs the exact bytes it sends. In Express, use express.raw({ type: '*/*' }) on the route. In Next.js, use await request.text(). In FastAPI, use await request.body(). Parse only after verification passes.
Deduplicate on metadata.id
metadata.id, also sent as jamie-delivery, identifies the delivery. Store it before processing and skip repeats. See our guide to webhook idempotency.
Use personal webhooks for personal data
Jamie's advice is to use a personal webhook if you only want your own meetings, rather than filtering a workspace webhook on the receiving end. Workspace webhooks carry every colleague's transcripts.
Handle nulls and large bodies
Use optional access for endTime, data.event.id, externalId, emails and assignees. Allow for request bodies the size of a full transcript.
Acknowledge fast and process asynchronously
Verify, persist, return 200, then process. See why to process webhooks asynchronously.
Conclusion
Jamie sends meeting.completed with the full summary, transcript and tasks, signed with x-jamie-signature: t=...,v0=..., a hex HMAC-SHA256 of the timestamp and raw body, when you choose Signature Verification. Choose that mode, store the secret the moment it appears, verify the raw bytes, deduplicate on metadata.id, keep any timestamp window off behind Hookdeck, and answer within 30 seconds.
Hookdeck Event Gateway sits in front of your receiver, checks every Jamie signature, and holds each meeting until your service has processed it.
Get started with Hookdeck for free and handle Jamie webhooks reliably in minutes.