Send a transactional email to up to 100 recipients in a single request. Each recipient carries its own personalization data, so a single call can deliver a genuinely different, personalized email to each person in the batch.
Endpoint and headers
POST https://email.useinsider.com/mail/v2/send
Header | Value | Purpose |
|---|---|---|
X-INS-AUTH-KEY | API token, format INS.{id}.{secret} | Authentication. See API Authentication Tokens. |
Content-Type | application/json | Request format |
Body parameters
Below are the body parameters for this request.
Parameter | Description | Data type | Required |
|---|---|---|---|
subject | Email subject line. Rendered per recipient with Liquid. | String | Yes |
from | Sender object. | Object | Yes |
from.email | Sender email. The domain must be authenticated. | String | Yes |
from.name | Sender display name. | String | No |
recipients | 1 to 100 recipients, each with its own personalization fields. | Array | Yes |
recipients[].email | Recipient email address. Required if the UUID is absent. | String | Yes* |
recipients[].uuid | User identifier, resolved to an email via profile lookup at processing time. Required if email is absent. | String | Yes* |
recipients[].name | Recipient name. | String | No |
recipients[].fields | Per-recipient template variables. Any JSON value: string, number, boolean, null, nested object, or array of objects. | Object | No |
content | Email body blocks. Required unless template_id is given. | Array | Yes** |
content[].type | text/html or text/plain. Each type may appear at most once; at most 2 blocks. | String | Yes |
content[].value | Body content. Rendered with Liquid. | String | Yes |
template_id | The ID of your transactional template in the Email Template Library, which you can copy from Templates > Transactional using the three-dot menu > Copy Template ID Required unless content is given. | String | Yes** |
reply_to | Reply-to address (email, name). | Object | No |
cc | CC recipients (email, name). | Array | No |
bcc | BCC recipients (email, name). | Array | No |
attachments | Base64-encoded files, including inline (CID) images. | Array | No |
unique_args | Custom tracking metadata. Up to 10 keys, up to 1 KB serialized. Returned in callbacks. | Object | No |
category | Category ID used for analytics grouping. Must be a category your partner has registered. | Integer | No |
* Each recipient must have at least one of email or UUID.
** Provide either content or template_id, exactly one. Sending both is rejected with 400.
Attachments: Each item has content (base64), file_name, optional disposition (attachment by default, or inline for embedded images), optional content_id (the CID an inline image is referenced by), and optional content_type.
Request examples
Send each JSON body with the headers above.
Personalized order confirmation, with an item list and attachment
{
"subject": "Order {{ order.id }} confirmed",
"from": {
"email": "no-reply@acme.com",
"name": "Acme Store"
},
"recipients": [
{
"email": "alice@example.com",
"name": "Alice",
"fields": {
"first_name": "Alice",
"order": {
"id": "A-1024",
"total": "129.90",
"items": [
{
"qty": 1,
"name": "Wireless Mouse"
},
{
"qty": 2,
"name": "USB-C Cable"
}
]
}
}
}
],
"content": [
{
"type": "text/html",
"value": "<p>Hi {{ first_name }}, order {{ order.id }} ({{ order.total }}) is confirmed.</p>{% for item in order.items %}<p>{{ item.qty }} x {{ item.name }}</p>{% endfor %}"
}
],
"category": 3,
"attachments": [
{
"content": "aW52b2ljZV9jb250ZW50",
"file_name": "invoice.pdf"
}
]
}Multiple recipients, each personalized
{
"subject": "Your order {{ order.id }} has shipped",
"from": {
"email": "no-reply@acme.com",
"name": "Acme Store"
},
"recipients": [
{
"email": "alice@example.com",
"fields": {
"first_name": "Alice",
"order": {
"id": "A-1024",
"tracking": "TRK-111"
}
}
},
{
"email": "bob@example.com",
"fields": {
"first_name": "Bob",
"order": {
"id": "A-1025",
"tracking": "TRK-222"
}
}
}
],
"content": [
{
"type": "text/html",
"value": "<p>Hi {{ first_name }}, order {{ order.id }} is on its way. Track it: {{ order.tracking }}</p>"
}
]
}Recipient by UUID (email resolved automatically)
{
"subject": "Your password was reset",
"from": {
"email": "security@acme.com",
"name": "Acme Security"
},
"recipients": [
{
"uuid": "9f8b7c6d-1234-4a2b-9c3d-abcdef012345",
"fields": {
"first_name": "Alice"
}
}
],
"content": [
{
"type": "text/html",
"value": "<p>Hi {{ first_name }}, your password was just changed.</p>"
}
],
"category": 7
}Conditional content with Liquid
A free shipping line appears only for orders over 100.
{
"subject": "Order {{ order.id }} confirmed",
"from": {
"email": "no-reply@acme.com",
"name": "Acme Store"
},
"recipients": [
{
"email": "alice@example.com",
"fields": {
"first_name": "Alice",
"order": {
"id": "A-1024",
"total": 129.9
}
}
}
],
"content": [
{
"type": "text/html",
"value": "<p>Hi {{ first_name }}, order {{ order.id }} is confirmed.</p>{% if order.total > 100 %}<p>You qualified for free shipping!</p>{% endif %}"
}
]
}HTML and plain-text body
{
"subject": "Order {{ order.id }} confirmed",
"from": {
"email": "no-reply@acme.com",
"name": "Acme Store"
},
"recipients": [
{
"email": "alice@example.com",
"fields": {
"first_name": "Alice",
"order": {
"id": "A-1024"
}
}
}
],
"content": [
{
"type": "text/html",
"value": "<p>Hi {{ first_name }}, order {{ order.id }} is confirmed.</p>"
},
{
"type": "text/plain",
"value": "Hi {{ first_name }}, order {{ order.id }} is confirmed."
}
]
}Inline image (CID) and PDF attachment
Embed a logo with disposition: inline and a content_id, then reference that id from an HTML image element whose source is cid:acme-logo. In the body below, replace [image element with source cid:acme-logo] with a standard HTML image tag.
{
"subject": "Your invoice for order {{ order.id }}",
"from": {
"email": "billing@acme.com",
"name": "Acme Billing"
},
"recipients": [
{
"email": "alice@example.com",
"fields": {
"first_name": "Alice",
"order": {
"id": "A-1024"
}
}
}
],
"content": [
{
"type": "text/html",
"value": "[image element with source cid:acme-logo]<p>Hi {{ first_name }}, your invoice is attached.</p>"
}
],
"attachments": [
{
"content": "iVBORw0KGgo...",
"file_name": "logo.png",
"disposition": "inline",
"content_id": "acme-logo",
"content_type": "image/png"
},
{
"content": "aW52b2ljZV9jb250ZW50",
"file_name": "invoice.pdf",
"disposition": "attachment",
"content_type": "application/pdf"
}
]
}Using a saved template (template_id)
When template_id is set, the HTML is fetched from the Email Template Library, and the content is omitted. Personalization still comes from each recipient's fields.
{
"subject": "Welcome, {{ first_name }}",
"from": {
"email": "no-reply@acme.com",
"name": "Acme Store"
},
"template_id": "welcome-series-01",
"recipients": [
{
"email": "alice@example.com",
"fields": {
"first_name": "Alice"
}
}
],
"unique_args": {
"transaction_type": "welcome"
}
}Sample response
A successful send returns 202 Accepted with a message_id. Use that ID with the delivery-status endpoint to track progress.
{
"status": "accepted",
"message_id": "550e8400-e29b-41d4-a716-446655440000"
}For duplicate, validation, and other error responses, refer to the Error Codes.
Error reference
Use this table to identify and resolve the responses this endpoint returns. Each row shows the status code, the response body you receive, what causes it, and how to fix it. For the shared reference covering every endpoint in this category, see Transactional Email Error Codes.
Status | JSON response | Cause | Fix |
|---|---|---|---|
202 Accepted | { "status": "accepted", "message_id": "550e8400-e29b-41d4-a716-446655440000" } | The request was validated and queued for asynchronous delivery. A 202 confirms acceptance only, not delivery, and it does not confirm that every recipient in the request will be mailed. | Store the message_id and track progress with the delivery-status endpoint. Do not report a send as successful to your end user on the basis of a 202 alone. A message_id is queryable for at least two days after submission. The delivery-status endpoint is limited to 1,000 requests per minute per partner, so poll rather than tight-loop. |
202 Accepted (recipient dropped) | { "status": "accepted", "message_id": "550e8400-e29b-41d4-a716-446655440000" } | Not an error, but a partial failure that the send response cannot show. A recipient given by uuid is resolved to an email address through a profile lookup at processing time, which happens after the 202 is returned. If no email is found, that recipient is dropped and the rest of the send proceeds. | Supply email alongside uuid whenever you have it, since the lookup resolves the email address only. When you send by uuid alone, reconcile against the delivery-status response, and read summary.dropped or scan recipients[].status for dropped. Do not read the top-level status: a message with some recipients sent and some dropped reports sent. |
400 Bad Request | { "status": "error", "message": "invalid JSON body" } | The request body is not parseable as JSON. This is a syntax failure, so no field-level validation runs and the errors array is absent. This is also what you receive when a unique_args value is not a string, since unique_args is typed as string to string. | Validate the body as JSON before sending. Use double quotes for keys and string values, and confirm that every unique_args value is a string. |
400 Bad Request | { "status": "error", "message": "expected content type application/json" } | The Content-Type header is missing or is not application/json. Note that this response has no errors array. | Send Content-Type: application/json. |
400 Bad Request | { "status": "error", "message": "validation failed", "errors": [ "recipients[0].email is not a valid email address", "subject is required" ] } | One or more validation rules failed. See Validation rules below for the full set. | Read every entry in the errors array. Validation errors are collected and returned together, so one response tells you everything that failed this check. Each entry names the failing field with its index, in the format recipients[0].email, which maps directly onto your request body. |
400 Bad Request | { "status": "error", "message": "validation failed", "errors": [ "exactly one of \"content\" or \"template_id\" may be set" ] } | A template rule failed. Two cases produce it: content and template_id both set, or neither set; and a template_id that does not exist for your partner, which returns template_id does not exist for this partner. | Send exactly one of content or template_id, and confirm the template ID in Templates > Transactional. These two rules are checked before the rest of validation and stop on the first failure, so this response carries a single entry even when other fields are also wrong. Fix it and resend to see the remaining errors. |
401 Unauthorized | { "status": "error", "message": "unauthorized" } | The X-INS-AUTH-KEY header is missing, or the token is invalid, expired, or revoked. This endpoint expects the format INS.{id}.{secret}. Permission and entitlement failures also return 401, not 403. | Send a current token in X-INS-AUTH-KEY, in the INS.{id}.{secret} format. Confirm the token is still active in your panel and generate a new one if it was revoked. See API Authentication Tokens. If you integrate through the Insider Gateway API with OAuth 2.0 instead, confirm your token carries the em-tx-v2-send scope. |
403 Forbidden | { "status": "error", "message": "ip restricted" } | The calling IP address is not on the allowlist configured for your account. | Add the IP addresses your backend sends from to your account's allowlist, or contact the Insider One team to have them added. This is distinct from an authentication failure, which returns 401. |
409 Conflict | { "status": "duplicate", "message": "duplicate request", "message_id": "550e8400-e29b-41d4-a716-446655440000" } | An identical request arrived inside the 15-minute deduplication window. The original is already being processed and this duplicate is ignored. The key is a hash of your partner ID and the raw request body, so any byte difference, including JSON key order or whitespace, is treated as a new request. Note that V1 signals the same condition with 208, so a client migrating from V1 must handle a new status code. | Treat 409 as a success, not a failure. It is the expected answer when you retry a request whose original response you did not receive, and the message_id returned is the original one. To send a genuinely different email, change the payload, for example by varying a field value or unique_args. |
413 Payload Too Large | An HTML error page, not the JSON envelope. | The request body exceeds 20 MB, which corresponds to roughly 14 MB of decoded attachments. This limit is enforced at the edge before the request reaches the API, which is why the response is not JSON. | Reduce the payload. Host large files and link to them instead of attaching them, compress inline images, and split multi-recipient sends so attachments are not duplicated across a large recipients array. Check the HTTP status code rather than parsing the body for this case. |
429 Too Many Requests | { "status": "error", "message": "rate limited", "retry_after": 1 } | The endpoint is rate limited. Two conditions produce a 429: exceeding the send rate limit, and transient throttling while attachments are offloaded to storage. The response carries retry_after in the body and an equivalent Retry-After header, both in seconds. | Wait for the interval in retry_after rather than guessing a backoff, then retry. Batch up to 100 recipients per request to reduce request volume. To review the limit that applies to your integration, contact the Insider One team. |
500 Internal Server Error | { "status": "error", "message": "internal error" } | An internal failure occurred, and the request was not processed. | Retry with exponential backoff. Retrying is safe here: the deduplication key is recorded only after the message is successfully queued, so a publish failure does not produce a phantom 409 on retry. If the response persists, contact the Insider One team with sample requests. |
500 Internal Server Error | { "status": "error", "message": "auth unavailable" } | The authentication service is temporarily unreachable. Your token is not the problem, and the request was not processed. | Retry after a short delay. This condition clears automatically within about 30 seconds. Do not regenerate your token in response to it. |
502, 503, 504, 520 | An HTML error page, not the JSON envelope. | A temporary gateway or upstream condition. These are produced by the edge, not by the API, so they do not carry the JSON envelope. | Retry after a short randomized delay, within about five minutes. Note that a retry following a 502 may legitimately return 409, because the original request may have been queued before the gateway error. Treat that 409 as success. |
Validation rules
The following rules are enforced and reported through the validation failed response.
Required fields
subject
from.email, and the sender domain must be authenticated
recipients, at least one entry
exactly one of content or template_id
each recipient must carry email or uuid
each content block needs a non-empty value
Format and content rules
content[].type must be text/html or text/plain, each at most once.
A text/html block is mandatory. A plain-text-only send is rejected.
subject and content[].value are rejected if they contain script, iframe, or inline event-handler markup.
Every email field must be a valid address. The accepted top-level domain is 2 to 10 letters.
The same address may not appear twice within recipients, cc, or bcc, nor across two of those lists.
attachments[].content must be valid base64, file_name is required, and disposition must be attachment or inline.
campaign_id may not contain control characters or a colon.
fields must be JSON-serializable.
Keep in mind
Personalization is explicit. All Liquid variables must be included in each recipient's fields. Attributes stored in a user's profile are not automatically pulled into the template.
UUID resolution. A recipient given by UUID is resolved to an email through a profile lookup during processing. If no email is found, that recipient is dropped, and the rest of the send proceeds. This lookup resolves only the email address.
Transactional only. These emails do not carry a promotional unsubscribe link or List-Unsubscribe header. Transactional mail is one-to-one and must not carry promotional unsubscribe links.
Limitations
Constraint | Value |
|---|---|
Recipients per request | 100 |
Top-level fields keys per recipient | 150 |
fields nesting depth | 5 |
Serialized fields per recipient | 256 KB |
Serialized fields across all recipients | 2 MB |
Array-of-object fields per recipient | 20 |
Objects per array-of-object field | 50 |
Keys per object in those arrays | 20 |
unique_args | 10 keys, 1 KB serialized |
Content blocks | 2, one text/html and one text/plain |
campaign_id | 64 characters |
Request body | 20 MB at the edge |
Deduplication window | 15 minutes |
Send Rate Limit | 10,000 requests per second per partner |