Skip to main content
import { createClient } from "@mailsai/sdk";

const client = createClient(); // reads MAILS_API_KEY

const result = await client.messages.send({
  agent: "hello",
  to: "jordan@example.com",
  subject: "Your order #10473 has shipped",
  body_text: "Your order #10473 is on its way.",
});
console.log(result);
{
  "id": "msg_01JZX8K3M9Q4P7VN2YB6RTDC0E",
  "agent_id": "agt_01JZ9F8H7KQ2M3N4P5R6S7T8U9",
  "thread_id": "thrd_01JZX8K3M9Q4P7VN2YB6RTDC11",
  "to": ["jordan@example.com"],
  "subject": "Your order #10473 has shipped",
  "classifier_score": 0.02,
  "status": "sent",
  "cost_usd": 0.0004,
  "tags": [{ "name": "category", "value": "shipping" }],
  "test_mode": false,
  "created_at": "2026-06-24T17:32:08.421Z"
}

Send a message

Sends an email from one of your agents.
POST/v1/messagesscope · send
import { createClient } from "@mailsai/sdk";

const client = createClient(); // reads MAILS_API_KEY

const result = await client.messages.send({
  agent: "hello",
  to: "jordan@example.com",
  subject: "Your order #10473 has shipped",
  body_text: "Your order #10473 is on its way.",
});
console.log(result);
{
  "id": "msg_01JZX8K3M9Q4P7VN2YB6RTDC0E",
  "agent_id": "agt_01JZ9F8H7KQ2M3N4P5R6S7T8U9",
  "thread_id": "thrd_01JZX8K3M9Q4P7VN2YB6RTDC11",
  "to": ["jordan@example.com"],
  "subject": "Your order #10473 has shipped",
  "classifier_score": 0.02,
  "status": "sent",
  "cost_usd": 0.0004,
  "tags": [{ "name": "category", "value": "shipping" }],
  "test_mode": false,
  "created_at": "2026-06-24T17:32:08.421Z"
}

Body Parameters

agentstring

Which agent sends this — its name or agt_ id. Optional: omit it and we use the workspace's single agent, or provision one named from from when the key may create agents (the manage scope); a workspace with no active or paused agent gets its first one that way even without from. Required only when the workspace has several agents and no from names one of them. When it is given it decides the sender: a from beside it picks no agent and creates none. A key tied to one agent always sends as that agent and may leave this out; naming another agent here, or in from when this is left out, is a 403 insufficient_scope.

fromstring

The sender you want, as a handle or address — billing, billing@acme.com and Acme <billing@acme.com> all mean the agent called billing. It SELECTS that agent: resolved if it exists. If it doesn't, it is created when the key may create agents (the manage scope) and your plan has room for it; otherwise the send is refused, with 403 insufficient_scope when the key may not create agents and 402 plan_limit_exceeded when your plan has no room. An address at a domain you have verified is matched whole: billing@yourdomain.com means the agent at exactly that address, created on your domain on the same terms if it doesn't exist yet. It is never resolved to a different agent, and it cannot spoof a sender — the address on the From line is always derived from the agent's own identity. Beside an explicit agent it selects nothing: agent decides the sender. Either way a display name in it, like Acme above, goes on the From line, a scheduled send's too. The address that actually sent comes back as from on the response.

tostring | string[]required

Who the message goes to: one address, or a list of 1 to 50.

ccstring[]

Addresses to copy, up to 50. Every recipient sees them.

bccstring[]

Addresses to blind-copy, up to 50. They receive the message but appear in none of its headers.

subjectstringrequired

Subject line, 1 to 998 characters.

body_textstring

Plain-text body, up to 1,000,000 characters. At least one of body_text and body_html is required, or their aliases text and html.

body_htmlstring

HTML body, up to 1,000,000 characters. With body_text as well, the email carries a plain-text part and an HTML part.

htmlstring

Alias of body_html: used when body_html is left out, ignored when both are sent.

textstring

Alias of body_text: used when body_text is left out, ignored when both are sent.

reply_tostring

The address replies should go to. Left out, replies go to the agent's receiving_address.

in_reply_to_message_idstring

Id of the message this one answers (msg_… or rcv_…), named in the email's In-Reply-To header. The send joins its thread when the same agent sent or received it. Left out, it starts a new thread.

referencesstring[]

Message-IDs for the References header. Write one from another system with its angle brackets (<…>); a value without them is read as a mails.ai message id.

attachmentsobject[]

Up to 10 files, 25 MB in total. Each is given by its bytes (content_base64) or by an https path that mails.ai downloads when it sends, and one with a content_id is shown inside the HTML body.

Not accepted with a future scheduled_at: a scheduled send cannot carry attachments yet, so that combination is refused with 400 invalid_field.

