Send a message

Deliver a single transactional email. Messages are queued, dispatched within 200 ms p95, and tracked end-to-end.

POST /v3/messages

Body parameters

ParameterTypeRequiredDescription
fromstringRequiredA verified sender. Format: "Name <email@domain.com>" or "email@domain.com". Internationalised domains are accepted and normalised to punycode.
from_namestringOptionalDisplay name for the From header — recipients see Display Name <from>. Overrides a name embedded in from.
tostring or array of stringsRequiredPrimary recipients — at most 1,000 addresses. Each unique address becomes its own independently tracked message; the response returns the first message id and siblings share a group id.
ccstring or array of stringsOptionalCC recipients (max 1,000), each tracked as its own message. Duplicates across to/cc/bcc are deduplicated case-insensitively (To wins over Cc over Bcc).
bccstring or array of stringsOptionalBCC recipients (max 1,000). Hidden from other recipients and tracked as their own messages.
reply_tostringOptionalReply-to address. Defaults to from.
subjectstringRequiredSubject line, 1–998 characters. Overridden by the template's subject when template_id is set.
htmlstringOptionalHTML body sent directly — no template required. Sanitized and rewritten for tracking when enabled. Send text alongside it: the email goes out multipart/alternative, so text-only clients (and spam filters) get the plain-text edition.
textstringOptionalPlain-text fallback. Can be sent on its own for plain-text-only emails.
template_idstringOptionalA stored template id (tpl_…) to render server-side. All three can coexist: the template’s subject/html/text win, and inline html/text only fill fields the template doesn’t define (e.g. a plain-text fallback). Unknown ids are rejected with 404.
varsobjectOptionalMerge values, substituted as {{key}} in the stored template's subject and body. Only applied when template_id is set.
tagsarray of stringsOptionalUp to 10 tags (max 128 chars each) for filtering events. Echoed back in every webhook payload for this message.
headersobjectOptionalCustom X-* headers — use them for correlation ids, e.g. {"X-TES-MSGID": "tes-7f3a9c"}. They round-trip end-to-end: stamped on the delivered email, stored with the message, echoed on every webhook payload for the message, and returned by GET /v3/messages/{id} (they also appear in the Console message drawer). Limits: 32 headers, 1,024 UTF-8 bytes per value; names must start with X-, and X-Wemail-* / X-SES-* are reserved. Non-X-* names are ignored.
attachmentsarrayOptionalFile attachments. Each: filename, content (base64), content_type, plus optional content_id + disposition (inline embeds an image in the body via <img src="cid:…">). Limits: 5 MB decoded per file, 20 MB per message (all files combined, decoded) and 30 GB total per send across all recipients. Host larger files via POST /v3/uploads. See Attachments.
trackingobjectOptionalOverride workspace defaults: { opens: bool, clicks: bool, https: bool }.
send_atISO-8601 timestampOptionalSchedule for future delivery. Must be in the future and at most 1 year out (a 5-minute past grace covers clock skew); values outside that window are rejected with 400. Scheduled messages can be cancelled with DELETE /v3/messages/{id} while still queued.
metadataobjectOptionalFree-form key/value pairs stored with the message — internal IDs, A/B variants. Not visible to the recipient.
verifybooleanOptionalVerify each recipient is deliverable before sending. Undeliverable addresses are dropped with a rejected webhook (reason: failed_verification). Overrides the account/domain/API-key default for this message. Uses 1 verification credit per recipient.
webhook_urlstring (URL)OptionalPer-message webhook. Every event for this message (queued, delivered, opened, clicked, …) is also POSTed to this HTTPS URL, in addition to your configured webhook endpoints. Payloads carry the Wemail-Signature-Ed25519 header. Also works over SMTP via the X-Wemail-Webhook header.

Responses

StatusDescription
202Accepted. { id, status: "queued", created_at }. With multiple recipients, id is the first recipient's message — the others are linked by a shared group id and each produces its own events.
400Validation error (e.g. a to/cc/bcc field over 1,000 addresses, or a send_at in the past or more than 1 year out), or the from domain isn't registered / verified on your account.
403Sandbox send to a recipient not on the confirmed test allow-list, or the account is suspended.
404The template_id doesn't exist on your account.
409A request with the same Idempotency-Key is still in flight.
429Monthly/daily sending quota, test-send allowance, or account request rate exceeded.
503Live sending is paused on the account.

Custom header policy

