---
title: "Send a message"
method: POST
path: "/messages"
tags: ["SMS & WhatsApp Messaging"]
---

# Send a message

`POST /messages`

## Request

- Base URL: `https://api.envoisms.ma/v1`
- URL: `https://api.envoisms.ma/v1/messages`
- Auth: HTTP bearer

Send one SMS, or one WhatsApp message from your own connected WhatsApp Business number (channel whatsapp: templates, text, media by id or link, interactive list/button/flow/cta_url/location_request_message, reactions, contact cards, locations, stickers, and a reply context on any of them). WhatsApp rule: a message that is not a template may only be sent to a contact who wrote to your number in the last 24 hours; otherwise it is refused with 400 OUT_OF_24H_WINDOW before anything is charged. Reactions are not billed. To show a typing indicator while you prepare a reply, see POST /messages/typing. Without a connected number, WhatsApp OTPs are available via /verify/send with channel whatsapp. Sending one-time codes? /verify/send manages the full OTP lifecycle (generation, delivery, validation, expiry) and is the recommended path; if you generate codes yourself and send them here, tag the request with metadata purpose: "otp" to enable the automatic one-shot re-send over an alternate SMS route on delivery failure (see the sms_otp example).

## Headers

- `Idempotency-Key` string

## Request body

- SendMessageRequest: `message` is required for SMS and for WhatsApp text and interactive messages. A WhatsApp message whose content is a template, a media object, a reaction, contacts, a location or a sticker needs no `message` (for media it becomes the caption when given). Exactly one of reaction, contacts, location or sticker per message, and none of them together with another content type.
  - `to` string, required: Destination phone in E.164 format
  - `message` string: Message content (max 1600 chars). See the object description for when it may be omitted on WhatsApp.
  - `from` string: Sender ID (alphanumeric, max 11 chars)
  - `channel` 'sms' | 'whatsapp' | 'voice' | 'rcs': Channel for message delivery: sms (default) for Moroccan operator routes; whatsapp to send from your own connected WhatsApp Business number (403 WHATSAPP_NOT_CONNECTED otherwise). To send an ordinary text to a customer who happens to use WhatsApp, sms is enough: no connection, no template, no 24-hour window. On whatsapp, a message that is not a template may only be sent to a contact who wrote to your number in the last 24 hours — otherwise it is refused with 400 OUT_OF_24H_WINDOW before any charge (with a cascade, WhatsApp is skipped and the next channel is used instead).
  - `cascade` boolean: When true, tries WhatsApp, then falls back to SMS: at once if WhatsApp refuses the message or reports it failed (not on WhatsApp, 24-hour window closed, ...), or when no delivery report arrives within cascade_timeout (120 s by default). Only a send that actually went out is billed: a refused or failed WhatsApp message is not, and you pay for the SMS alone. A WhatsApp message still waiting for its delivery report did go out (it can still be delivered when the phone reconnects), so it is billed along with its SMS fallback, and refunded if WhatsApp later reports it failed. Tip: give your WhatsApp template a time-to-live (TTL) no longer than cascade_timeout, so a phone that comes back online does not receive both.
  - `cascade_timeout` integer: With cascade: seconds to wait for a delivery report before the next channel. Shorter falls back faster but bills more double sends; longer means fewer duplicates. Out of range: 400 INVALID_FIELD.
  - `metadata` object: Custom key-value pairs, stored with the message and returned in DLR webhooks. One key carries platform semantics: set purpose: "otp" when the message carries a one-time code you generated yourself — if the network reports a hard delivery failure on a Moroccan number while the code is still fresh (within 10 minutes), the platform automatically re-sends it once over an alternate SMS route, under the same message id and at no extra charge. Prefer /verify/send when you want the whole code lifecycle (generation, delivery cascade, validation, expiry) managed for you. Set no_failover: true to opt out of this automatic re-send entirely — the first delivery attempt's outcome is then final.
  - `buttons` MessageButton[]: Optional WhatsApp interactive buttons (max 3). Only supported on the whatsapp channel.
    - `type` 'copy' | 'url' | 'call', required: Button action type
    - `text` string, required: Button text label
    - `copy_code` string: Value to copy (only for copy type)
    - `url` string: Target URL (only for url type). Must start with http:// or https://
    - `phone_number` string: Target phone number (only for call type) in E.164 format
  - `interactive` object: WhatsApp Cloud API interactive message payload. cta_url: action {name: "cta_url", parameters: {display_text (max 20 chars), url}}. location_request_message: body text plus an optional action {name: "send_location"}; the contact answers with a location.
    - `type` 'list' | 'button' | 'product' | 'product_list' | 'flow' | 'carousel' | 'order_details' | 'cta_url' | 'location_request_message'
    - `header` object
    - `body` object
      - `text` string
    - `footer` object
      - `text` string
    - `action` object
  - `template` object: WhatsApp only. An approved template of your account; exempt from the 24-hour window. Priced by the template's category as registered on your account.
    - `name` string
    - `language` string
    - `components` object[]
  - `phone_number_id` string: WhatsApp only. Which of your connected numbers sends; defaults to your default number.
  - `image` WhatsAppMedia: WhatsApp media by exactly one of id (a media id uploaded to WhatsApp) or link (an https URL). caption on image, video and document only; filename on document only.
    - `id` string
    - `link` string
    - `caption` string
    - `filename` string
  - `video` WhatsAppMedia: WhatsApp media by exactly one of id (a media id uploaded to WhatsApp) or link (an https URL). caption on image, video and document only; filename on document only.
    - `id` string
    - `link` string
    - `caption` string
    - `filename` string
  - `audio` WhatsAppMedia: WhatsApp media by exactly one of id (a media id uploaded to WhatsApp) or link (an https URL). caption on image, video and document only; filename on document only.
    - `id` string
    - `link` string
    - `caption` string
    - `filename` string
  - `document` WhatsAppMedia: WhatsApp media by exactly one of id (a media id uploaded to WhatsApp) or link (an https URL). caption on image, video and document only; filename on document only.
    - `id` string
    - `link` string
    - `caption` string
    - `filename` string
  - `sticker` WhatsAppMedia: WhatsApp media by exactly one of id (a media id uploaded to WhatsApp) or link (an https URL). caption on image, video and document only; filename on document only.
    - `id` string
    - `link` string
    - `caption` string
    - `filename` string
  - `reaction` object: WhatsApp only. React to a message of the conversation. Not billed. An empty emoji removes your reaction.
    - `message_id` string, required: WhatsApp message id (wamid) of the message you react to
    - `emoji` string, required
  - `contacts` object[]: WhatsApp only. Contact cards (WhatsApp Cloud API contacts object). name.formatted_name is required; allowed keys: name, phones, emails, urls, addresses, org, birthday (YYYY-MM-DD).
    - `name` object, required
      - `formatted_name` string, required
      - `first_name` string
      - `last_name` string
    - `phones` object[]
      - `phone` string
      - `type` string
      - `wa_id` string
  - `location` object: WhatsApp only. Send a location pin.
    - `latitude` number, required
    - `longitude` number, required
    - `name` string
    - `address` string
  - `context` object: WhatsApp only. Reply to (quote) an earlier message of the conversation. Works with every type except reaction.
    - `message_id` string, required

