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
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Required | A verified sender. Format: "Name <email@domain.com>" or "email@domain.com". Internationalised domains are accepted and normalised to punycode. |
from_name | string | Optional | Display name for the From header — recipients see Display Name <from>. Overrides a name embedded in from. |
to | string or array of strings | Required | Primary 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. |
cc | string or array of strings | Optional | CC 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). |
bcc | string or array of strings | Optional | BCC recipients (max 1,000). Hidden from other recipients and tracked as their own messages. |
reply_to | string | Optional | Reply-to address. Defaults to from. |
subject | string | Required | Subject line, 1–998 characters. Overridden by the template's subject when template is set. |
html | string | Optional | HTML 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. |
text | string | Optional | Plain-text fallback. Can be sent on its own for plain-text-only emails. |
template | string | Optional | A 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. |
vars | object | Optional | Merge values, substituted as {{key}} in the stored template's subject and body. Only applied when template is set. |
tags | array of strings | Optional | Up to 10 tags (max 128 chars each) for filtering events. Echoed back in every webhook payload for this message. |
headers | object | Optional | Custom 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. |
attachments | array | Optional | File 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. |
tracking | object | Optional | Override workspace defaults: { opens: bool, clicks: bool, https: bool }. |
send_at | ISO-8601 timestamp | Optional | Schedule 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. |
metadata | object | Optional | Free-form key/value pairs stored with the message — internal IDs, A/B variants. Not visible to the recipient. |
verify | boolean | Optional | Verify 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_url | string (URL) | Optional | Per-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
| Status | Description |
|---|---|
202 | Accepted. { 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. |
400 | Validation 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. |
403 | Sandbox send to a recipient not on the confirmed test allow-list, or the account is suspended. |
404 | The template id doesn't exist on your account. |
409 | A request with the same Idempotency-Key is still in flight. |
429 | Monthly/daily sending quota, test-send allowance, or account request rate exceeded. |
503 | Live 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" }
}