Fastify

Send transactional emails from your Fastify 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 a route that sends a transactional email:

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

const fastify = Fastify({ logger: true });

fastify.post('/api/send-welcome', async (request, reply) => {
  const { email, name } = request.body as { email: string; name: string };

  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>`,
  });

  return { emailId: result.id };
});

fastify.listen({ port: 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
fastify.post('/api/send-invoice', async (request, reply) => {
  const { email, invoiceId, amount } = request.body as {
    email: string;
    invoiceId: string;
    amount: number;
  };

  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(),
    },
  });

  return { emailId: result.id };
});

5. Handle Webhooks

Receive real-time event notifications. Use Fastify's rawBody option to access the raw request body for signature verification.

src/webhooks.ts
import Fastify from 'fastify';
import fastifyRawBody from 'fastify-raw-body';
import crypto from 'crypto';

const fastify = Fastify({ logger: true });

// Register the raw body plugin
await fastify.register(fastifyRawBody, {
  field: 'rawBody',
  global: false,
  runFirst: true,
});

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)
  );
}

fastify.post('/webhooks/veilmail', {
  config: { rawBody: true },
}, async (request, reply) => {
  const signature = request.headers['x-veilmail-signature'] as string;
  const payload = request.rawBody as string;

  if (!signature || !verifySignature(payload, signature, process.env.VEILMAIL_WEBHOOK_SECRET!)) {
    return reply.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;
  }

  return reply.status(200).send('OK');
});

Important: Install fastify-raw-body to access the unparsed request body. Without it, Fastify parses JSON automatically and the signature check will fail.

6. Manage Subscribers

Add and manage subscribers from your API:

src/routes/subscribers.ts
// Add a subscriber when a user signs up
fastify.post('/api/subscribe', async (request, reply) => {
  const { email, firstName, lastName } = request.body as {
    email: string;
    firstName: string;
    lastName: string;
  };

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

  return { subscriberId: subscriber.id };
});

// Unsubscribe
fastify.post('/api/unsubscribe', async (request, reply) => {
  const { subscriberId } = request.body as { subscriberId: string };

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

  return { 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';

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