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.

Every send is accepted, not delivered

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.

The email body must be Base64

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”.

Download for AI review

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.

PropertyValue
Environment
Base URLhttps://email-dev.jirafix.net
All pathsare 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.

Keep your credentials on your server

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.

POST https://authenticate-dev.jirafix.net/v1/token

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

NameTypeRequiredDescription
usernamestringYesThe account's username, usually an email address.
passwordstringYesThe account's password. Server-side only.
privatetokenstringYesYour account's private token, from the portal's configuration section. Note the spelling — one word, all lower case.
validityintegerNoHow long the token should last, in seconds. Defaults to 3600.

Responses

StatusMeaning
200Returns access_token, refresh_token, token_type and expires_in.
400The body was missing or a required field was absent.
401The 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 response is snake_case

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.

Use the Authenticate host for this same environment

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 thisWhenRecipients
/sendOne person, your own HTMLOne
/singleOne person, a saved campaign or attachmentsOne
/Single/ToMultipleRecipientsThe same message to several addresses in one callMany, one message
/batchA list, or up to 1,000 individually personalised recipientsUp to 1,000
/predefinedemailA template already built in the portalOne or many

Send to one recipient

POST /v1/email/send

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

NameTypeRequiredDescription
SenderFromobjectYesWho the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417.
ReplyToobjectYesWhere replies go. Use ReplyToList instead for several addresses.
SubjectstringYesSubject line.
PreHeaderTextstringNoPreview line shown after the subject in most mail apps.
EmailBodystringYesBase64-encoded HTML. Raw HTML is rejected with 417.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped.
FrequencystringNoRepeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it.
WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.
Recipient.TostringYesThe recipient's address.
Recipient.CcEmailsarrayNoCopied addresses. Note the name — it is CcEmails, not Ccs.
Recipient.BCcEmailsarrayNoBlind-copied addresses.
Recipient.PersonalizationSubstitutionTagsarrayNoValues substituted into the body, for greeting someone by name. Omit if you do not personalise.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The 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.
401Missing, expired or invalid bearer token.
417A 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.
424We 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

POST /v1/email/single

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

NameTypeRequiredDescription
SenderFromobjectYesWho the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417.
ReplyToobjectYesWhere replies go. Use ReplyToList instead for several addresses.
SubjectstringYesSubject line.
PreHeaderTextstringNoPreview line shown after the subject in most mail apps.
EmailBodystringYesBase64-encoded HTML. Raw HTML is rejected with 417.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped.
FrequencystringNoRepeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it.
WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.
EmailCampaignIdguidNoA campaign built in the portal, used instead of EmailBody.
AttachmentsarrayNoEach 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.TostringYesThe recipient's address.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The 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.
401Missing, expired or invalid bearer token.
417A 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.
424We 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

POST /v1/email/Single/ToMultipleRecipients

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

NameTypeRequiredDescription
SenderFromobjectYesWho the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417.
ReplyToobjectYesWhere replies go. Use ReplyToList instead for several addresses.
SubjectstringYesSubject line.
PreHeaderTextstringNoPreview line shown after the subject in most mail apps.
EmailBodystringYesBase64-encoded HTML. Raw HTML is rejected with 417.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped.
FrequencystringNoRepeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it.
WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.
Recipients.ToarrayYesThe addresses to send to.
Recipients.CcEmailsarrayNoCopied addresses.
Recipients.BCcEmailsarrayNoBlind-copied addresses.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The 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.
401Missing, expired or invalid bearer token.
417A 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.
424We 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

POST /v1/email/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

NameTypeRequiredDescription
SendingDetails.SenderFromobjectYesWho the mail is from: { "Email": "...", "DisplayName": "..." }. Missing or empty Email is rejected with 417.
SendingDetails.ReplyToobjectYesWhere replies go. Use ReplyToList instead for several addresses.
SendingDetails.SubjectstringYesSubject line.
SendingDetails.PreHeaderTextstringNoPreview line shown after the subject in most mail apps.
SendingDetails.EmailBodystringYesBase64-encoded HTML. Raw HTML is rejected with 417.
SendingDetails.ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped.
SendingDetails.FrequencystringNoRepeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it.
SendingDetails.WebHookUrlstringNoWe post delivery reports here as they happen, so you do not have to poll.
SendingDetails.ListIdarrayNoSubscriber lists to send to. Required unless you supply Recipients.
RecipientsarrayNoUp 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[].PersonalizationSubstitutionTagsarrayNoPer-recipient values substituted into the body — this is what batch gives you that the multi-address send does not.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The 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.
401Missing, expired or invalid bearer token.
417A 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.
424We 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

