Express

Send transactional emails from your Express.js API using the Veil Mail SDK.

Prerequisites

1. Install the SDK

npm install @resonia/veilmail-sdk

2. Configure the Client

Create a shared client instance. Store your API key in an environment variable — never commit it to source control.

.env
VEILMAIL_API_KEY=veil_live_xxxxx
VEILMAIL_WEBHOOK_SECRET=whsec_xxxxx
src/lib/veilmail.ts
import { VeilMail } from '@resonia/veilmail-sdk';

export const veilmail = new VeilMail({
  apiKey: process.env.VEILMAIL_API_KEY,
});

3. Send Your First Email

Add an endpoint that sends a transactional email:

src/index.ts
import express from 'express';
import { veilmail } from './lib/veilmail';

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

app.post('/api/send-welcome', async (req, res) => {
  try {
    const { email, name } = req.body;

    const result = await veilmail.emails.send({
      from: 'hello@yourdomain.com',
      to: email,
      subject: `Welcome, ${name}!`,
      html: `<h1>Welcome</h1><p>Hi ${name}, thanks for joining.</p>`,
    });

    res.json({ emailId: result.id });
  } catch (error) {
    console.error('Failed to send email:', error);
    res.status(500).json({ error: 'Failed to send email' });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

4. Send with a Template

Use templates to manage email content outside your code:

src/routes/billing.ts
app.post('/api/send-invoice', async (req, res) => {
  const { email, invoiceId, amount } = req.body;

  const result = await veilmail.emails.send({
    from: 'billing@yourdomain.com',
    to: email,
    subject: `Invoice #${invoiceId}`,
    templateId: 'template_xxxxx',
    templateData: {
      invoiceId,
      amount: `$${amount}`,
      dueDate: new Date(Date.now() + 30 * 86400000).toLocaleDateString(),
    },
  });

  res.json({ emailId: result.id });
});

5. Handle Webhooks

Receive real-time event notifications. Use express.raw() to get the raw body for signature verification.

src/webhooks.ts
import crypto from 'crypto';
import express from 'express';

function verifySignature(payload: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Important: use express.raw() so the body isn't parsed as JSON
app.post(
  '/webhooks/veilmail',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-veilmail-signature'] as string;
    const payload = req.body.toString();

    if (!signature || !verifySignature(payload, signature, process.env.VEILMAIL_WEBHOOK_SECRET!)) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);

    switch (event.type) {
      case 'email.delivered':
        console.log('Delivered:', event.data.emailId);
        break;
      case 'email.bounced':
        console.log('Bounced:', event.data.emailId);
        // Remove from mailing list or flag user
        break;
      case 'email.complained':
        console.log('Complaint:', event.data.emailId);
        // Unsubscribe the user
        break;
      case 'subscriber.unsubscribed':
        console.log('Unsubscribed:', event.data.subscriberId);
        break;
    }

    res.status(200).send('OK');
  }
);

Important: The webhook route must use express.raw() instead of express.json() so the body is available as a raw Buffer for signature verification.

6. Manage Subscribers

Add and manage subscribers from your API:

src/routes/subscribers.ts
// Add a subscriber when a user signs up
app.post('/api/subscribe', async (req, res) => {
  const { email, firstName, lastName } = req.body;

  const subscriber = await veilmail.audiences
    .subscribers('audience_xxxxx')
    .add({
      email,
      firstName,
      lastName,
      metadata: { source: 'website' },
    });

  res.json({ subscriberId: subscriber.id });
});

// Unsubscribe
app.post('/api/unsubscribe', async (req, res) => {
  const { subscriberId } = req.body;

  await veilmail.audiences
    .subscribers('audience_xxxxx')
    .remove(subscriberId);

  res.json({ success: true });
});

7. Error Handling

The SDK throws typed errors you can catch and handle:

src/routes/email.ts
import { VeilMail, VeilMailError, RateLimitError } from '@resonia/veilmail-sdk';

app.post('/api/send', async (req, res) => {
  try {
    const result = await veilmail.emails.send({
      from: 'hello@yourdomain.com',
      to: req.body.email,
      subject: 'Hello',
      html: '<p>Hi!</p>',
    });
    res.json({ id: result.id });
  } catch (error) {
    if (error instanceof RateLimitError) {
      res.status(429).json({ error: 'Rate limited, try again later' });
    } else if (error instanceof VeilMailError) {
      res.status(error.statusCode).json({ error: error.message });
    } else {
      res.status(500).json({ error: 'Internal error' });
    }
  }
});