Fastify
Send transactional emails from your Fastify API using the Veil Mail SDK.
Prerequisites
- Fastify 4 or later
- Node.js 18+
- A Veil Mail API key
- A verified domain
1. Install the SDK
npm install @resonia/veilmail-sdk2. 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_xxxxxsrc/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' });
}
}
});