Show propertiesHide properties
filenamestringrequired

The file's name, as the recipient sees it. A file shown inside the HTML can reach the recipient named by its content_id instead (see content_id).

content_base64string

The file's bytes, base64-encoded. Give this or path, not both.

pathstring

An https URL that mails.ai downloads when it sends, instead of content_base64. It must answer within 10 seconds, after at most 3 redirects, each to https, and must not be a private, loopback, link-local or cloud metadata address. A file that cannot be fetched refuses the send with 400 invalid_field, its param naming it (attachments.0.path).

content_typestring

The file's MIME type, such as application/pdf. Required with content_base64. With path it may be left out, and the type the URL answers with is used.

content_idstring

Show the file inside the HTML body instead of as an attachment: <img src="cid:logo"> in html shows the file whose content_id is logo. Letters, digits and . _ - @ + = $, up to 127 characters, unique within the message. Mail with no html has nowhere to show it, so the file is sent as an ordinary attachment under its filename. Shown in the HTML, it can reach the recipient named by its content_id: one of the email services mails.ai sends through names such a file by the cid: the HTML uses.

metadataobject

Your own key-value strings, kept with the message and returned by GET /v1/messages/{id}; they are not part of the email. Keys up to 64 characters, values up to 500.

scheduled_atstring

Send at this time (ISO 8601) instead of now. A time in the past sends at once. When a passing failure stops a scheduled send (one of ours, or the email service not taking more mail for now), it is tried again a minute or more later, up to 3 tries in all, and then fails with message.failed; a scheduled draft is tried the same way and fails with draft.failed.

A future time cannot be combined with attachments.

tagsobject[]

Up to 10 name-value labels, kept with the message and included in its message.sent event; they are not part of the email.

Show propertiesHide properties
namestringrequired

Tag name: 1 to 256 letters, digits, _ or -.

valuestringrequired

Tag value, up to 256 characters; it may be empty.

list_unsubscribeboolean

Opt in to RFC 8058 one-click unsubscribe: the message carries List-Unsubscribe and List-Unsubscribe-Post: List-Unsubscribe=One-Click headers, so the recipient's mail app shows an Unsubscribe button. One click suppresses the address for your whole workspace: every later send to it from any of your agents is refused with recipient_suppressed until you allowlist it (POST /v1/suppression/allow). Turn it on for newsletters, digests and other recurring mail. Leave it off (the default) for password resets, sign-in links, receipts and anything else the recipient must keep receiving.

Needs exactly one to recipient and no cc or bcc.

Headers

Idempotency-Keystring

Replays the stored response for a repeated key (kept 24 hours), so a retried request never double-sends; the replay carries Idempotent-Replay: true. An error is kept on the key only once the mail has gone out, so a replayed error means it was sent. Once a scheduled send is stored, a failure after that keeps the message's answer on the key instead: the retry answers with the message (201) and raises its message.scheduled if the first request did not. After a failure before either, the same key sends again, as it does after a batch that stored no message and sent no mail.

With a mk_test_ key the message is stored and its message.sent event reaches your webhooks (marked test_mode: true), but no email is sent and nothing is billed. Quota, suppression and rate-limit refusals are skipped, and a message the cold-email firewall would refuse comes back 201 with classifier_warning instead. Send to reply@test.mails.ai to get an automatic reply within about a second; it answers at most 3 times in one thread, so a 4th message there gets no reply. It is the only address at test.mails.ai: a send naming any other there is refused with 422 unknown_test_address, and nothing is sent.

Response

idstring

Message id (msg_…), the {id} in /v1/messages/{id}.

agent_idstring

Id of the agent that sent the message (agt_…).

fromstring

The agent address this was sent as — the resolved from. Read it back when you pass from as a naming hint instead of an explicit agent: it is the receipt for which identity the API picked. On a verified custom domain the message goes out from this exact address. On the shared mails.ai domain it goes out from <name>.<workspace>@send.mails.ai, and replies come back through <name>.<workspace>@in.mails.ai.

thread_idstring

The thread the message belongs to (thrd_…).

in_reply_to_message_idstring

The message this one answers: the message replied to or forwarded, or the in_reply_to_message_id the send set. Present on reply responses and on the single-message GET, when set.

tostring[]

The To recipients, always a list. On a reply they come from the original: its Reply-To or sender for a received message, its to for one of your sent messages.

ccstring[]

Cc recipients. Always present on reply responses ([] when none); on the list and the single-message GET, present when there are any.

bccstring[]

Bcc recipients. Present only on the list and the single-message GET, when there are any.

subjectstring

The subject as sent. On a reply, Re: and the original's subject, unless that already starts with Re:; on a forward, the subject you gave or Fwd: and the original's.

