Emails
Send transactional and marketing emails with automatic PII protection.
Send an Email
Send a single email to one or more recipients.
POST /v1/emailsRequest Body
| Parameter | Type | Description |
|---|---|---|
| from | string | Sender email address (required) |
| to | string | string[] | Recipient email address(es) (required) |
| subject | string | Email subject line (required) |
| html | string | HTML body content |
| text | string | Plain text body content |
| cc | string | string[] | CC recipients |
| bcc | string | string[] | BCC recipients |
| replyTo | string | Reply-to address |
| templateId | string | Template ID to use |
| templateData | object | Data for template variable substitution |
| scheduledFor | string | ISO 8601 datetime for scheduled delivery |
| tags | string[] | Tags for categorization |
| metadata | object | Custom key-value metadata |
| type | string | "transactional" (default) or "marketing" |
| unsubscribeUrl | string | Custom unsubscribe URL (auto-generated for marketing) |
Example
const email = await client.emails.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome to our platform!',
html: `
<h1>Welcome, {{name}}!</h1>
<p>Thanks for signing up.</p>
`,
templateData: {
name: 'John',
},
tags: ['welcome', 'onboarding'],
});Batch Send
Send up to 100 emails in a single API call. Each email is processed independently with per-email error reporting.
POST /v1/emails/batchconst result = await client.emails.sendBatch([
{
from: 'hello@yourdomain.com',
to: 'user1@example.com',
subject: 'Hello User 1',
html: '<p>Welcome!</p>',
},
{
from: 'hello@yourdomain.com',
to: 'user2@example.com',
subject: 'Hello User 2',
html: '<p>Welcome!</p>',
},
]);
console.log(result.successful); // 2
console.log(result.failed); // 0
console.log(result.results);
// [
// { index: 0, id: 'email_xxx', status: 'sent' },
// { index: 1, id: 'email_yyy', status: 'sent' },
// ]Returns 207 Multi-Status with per-email results. Failed emails include an error object with code and message. The total recipient count is checked against your email limit upfront.
Send with Template
Use a pre-defined template instead of providing HTML inline.
const email = await client.emails.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
templateId: 'template_xxxxx',
templateData: {
firstName: 'John',
orderNumber: '12345',
items: [
{ name: 'Widget', price: 29.99 },
{ name: 'Gadget', price: 49.99 },
],
},
});Schedule Email
Schedule an email to be sent at a specific time.
const email = await client.emails.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
subject: 'Your weekly digest',
html: '<h1>Weekly Digest</h1>',
scheduledFor: '2024-12-25T09:00:00Z',
});
// Cancel a scheduled email
await client.emails.cancel(email.id);
// Reschedule to a different time
await client.emails.update(email.id, {
scheduledFor: '2025-01-01T09:00:00Z',
});Cancel Endpoint
POST /v1/emails/:id/cancelOnly emails with scheduled status can be cancelled. Returns 400 for non-scheduled emails.
Reschedule Endpoint
PATCH /v1/emails/:idUpdate a scheduled email. Currently supports changing scheduledFor to a future time. Returns 400 if the new time is in the past.
List Emails
Retrieve a list of emails with optional filters.
GET /v1/emails// List all emails
const { data, hasMore, nextCursor } = await client.emails.list();
// Filter by status
const delivered = await client.emails.list({
status: 'delivered',
limit: 50,
});
// Paginate through results
let cursor;
do {
const page = await client.emails.list({ cursor, limit: 100 });
console.log(page.data);
cursor = page.nextCursor;
} while (cursor);Get Email
Retrieve a single email by ID.
GET /v1/emails/:idconst email = await client.emails.get('email_xxxxx');
console.log(email.status); // 'delivered'
console.log(email.sentAt); // '2024-01-01T12:00:00Z'
console.log(email.piiDetected); // falseEmail Status
Emails progress through the following statuses:
| Status | Description |
|---|---|
pending | Email is being processed |
queued | Email is queued for sending |
sending | Email is being sent |
sent | Email was sent successfully |
delivered | Email was delivered to recipient |
bounced | Email bounced (hard or soft) |
failed | Email failed to send |
cancelled | Scheduled email was cancelled |
Email Type Classification
Set the type field to marketing for commercial emails. Marketing emails automatically get compliance features:
- List-Unsubscribe and List-Unsubscribe-Post headers
- Unsubscribe footer with your physical address
- Physical address enforcement (returns
422if not configured)
// Marketing email with automatic compliance
const email = await client.emails.send({
from: 'news@yourdomain.com',
to: 'subscriber@example.com',
subject: 'Monthly Newsletter',
html: '<h1>December Updates</h1>',
type: 'marketing',
});
// Custom unsubscribe URL (for transactional emails with opt-in compliance)
const email2 = await client.emails.send({
from: 'updates@yourdomain.com',
to: 'user@example.com',
subject: 'Product Update',
html: '<h1>New Features</h1>',
unsubscribeUrl: 'https://yourdomain.com/unsubscribe?token=abc',
});Pre-Send Suppression Check
All emails are checked against your organization's suppression list before sending. If any recipient is suppressed (bounced, unsubscribed, complained), the request returns a 422 with error code recipient_suppressed.
try {
await client.emails.send({
from: 'hello@yourdomain.com',
to: 'bounced-user@example.com',
subject: 'Hello!',
html: '<h1>Hi there</h1>',
});
} catch (error) {
// error.code === 'recipient_suppressed'
// error.details.suppressedRecipients === [
// { email: 'bounced-user@example.com', reason: 'hard_bounce' }
// ]
}Breaking Change
Emails to suppressed recipients that previously would have been accepted are now rejected with a 422 error. Remove suppressed recipients before sending, or use the POST /v1/suppressions/check endpoint to pre-filter.
Attachments
Attach files to emails using base64-encoded content or a URL. Maximum 10 attachments, 10MB total size.
import { readFileSync } from 'fs';
const pdf = readFileSync('invoice.pdf');
const email = await client.emails.send({
from: 'billing@yourdomain.com',
to: 'customer@example.com',
subject: 'Your Invoice',
html: '<p>Please find your invoice attached.</p>',
attachments: [
{
filename: 'invoice.pdf',
content: pdf.toString('base64'),
contentType: 'application/pdf',
},
],
});
// Inline image attachment
const email2 = await client.emails.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
subject: 'Check this out',
html: '<p>Here is the image: <img src="cid:logo" /></p>',
attachments: [
{
filename: 'logo.png',
content: logoBase64,
contentType: 'image/png',
contentId: 'logo',
},
],
});Limits & Restrictions
- Maximum 10 attachments per email
- Maximum 10MB total attachment size
- URL-based attachments must use HTTPS
- Executable content types are blocked (e.g., .exe, .sh, .bat)
- Attachment filenames are scanned for PII
Test Email Addresses
Use special test addresses to simulate email delivery behaviors without sending real emails. Test emails skip PII scanning, suppression checks, and don't count against your email limits.
Testing Made Easy
Send to any of the addresses below using your existing client.emails.send() call. No SDK changes needed — test emails are completely transparent.
Available Test Addresses
| Address | Simulated Behavior |
|---|---|
| delivered@test.veilmail.xyz | Simulates successful delivery (SENT → DELIVERED) |
| bounced@test.veilmail.xyz | Simulates hard bounce (SENT → BOUNCED) |
| complained@test.veilmail.xyz | Simulates spam complaint (SENT → DELIVERED → COMPLAINED) |
| delayed@test.veilmail.xyz | Simulates deferred then delivered (SENT → DEFERRED → DELIVERED) |
| opened@test.veilmail.xyz | Simulates delivery + open (SENT → DELIVERED → OPENED) |
| clicked@test.veilmail.xyz | Simulates delivery + open + click (SENT → DELIVERED → OPENED → CLICKED) |
Example
// Test a successful delivery
const email = await client.emails.send({
from: 'hello@yourdomain.com',
to: 'delivered@test.veilmail.xyz',
subject: 'Test delivery',
html: '<p>Testing!</p>',
});
console.log(email.status); // 'delivered'
// Test a bounce
const bounced = await client.emails.send({
from: 'hello@yourdomain.com',
to: 'bounced@test.veilmail.xyz',
subject: 'Test bounce',
html: '<p>This will bounce</p>',
});
// Check events
const detail = await client.emails.get(bounced.id);
console.log(detail.events);
// [{ type: 'sent', ... }, { type: 'bounced', ... }]Important Notes
- You cannot mix test addresses with real email addresses in the same request
- Unknown local parts (e.g.,
invalid@test.veilmail.xyz) return a400error - Test emails appear in your email list with a
testtag - Rate limiting still applies to test email requests
PII Detection
Emails are automatically scanned for PII before sending. The behavior depends on your account settings:
PII Blocking Mode
When enabled, emails containing PII will be blocked and return a 422 error with details about the detected PII types.
import { PiiDetectedError } from '@resonia/veilmail-sdk';
try {
await client.emails.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
subject: 'Your order',
html: 'Your SSN is 123-45-6789', // Contains PII!
});
} catch (error) {
if (error instanceof PiiDetectedError) {
console.log('PII detected:', error.piiTypes);
// ['US_SOCIAL_SECURITY_NUMBER']
}
}