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": "customer@example.com",
      "to": "support@mail.myxara.ai",
      "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: 'your-inbox@mail.myxara.ai',
  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)