Webhooks Example
Learn how to handle real-time email notifications with webhooks instead of polling.
Why Webhooks?
Webhooks provide instant notifications when emails arrive, making your AI agents more responsive:
Polling (❌ Slow & Inefficient):
- Check for new emails every 30-60 seconds
- Wastes API quota on empty requests
- Delayed responses (up to 60s latency)
- Difficult to scale
Webhooks (✅ Fast & Efficient):
- Get notified instantly when emails arrive
- No wasted API calls
- Real-time responses (< 1s latency)
- Scales effortlessly
Prerequisites
- Myxara API key
- A server to receive webhooks
- For local development: ngrok or similar tunneling tool
Basic Webhook Handler
Express.js
import express from 'express'
import crypto from 'crypto'
import { MyxaraClient } from '@myxara/sdk-js'
const app = express()
app.use(express.json())
const client = new MyxaraClient({
apiKey: process.env.MYXARA_API_KEY!
})
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!
// Verify webhook signature
function verifySignature(signature: string, payload: any): boolean {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(payload))
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
// Webhook endpoint
app.post('/webhook', async (req, res) => {
// 1. Verify signature
const signature = req.headers['myxara-signature'] as string
if (!verifySignature(signature, req.body)) {
console.error('❌ Invalid webhook signature')
return res.status(401).send('Invalid signature')
}
// 2. Parse event
const event = req.body
console.log(`✅ Received event: ${event.type}`)
// 3. Handle different event types
switch (event.type) {
case 'message.received':
await handleMessageReceived(event.data.message)
break
case 'message.sent':
console.log(`Message sent: ${event.data.message.id}`)
break
case 'message.failed':
console.error(`Message failed: ${event.data.message.id}`)
await handleMessageFailed(event.data.message)
break
case 'message.bounced':
console.warn(`Message bounced: ${event.data.message.id}`)
await handleMessageBounced(event.data.message)
break
}
// 4. Always respond quickly (< 5 seconds)
res.sendStatus(200)
})
async function handleMessageReceived(message: any) {
console.log(`New message from ${message.from}`)
console.log(`Subject: ${message.subject}`)
// Auto-reply
await client.inboxes.messages(message.inbox_id).send({
to: message.from,
subject: `Re: ${message.subject}`,
text: 'Thanks for your message. We will respond shortly.',
in_reply_to: message.id
})
}
async function handleMessageFailed(message: any) {
// Alert admin
console.error('Message delivery failed:', message)
}
async function handleMessageBounced(message: any) {
// Mark email as invalid, remove from list
console.warn('Email bounced:', message.to)
}
app.listen(3000, () => {
console.log('🚀 Webhook server running on port 3000')
})Next.js API Route
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
import { MyxaraClient } from '@myxara/sdk-js'
const client = new MyxaraClient({
apiKey: process.env.MYXARA_API_KEY!
})
function verifySignature(signature: string, payload: any): boolean {
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(JSON.stringify(payload))
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
export async function POST(req: NextRequest) {
const signature = req.headers.get('myxara-signature')
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
)
}
const event = await req.json()
if (!verifySignature(signature, event)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
// Handle event
if (event.type === 'message.received') {
const { message } = event.data
// Process message...
await processMessage(message)
}
return NextResponse.json({ received: true })
}
async function processMessage(message: any) {
// Your logic here
console.log(`Processing ${message.id}`)
}Setup Webhook
Create a webhook to receive notifications:
import { MyxaraClient } from '@myxara/sdk-js'
const client = new MyxaraClient({
apiKey: process.env.MYXARA_API_KEY!
})
// Create webhook
const webhook = await client.webhooks.create({
url: 'https://your-server.com/webhook',
events: ['message.received', 'message.failed'],
description: 'Production webhook'
})
console.log('Webhook created:', webhook.id)
console.log('Secret:', webhook.secret)
console.log('\n⚠️ Save this secret to your environment:')
console.log(`WEBHOOK_SECRET=${webhook.secret}`)Save the Secret
The webhook secret is only shown once. Save it to your environment variables immediately!
Event Types
message.received
Triggered when a new email arrives:
{
"id": "evt_abc123",
"type": "message.received",
"created_at": "2025-01-15T10:30:00Z",
"data": {
"message": {
"id": "msg_xyz789",
"inbox_id": "inbox_abc123",
"from": "[email protected]",
"to": "[email protected]",
"subject": "Need help",
"text": "I need assistance with...",
"html": "<p>I need assistance with...</p>",
"direction": "in",
"created_at": "2025-01-15T10:30:00Z"
}
}
}message.sent
Triggered when an email is successfully sent:
{
"id": "evt_def456",
"type": "message.sent",
"created_at": "2025-01-15T10:31:00Z",
"data": {
"message": {
"id": "msg_sent123",
"inbox_id": "inbox_abc123",
"from": "[email protected]",
"to": "[email protected]",
"subject": "Re: Need help",
"status": "sent"
}
}
}message.failed
Triggered when email delivery fails:
{
"id": "evt_ghi789",
"type": "message.failed",
"created_at": "2025-01-15T10:32:00Z",
"data": {
"message": {
"id": "msg_failed123",
"status": "failed",
"error": "Recipient address rejected"
}
}
}Background Processing
Always respond to webhooks within 5 seconds. For long tasks, use background processing:
Using Bull Queue
import Queue from 'bull'
import express from 'express'
const app = express()
app.use(express.json())
// Create queue
const messageQueue = new Queue('messages', {
redis: {
host: 'localhost',
port: 6379
}
})
// Webhook handler (fast response)
app.post('/webhook', async (req, res) => {
const signature = req.headers['myxara-signature'] as string
if (!verifySignature(signature, req.body)) {
return res.status(401).send('Invalid signature')
}
const event = req.body
// Queue for background processing
if (event.type === 'message.received') {
await messageQueue.add('process-message', event.data.message, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
})
}
// Respond immediately
res.sendStatus(200)
})
// Background worker
messageQueue.process('process-message', async (job) => {
const message = job.data
console.log(`Processing message ${message.id}`)
// Long-running task here...
await processWithAI(message)
console.log(`Completed processing ${message.id}`)
})
app.listen(3000)Using Worker Threads
import { Worker } from 'worker_threads'
app.post('/webhook', async (req, res) => {
// Verify signature...
const event = req.body
if (event.type === 'message.received') {
// Process in worker thread
const worker = new Worker('./message-worker.js', {
workerData: event.data.message
})
worker.on('message', (result) => {
console.log('Worker completed:', result)
})
worker.on('error', (error) => {
console.error('Worker error:', error)
})
}
// Respond immediately
res.sendStatus(200)
})Testing Locally
Using ngrok
# Install ngrok
npm install -g ngrok
# Start your server
bun run server.ts
# In another terminal, expose to internet
ngrok http 3000Copy the ngrok URL and create a webhook:
const webhook = await client.webhooks.create({
url: 'https://abc123.ngrok.io/webhook',
events: ['message.received']
})Send Test Event
Send an email to trigger the webhook:
await client.inboxes.messages(inboxId).send({
to: '[email protected]',
subject: 'Test webhook',
text: 'This should trigger a webhook event'
})Security Best Practices
1. Always Verify Signatures
// ✅ Good - verifies signature
if (!verifySignature(signature, req.body)) {
return res.status(401).send('Invalid signature')
}
// ❌ Bad - no verification
app.post('/webhook', async (req, res) => {
const event = req.body
// Process without verifying - DANGEROUS!
})2. Use HTTPS Only
// ✅ Good - HTTPS
const webhook = await client.webhooks.create({
url: 'https://your-server.com/webhook',
events: ['message.received']
})
// ❌ Bad - HTTP (insecure)
const webhook = await client.webhooks.create({
url: 'http://your-server.com/webhook', // Won't work
events: ['message.received']
})3. Keep Secrets Safe
# .env
WEBHOOK_SECRET=whsec_abc123def456
# .gitignore
.env
.env.local4. Rate Limiting
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100 // Max 100 requests per minute
})
app.use('/webhook', limiter)Error Handling
Handle webhook processing errors gracefully:
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['myxara-signature'] as string
if (!verifySignature(signature, req.body)) {
return res.status(401).send('Invalid signature')
}
const event = req.body
if (event.type === 'message.received') {
await handleMessageReceived(event.data.message)
}
res.sendStatus(200)
} catch (error) {
console.error('Webhook processing error:', error)
// Still return 200 to prevent retries for unrecoverable errors
// Myxara will retry 5xx errors
res.sendStatus(200)
}
})
async function handleMessageReceived(message: any) {
try {
// Process message
await processMessage(message)
} catch (error) {
console.error(`Failed to process message ${message.id}:`, error)
// Log to error tracking service
logError(error, { message_id: message.id })
// Send fallback response
await client.inboxes.messages(message.inbox_id).send({
to: message.from,
subject: `Re: ${message.subject}`,
text: 'We received your message but encountered an issue. Our team will respond shortly.',
metadata: { error: true }
})
}
}Monitoring
Track webhook health and performance:
interface WebhookMetrics {
total_events: number
successful: number
failed: number
avg_processing_time: number
}
const metrics: WebhookMetrics = {
total_events: 0,
successful: 0,
failed: 0,
avg_processing_time: 0
}
app.post('/webhook', async (req, res) => {
const startTime = Date.now()
try {
// Verify signature...
// Process event...
metrics.successful++
res.sendStatus(200)
} catch (error) {
metrics.failed++
res.sendStatus(500)
} finally {
metrics.total_events++
const processingTime = Date.now() - startTime
// Update average processing time
metrics.avg_processing_time =
(metrics.avg_processing_time * (metrics.total_events - 1) + processingTime) /
metrics.total_events
}
})
// Metrics endpoint
app.get('/metrics', (req, res) => {
res.json({
...metrics,
success_rate: ((metrics.successful / metrics.total_events) * 100).toFixed(2) + '%',
uptime: process.uptime()
})
})Retry Behavior
Myxara automatically retries failed webhooks:
- 1st retry: 1 minute after failure
- 2nd retry: 5 minutes after failure
- 3rd retry: 30 minutes after failure
- 4th retry: 2 hours after failure
- 5th retry: 12 hours after failure
After 5 failed attempts, the webhook is disabled.
Idempotency
Make your webhook handler idempotent - it should safely handle the same event multiple times without side effects.
const processedEvents = new Set<string>()
app.post('/webhook', async (req, res) => {
const event = req.body
// Check if already processed
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed - skipping`)
return res.sendStatus(200)
}
// Process event
await handleEvent(event)
// Mark as processed
processedEvents.add(event.id)
res.sendStatus(200)
})Complete Production Example
import express from 'express'
import crypto from 'crypto'
import Queue from 'bull'
import { MyxaraClient } from '@myxara/sdk-js'
const app = express()
app.use(express.json())
const client = new MyxaraClient({
apiKey: process.env.MYXARA_API_KEY!
})
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!
// Queue for background processing
const queue = new Queue('webhooks', process.env.REDIS_URL!)
// Processed events cache (use Redis in production)
const processedEvents = new Set<string>()
function verifySignature(signature: string, payload: any): boolean {
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(payload))
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
app.post('/webhook', async (req, res) => {
const startTime = Date.now()
try {
// 1. Verify signature
const signature = req.headers['myxara-signature'] as string
if (!signature || !verifySignature(signature, req.body)) {
console.error('Invalid signature')
return res.status(401).send('Invalid signature')
}
const event = req.body
// 2. Check if already processed (idempotency)
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed`)
return res.sendStatus(200)
}
// 3. Queue for background processing
await queue.add('process-event', event, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
})
// 4. Mark as processed
processedEvents.add(event.id)
// 5. Log metrics
const duration = Date.now() - startTime
console.log(`Event ${event.id} queued in ${duration}ms`)
// 6. Respond quickly
res.sendStatus(200)
} catch (error) {
console.error('Webhook error:', error)
res.sendStatus(500)
}
})
// Background worker
queue.process('process-event', async (job) => {
const event = job.data
console.log(`Processing event ${event.type}`)
if (event.type === 'message.received') {
const { message } = event.data
// Your processing logic here
await processMessage(message)
}
})
async function processMessage(message: any) {
// Auto-reply logic
console.log(`Processing message from ${message.from}`)
await client.inboxes.messages(message.inbox_id).send({
to: message.from,
subject: `Re: ${message.subject}`,
text: 'Thanks for your message!',
in_reply_to: message.id
})
}
app.listen(3000, () => {
console.log('🚀 Production webhook server running')
})Next Steps
- Customer Support Example → - Build an AI support agent
- SDK Reference → - Full webhook API documentation
- Error Handling → - Handle webhook errors gracefully