Skip to content
WeZend

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, and htmlmessage (or the legacy html alias) for the HTML body. message is the plain-text fallback.
  • WhatsApp: pass a whatsapp object, e.g. { "template": "order_shipped" }, for template messages outside the 24-hour session window.
  • Voice: pass a voice object (e.g. { "voice": "female", "language": "da-DK" }) for TTS parameters.
  • Sender ID: sender must be a sender ID approved in Settings → Sender IDs for SMS/RCS — unless your account has allow_unverified_sender enabled 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