Transactional Email API
Send transactional and campaign email over HTTP. Use this API to send a single message, a batch, a pre-built template, or one message addressed to many recipients at once — then track what happened to it.
A successful send returns 202 Accepted and a QueueId. That means we have taken the message, not that it reached the inbox. Delivery outcome comes from Check delivery status — never from the send response.
EmailBody is Base64-encoded HTML on every endpoint that accepts one. Sending raw HTML is rejected with 417 and the message “Email body should be in base64”.
Copy or download this guide as Markdown to paste into an AI assistant for help integrating against it.
Environment
You are reading the guide for the host that served this page. The badge in the header and every example below already point at it — there is nothing to substitute by hand, and no other environment's addresses appear on this page.
| Property | Value |
|---|---|
| Environment | |
| Base URL | https://email-dev.jirafix.net |
| All paths | are under /v1/email |
Authentication
Every endpoint needs a bearer token, and this host does not issue one. Exchange your credentials at the Authenticate API — on this environment that is https://authenticate-dev.jirafix.net — then send what it returns as Authorization: Bearer <access_token> on each request here.
Credentials embedded in a browser page or a mobile app are published credentials — anyone can read them and send mail as you. Request the token from your own backend and never ship it to a client.
Exchanges your credentials for an access token.
Tokens last 3600 seconds by default. Request a new one when it expires — there is no separate refresh call, though the response does include a refresh_token. Full detail is on the Authenticate guide at https://authenticate-dev.jirafix.net/docs.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
username | string | Yes | The account's username, usually an email address. |
password | string | Yes | The account's password. Server-side only. |
privatetoken | string | Yes | Your account's private token, from the portal's configuration section. Note the spelling — one word, all lower case. |
validity | integer | No | How long the token should last, in seconds. Defaults to 3600. |
Responses
| Status | Meaning |
|---|---|
200 | Returns access_token, refresh_token, token_type and expires_in. |
400 | The body was missing or a required field was absent. |
401 | The username, password or private token was not accepted. |
# 1. get a token from the Authenticate host
ACCESS_TOKEN=$(curl -s -X POST https://authenticate-dev.jirafix.net/v1/token \
-H "Content-Type: application/json" \
-d '{"username":"you@yourcompany.com","password":"'"$OLANZO_PASSWORD"'","privatetoken":"'"$OLANZO_PRIVATE_TOKEN"'"}' \
| jq -r .access_token)
# 2. spend it here
curl -X GET "https://email-dev.jirafix.net/v1/email/lists" \
-H "Authorization: Bearer $ACCESS_TOKEN"
using var auth = new HttpClient { BaseAddress = new Uri("https://authenticate-dev.jirafix.net") };
var tokenResponse = await auth.PostAsJsonAsync("/v1/token", new
{
username = "you@yourcompany.com",
password = Environment.GetEnvironmentVariable("OLANZO_PASSWORD"),
privatetoken = Environment.GetEnvironmentVariable("OLANZO_PRIVATE_TOKEN"),
});
// the property is access_token, not accessToken
var payload = await tokenResponse.Content.ReadFromJsonAsync<JsonElement>();
var token = payload.GetProperty("access_token").GetString();
using var api = new HttpClient { BaseAddress = new Uri("https://email-dev.jirafix.net") };
api.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var response = await api.SendAsync(
new HttpRequestMessage(HttpMethod.Get, "/v1/email/lists"));
// 1. get a token from the Authenticate host
const tokenResponse = await fetch("https://authenticate-dev.jirafix.net/v1/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: "you@yourcompany.com",
password: process.env.OLANZO_PASSWORD,
privatetoken: process.env.OLANZO_PRIVATE_TOKEN,
}),
});
// note the underscore — accessToken is undefined
const { access_token } = await tokenResponse.json();
// 2. spend it here
const response = await fetch("https://email-dev.jirafix.net/v1/email/lists", {
method: "GET",
headers: { Authorization: `Bearer ${access_token}` },
});
import os, requests
# 1. get a token from the Authenticate host
token_response = requests.post(
"https://authenticate-dev.jirafix.net/v1/token",
json={
"username": "you@yourcompany.com",
"password": os.environ["OLANZO_PASSWORD"],
"privatetoken": os.environ["OLANZO_PRIVATE_TOKEN"],
},
)
# note the underscore — "accessToken" raises KeyError
access_token = token_response.json()["access_token"]
# 2. spend it here
response = requests.get(
"https://email-dev.jirafix.net/v1/email/lists",
headers={"Authorization": f"Bearer {access_token}"},
)
200 OK
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
The fields are access_token, refresh_token, token_type and expires_in — not accessToken or expiresIn. Reading the camelCase spelling gives you nothing, with no error to explain it.
A token carries the API domains it is allowed to reach. One issued by a different environment's Authenticate host is rejected here with a 401 that reads like bad credentials, so check the pair before you check your password: this page is https://email-dev.jirafix.net and its Authenticate host is https://authenticate-dev.jirafix.net.
Send your first email
Base64-encode your HTML body, then post it. The response gives you a QueueId to track the send.
curl -X POST https://email-dev.jirafix.net/v1/email/send \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"SenderFrom": { "Email": "no-reply@yourcompany.com", "DisplayName": "Your Company" },
"ReplyTo": { "Email": "support@yourcompany.com" },
"Subject": "Your order is confirmed",
"EmailBody": "PHA+VGhhbmsgeW91IGZvciB5b3VyIG9yZGVyLjwvcD4=",
"Recipient": { "To": "customer@example.com" }
}'
202 Accepted
{
"QueueId": "9f1c7d2e-4b3a-4c1d-9e8f-2a7b6c5d4e3f",
"QueuedTimestamp": 1755590400000
}
Sending email
Five ways to send. They differ in who receives the message and where the content comes from — pick by the row that matches your case.
| Use this | When | Recipients |
|---|---|---|
/send | One person, your own HTML | One |
/single | One person, a saved campaign or attachments | One |
/Single/ToMultipleRecipients | The same message to several addresses in one call | Many, one message |
/batch | A list, or up to 1,000 individually personalised recipients | Up to 1,000 |
/predefinedemail | A template already built in the portal | One or many |
Send to one recipient
Sends one message to one recipient, using HTML you supply.
The simplest send, and the right default for order confirmations, password resets and one-off alerts.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
SenderFrom | object | Yes | Who the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417. |
ReplyTo | object | Yes | Where replies go. Use ReplyToList instead for several addresses. |
Subject | string | Yes | Subject line. |
PreHeaderText | string | No | Preview line shown after the subject in most mail apps. |
EmailBody | string | Yes | Base64-encoded HTML. Raw HTML is rejected with 417. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped. |
Frequency | string | No | Repeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it. |
WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
Recipient.To | string | Yes | The recipient's address. |
Recipient.CcEmails | array | No | Copied addresses. Note the name — it is CcEmails, not Ccs. |
Recipient.BCcEmails | array | No | Blind-copied addresses. |
Recipient.PersonalizationSubstitutionTags | array | No | Values substituted into the body, for greeting someone by name. Omit if you do not personalise. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The request failed field validation before it was processed — a missing SenderFrom, ReplyTo, Subject or recipient, or a value of the wrong type. The body names the offending fields. Rules applied after the request binds, including the Base64 body check, come back as 417 instead. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message naming the problem. Retrying the same request unchanged will fail again. |
424 | We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying. |
curl -X POST https://email-dev.jirafix.net/v1/email/send \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"SenderFrom": { "Email": "no-reply@yourcompany.com", "DisplayName": "Your Company" },
"ReplyTo": { "Email": "support@yourcompany.com" },
"Subject": "Your order is confirmed",
"EmailBody": "PHA+VGhhbmsgeW91IGZvciB5b3VyIG9yZGVyLjwvcD4=",
"Recipient": {
"To": "customer@example.com",
"CcEmails": ["records@yourcompany.com"]
}
}'
Send a campaign or add attachments
Sends one message to one recipient, from a saved campaign or with attachments.
Everything /send accepts, plus EmailCampaignId and Attachments. Supply either an EmailBody or an EmailCampaignId — with neither, the request is rejected.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
SenderFrom | object | Yes | Who the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417. |
ReplyTo | object | Yes | Where replies go. Use ReplyToList instead for several addresses. |
Subject | string | Yes | Subject line. |
PreHeaderText | string | No | Preview line shown after the subject in most mail apps. |
EmailBody | string | Yes | Base64-encoded HTML. Raw HTML is rejected with 417. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped. |
Frequency | string | No | Repeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it. |
WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
EmailCampaignId | guid | No | A campaign built in the portal, used instead of EmailBody. |
Attachments | array | No | Each needs Content (Base64), Filename, Type (the MIME type, e.g. application/pdf) and Disposition, which must be exactly attachment or inline. The whole request, attachments included, must stay under 39 MB. |
Recipient.To | string | Yes | The recipient's address. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The request failed field validation before it was processed — a missing SenderFrom, ReplyTo, Subject or recipient, or a value of the wrong type. The body names the offending fields. Rules applied after the request binds, including the Base64 body check, come back as 417 instead. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message naming the problem. Retrying the same request unchanged will fail again. |
424 | We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying. |
curl -X POST https://email-dev.jirafix.net/v1/email/single \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"SenderFrom": { "Email": "no-reply@yourcompany.com" },
"ReplyTo": { "Email": "support@yourcompany.com" },
"Subject": "Your invoice",
"EmailBody": "PHA+SW52b2ljZSBhdHRhY2hlZC48L3A+",
"Recipient": { "To": "customer@example.com" },
"Attachments": [{
"Content": "<base64-of-the-file>",
"Filename": "invoice-1042.pdf",
"Type": "application/pdf",
"Disposition": "attachment"
}]
}'
Send one message to several addresses
Sends the same message to a list of addresses in one call.
Use this when the content is identical for everyone. If each person needs different content, use batch instead — this endpoint cannot personalise per recipient.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
SenderFrom | object | Yes | Who the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417. |
ReplyTo | object | Yes | Where replies go. Use ReplyToList instead for several addresses. |
Subject | string | Yes | Subject line. |
PreHeaderText | string | No | Preview line shown after the subject in most mail apps. |
EmailBody | string | Yes | Base64-encoded HTML. Raw HTML is rejected with 417. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped. |
Frequency | string | No | Repeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it. |
WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
Recipients.To | array | Yes | The addresses to send to. |
Recipients.CcEmails | array | No | Copied addresses. |
Recipients.BCcEmails | array | No | Blind-copied addresses. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The request failed field validation before it was processed — a missing SenderFrom, ReplyTo, Subject or recipient, or a value of the wrong type. The body names the offending fields. Rules applied after the request binds, including the Base64 body check, come back as 417 instead. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message naming the problem. Retrying the same request unchanged will fail again. |
424 | We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying. |
curl -X POST https://email-dev.jirafix.net/v1/email/Single/ToMultipleRecipients \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"SenderFrom": { "Email": "no-reply@yourcompany.com" },
"ReplyTo": { "Email": "support@yourcompany.com" },
"Subject": "Scheduled maintenance this Sunday",
"EmailBody": "PHA+V2Ugd2lsbCBiZSBvZmZsaW5lIGJyaWVmbHkuPC9wPg==",
"Recipients": { "To": ["a@example.com", "b@example.com"] }
}'
Send a batch
Sends to a saved list, or to recipients you supply — each personalised individually.
Unlike the other sends, batch nests the sending fields under SendingDetails — see the example. Give either SendingDetails.ListId or a Recipients array. With neither, the request is rejected.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
SendingDetails.SenderFrom | object | Yes | Who the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417. |
SendingDetails.ReplyTo | object | Yes | Where replies go. Use ReplyToList instead for several addresses. |
SendingDetails.Subject | string | Yes | Subject line. |
SendingDetails.PreHeaderText | string | No | Preview line shown after the subject in most mail apps. |
SendingDetails.EmailBody | string | Yes | Base64-encoded HTML. Raw HTML is rejected with 417. |
SendingDetails.ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped. |
SendingDetails.Frequency | string | No | Repeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it. |
SendingDetails.WebHookUrl | string | No | We post delivery reports here as they happen, so you do not have to poll. |
SendingDetails.ListId | array | No | Subscriber lists to send to. Required unless you supply Recipients. |
Recipients | array | No | Up to 1,000 per call. Every entry needs a To, and every To must be unique — duplicates are rejected and the response lists the offending addresses. Required unless you supply a ListId. |
Recipients[].PersonalizationSubstitutionTags | array | No | Per-recipient values substituted into the body — this is what batch gives you that the multi-address send does not. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The request failed field validation before it was processed — a missing SenderFrom, ReplyTo, Subject or recipient, or a value of the wrong type. The body names the offending fields. Rules applied after the request binds, including the Base64 body check, come back as 417 instead. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message naming the problem. Retrying the same request unchanged will fail again. |
424 | We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying. |
curl -X POST https://email-dev.jirafix.net/v1/email/batch \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"SendingDetails": {
"SenderFrom": { "Email": "no-reply@yourcompany.com" },
"ReplyTo": { "Email": "support@yourcompany.com" },
"Subject": "Your statement is ready",
"EmailBody": "PHA+SGVsbG8ge3tmaXJzdE5hbWV9fTwvcD4="
},
"Recipients": [
{ "To": "a@example.com" },
{ "To": "b@example.com" }
]
}'
Send a saved template
Sends a template already built in the portal.
The template owns the subject, body and sender, so you supply only who it goes to. Find template ids with List trigger templates.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
PredefinedTemplateId | string | Yes | The template to send, e.g. TEMAIL-822. |
Recipients | array | No | Each entry needs a To. Required unless you supply a ListId. |
ListId | array | No | Send to saved lists instead of explicit recipients. |
CcEmails | array | No | Copied addresses. |
BCcEmails | array | No | Blind-copied addresses. |
ScheduleDateTime | date-time | No | Send later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped. |
Frequency | string | No | Repeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it. |
Responses
| Status | Meaning |
|---|---|
202 | Accepted and queued. Returns QueueId and QueuedTimestamp. |
400 | The request failed field validation before it was processed — a missing SenderFrom, ReplyTo, Subject or recipient, or a value of the wrong type. The body names the offending fields. Rules applied after the request binds, including the Base64 body check, come back as 417 instead. |
401 | Missing, expired or invalid bearer token. |
417 | A rule rejected the request — see Errors. The body carries status: 0 and a message naming the problem. Retrying the same request unchanged will fail again. |
424 | We could not queue the message. Nothing was sent and no QueueId exists — this one is worth retrying. |
curl -X POST https://email-dev.jirafix.net/v1/email/predefinedemail \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"PredefinedTemplateId": "TEMAIL-822",
"Recipients": [{ "To": "customer@example.com" }]
}'
Check delivery status
Take the QueueId from any send and ask what happened to it. This is the only authoritative answer on delivery — the 202 from the send means accepted, nothing more.
Reads the outcome of a previously queued send.
Details counts recipients by stage — Total, Queued, Submitted, Delivered and Failed — so a batch can be tracked as it drains.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | guid | Yes | The QueueId returned by the send. |
Responses
| Status | Meaning |
|---|---|
200 | Current status, with a per-stage recipient breakdown in Details. |
400 | No send matches that id. Note this is a 400, not a 404. |
401 | Missing, expired or invalid bearer token. |
curl -X GET https://email-dev.jirafix.net/v1/email/9f1c7d2e-4b3a-4c1d-9e8f-2a7b6c5d4e3f/Status \ -H "Authorization: Bearer <your-token>"
200 OK
{
"QueueId": "9f1c7d2e-4b3a-4c1d-9e8f-2a7b6c5d4e3f",
"Status": "Completed",
"Message": "Sent",
"Details": {
"Total": 2,
"Queued": 0,
"Submitted": 2,
"Delivered": 2,
"Failed": 0
}
}
If the account has run out of credit the status reads InSufficient Fund and a Balance object is included. Top up and send again — the original send does not resume on its own.
Campaigns
Lists the email campaigns on the account.
Use this to find an EmailCampaignId for /single.
Responses
| Status | Meaning |
|---|---|
200 | The campaigns on the account. |
400 | The request could not be read. |
401 | Missing, expired or invalid bearer token. |
curl -X GET https://email-dev.jirafix.net/v1/email/emailCampaigns \ -H "Authorization: Bearer <your-token>"
Trigger templates
Lists the saved templates you can send.
The ids here are what /predefinedemail expects.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
isCalledFromSignUp | boolean | No | Narrows the list to sign-up templates. |
Responses
| Status | Meaning |
|---|---|
200 | The available templates. |
401 | Missing, expired or invalid bearer token. |
curl -X GET https://email-dev.jirafix.net/v1/email/trigger-templates \ -H "Authorization: Bearer <your-token>"
Lists and contact counts
Lists the subscriber lists reachable by email, with contact counts.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
excludeCount | boolean | No | Set to true to skip counting, which returns faster on large lists. |
Responses
| Status | Meaning |
|---|---|
200 | The lists, with counts unless you excluded them. |
401 | Missing, expired or invalid bearer token. |
curl -X GET "https://email-dev.jirafix.net/v1/email/lists?excludeCount=true" \ -H "Authorization: Bearer <your-token>"
Contact count and segment breakdown for one subscriber list.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
listId | guid | Yes | The subscriber list to count. |
Responses
| Status | Meaning |
|---|---|
200 | The count and segment breakdown. An unknown list also answers 200, with nothing in it — check the payload, not the status code. |
401 | Missing, expired or invalid bearer token. |
curl -X GET https://email-dev.jirafix.net/v1/email/3fa85f64-5717-4562-b3fc-2c963f66afa6/contacts/count \ -H "Authorization: Bearer <your-token>"
Errors
This API answers 417 where many APIs would answer 400. A 417 means the request was understood and a rule rejected it; the body carries status: 0 and a message saying which.
| Status | What it means | What to do |
|---|---|---|
202 | Accepted and queued. | Track it with the status endpoint. |
400 | A required field is missing or has the wrong type, or the id you asked about does not exist. | Read the body — it names the offending fields. |
401 | The token is missing, expired or invalid. | Fetch a new token and retry once. |
417 | A rule rejected the request — see the table below. | Fix the request. Retrying it unchanged will fail again. |
424 | We could not queue the message. Nothing was sent. | Retry. If it persists, contact support. |
What a 417 is telling you
| Message | Cause |
|---|---|
| Either Email body is required | EmailBody was empty on /send. |
| Either Email body or Campaign Id is required | Neither content nor a campaign was supplied. |
| Email body should be in base64 | The body was raw HTML. Base64-encode it. |
| Sender From is required | SenderFrom.Email was missing or empty. |
| Predefined Template Id is required | PredefinedTemplateId was missing. |
| Either list or recipients is required to send | A batch had neither a ListId nor Recipients. |
| Aha! batch limit exceeded. Limit is per batch 1000 | More than 1,000 recipients in one batch. Split it. |
| Each recipients must have email | A recipient entry had no To. |
| Each recipients must have unique email | The batch repeated an address; the response lists which. |
| Content is required and should be base 64 | An attachment's Content was missing or not Base64. |
| Disposition is required and should be either attachment or inline | An attachment's Disposition was something else. |
Limits
| Limit | Value |
|---|---|
| Recipients per batch | 1,000 |
| Request size, attachments included | 39 MB on /send, /single, /batch and /Single/ToMultipleRecipients |
| Schedule precision | One minute — seconds are dropped |