Skip to content

Webhooks ​

Get real-time notifications when emails arrive instead of polling for new messages.

Why Webhooks? ​

Webhooks push notifications to your server instantly when events occur:

Without Webhooks (Polling):

typescript
// Poll every 30 seconds - slow and inefficient
setInterval(async () => {
  const messages = await client.inboxes.messages(inboxId).list({
    direction: 'in',
    limit: 10
  })
  // Process new messages...
}, 30000)

With Webhooks (Real-time):

typescript
// Get notified instantly when emails arrive
app.post('/webhook', (req, res) => {
  const event = req.body
  if (event.type === 'message.received') {
    // Process message immediately!
  }
  res.sendStatus(200)
})

Create a Webhook ​

typescript
const webhook = await client.webhooks.create({
  url: 'https://your-server.com/webhook',
  events: ['message.received'],
  inbox_id: 'inbox_abc123'  // Optional: specific inbox
})

console.log(webhook.id)
// → webhook_xyz789

console.log(webhook.secret)
// → whsec_abc123def456 (save this!)

Save the Secret

The webhook secret is only shown once during creation. Save it securely - you'll need it to verify webhook signatures.

Parameters ​

ParameterTypeRequiredDescription
urlstringYesYour webhook endpoint URL (must be HTTPS)
eventsstring[]YesEvents to subscribe to
inbox_idstringNoReceive events for specific inbox only
descriptionstringNoHuman-readable description

Available Events ​

EventDescription
message.receivedNew email received in an inbox
message.sentEmail successfully sent
message.failedEmail delivery failed
message.bouncedEmail bounced

Subscribe to All Inboxes ​

Receive events for all your inboxes:

typescript
const webhook = await client.webhooks.create({
  url: 'https://your-server.com/webhook',
  events: ['message.received', 'message.failed'],
  description: 'Main webhook for all inboxes'
})

Subscribe to Specific Inbox ​

Receive events for a single inbox:

typescript
const webhook = await client.webhooks.create({
  url: 'https://support-handler.com/webhook',
  events: ['message.received'],
  inbox_id: 'inbox_support123',
  description: 'Support inbox webhook'
})

List Webhooks ​

typescript
const response = await client.webhooks.list({
  limit: 20,
  offset: 0
})

for (const webhook of response.data) {
  console.log(`${webhook.id}: ${webhook.url}`)
  console.log(`Events: ${webhook.events.join(', ')}`)
}

Get a Webhook ​

typescript
const webhook = await client.webhooks.get('webhook_xyz789')

console.log(webhook.url)
console.log(webhook.events)
console.log(webhook.inbox_id)

Update a Webhook ​

Change the URL, events, or description:

typescript
const updated = await client.webhooks.update('webhook_xyz789', {
  url: 'https://new-server.com/webhook',
  events: ['message.received', 'message.sent'],
  description: 'Updated webhook endpoint'
})

Can't Rotate Secret

You cannot change the webhook secret after creation. If you need a new secret, delete the webhook and create a new one.

Delete a Webhook ​

typescript
await client.webhooks.delete('webhook_xyz789')

Webhook Payload ​

When an event occurs, Myxara sends a POST request to your webhook URL:

json
{
  "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"
    }
  }
}

Handling Webhooks ​

Express.js Example ​

typescript
import express from 'express'
import { MyxaraClient } from '@myxara/sdk-js'

const app = express()
app.use(express.json())

const client = new MyxaraClient({
  apiKey: process.env.MYXARA_API_KEY!
})

