Gareth Wilson Gareth Wilson

Guide to Jamie Webhooks: Features and Best Practices

Published


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_...) in x-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

FeatureDetails
ConfigurationSettings > Integrations > Webhooks > Create Webhook
AvailabilityPaid plans. Jamie's docs say "Plus or higher" in one place and list Pro, Team and Enterprise in another
ScopesPersonal (3 per user) or workspace (5 per workspace, admins only)
Eventsmeeting.completed
Method and formatPOST, Content-Type: application/json
Signature headerx-jamie-signature: t=<timestamp>,v0=<signature>
SchemeHMAC-SHA256, lowercase hex
Signed content{t}.{raw body}
SecretShown once at creation. Regenerate it from the webhook settings if lost
Other headersuser-agent: Jamie-Webhooks/1.0, jamie-event, jamie-delivery
Replay windowJamie recommends rejecting timestamps older than 5 minutes
Response window2xx within 30 seconds
Retries5 attempts in all: immediately, then after 10 s, 60 s, 10 min and 1 h
After the last failureDead-lettered, and the owner or workspace admins are emailed (at most once an hour per endpoint)
TestingA Test button sends a mock payload through the normal delivery path
Source IPsNot 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.id identifies the delivery and matches the jamie-delivery header. metadata.event matches jamie-event.
  • metadata.created is in Unix seconds. The meeting times are ISO 8601 strings.
  • participants are people who spoke, detected from the transcript. data.event.attendees are people invited to the calendar event, who may not have attended. Don't treat them as the same list.
  • data.event.externalId is the calendar provider's event ID, from Microsoft Graph or Google Calendar, so you can match the meeting to your own calendar data. It and data.event.id can be null, and attendees is empty when no calendar event is linked.
  • endTime, participant emails and task assignees can all be null.

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.

  1. In Jamie, go to Settings > Integrations > Webhooks and click Create Webhook.
  2. Enter your HTTPS URL and, optionally, a Description.
  3. Under Events, select meeting.completed.
  4. Choose Personal webhook or Workspace webhook.
  5. Choose Signature Verification as the authentication method.
  6. Click Create, then copy the signing secret straight away, for example into JAMIE_WEBHOOK_SECRET.
  7. 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_TIMESTAMP_TOLERANCE_SECONDS 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-signature header 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.