body_textstring

The plain-text body as sent; on a forward it includes the quoted original. Present only on the single-message GET, when there is one.

body_htmlstring

The HTML body as sent. Present only on the single-message GET, when there is one.

classifier_scorenumber

The cold-email firewall's score for the message, from 0 (clean) to 1 (cold or spam). Present on the send response, the list and the single-message GET.

statusstring

sent once handed off; delivered, bounced or complained when the receiving side reports it (with several recipients the latest report wins). delayed when a transient (soft) bounce came back: the receiving server has not taken the message yet; it is not a bounce, does not count toward the bounce rate, raises message.delayed (sent only to endpoints that list it) and may still become delivered; with no delivery confirmed within 5 days of sending, it becomes bounced. sending only while a scheduled send is going out; rejected when the send failed, or a scheduled send was refused when it came due; canceled when a scheduled send was stopped. A live send only to the reserved test address reply@test.mails.ai is delivered at once, with a message.delivered event, since it is never transmitted; with a test key it stays sent, as every test-mode send does. Either way its automatic reply arrives as reply.received (at most 3 replies in one thread; a 4th message there gets none).

One of: scheduled, sending, sent, delivered, delayed, bounced, complained, rejected, canceled

refused_recipientsstring[]

The recipients the email service refused when the message was sent, while it took the message for the others: it went to everyone else, and not to these. The message is still a send, so sending it again with the same Idempotency-Key gives this answer back and sends nothing; to reach them, check the addresses and send them a new message. Present only when there were any: on the send, batch, reply and forward responses, the list, the single-message GET and the message's message.sent event. When every recipient is refused, nothing is sent and the send fails instead.

cost_usdnumber

USD cost of the send. 0 for scheduled or test-mode sends.

ses_message_idstring

The sending provider's id for the message, which its delivery, bounce and complaint reports refer to; test-<message id> for a test-mode send. Present only on the list and the single-message GET, once the message has been handed off.

attachmentsobject[]

Name, type and size of each file the send carried; the files themselves are not kept. Present when there were any, on the send response, the list and the single-message GET.

Show propertiesHide properties
filenamestring

The file name, as given in the send's attachments.

content_typestring

The MIME type given in the send's attachments. A file fetched from its path without one takes the type the URL answered with, or application/octet-stream when it named none.

size_bytesinteger

Size of the file in bytes: content_base64 decoded, or the bytes fetched from path.

content_idstring

Only on a file shown inside the HTML body: the id its cid: names.

referencesstring[]

The References chain kept with the message: the references the send passed, or on a reply the ids of the earlier messages it answers. Present only on the single-message GET, when there is one.

metadataobject

The metadata the send passed; a scheduled send refused when it came due also carries rejected_reason and rejected_at. Present only on the single-message GET, when there is any.

tagsobject[]

The tags the message was sent with. Present when there are any, on send, batch and reply responses, the list and the single-message GET.

Show propertiesHide properties
namestring

The tag's name, as given on the send, reply or draft.

valuestring

The tag's value, as given; it may be an empty string.

sent_atstring

When the message was handed off for sending: at once for an immediate send, when it came due for a scheduled one. Present only on the list and the single-message GET, once that has happened.

delivered_atstring

When delivery was confirmed; with several recipients, the time of the latest report. Present only on the list and the single-message GET, once a delivery is confirmed.

scheduled_atstring

The time a scheduled send was set for. On send, batch and reply responses it is present only when that time was in the future; the list and the single-message GET keep it after the message has gone out or been canceled.

canceled_atstring

When the scheduled send was canceled. Present only on the list and the single-message GET, for a canceled message.

bounced_atstring

When a bounce was last reported for the message; a soft bounce, which marks it delayed, does not set it. Present only on the single-message GET.

complained_atstring

When a recipient's complaint about the message was last reported. Present only on the single-message GET.

forwarded_fromstring

Source message id (forward responses only).

test_modeboolean

True for a message sent with a test key (mk_test_…): nothing was transmitted and nothing was billed.

classifier_warningobject

Present only on a test-mode send that a live key would refuse: the cold-email firewall's verdict, reported instead of thrown. Nothing was transmitted.

Show propertiesHide properties
codestring

Always cold_email_prohibited, the error code a live key would get.

One of: cold_email_prohibited

messagestring

The verdict in words: why a live key would refuse the message, what would pass instead, and that nothing was transmitted.

scorenumber

The firewall's score for the message, from 0 (clean) to 1 (cold or spam).

reasonstring

What the message was classified as, such as cold_outreach, spam_pattern or phishing_lure.

created_atstring

When the message was created; for a scheduled send, when it was scheduled, not when it went out.

Was this page helpful?