# Jamie Webhooks

## When to Use This Skill

* How do I receive Jamie webhooks?
* How do I verify the `x-jamie-signature` header?
* How do I handle the `meeting.completed` event (summary, transcript, tasks)?
* Why is my Jamie webhook signature verification failing?
* Which Jamie authentication method works with Hookdeck?

## Choose Signature Verification

Choose Signature Verification when creating the Jamie webhook and paste its signing secret. API key mode is not supported.

Jamie offers two modes at webhook creation: API Key (its recommended default, a static key in a header of your choice, `x-jamie-api-key` by default) and Signature Verification. Hookdeck's Jamie source (`JAMIE`) verifies the signature only, so this skill covers that mode.

## Essential Code (USE THIS)

Jamie sends `x-jamie-signature: t=<unix seconds>,v0=<lowercase hex>`. `v0` is HMAC-SHA256 of `"<t>.<raw body>"`, keyed with the signing secret string's UTF-8 bytes **as-is** (not decoded). Use the **raw body bytes**, not re-serialised JSON. Jamie has no verification SDK, so verify manually.

### Verification (Node.js)

```javascript
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyJamie(rawBody, signatureHeader, secret, toleranceSeconds = 0) {
  if (!secret || !signatureHeader) return false;
  let t; const v0 = [];
  for (const kv of signatureHeader.split(',')) {
    const i = kv.indexOf('=');
    if (i <= 0) continue;
    const key = kv.slice(0, i).trim(), value = kv.slice(i + 1).trim();
    if (key === 't') t = value; else if (key === 'v0') v0.push(value);
  }
  if (!t || !/^\d+$/.test(t) || v0.length === 0) return false;
  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');
  return v0.some((sig) => sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
}

```

### Verification (Python)

```python
import hashlib, hmac, time

def verify_jamie(raw_body: bytes, header: str, secret: str, tolerance: int = 0) -> bool:
    if not secret or not header:
        return False
    t, v0 = None, []
    for kv in header.split(","):
        key, sep, value = kv.partition("=")
        if sep and key.strip() == "t": t = value.strip()
        elif sep and key.strip() == "v0": v0.append(value.strip())
    if not t or not t.isdigit() or not v0:
        return False
    if tolerance > 0 and abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(sig.encode(), expected.encode()) for sig in v0)

```

Respond **401** when the header is missing or malformed or the signature does not match.

### Timestamp window

The timestamp window is **opt-in**: `JAMIE_TIMESTAMP_TOLERANCE_SECONDS` defaults to `0` (off). Jamie's docs recommend rejecting timestamps older than 5 minutes (300 s) when Jamie calls you directly, so set `300` there if you want that. Behind Hookdeck keep `0`: Hookdeck retries and replays resend the **original** timestamp (and Jamie's own retries run up to about an hour after the first attempt), so a window rejects late redeliveries.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/jamie-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/jamie-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/jamie-webhooks/examples/fastapi/).

## Common Event Types

The event name is in `metadata.event` and in the `jamie-event` header.

| Event | Description |
| --- | --- |
| `meeting.completed` | A meeting is fully processed: summary (Markdown/HTML/short), transcript, tasks, participants, calendar event and tags |

`meeting.completed` is currently the only event. Jamie's `metadata.id` (also the `jamie-delivery` header) identifies the delivery, so dedupe on it. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jamie-webhooks/references/overview.md).

> **For the full payload reference**, see [Jamie webhooks getting started](https://docs.meetjamie.ai/developers/webhooks/getting-started).

## Important Headers

| Header | Description |
| --- | --- |
| `x-jamie-signature` | `t=<unix seconds>,v0=<lowercase hex HMAC-SHA256>` (Signature Verification mode) |
| `jamie-event` | Event type, e.g. `meeting.completed` |
| `jamie-delivery` | Unique ID for this delivery |

## Environment Variables

```bash
JAMIE_WEBHOOK_SECRET=your_signing_secret   # shown once when the webhook is created
JAMIE_TIMESTAMP_TOLERANCE_SECONDS=0        # opt-in replay window; Jamie's docs suggest 300 when called directly

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 jamie --path /webhooks/jamie

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jamie-webhooks/references/overview.md) - Jamie webhook concepts, event and payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jamie-webhooks/references/setup.md) - Creating a webhook with Signature Verification
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/jamie-webhooks/references/verification.md) - Signature verification details and a standalone verifier