The headers object (and, over SMTP, arbitrary MIME headers) is filtered before a message is stored or delivered. The rules below apply to both the REST headers field and SMTP:

  • `X-` prefix required. Only names starting with X- are kept; any other name is silently ignored.
  • Reserved namespaces. X-Wemail-* and X-SES-* are reserved for the platform and are dropped. (X-Wemail-Tag, X-Wemail-Variables, X-Wemail-Template and X-Wemail-Webhook are consumed as control headers at submission, not round-tripped.)
  • At most 32 headers. The first 32 qualifying X-* headers are kept; the 33rd onward are silently dropped.
  • Value ≤ 1,024 bytes (UTF-8). A header whose value exceeds 1,024 UTF-8 bytes — note multi-byte characters count per byte, so e.g. Greek text fits ~512 characters — is silently dropped (the rest of the message is unaffected).
  • No CR/LF in values (header-injection guard) — such a header is dropped.

Code examples

cURL
curl -X POST https://api.wemail.io/v3/messages \
  -H "Authorization: Bearer afn_live_pK7…b9aF" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "noreply@updates.acme.io",
    "to":   ["alex@example.com"],
    "subject": "Welcome to Acme",
    "html": "<h1>Hi Alex</h1><p>Glad to have you.</p>",
    "tags": ["welcome", "v2"],
    "headers": { "X-TES-MSGID": "tes-7f3a9c" }
  }'
Node.js
import { WemailClient } from "@wemail/sdk";
const wemail = new WemailClient(process.env.WEMAIL_API_KEY);

const { id } = await wemail.sendMessage({
  from:    "noreply@updates.acme.io",
  to:      ["alex@example.com"],
  subject: "Welcome to Acme",
  html:    "<h1>Hi Alex</h1><p>Glad to have you.</p>",
  tags:    ["welcome", "v2"],
  // stamped on the email, echoed on every webhook, returned by GET /v3/messages/{id}
  headers: { "X-TES-MSGID": "tes-7f3a9c" },
});
Python
from wemail.client import WemailClient

wemail = WemailClient(api_key=os.environ["WEMAIL_API_KEY"])

msg = wemail.send_message({
    "from": "noreply@updates.acme.io",
    "to": ["alex@example.com"],
    "subject": "Welcome to Acme",
    "html": "<h1>Hi Alex</h1><p>Glad to have you.</p>",
    "tags": ["welcome", "v2"],
})
PHP
<?php
$wemail = new \Wemail\WemailClient(getenv('WEMAIL_API_KEY'));
$msg = $wemail->sendMessage([
    'from'    => 'noreply@updates.acme.io',
    'to'      => ['alex@example.com'],
    'subject' => 'Welcome to Acme',
    'html'    => '<h1>Hi Alex</h1>',
    'tags'    => ['welcome', 'v2'],
]);
Ruby
wemail = Wemail::Client.new(api_key: ENV["WEMAIL_API_KEY"])
msg = wemail.send_message(
  from: "noreply@updates.acme.io",
  to:   ["alex@example.com"],
  subject: "Welcome to Acme",
  html: "<h1>Hi Alex</h1>",
  tags: ["welcome", "v2"]
)
Go
client, err := wemail.NewClient(os.Getenv("WEMAIL_API_KEY"))
msg, err := client.SendMessage(wemail.SendMessageRequest{
    From:    "noreply@updates.acme.io",
    To:      []string{"alex@example.com"},
    Subject: "Welcome to Acme",
    Html:    wemail.PtrString("<h1>Hi Alex</h1>"),
    Tags:    []string{"welcome", "v2"},
})
Java
WemailClient wemail = new WemailClient(System.getenv("WEMAIL_API_KEY"));
SendMessage202Response msg = wemail.sendMessage(new SendMessageRequest()
    .from("noreply@updates.acme.io")
    .to(List.of("alex@example.com"))
    .subject("Welcome to Acme")
    .html("<h1>Hi Alex</h1>")
    .tags(List.of("welcome", "v2")));
.NET
var wemail = new WemailClient(Environment.GetEnvironmentVariable("WEMAIL_API_KEY"));
var msg = await wemail.SendMessageAsync(new SendMessageRequest {
  From = "noreply@updates.acme.io",
  To = new List<string> { "alex@example.com" },
  Subject = "Welcome to Acme",
  Html = "<h1>Hi Alex</h1>",
  Tags = new List<string> { "welcome", "v2" },
});
ResponseExample response
{
  "id": "msg_01HBYE7K9Z4PMQDR3W",
  "status": "queued",
  "created_at": "2026-05-10T09:42:18Z",
  "from": "noreply@updates.acme.io",
  "to": ["alex@example.com"],
  "tags": ["welcome", "v2"],
  "headers": { "X-TES-MSGID": "tes-7f3a9c" }
}