Send a message

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

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 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.
templatestringOptionalA stored template ID 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 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 KB 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.

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" }
}