Emails
Send transactional emails using Handlebars templates with layouts and partials. Delivery status, the rendered HTML, and every provider event land in Email Logs.
/api/v1/emails/sendemails:sendSend a transactional email using a registered template. Supports idempotency keys to prevent duplicate sends, and an optional sendAt to schedule the send up to 30 days ahead.
Request
const result = await helmdesk.emails.send({
templateKey: 'welcome',
to: { email: 'user@example.com', name: 'Jane' },
variables: { activationUrl: 'https://...' },
}, { idempotencyKey: 'signup-jane-2024' })
// Schedule instead of sending now:
const scheduled = await helmdesk.emails.send({
templateKey: 'trial-expiring',
to: { email: 'user@example.com' },
sendAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // in 3 days
})
// scheduled.status === 'scheduled', scheduled.scheduledAt setResponse
{
"id": "d41f9a6b-88c5-4e2a-b7d0-1e5c3f9a2b64",
"status": "queued",
"to": "user@example.com",
"subject": "Welcome to Acme",
"deduplicated": false,
"blocked": false,
"blockReason": null,
"scheduledAt": null,
"createdAt": "2026-07-21T09:15:00.000Z"
}/api/v1/emails/batchemails:sendSend up to 100 transactional emails in one request. Each item is delivered independently — one failed item does not fail the batch. Idempotency is a per-item field (idempotencyKey) rather than a header. Requires the emails:send scope and the Starter plan or higher.
Request
const result = await helmdesk.emails.sendBatch({
emails: [
{
templateKey: 'welcome',
to: { email: 'a@example.com', name: 'Ann' },
variables: { firstName: 'Ann' },
idempotencyKey: 'welcome-a-2026',
},
{
templateKey: 'welcome',
to: { email: 'b@example.com' },
variables: { firstName: 'Bo' },
},
],
})
// result.total / result.sent / result.blocked / result.failed
// result.results[i] -> { index, id, status, to, ... } on success,
// { index, to, error } on failureResponse
{
"total": 2,
"sent": 1,
"blocked": 0,
"failed": 1,
"results": [
{
"index": 0,
"id": "d41f9a6b-88c5-4e2a-b7d0-1e5c3f9a2b64",
"status": "queued",
"to": "a@example.com",
"subject": "Welcome",
"deduplicated": false,
"blocked": false,
"blockReason": null,
"createdAt": "2026-07-21T09:15:00.000Z"
},
{ "index": 1, "to": "b@example.com", "error": "Template \"welcome\" not found" }
]
}/api/v1/emails/previewemails:sendPreview a rendered email template without sending. Uses sample data if no variables are provided.
Request
const { subject, html } = await helmdesk.emails.preview({
templateKey: 'welcome',
variables: { name: 'Test User' },
})Response
{
"subject": "Welcome, Test User",
"html": "<html><body><h1>Welcome, Test User</h1>…</body></html>"
}/api/v1/email-templatesemails:sendList the project's email templates (with current-version subject and detected variables) plus your account's shared templates (keys already @account/-prefixed). This is how integrations discover valid templateKey values for send and preview. Requires the emails:send scope.
Request
const { templates, accountTemplates } = await helmdesk.emails.templates.list()
// templates[0] -> { key: 'welcome', subject: 'Welcome!', variables: [...] }Response
{
"templates": [
{
"key": "welcome",
"name": "Welcome",
"description": "Subject: Welcome to {{brandName}}",
"subject": "Welcome to {{brandName}}",
"variables": ["name", "activationUrl"],
"updatedAt": "2026-07-20T16:31:12.004Z"
}
],
"accountTemplates": [
{
"key": "@account/invoice-paid",
"name": "Invoice Paid",
"description": null,
"subject": "Your invoice is paid",
"updatedAt": "2026-06-02T10:00:00.000Z"
}
]
}/api/v1/email-templates/:key/schemaemails:manageThe variables a template expects, detected from its current version. Built-in branding variables (brandName, brandLogoUrl, supportEmail, …) are excluded — Helmdesk fills those in at send time.
Request
const schema = await helmdesk.emails.getTemplateSchema('welcome')
// schema.variables -> ['name', 'activationUrl']Response
{
"key": "welcome",
"name": "Welcome",
"subject": "Welcome to {{brandName}}",
"variables": ["name", "activationUrl"]
}Creating templates, and wrapping them in a layout
Templates are created by importing a .hbs file, not by a JSON POST. The import call is also where you attach a layout — see Email Templates for the full flow, including why a template can come back with layoutLinked: false.
Batch sending
POST /api/v1/emails/batch accepts an emails array (min 1, max 100 items). Each item has the same shape as a single send — templateKey and to.email are required; to.name, variables, and environment are optional — plus an optional per-item idempotencyKey (batch idempotency is a field on each item, not the Idempotency-Key header used by single send).
The response returns summary counts (total, sent, blocked, failed) plus a results array. Each item is delivered independently — a failed item does not fail the batch. On success a slot looks like { index, id, status, to, subject, deduplicated, blocked, blockReason, createdAt }; on failure it is { index, to, error }. Always inspect each result's error and blocked fields. Batch sending requires the emails:send scope and the Starter plan or higher (the Free plan returns 403). One batch counts as a single request against the 100 requests/minute rate limit.
Via MCP, use the send_email_batch tool with an emails array where each item is { templateKey, to_email, to_name?, variables?, idempotencyKey? }.
Scheduled sends
Pass sendAt (ISO timestamp or Date, up to 30 days ahead) on a single or batch send. The email renders immediately and waits in status scheduled; suppression, pause, and allowlist rules are re-checked at fire time. A scheduled send can be canceled from Email Logs any time before it fires. A past sendAt sends immediately.
Environments & sending modes
Which data plane a send lands in is decided by your API key — sk_sandbox_ keys capture emails without delivering them (see Sandbox). For live sending, modes include live, test recipient (redirects to a single address), allowlist (only certain domains), and paused (mail is held until you resume).
See Email Templates for the template import API and shared template system.