Skip to main content

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.
Both paths run the same suppression, opt-out, and consent enforcement as a campaign. The difference is bookkeeping: a direct send writes message history rows and nothing else — no campaign object, no draft lifecycle, no analytics rollup.

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:
  1. Send to a list (CSV) links straight to the batch wizard — the next section covers it.
  2. Send to one contact opens the contact picker.
Below the cards, a shortcut row jumps directly into the SMS, WhatsApp, or Email composer if you already know the channel and intend to address it yourself.

Single contact: pick a contact, choose a channel

The Send to one contact card opens a picker:
  1. Search your contacts by name, phone, or email.
  2. 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.
  3. 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>. The to parameter prefills the To field; contactId records which contact row you came from.
A contact with no phone number and no email is not selectable — the picker labels it “no reachable channel” and disables the row. WhatsApp does not accept a prefilled recipient from this picker; reach it from the WhatsApp shortcut on the chooser page (WhatsApp sends need a connected WABA sender).

Batch: CSV to a finished send

The Send to a list (CSV) card opens Messages → Batch SMS, a three-step wizard:
  1. Upload — drop a .csv (or paste numbers directly), or load recipients from an existing contact list.
  2. Preview — review the per-row rendered message, segment counts, and the cost breakdown resolved per destination.
  3. Send — the wizard submits the whole batch as a single POST /messages/batch request and reports each recipient’s outcome.
The full review, pricing, and failure-retry walkthrough is in Batch SMS: send a one-off message to many recipients. The sections below cover the parts that matter most when you arrive through direct send.

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, and tel. 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, or sms_body header, 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 example name) bind those variables per row.
  • Invalid and duplicate rows are excluded, not fatal. A partial number like +123 is 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.
Keep a batch to one message theme. When the rows stop resembling one another — different purposes, different opt-in bases — that is two batches, or a campaign.

Retries: idempotency and remapping

The batch is submitted as one POST /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 200 when every recipient succeeded and 207 (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_code as 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-batch on 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.

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:write scope; a read-only key is rejected with 403.
  • Per-channel limits. POST /messages/batch accepts up to 10,000 recipients per call (resolved per tenant) across sms, whatsapp, email, rcs, viber, instagram, messenger, line, and telegram. 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).
All of these are controls you own at tenant level — configure them, and direct send respects them exactly as a campaign does.

See also