## Response `202`

Message queued

- Message: Response to POST /messages — the message as accepted into the send queue.
  - `id` string
  - `to` string
  - `channel` 'sms' | 'whatsapp' | 'voice' | 'rcs': sms is generally available with multi-operator routing. whatsapp sends from your own connected WhatsApp Business number (templates, text, media, interactive messages, reactions, contacts, locations) via the official WhatsApp Cloud API. voice and rcs are available on request.
  - `cascade` boolean
  - `status` string
  - `cost` object: What was actually charged. Always zero on a sandbox key.
    - `eur` number
    - `mad` number
  - `segments` integer
  - `created_at` string, date-time
  - `sandbox` boolean: Present and true only when the request used an `env_test_` key. Nothing was delivered and nothing was charged.
  - `simulated_cost` object: Sandbox only. What this exact send would have cost on a live key, at the account's own rates — so an integration can be costed before it goes live. Reported separately from `cost` so a rehearsal can never be mistaken for revenue.
    - `eur` number
    - `mad` number
  - `warnings` object[]: Sandbox only. What a live key would have been refused for — today `OUT_OF_24H_WINDOW` for a free-form WhatsApp message outside the customer service window. The sandbox still simulates the send.
    - `code` string
    - `message` string

## Other responses

- `400`: Bad request — including OUT_OF_24H_WINDOW (WhatsApp free-form message outside the customer service window; the error also carries window_expired: true and window_expires_at) and the WhatsApp payload codes listed in Error Format. Nothing is charged.
- `402`: Insufficient balance
- `403`: WHATSAPP_NOT_CONNECTED (no connected WhatsApp Business number), FORBIDDEN_ASSET, OPTED_OUT, or a Sender ID that is not usable yet
- `429`: Rate limit exceeded

---

[API](https://skmtc.dev/envoisms/apis/envoisms-ma-api.md) · [All operations](https://skmtc.dev/envoisms/apis/envoisms-ma-api/llms.txt) · [OpenAPI document](https://skmtc.dev/envoisms/apis/envoisms-ma-api/revisions/662db074bd86?raw)
