mymx
v0.3.91
Published
Official MyMX Node.js SDK for webhook signature verification
Downloads
1,121
Maintainers
Readme
mymx
Official Node.js SDK for MyMX webhook handling. For full documentation, see mymx.dev/docs.
Installation
npm install mymxUsage
import { handleWebhook, MyMXWebhookError } from 'mymx';
app.post('/webhooks/email', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = handleWebhook({
body: req.body,
headers: req.headers,
secret: process.env.MYMX_WEBHOOK_SECRET,
});
console.log('Email from:', event.email.headers.from);
console.log('Subject:', event.email.headers.subject);
res.json({ received: true });
} catch (err) {
if (err instanceof MyMXWebhookError) {
console.error(`[${err.code}] ${err.message}`);
return res.status(400).json({ error: err.code });
}
throw err;
}
});That's it. handleWebhook verifies the signature, parses the JSON, validates the payload with Zod, and returns a fully typed EmailReceivedEvent.
Retry Behavior
MyMX uses standard HTTP status codes for retry logic:
- 2XX = Success, no retry
- Non-2XX = Failure, will retry with exponential backoff
That's it. No special headers required.
Content Discard (Optional)
If you've enabled the "content discard" setting in your MyMX dashboard, you can tell us to permanently delete the email content after successful delivery by returning the X-MyMX-Confirmed header:
import { confirmedHeaders } from 'mymx';
// Signal that you've durably processed the email
res.set(confirmedHeaders()).json({ received: true });This is useful for:
- Security-conscious users who don't want email content stored on our servers
- Storage-conscious users who want to minimize their footprint
Important: This requires both the dashboard setting enabled and the header sent. We made it a double-confirmation on purpose.
Again, if you have this setting on and send this header, your email content will be gone forever (on purpose - you told us to!). Only use this feature if you can durably guarantee you've processed the email. If your handler crashes after returning 200 but before you've saved the data, that email is gone.
Working with Raw Email
Small emails include raw content inline. Large emails (>256KB) must be downloaded:
import {
isRawIncluded,
decodeRawEmail,
verifyRawEmailDownload
} from 'mymx';
if (isRawIncluded(event)) {
// Inline - decode directly (verifies SHA-256 hash)
const raw = decodeRawEmail(event);
} else {
// Large - download and verify
const response = await fetch(event.email.content.download.url);
const buffer = Buffer.from(await response.arrayBuffer());
const raw = verifyRawEmailDownload(buffer, event);
}
// raw is a Buffer containing the RFC 5322 emailAdvanced Usage
For more control, you can use the lower-level functions:
import {
verifyWebhookSignature,
parseWebhookEvent,
WebhookVerificationError
} from 'mymx';
// Step 1: Verify signature
verifyWebhookSignature({
rawBody: req.body,
signatureHeader: req.headers['mymx-signature'],
secret: process.env.MYMX_WEBHOOK_SECRET,
});
// Step 2: Parse (lightweight, no Zod validation)
const event = parseWebhookEvent(JSON.parse(req.body));Strict Validation with Zod
For full schema validation:
import { validateEmailReceivedEvent } from 'mymx/zod';
const event = validateEmailReceivedEvent(JSON.parse(rawBody));Test Fixtures with mymx/contract
Build test fixtures that match real payloads exactly:
import { buildEmailReceivedEvent, signWebhookPayload } from 'mymx/contract';
const event = buildEmailReceivedEvent({
email_id: 'email-123',
endpoint_id: 'endpoint-456',
// ... other fields
});
// Sign it for testing
const body = JSON.stringify(event);
const { header } = signWebhookPayload(body, 'your-test-secret');Framework Integration
Express
Use express.raw() to get the raw body - do NOT use express.json():
import express from 'express';
import { handleWebhook, MyMXWebhookError } from 'mymx';
const app = express();
// Use express.raw() to get the raw body for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = handleWebhook({
body: req.body,
headers: req.headers,
secret: process.env.MYMX_WEBHOOK_SECRET!,
});
// Process event...
res.json({ received: true });
} catch (err) {
if (err instanceof MyMXWebhookError) {
return res.status(400).json({ error: err.code });
}
throw err;
}
});Next.js App Router
// app/api/webhook/route.ts
import { handleWebhook, MyMXWebhookError } from 'mymx';
export async function POST(request: Request) {
try {
const body = await request.text();
const event = handleWebhook({
body,
headers: request.headers,
secret: process.env.MYMX_WEBHOOK_SECRET!,
});
// Process event...
return Response.json({ received: true });
} catch (err) {
if (err instanceof MyMXWebhookError) {
return Response.json({ error: err.code }, { status: 400 });
}
throw err;
}
}Cloudflare Workers
import { handleWebhook, MyMXWebhookError } from 'mymx';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const body = await request.text();
const event = handleWebhook({
body,
headers: request.headers,
secret: env.MYMX_WEBHOOK_SECRET,
});
// Process event...
return Response.json({ received: true });
} catch (err) {
if (err instanceof MyMXWebhookError) {
return Response.json({ error: err.code }, { status: 400 });
}
throw err;
}
},
};Error Handling
All errors extend MyMXWebhookError, so you can catch them all with a single instanceof check:
| Error | When |
|-------|------|
| WebhookVerificationError | Signature verification failed |
| WebhookPayloadError | JSON parsing failed |
| WebhookValidationError | Zod schema validation failed |
Each error has:
code- Programmatic error code for monitoringmessage- Human-readable descriptionsuggestion- Actionable fix
Server-Side Contract (mymx/contract)
If you're building a service that sends MyMX-compatible webhooks, use mymx/contract for:
- Build-time type safety - Your server won't compile if you break the webhook contract
- Test fixture generation - Build payloads that match real webhooks exactly
- Webhook signing - Sign payloads for testing or internal use
See contract/README.md for full documentation.
import { buildEmailReceivedEvent, signWebhookPayload } from 'mymx/contract';
const event = buildEmailReceivedEvent({ /* ... */ });
const { header } = signWebhookPayload(JSON.stringify(event), secret);License
MIT