POST /v1/email/predefinedemail

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

NameTypeRequiredDescription
PredefinedTemplateIdstringYesThe template to send, e.g. TEMAIL-822.
RecipientsarrayNoEach entry needs a To. Required unless you supply a ListId.
ListIdarrayNoSend to saved lists instead of explicit recipients.
CcEmailsarrayNoCopied addresses.
BCcEmailsarrayNoBlind-copied addresses.
ScheduleDateTimedate-timeNoSend later instead of now. Format yyyy-MM-dd HH:mm — seconds are dropped.
FrequencystringNoRepeat a scheduled send. Defaults to None. Pair with RecurrenceEndDate to stop it.

Responses

StatusMeaning
202Accepted and queued. Returns QueueId and QueuedTimestamp.
400The 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.
401Missing, expired or invalid bearer token.
417A 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.
424We 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.

GET /v1/email/{id}/Status

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

NameTypeRequiredDescription
idguidYesThe QueueId returned by the send.

Responses

StatusMeaning
200Current status, with a per-stage recipient breakdown in Details.
400No send matches that id. Note this is a 400, not a 404.
401Missing, 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
  }
}
Out of funds

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

GET /v1/email/emailCampaigns

Lists the email campaigns on the account.

Use this to find an EmailCampaignId for /single.

Responses

StatusMeaning
200The campaigns on the account.
400The request could not be read.
401Missing, expired or invalid bearer token.
curl -X GET https://email-dev.jirafix.net/v1/email/emailCampaigns \
  -H "Authorization: Bearer <your-token>"

Trigger templates

GET /v1/email/trigger-templates

Lists the saved templates you can send.

The ids here are what /predefinedemail expects.

Parameters

NameTypeRequiredDescription
isCalledFromSignUpbooleanNoNarrows the list to sign-up templates.

Responses

StatusMeaning
200The available templates.
401Missing, 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

GET /v1/email/lists

Lists the subscriber lists reachable by email, with contact counts.

Parameters

NameTypeRequiredDescription
excludeCountbooleanNoSet to true to skip counting, which returns faster on large lists.

Responses

StatusMeaning
200The lists, with counts unless you excluded them.
401Missing, expired or invalid bearer token.
curl -X GET "https://email-dev.jirafix.net/v1/email/lists?excludeCount=true" \
  -H "Authorization: Bearer <your-token>"
GET /v1/email/{listId}/contacts/count

Contact count and segment breakdown for one subscriber list.

Parameters

NameTypeRequiredDescription
listIdguidYesThe subscriber list to count.

Responses

StatusMeaning
200The count and segment breakdown. An unknown list also answers 200, with nothing in it — check the payload, not the status code.
401Missing, 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.

StatusWhat it meansWhat to do
202Accepted and queued.Track it with the status endpoint.
400A 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.
401The token is missing, expired or invalid.Fetch a new token and retry once.
417A rule rejected the request — see the table below.Fix the request. Retrying it unchanged will fail again.
424We could not queue the message. Nothing was sent.Retry. If it persists, contact support.

What a 417 is telling you

MessageCause
Either Email body is requiredEmailBody was empty on /send.
Either Email body or Campaign Id is requiredNeither content nor a campaign was supplied.
Email body should be in base64The body was raw HTML. Base64-encode it.
Sender From is requiredSenderFrom.Email was missing or empty.
Predefined Template Id is requiredPredefinedTemplateId was missing.
Either list or recipients is required to sendA batch had neither a ListId nor Recipients.
Aha! batch limit exceeded. Limit is per batch 1000More than 1,000 recipients in one batch. Split it.
Each recipients must have emailA recipient entry had no To.
Each recipients must have unique emailThe batch repeated an address; the response lists which.
Content is required and should be base 64An attachment's Content was missing or not Base64.
Disposition is required and should be either attachment or inlineAn attachment's Disposition was something else.

Limits

LimitValue
Recipients per batch1,000
Request size, attachments included39 MB on /send, /single, /batch and /Single/ToMultipleRecipients
Schedule precisionOne minute — seconds are dropped