Emails

Send transactional and marketing emails with automatic PII protection.

Send an Email

Send a single email to one or more recipients.

POST /v1/emails

Request Body

ParameterTypeDescription
fromstringSender email address (required)
tostring | string[]Recipient email address(es) (required)
subjectstringEmail subject line (required)
htmlstringHTML body content
textstringPlain text body content
ccstring | string[]CC recipients
bccstring | string[]BCC recipients
replyTostringReply-to address
templateIdstringTemplate ID to use
templateDataobjectData for template variable substitution
scheduledForstringISO 8601 datetime for scheduled delivery
tagsstring[]Tags for categorization
metadataobjectCustom key-value metadata
typestring"transactional" (default) or "marketing"
unsubscribeUrlstringCustom unsubscribe URL (auto-generated for marketing)

Example

send-email.ts
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/batch
batch-send.ts
const 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.

send-with-template.ts
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.

schedule-email.ts
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/cancel

Only emails with scheduled status can be cancelled. Returns 400 for non-scheduled emails.

Reschedule Endpoint

PATCH /v1/emails/:id

Update 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-emails.ts
// 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/:id
get-email.ts
const 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); // false

Email Status

Emails progress through the following statuses:

StatusDescription
pendingEmail is being processed
queuedEmail is queued for sending
sendingEmail is being sent
sentEmail was sent successfully
deliveredEmail was delivered to recipient
bouncedEmail bounced (hard or soft)
failedEmail failed to send
cancelledScheduled 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 422 if not configured)
email-types.ts
// 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.

suppression-check.ts
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.

attachments.ts
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

AddressSimulated Behavior
delivered@test.veilmail.xyzSimulates successful delivery (SENT → DELIVERED)
bounced@test.veilmail.xyzSimulates hard bounce (SENT → BOUNCED)
complained@test.veilmail.xyzSimulates spam complaint (SENT → DELIVERED → COMPLAINED)
delayed@test.veilmail.xyzSimulates deferred then delivered (SENT → DEFERRED → DELIVERED)
opened@test.veilmail.xyzSimulates delivery + open (SENT → DELIVERED → OPENED)
clicked@test.veilmail.xyzSimulates delivery + open + click (SENT → DELIVERED → OPENED → CLICKED)

Example

test-emails.ts
// 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 a 400 error
  • Test emails appear in your email list with a test tag
  • 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.

pii-detection.ts
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']
  }
}