app.post('/webhook', async (req, res) => {
  const event = req.body

  // Verify signature (see below)
  const signature = req.headers['myxara-signature'] as string
  const isValid = verifySignature(signature, req.body, WEBHOOK_SECRET)

  if (!isValid) {
    return res.status(401).send('Invalid signature')
  }

  // Handle event
  if (event.type === 'message.received') {
    const message = event.data.message

    console.log(`New email 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
    })
  }

  // Always return 200
  res.sendStatus(200)
})

app.listen(3000)

Next.js API Route ​

typescript
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { MyxaraClient } from '@myxara/sdk-js'

const client = new MyxaraClient({
  apiKey: process.env.MYXARA_API_KEY!
})

export async function POST(req: NextRequest) {
  const event = await req.json()

  // Verify signature
  const signature = req.headers.get('myxara-signature')
  // ... verification logic

  // Handle event
  if (event.type === 'message.received') {
    const { message } = event.data

    // Process the message...
    await processMessage(message)
  }

  return NextResponse.json({ received: true })
}

async function processMessage(message: any) {
  // Your logic here
  console.log(`Processing message: ${message.id}`)
}

Verifying Signatures ​

Always verify webhook signatures to ensure requests are from Myxara:

typescript
import crypto from 'crypto'

function verifySignature(
  signature: string,
  payload: any,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

// Usage
const signature = req.headers['myxara-signature'] as string
const isValid = verifySignature(signature, req.body, WEBHOOK_SECRET)

if (!isValid) {
  return res.status(401).send('Invalid signature')
}

Always Verify Signatures

Never skip signature verification in production. Without it, anyone can send fake webhook events to your server.

Retry Behavior ​

If your webhook endpoint fails (5xx error or timeout), Myxara automatically retries:

  • 1st retry: 1 minute later
  • 2nd retry: 5 minutes later
  • 3rd retry: 30 minutes later
  • 4th retry: 2 hours later
  • 5th retry: 12 hours later

After 5 failed attempts, the webhook is disabled and you'll receive an email notification.

Responding to Webhooks

Always respond with 200 OK within 5 seconds, even if you haven't finished processing. Queue long-running tasks in the background.

Best Practice: Background Processing ​

typescript
import { Queue } from 'bull'

const messageQueue = new Queue('messages')

app.post('/webhook', async (req, res) => {
  const event = req.body

  // Verify signature
  const isValid = verifySignature(...)
  if (!isValid) {
    return res.status(401).send('Invalid signature')
  }

  // Queue for processing (fast response)
  if (event.type === 'message.received') {
    await messageQueue.add(event.data.message)
  }

  // Respond immediately
  res.sendStatus(200)
})

// Process in background
messageQueue.process(async (job) => {
  const message = job.data
  // Long-running processing here...
})

Testing Webhooks ​

Local Development with ngrok ​

bash
# Install ngrok
npm install -g ngrok

# Start your local server
npm run dev

# Expose to internet
ngrok http 3000

Use the ngrok URL for your webhook:

typescript
const webhook = await client.webhooks.create({
  url: 'https://abc123.ngrok.io/webhook',
  events: ['message.received']
})

Test Event Delivery ​

Send a test email to trigger the webhook:

typescript
await client.inboxes.messages(inboxId).send({
  to: '[email protected]',
  subject: 'Test webhook',
  text: 'This should trigger a webhook event'
})

Complete Example ​

typescript
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) => {
  // 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')
  }

  const event = req.body

  console.log(`Received event: ${event.type}`)

  // Handle different event types
  switch (event.type) {
    case 'message.received':
      await handleMessageReceived(event.data.message)
      break

    case 'message.sent':
      console.log(`Message ${event.data.message.id} sent`)
      break

    case 'message.failed':
      console.error(`Message ${event.data.message.id} failed`)
      break
  }

  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 email. Our AI agent will respond shortly.',
    in_reply_to: message.id
  })
}

// Create webhook on startup
async function setupWebhook() {
  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('Save this secret to your environment variables!')
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000')
})

// Uncomment to create webhook:
// setupWebhook().catch(console.error)

TypeScript Types ​

typescript
import type {
  Webhook,
  CreateWebhookParams,
  UpdateWebhookParams,
  WebhookEvent
} from '@myxara/sdk-js'

const params: CreateWebhookParams = {
  url: 'https://your-server.com/webhook',
  events: ['message.received'],
  inbox_id: 'inbox_abc123'
}

const webhook: Webhook = await client.webhooks.create(params)

// Event handler
function handleEvent(event: WebhookEvent) {
  if (event.type === 'message.received') {
    const message = event.data.message
    // TypeScript knows the shape of message
  }
}

Next Steps ​

Released under the MIT License (SDK) & Elastic License 2.0 (Server)