How-to
Send an email
Send email with POST /emails, covering sender rules, recipients, content, templates, tracking, the response, webhook events and every error code.
This guide explains each part of a POST /emails request and what Emailit does with it, from the From address to the errors you can get back. For the complete parameter reference, see Send an email in the API reference.
Before you begin
- A verified sending domain in your workspace. See Add a domain.
- An API key with Full Access or Sending Only scope. See API keys.
- Production access if you send to anyone other than your workspace members. See Production access.
- Enough credits for every recipient (1 credit each).
Send a basic email
curl https://api.emailit.com/v2/emails \
-H "Authorization: Bearer $EMAILIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme Billing <billing@acme.com>",
"to": ["ada@example.com", "Grace Hopper <grace@example.com>"],
"cc": "accounts@example.com",
"reply_to": "support@acme.com",
"subject": "Your invoice for October",
"html": "<p>Your invoice is ready.</p>",
"text": "Your invoice is ready."
}'import { Emailit } from '@emailit/node';
const emailit = new Emailit(process.env.EMAILIT_API_KEY);
const email = await emailit.emails.send({
from: 'Acme Billing <billing@acme.com>',
to: ['ada@example.com', 'Grace Hopper <grace@example.com>'],
cc: 'accounts@example.com',
reply_to: 'support@acme.com',
subject: 'Your invoice for October',
html: '<p>Your invoice is ready.</p>',
text: 'Your invoice is ready.',
});import os
from emailit import EmailitClient
client = EmailitClient(os.environ["EMAILIT_API_KEY"])
email = client.emails.send({
"from": "Acme Billing <billing@acme.com>",
"to": ["ada@example.com", "Grace Hopper <grace@example.com>"],
"cc": "accounts@example.com",
"reply_to": "support@acme.com",
"subject": "Your invoice for October",
"html": "<p>Your invoice is ready.</p>",
"text": "Your invoice is ready.",
})$emailit = Emailit::client(getenv('EMAILIT_API_KEY'));
$email = $emailit->emails()->send([
'from' => 'Acme Billing <billing@acme.com>',
'to' => ['ada@example.com', 'Grace Hopper <grace@example.com>'],
'cc' => 'accounts@example.com',
'reply_to' => 'support@acme.com',
'subject' => 'Your invoice for October',
'html' => '<p>Your invoice is ready.</p>',
'text' => 'Your invoice is ready.',
]);Set the From address
from is required and takes one address in either form:
billing@acme.comAcme Billing <billing@acme.com>, or with quotes,"Acme, Inc." <billing@acme.com>
The domain after the @ must be a verified sending domain in the same workspace:
- The match is exact. Domains are compared without regard to case, but
mail.acme.comandacme.comare different domains. Add and verify every subdomain you send from. - Any local part works. You don’t need a mailbox for
billing@orno-reply@. - Pending domains can’t send. A domain that is still awaiting review (Pending verification) is treated as not verified.
- Restricted keys stay on their domain. A Sending Only key restricted to one domain can only send from that domain.
- Paused domains are blocked. If sending health paused the domain, sends from it are rejected until the pause is lifted.
Add recipients
to is required. cc and bcc are optional. Each field accepts a string or an array of strings, with or without display names, and holds up to 50 addresses. A string can contain several comma-separated addresses; use an array when a display name itself contains a comma.
Emailit removes duplicates across to, cc and bcc (ignoring case), then creates one email per unique recipient, each with its own em_ ID. Every copy carries the same To and Cc headers, so recipients see the conversation as usual, and Bcc recipients never appear in any copy’s headers.
When a request has more than one recipient, the response includes an ids map from recipient to email ID. id is the email of the first recipient.
{
"object": "email",
"id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa",
"ids": {
"ada@example.com": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa",
"grace@example.com": "em_33VtK8nB5rTq0YxW3mJk9PdLsUe",
"accounts@example.com": "em_33VtK8nQ2wErT6yU8iOp4AsDf7g"
}
}Each recipient costs 1 credit and counts toward your rate limits. A recipient with a recipient-type suppression is accepted and then marked suppressed instead of being delivered.
Write the content
| Field | Rules |
|---|---|
subject |
Required, unless a template provides it. Non-ASCII characters are encoded for you. |
html |
The HTML body. You need html, text or both, unless a template provides them. |
text |
The plain-text body. Send it alongside html: some clients and spam filters prefer messages with both. |
reply_to |
A string or an array of addresses where replies should go. |
If reply_to names the same address as from, Emailit drops the Reply-To header because it adds nothing and some spam filters penalize it.
Send with a template
Set template to a template alias or a tem_ ID, and pass variables for the Temple placeholders in it.
- An alias sends the version that is currently published for that alias. If no version is published, the request fails with
404. - A
tem_ID sends that exact version, published or not. Use it to test a draft version before you publish it.
Fields in the request take precedence over the template: a subject, html or text you send replaces the template’s value. If you don’t send reply_to, the template’s Reply-To is used. from is always required in the request. See Template versions for how publishing works.
curl https://api.emailit.com/v2/emails \
-H "Authorization: Bearer $EMAILIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": "ada@example.com",
"template": "welcome-email",
"variables": {
"first_name": "Ada",
"plan": "Pro",
"activation_url": "https://acme.com/activate?token=8f3k2"
}
}'const email = await emailit.emails.send({
from: 'Acme <hello@acme.com>',
to: 'ada@example.com',
template: 'welcome-email',
variables: {
first_name: 'Ada',
plan: 'Pro',
activation_url: 'https://acme.com/activate?token=8f3k2',
},
});email = client.emails.send({
"from": "Acme <hello@acme.com>",
"to": "ada@example.com",
"template": "welcome-email",
"variables": {
"first_name": "Ada",
"plan": "Pro",
"activation_url": "https://acme.com/activate?token=8f3k2",
},
})$email = $emailit->emails()->send([
'from' => 'Acme <hello@acme.com>',
'to' => 'ada@example.com',
'template' => 'welcome-email',
'variables' => [
'first_name' => 'Ada',
'plan' => 'Pro',
'activation_url' => 'https://acme.com/activate?token=8f3k2',
],
]);variables also works without a template: Emailit renders Temple placeholders in the subject, html and text you send inline.
Control tracking
By default, each email follows its sending domain’s Track loads and Track clicks settings. Override them per email with tracking:
"tracking": trueorfalseturns both load (open) and click tracking on or off."tracking": { "loads": true, "clicks": false }sets each one separately.
Tracking only works when the domain’s tracking CNAME is verified. Without it, the email is sent untracked and the request still succeeds. The tracking object in the response shows the settings that were actually applied. See Open and click tracking.
Add headers and metadata
Use headers for custom email headers, such as List-Unsubscribe, and meta for your own string key-value pairs. Emailit stores meta with the email and includes it in webhook events. See Headers and metadata.
To attach files, schedule the send, or make retries safe, see Attachments, Scheduling and Idempotency.
Read the response
A successful request returns 200:
| Field | Description |
|---|---|
object |
Always email. |
id |
The em_ ID of the first recipient’s email. |
ids |
Map of recipient address to email ID. Present only when there is more than one recipient. |
token |
Internal token of the first email, also used in its Message-ID. |
message_id |
The Message-ID header of the first email, in the form <token@your-domain>. |
from |
The From address as you sent it. |
to |
The to addresses, without display names. |
cc, bcc |
The cc and bcc addresses. Present only when you sent them. |
subject |
The final subject, after template rendering. |
status |
accepted, or scheduled when the email has a future send time. |
scheduled_at |
The send time in ISO 8601, or null. |
created_at |
When the email was created. |
tracking |
The applied loads and clicks settings. |
Store the id (or the ids map) so you can match later webhook events and look the email up with Retrieve an email.
Events
Every recipient’s email emits its own events:
email.acceptedright after the request, oremail.scheduledif it has a future send time.- Delivery events as the email moves through the pipeline:
email.delivered,email.attempted(temporary failure, will retry),email.bounced,email.failed,email.rejectedoremail.suppressed. An email held for review emitsemail.held. - Engagement events, if tracking is on:
email.loadedandemail.clicked. Spam reports emitemail.complained.
See Email statuses for what each status means.
Errors
Validation errors return a list of every problem found:
{
"error": "Validation failed",
"validation_errors": [
"Missing required field: subject",
"Invalid to email address at index 1: grace@"
]
}| Status | error |
Cause | Fix |
|---|---|---|---|
400 |
Validation failed |
A required field is missing, an address is malformed, a field has more than 50 recipients, or an attachment is invalid. | Fix each item in validation_errors. |
400 |
Invalid Idempotency-Key |
The Idempotency-Key header has a bad format. |
Use 1–256 letters, digits, - or _. See Idempotency. |
401 |
Unauthorized |
The API key is missing or invalid. | Send Authorization: Bearer with a current key. |
402 |
Insufficient credits |
The workspace can’t pay for every recipient. | Buy credits or turn on auto-refill. |
403 |
Workspace not verified |
The workspace is in sandbox mode and a recipient isn’t a workspace member. code is unverified_workspace_recipient and blocked_recipients lists the addresses. |
Request production access, or test with members’ addresses. |
403 |
Domain not authorized |
The API key is restricted to a different sending domain. | Send from the key’s domain, or use a key without a domain restriction. |
403 |
Domain paused |
Sending health paused the From domain. | See Sending health. |
404 |
Template not found |
The alias has no published version, or the tem_ ID doesn’t exist in this workspace. |
Publish a version or check the ID. |
409 |
Idempotency key in progress |
Another request with the same key is still running. | Wait, then retry with the same key. |
413 |
Message too large |
The encoded message is larger than 40 MB. | Send fewer or smaller attachments, or link to large files. |
422 |
Domain not verified |
The From domain isn’t a verified sending domain in this workspace. | Verify the domain, or check for a subdomain or typo. |
422 |
Attachment error |
An attachment URL couldn’t be downloaded or is larger than 25 MB. | See Attachments. |
429 |
Rate limit exceeded or Daily limit exceeded |
You’re over the per-second or daily sending limit. | Wait for retry-after seconds, or request a higher limit. |
503 |
Idempotency unavailable |
The idempotency store couldn’t be reached. | Retry with the same key. |
A suspended workspace gets 403 with Workspace is suspended on every send. For the general error format, see Errors.