Authentication Methods
API Key (Recommended)
The simplest way to verify webhook requests. Jamie sends a static API key in a header that you specify during webhook creation. How it works:- When creating the webhook, choose “API Key” authentication
- Optionally specify a custom header name (default:
x-jamie-api-key) - Save the generated secret key (
sk_...) - In your webhook handler, compare the header value with your stored secret
Verification Code Examples
- Node.js
- Python
- Go
Copy
Ask AI
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/jamie', (req, res) => {
// Get the API key from header (use your custom header name if configured)
const apiKey = req.headers['x-jamie-api-key'];
// Compare with your stored secret
if (apiKey !== process.env.JAMIE_WEBHOOK_SECRET) {
console.error('Invalid API key');
return res.status(401).send('Unauthorized');
}
// API key is valid, process the webhook
const { data } = req.body;
console.log('Meeting completed:', data.event.title);
console.log('Summary:', data.summary.markdown);
// Process tasks
data.tasks.forEach(task => {
console.log('Task:', task.content);
});
res.status(200).send('OK');
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
Copy
Ask AI
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/jamie', methods=['POST'])
def handle_webhook():
# Get the API key from header (use your custom header name if configured)
api_key = request.headers.get('x-jamie-api-key')
# Compare with your stored secret
if api_key != os.environ.get('JAMIE_WEBHOOK_SECRET'):
print('Invalid API key')
return 'Unauthorized', 401
# API key is valid, process the webhook
data = request.get_json()
print('Meeting completed:', data['data']['event']['title'])
print('Summary:', data['data']['summary']['markdown'])
# Process tasks
for task in data['data']['tasks']:
print('Task:', task['content'])
return 'OK', 200
if __name__ == '__main__':
app.run(port=3000)
Copy
Ask AI
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type WebhookPayload struct {
Data struct {
Event struct {
Title string `json:"title"`
} `json:"event"`
Summary struct {
Markdown string `json:"markdown"`
} `json:"summary"`
Tasks []struct {
Content string `json:"content"`
} `json:"tasks"`
} `json:"data"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
// Get the API key from header (use your custom header name if configured)
apiKey := r.Header.Get("x-jamie-api-key")
// Compare with your stored secret
if apiKey != os.Getenv("JAMIE_WEBHOOK_SECRET") {
fmt.Println("Invalid API key")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// API key is valid, process the webhook
body, _ := io.ReadAll(r.Body)
var payload WebhookPayload
json.Unmarshal(body, &payload)
fmt.Println("Meeting completed:", payload.Data.Event.Title)
fmt.Println("Summary:", payload.Data.Summary.Markdown)
// Process tasks
for _, task := range payload.Data.Tasks {
fmt.Println("Task:", task.Content)
}
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhooks/jamie", webhookHandler)
fmt.Println("Webhook server running on port 3000")
http.ListenAndServe(":3000", nil)
}
Signature Verification (Advanced)
For maximum security, Jamie can sign all webhook requests using HMAC-SHA256. This ensures requests are authentic and haven’t been tampered with.Signature Format
Thex-jamie-signature header uses the following format:
Copy
Ask AI
t=<timestamp>,v0=<signature>
t: Unix timestamp (seconds) when the signature was createdv0: HMAC-SHA256 signature of the payload
Verification Steps
- Extract the timestamp and signature from the
x-jamie-signatureheader - Validate the timestamp - Reject requests with timestamps older than 5 minutes (300 seconds) to prevent replay attacks
- Recreate the signed message - Concatenate the timestamp, a period (
.), and the raw request body:{timestamp}.{raw_body} - Compute the expected signature - Generate an HMAC-SHA256 hash of the message using your signing secret
- Compare signatures - Use a constant-time comparison to prevent timing attacks
Verification Code Examples
- Node.js
- Python
- Go
Copy
Ask AI
const crypto = require('crypto');
async function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): Promise<boolean> {
// Parse signature header: t=timestamp,v0=hash
const parts = signature.split(',');
const timestamp = parts[0].split('=')[1];
const expectedSignature = parts[1].split('=')[1];
// Validate timestamp (prevent replay attacks - 5 min tolerance)
const now = Math.floor(Date.now() / 1000);
if (now - parseInt(timestamp) > 300) {
throw new Error('Signature expired');
}
// Recreate the signed message
const message = `${timestamp}.${payload}`;
// Generate HMAC signature
const hmac = crypto.createHmac('sha256', secret);
hmac.update(message);
const actualSignature = hmac.digest('hex');
// Compare signatures using constant-time comparison
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
const actualBuffer = Buffer.from(actualSignature, 'hex');
if (!crypto.timingSafeEqual(expectedBuffer, actualBuffer)) {
throw new Error('Invalid signature');
}
return true;
}
// Usage in your webhook endpoint (Express example):
app.post('/webhooks/jamie', async (req, res) => {
const signature = req.headers['x-jamie-signature'];
const payload = JSON.stringify(req.body);
try {
await verifyWebhookSignature(
payload,
signature,
process.env.JAMIE_WEBHOOK_SECRET
);
// Signature is valid, process the webhook
const { data } = req.body;
console.log('Meeting completed:', data.event.title);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook verification failed:', error);
res.status(401).send('Unauthorized');
}
});
Copy
Ask AI
import hmac
import hashlib
import time
import json
from typing import Dict
def verify_webhook_signature(
payload: str,
signature: str,
secret: str
) -> bool:
"""Verify Jamie webhook signature
Args:
payload: The raw request body as a string
signature: The x-jamie-signature header value
secret: Your webhook signing secret
Returns:
True if signature is valid
Raises:
ValueError: If signature is invalid or expired
"""
# Parse signature header: t=timestamp,v0=hash
parts: Dict[str, str] = {
p.split('=')[0]: p.split('=')[1]
for p in signature.split(',')
}
timestamp: int = int(parts['t'])
expected_signature: str = parts['v0']
# Validate timestamp (prevent replay attacks - 5 min tolerance)
now: int = int(time.time())
if now - timestamp > 300:
raise ValueError('Signature expired')
# Recreate the signed message
message: str = f"{timestamp}.{payload}"
# Generate HMAC signature
signature_bytes: str = hmac.new(
secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures (constant-time comparison)
if not hmac.compare_digest(signature_bytes, expected_signature):
raise ValueError('Invalid signature')
return True
# Usage in your webhook endpoint (Flask example):
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/jamie', methods=['POST'])
def handle_webhook():
signature: str = request.headers.get('x-jamie-signature')
payload: str = request.get_data(as_text=True)
try:
verify_webhook_signature(
payload,
signature,
os.environ['JAMIE_WEBHOOK_SECRET']
)
# Signature is valid, process the webhook
data: Dict = json.loads(payload)
print('Meeting completed:', data['data']['event']['title'])
return 'OK', 200
except Exception as e:
print('Webhook verification failed:', str(e))
return 'Unauthorized', 401
Copy
Ask AI
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// verifyWebhookSignature verifies the Jamie webhook signature
func verifyWebhookSignature(payload, signature, secret string) error {
// Parse signature header: t=timestamp,v0=hash
parts := make(map[string]string)
for _, part := range strings.Split(signature, ",") {
kv := strings.SplitN(part, "=", 2)
if len(kv) == 2 {
parts[kv[0]] = kv[1]
}
}
timestamp, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp: %w", err)
}
expectedSignature := parts["v0"]
// Validate timestamp (prevent replay attacks - 5 min tolerance)
now := time.Now().Unix()
if now-timestamp > 300 {
return fmt.Errorf("signature expired")
}
// Recreate the signed message
message := fmt.Sprintf("%d.%s", timestamp, payload)
// Generate HMAC signature
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(message))
actualSignature := hex.EncodeToString(h.Sum(nil))
// Compare signatures (constant-time comparison)
if !hmac.Equal([]byte(actualSignature), []byte(expectedSignature)) {
return fmt.Errorf("invalid signature")
}
return nil
}
// WebhookPayload represents the structure of Jamie webhook data
type WebhookPayload struct {
Metadata struct {
ID string `json:"id"`
Event string `json:"event"`
Created int64 `json:"created"`
} `json:"metadata"`
Data struct {
Event struct {
Title string `json:"title"`
} `json:"event"`
} `json:"data"`
}
// webhookHandler handles incoming webhooks from Jamie
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("x-jamie-signature")
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
secret := os.Getenv("JAMIE_WEBHOOK_SECRET")
if err := verifyWebhookSignature(
string(body),
signature,
secret,
); err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Signature is valid, process the webhook
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
fmt.Printf("Meeting completed: %s\n", payload.Data.Event.Title)
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhooks/jamie", webhookHandler)
http.ListenAndServe(":8080", nil)
}
Security Best Practices:
- Always verify the API key or signature before processing webhook data
- For signature verification, use constant-time comparison to prevent timing attacks
- Validate timestamps to prevent replay attacks (recommended tolerance: 5 minutes)
- Store your secret key securely (use environment variables or a secrets manager)
- Never commit secret keys to version control

