Direct send: one-off messages, batch or single
Some sends are not campaigns. A support follow-up to one customer, a one-time notification to a finite list — no named audience, no schedule, no per-variant analytics to justify a campaign object. Outbound → Direct send is the entry point for those sends. It is a chooser page with two paths:- Send to a list (CSV) — the batch SMS wizard at Messages → Batch SMS: upload a recipient list, render a template per row, review cost, and send everything in one batch.
- Send to one contact — a picker that searches your contacts, shows every channel the contact is reachable on, and opens that channel’s composer with the recipient prefilled.
When direct send instead of a campaign
If a recurring weekly CSV run or per-variant measurement appears, the send has outgrown direct send — move it to a campaign and follow Send a campaign end-to-end.
The chooser page
Open Outbound → Direct send. Two cards lead:- Send to a list (CSV) links straight to the batch wizard — the next section covers it.
- Send to one contact opens the contact picker.
Single contact: pick a contact, choose a channel
The Send to one contact card opens a picker:- Search your contacts by name, phone, or email.
- Select a contact. The picker lists every identifier on the contact — a contact with both a phone number and an email shows both — and offers every channel it can route to: SMS for a phone number, Email for an email address.
- Choose the channel and continue. The picker routes to that channel’s composer with the recipient prefilled:
/messages/sms?to=<phone>&contactId=<id>or/messages/email?to=<email>&contactId=<id>. Thetoparameter prefills the To field;contactIdrecords which contact row you came from.
Batch: CSV to a finished send
The Send to a list (CSV) card opens Messages → Batch SMS, a three-step wizard:- Upload — drop a
.csv(or paste numbers directly), or load recipients from an existing contact list. - Preview — review the per-row rendered message, segment counts, and the cost breakdown resolved per destination.
- Send — the wizard submits the whole batch as a single
POST /messages/batchrequest and reports each recipient’s outcome.
CSV rules for batch sends
The wizard’s Upload step understands three recipient sources — CSV, pasted numbers, or a saved contact list — and only one source is active at a time: choosing a second source replaces the first rather than combining both. The CSV contract:- One phone column is required. The header row is auto-detected. Recognized phone-column headers include
phone,phone_number,to,number,mobile,telephone, andtel. Recipients are E.164 numbers; whitespace, dashes, and parentheses are stripped before validation. - Per-row message column switches to per-row mode. If the CSV has a
message,body,text,content,sms, orsms_bodyheader, every row carries its own body and the shared compose step is skipped. Otherwise the wizard applies one shared template with{{variable}}placeholders, and your extra CSV columns (for examplename) bind those variables per row. - Invalid and duplicate rows are excluded, not fatal. A partial number like
+123is flagged and dropped; duplicates are merged with a count. Excluded rows are never priced and never sent — the review footnote discloses exactly how many were skipped. - Rejections stay per-row. A post-upload rejection (validation, quota, sender-validation, country gate set in your compliance controls) flips only that row to
failed; the rest of the batch still sends.
Retries: idempotency and remapping
The batch is submitted as onePOST /messages/batch call; failures come back as per-row outcomes, never as a silent whole-batch loss:
- Every recipient is persisted before it is attempted. Each recipient becomes a message row first — a rejected row is visible in the failed-recipients table immediately with its reason, not dropped. The response is
200when every recipient succeeded and207 (Multi-Status)when at least one failed alongside at least one success. - Retry failed rows only. The failed-recipients table lists each row with a remapped, friendly reason and the raw machine-readable
error_codeas a badge. For carrier-side or transient failures, the messaging retry policy applies server-side — retry the failed recipients; already-sent rows are not re-sent when you re-submit the batch envelope with the same idempotency behaviour. - Batch envelope rate limit. One batch is one request, capped at 30 batch envelopes per minute per tenant (
X-RateLimit-Bucket: messages-batchon the response headers tells your SDK when to back off). The per-recipient cost — carrier throughput, frequency caps, quota — is enforced inside the send pipeline, not by this limit.
Batch over the API
Everything the wizard does on the screen,POST /api/v1/messages/batch does over the wire. The envelope below is the scripted equivalent of a CSV upload: a shared template with {{variable}} placeholders, and per-row bindings that map your extra CSV columns onto those variables.
Recommendation: automate only when the send is truly recurring — a one-off upload is faster through the wizard. Once you find yourself re-uploading the same shape, the API is the winning move.
A batch request
This is the CSV contract restated as JSON.body is the shared template; each recipient carries a variables bag that binds the placeholders, exactly the way your extra CSV columns (name, order) bound the wizard’s variables. A recipient that carries its own body switches that row to per-row mode — the same switch the message CSV column triggers.
data/meta pair every endpoint returns. data carries batch_id, a results[] row per recipient, and a summary with the counters.
200 vs 207 — read the per-row outcome
The HTTP status tells you whether anything failed; theresults[] array tells you which row and why. You get 200 OK only when every recipient succeeded. The moment at least one row fails alongside a success — or every row fails — the response is 207 (Multi-Status) and each failed row carries a friendly error_message (the remapped reason a human reads) plus the raw, machine-readable error_code a program branches on.
A 207 with one succeeded and one failed row:
status is one of sent, queued, scheduled, delivered, or failed. error_code and error_message are present only on failed rows. Retry the failed ones only — the next section shows the batch knows how to skip a row you already sent.
Re-submit with the same idempotency key
Send the same payload again with the sameIdempotency-Key and already-sent rows are not re-sent — the re-run skips them and only attempts the rows that failed. This is the retry loop to build: treat a 207 as “retry the failed subset”, reuse the idempotency key, and the send pipeline keeps the retry from becoming a double-send.
The batch cap and the back-off receipt
The batch envelope is rate-limited to 30 per minute per tenant. Every response — success or failure — carries the bucket name so your client knows which cap it is up against:429 Too Many Requests with a structured receipt your back-off loop should honor:
retry_after (seconds) before retrying. Any SDK that understands X-RateLimit-Bucket: messages-batch can back off cap-specifically; a hardcoded sleep ignores which limit actually fired. The per-recipient costs — carrier throughput, frequency caps, quota — are still enforced inside the pipeline and are not what this cap protects.
Python SDK
The same batch from Python. The batch route is not on the SDK’s per-channelclient.messages convenience layer, so call request(...) directly — that is the documented path for a shape the wrappers do not cover. The SDK auto-generates an Idempotency-Key on every POST; pass your own to control the retry-skip semantics above.
messages:write scope, and SMS batches from an API key must name an explicit from sender.
Permissions and limits per channel
Direct send is a send, so it is gated like every other send in the workspace:- Role. Sending — from the batch wizard, the single-contact composer, or the API — requires an owner, admin, or developer role. Members with a read-only (viewer / light-seat) role cannot fire a direct send.
- API key scope. API-driven sends need a key minted with the
messages:writescope; a read-only key is rejected with 403. - Per-channel limits.
POST /messages/batchaccepts up to 10,000 recipients per call (resolved per tenant) acrosssms,whatsapp,email,rcs,viber,instagram,messenger,line, andtelegram. The dashboard batch wizard drives SMS; the same route handles the other channels over the API. - Sender and channel setup. SMS needs a sender — an owned number, an alphanumeric sender ID, or a sender pool. Email needs a verified sending address. WhatsApp needs a connected WABA sender. A contact with no reachable identifier for the channel (for example an SMS-only contact sent to the Email composer) has nothing to address — the composer starts empty.
- Wallet and attestation. The batch wizard’s confirm step is gated on a wallet check and a compliance attestation that recipients are authorized and opt-outs honored.
How direct send relates to campaigns
Both surfaces end at the same send pipeline. What a campaign adds is everything around the send:
Guardrails do not relax because a send is one-off. Direct sends pass through the same tenant controls as campaign sends:
- Suppression lists and channel-level opt-outs are checked per recipient — a suppressed or opted-out recipient is held back either way (see Opt-out lists and suppression).
- Frequency caps count a direct send toward the recipient’s budget (see Frequency caps).
- Quiet hours and send gating defer or block sends inside a recipient-local quiet window, direct send or campaign alike (see Quiet hours configuration).
See also
- Batch SMS: send a one-off message to many recipients — the wizard’s review, pricing, and failure-retry mechanics in full
- Send and receive messages — the per-channel composer the contact picker opens
- Send a campaign end-to-end — when the send has outgrown direct send
- Outbound goals, approvals, and direct send — the approval gate and goal attribution a campaign adds on top
- Message scheduling — one-off
scheduled_atsemantics on batches