Core APIs
Sending messages
Every field POST /v1/messages/send accepts, channel fallback, and how per-category frequency caps work.
POST /v1/messages/send is the single endpoint for every channel.
Required fields
to, channel (one of sms, rcs, whatsapp, email, voice) and message are required — or pass a channels array instead of channel/fallback (see below).
Channel fallback
Provide an ordered fallback list, or the newer channels array with the primary channel first:
{
"to": "+4512345678",
"channels": ["whatsapp", "sms"],
"message": "Your order has shipped."
}
If WhatsApp delivery fails, the platform automatically retries on SMS — no separate API call.
Channel-specific fields
- Email: set
subject, andhtmlmessage(or the legacyhtmlalias) for the HTML body.messageis the plain-text fallback. - WhatsApp: pass a
whatsappobject, e.g.{ "template": "order_shipped" }, for template messages outside the 24-hour session window. - Voice: pass a
voiceobject (e.g.{ "voice": "female", "language": "da-DK" }) for TTS parameters. - Sender ID:
sendermust be a sender ID approved in Settings → Sender IDs for SMS/RCS — unless your account hasallow_unverified_senderenabled for system integrations.
Reply-To, From name and custom headers
Three optional email fields:
reply_to— where replies go. Either a bare address (support@yourcompany.com) or the display-name form (Support <support@yourcompany.com>); the address part is validated as an email. It is deliberately not domain-gated — Reply-To isn't the envelope sender, so you can point replies at any inbox you control.from_name— the From display name, set independently of the From address.headers— an object of header name → value, e.g.{ "X-Order-Id": "10432" }. Names must match[A-Za-z0-9][A-Za-z0-9-]*.
headers is a safelist, not passthrough. These names are reserved and cannot be set: List-Unsubscribe, List-Unsubscribe-Post, From, Sender, Return-Path, Reply-To, DKIM-Signature, Received, and anything starting with X-WeZend — all matched case-insensitively. They protect one-click unsubscribe compliance, email authentication and envelope identity. Reply-To is reserved because it has its own validated reply_to field; a raw header would bypass that validation.
A reserved name is rejected, not silently dropped, and the error names the offending header:
{
"to": "customer@example.com",
"channel": "email",
"subject": "Your receipt",
"message": "Thanks for your order.",
"headers": { "List-Unsubscribe": "<mailto:opt-out@example.com>" }
}
{ "error": "Header \"List-Unsubscribe\" is reserved and cannot be set" }
All three fields are persisted in the message's send_options, so they survive the queue.
Scheduling and per-message webhooks
scheduled_at (ISO timestamp) delays dispatch. webhook_url receives delivery status updates for this specific message — see Webhooks.
Marketing frequency caps
Pass a category (e.g. "newsletter") to have this send count against that category's frequency cap, configured account-wide in messaging policy — independent of the global cross-channel cap and any per-channel cap.
Transactional sends
Pass transactional: true (the boolean, or the string "true") to mark a send as transactional — a password reset, a receipt, an opt-in confirmation. It defaults to false: a normal send is marketing.
Transactional mail is exempt from the marketing band cap. Over the cap it is billed per message as overage instead of being rejected with quota_exceeded, so a heavy campaign month can't hard-stop your password-reset mail. It is still counted, so the volume stays visible in usage reporting, and the flag is persisted on the message row — it survives the queue and any retries.
There is exactly one quota case where transactional mail is still blocked: a free plan with no card on file, where there is no way to bill the overage. region_blocked and payment_method_required are not quota decisions, so transactional does not override those either.
transactional: true also exempts the send from frequency caps and quiet hours.
All four per-send fields — transactional, reply_to, from_name and headers — are accepted per item in POST /v1/messages/bulk too. There an invalid reply_to or a reserved header name fails only that item (it's counted in failed) rather than rejecting the whole batch.
Response
{
"message_id": "3fa1e2c0-...",
"status": "queued",
"channel": "sms",
"to": "+4512345678",
"cost": 0.045,
"currency": "EUR",
"created_at": "2026-07-08T10:00:00.000Z"
}
A 402 means insufficient balance or a missing payment method; 403 means the account is suspended, pending activation, or the sender ID isn't approved; 409 means the recipient has opted out of that channel.
Build the request
Fill in the fields you need and copy the request in cURL, Node, Python or PHP — this builds the code, it does not send anything.
On SMS and RCS this must be a sender ID approved under Settings → Sender IDs — an unapproved value is rejected with 403. Leave it empty to use your account default, which always works.
Empty fields are left out of the request. Email-only fields appear when the channel is email.
curl https://api.wezend.com/v1/messages/send \
-H "X-API-Key: $WEZEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+4512345678",
"channel": "sms",
"message": "Hello from WeZend"
}'One platform. Every customer interaction.
Replace your patchwork of messaging APIs, CDP and automation tools with a single engagement platform built for scale.
No credit card required · EU data residency · 99.99% uptime SLA