Transactional Email API Reference
Endpoint
POST https://transactional.emailelement.com/api/Email
Authentication
Include your API key in the request header:
x-api-key: YOUR_API_KEY
The API key must have Transactional Email API permission set to Full Access. A key without
it gets a 403 with an empty body.
Request Body
Required Fields
| Field | Type | Description |
|---|---|---|
to |
object | The recipient |
to.email |
string | Recipient address. Must be a valid email address |
to.name |
string | Recipient name. Optional |
subject |
string | Email subject line |
transactionRelayId |
integer | The Transactional Relay to send through |
Content (one required)
At least one of these three must be supplied. Leaving all three empty is rejected.
| Field | Type | Description |
|---|---|---|
htmlBody |
string | HTML content. Must be at or under 1 MB |
textBody |
string | Plain text content. Must be at or under 1 MB |
templateId |
integer | The Email Content template to send |
Optional Fields
| Field | Type | Description |
|---|---|---|
from |
object | Overrides the sender |
from.name |
string | Sender name |
from.localPart |
string | The part before the @. 64 characters or fewer, and cannot contain @, a space, a leading or trailing dot, or two dots in a row |
fromName |
string | Kept for older integrations. Use from.name instead |
triggerId |
integer | A trigger to fire for this email. The trigger event must be of type Transactional Email Api |
substitutions |
object | Key-value pairs replaced in the content. See Substitutions |
delaySeconds |
integer | Hold the email before sending. 0 to 43199, so at most just under 12 hours |
attachments |
array | File attachments. See Attachments |
trackId1 to trackId5 |
string | Your own identifiers, returned in webhook event payloads. 255 characters or fewer each |
replyTo, cc and bcc are not supported.
Example: Basic Request
{
"to": {
"name": "Jane Smith",
"email": "jane@example.com"
},
"subject": "Your Order Confirmation",
"htmlBody": "<h1>Hello {firstName}!</h1><p>Your order #{orderId} is confirmed.</p>",
"transactionRelayId": 2,
"substitutions": {
"firstName": "Jane",
"orderId": "12345"
}
}
Example: With Attachments
{
"to": {
"name": "Jane Smith",
"email": "jane@example.com"
},
"subject": "Your Invoice",
"htmlBody": "<p>Please find your invoice attached.</p>",
"transactionRelayId": 2,
"attachments": [
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"content": "base64-encoded-file-content-here"
}
]
}
Attachments
Each attachment object requires:
| Field | Type | Description |
|---|---|---|
filename |
string | File name with extension (e.g., invoice.pdf) |
contentType |
string | MIME type (e.g., application/pdf, image/png, text/csv) |
content |
string | Base64-encoded file content |
The content values of all attachments on one request must add up to 20 MB or less. Content that
is not valid Base64 is rejected.
Responses
Branch on the HTTP status code. None of the responses carry a success flag, so there is nothing
else to branch on. A 403 has an empty body. Every other failure is a problem document sent as
application/problem+json, but the two kinds carry the reason in different places: field-level
rejections put it in an errors object keyed by field path and have no detail, while the rest
put it in detail. Read both.
200 - Email accepted
The email passed validation and was queued. messageId is the run id for this email. Keep it, as
it is the value that ties webhook events back to this request. errorCode is always 0 and
message is always "Ok" on a 200, so neither is a useful branch.
{
"to": "jane.doe@example.com",
"submittedAt": "2026-08-13T17:04:22.1874316Z",
"messageId": "84512300071",
"errorCode": 0,
"message": "Ok"
}
A 200 means the email was validated and queued, not that it was delivered. Suppression, bounce
handling and the sending relay itself run afterwards and can still stop the send, so use the
webhook events for that messageId to confirm delivery.
400 - Request failed validation
One or more fields were rejected before the email was queued. errors is keyed by the field path,
and a single field can carry more than one message. This shape covers every rule above: a missing
to, to.email or subject, a malformed to.email, a missing transactionRelayId, a body or
attachment over its size limit, a delaySeconds outside 0 to 43199, an attachment that is not
Base64, a localPart with prohibited characters, or a trackId over 255 characters.
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"To.Email": [
"The ToEmail field must be a valid email address."
],
"TransactionRelayId": [
"The TransactionRelayId field is required."
]
}
}
400 - No content supplied
None of htmlBody, textBody or templateId was supplied. The same message is repeated under all
three field paths, because any one of them satisfies the rule.
{
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"TemplateId": [
"At least one of HtmlBody, TextBody or TemplateId fields is required."
],
"HtmlBody": [
"At least one of HtmlBody, TextBody or TemplateId fields is required."
],
"TextBody": [
"At least one of HtmlBody, TextBody or TemplateId fields is required."
]
}
}
400 - Referenced record missing or inactive
The request was well formed, but the transactionRelayId, templateId or triggerId it points at
could not be used. detail carries the reason and is one of: "Transactional Relay does not exist
for account.", "Transactional Relay is not active.", "Email Content Template does not exist for
account.", "Email Content Template is not active.", "Trigger does not exist for account.", "Trigger
is not active.", or "Trigger event is not of type Transactional Email Api."
A record belonging to another account is reported as not existing, so the endpoint cannot be used to probe for valid ids.
{
"type": "https://tools.ietf.org/html/rfc7807",
"title": "Bad Request",
"status": 400,
"detail": "Transactional Relay is not active.",
"instance": "/api/Email",
"errors": {
"message": [
"Transactional Relay is not active."
]
},
"traceId": "0HNC1P8QK4T3G:00000001"
}
401 - Missing or invalid API key
The x-api-key header was absent or not recognised. A missing header returns the same shape with
"ApiKey was not provided".
{
"type": "https://tools.ietf.org/html/rfc7235",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid ApiKey",
"instance": "/api/Email",
"errors": {
"message": [
"Invalid ApiKey"
]
},
"traceId": "0HNC1P8QK4T3G:00000001"
}
403 - Insufficient permissions
The API key is valid but does not carry Transactional Email API: Full Access. The response body is empty.
500 - Unexpected error
An unhandled error occurred. detail carries the underlying error text and varies, so do not match
on it. Safe to retry with backoff, but retry with a fresh request rather than assuming the original
was not queued.
{
"type": "https://tools.ietf.org/html/rfc7807",
"title": "Internal Server Error",
"status": 500,
"detail": "<error text>",
"instance": "/api/Email",
"errors": {
"message": [
"<error text>"
]
},
"traceId": "0HNC1P8QK4T3G:00000001"
}
Substitutions
substitutions is a set of key and value pairs that fill in parts of the subject, the HTML body or
the text body at send time. Leave a key out and, when the recipient is on a Contact List, the value
comes from their stored data instead.
How a value goes in depends on what the value is.
Plain values are matched on the key exactly as written
A key made only of letters, digits and underscores is matched as that bare word, and it is deliberately not matched inside braces. A key holding anything else is matched exactly as you wrote it.
So to fill in {email} in the body, the key has to be {email}, braces and all:
{
"to": { "email": "jane.doe@example.com" },
"subject": "Your order",
"transactionRelayId": 42,
"substitutions": {
"{email}": "jane@example.com"
},
"textBody": "Hi {email}"
}
Leave the substitution out and {email} is filled from the contact's stored data instead:
{
"to": { "email": "jane.doe@example.com" },
"subject": "Your order",
"transactionRelayId": 42,
"textBody": "Hi {email}"
}
A custom name works the same way. With "{pet}": "dogs" supplied, Hi {email}, I heard you like {pet}
reads "I heard you like dogs". Without it, {pet} is read from the contact's stored data.
Object and array values are only reachable through handlebars
A value that is an object or an array is never pasted in as text. It is handed to the handlebars pass, so reach into it with double braces and dot notation. Single braces will not find it.
Deep object replacement
{
"htmlBody": "<p>Hello {{user.profile.firstName}} {Last Name}</p>",
"transactionRelayId": 2,
"substitutions": {
"user": { "profile": { "firstName": "Ben" }, "active": false },
"supportPhone": "1-800-555-5555"
}
}
For a contact whose stored Last Name is "Parker", that renders as Hello Ben Parker. Note the two
styles side by side: {{user.profile.firstName}} comes from the substitutions object, {Last Name}
from the contact.
Each loop
{
"htmlBody": "<ol>{{#each user.orderHistory}}<li>You ordered {{this.item}} on {{this.date}}</li>{{/each}}</ol>",
"transactionRelayId": 2,
"substitutions": {
"user": {
"orderHistory": [
{ "date": "2/1/2018", "item": "shoes" },
{ "date": "1/4/2017", "item": "hat" }
]
}
}
}
Renders one list item per order:
You ordered shoes on 2/1/2018
You ordered hat on 1/4/2017
If, else if, else
{
"htmlBody": "{{#if user.profile.male}}<p>Dear Sir</p>{{else if user.profile.female}}<p>Dear Madame</p>{{else}}<p>Dear Customer</p>{{/if}}",
"transactionRelayId": 2,
"substitutions": {
"user": { "profile": { "male": false, "female": false } }
}
}
Both flags are false, so this renders Dear Customer.
Unless
{
"htmlBody": "{{#unless user.active}}<p>Your account is suspended, please call {{supportPhone}}</p>{{/unless}}",
"transactionRelayId": 2,
"substitutions": {
"user": { "active": false },
"supportPhone": "1-800-555-5555"
}
}
each, if, unless and deep object replacement are only available on the Transactional Email
API. They are not part of Email Content replacement tags.
Stored Contact Data
If the recipient email is on a Contact List, their stored field values fill any tag the request did not supply. A substitution sent in the request wins over the stored value for the same name.