POST /mail/v2/send

Prev Next

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