Skip to main content

Direct send recipe: one message to one contact, no campaign

This recipe walks one complete pass through Outbound → Direct send: deciding that the send should not be a campaign, picking the contact and channel, choosing a stored template or an ad-hoc body, understanding the approval chain, and reading the audit record afterward. The fuller coordinates — CSV semantics, batch idempotency, template lifecycle — live in the Direct send reference and the goals/approvals/direct-send guide; this page is the operator’s decision loop.

1. When to bypass campaign machinery

Direct send earns its name when none of the campaign apparatus applies to the message. Pick direct send when:
  • A template fix is worth shipping tonight, not next sprint. A corrected reminder or a follow-up phrased fresh reaches one contact now; a campaign draft → launch → audience-resolve round trip adds bookkeeping a single send never benefits from.
  • Your backend sends transactional system events. Order-confirmation receipts, appointment reminders, status page updates — one-off events with a natural recipient, not an evaluated segment. A campaign would spend its audience-resolution machinery on a single-ID list anyway.
  • A support or ops agent answers one customer. The agent’s desktop needs “send this person a note,” not a draft campaign detached from the conversation. Direct send keeps the message in the same message-history ledger as the conversation.
Prefer a campaign when the audience is a live segment resolved at launch, when A/B variants or holdouts matter, when the same send recurs on a schedule, or when launch needs supervisor approval at audience scale — see Outbound goals, approvals, and direct send for the gate’s thresholds. Prefer sandbox magic numbers (+15005550000-range recipients) when you are testing integration shape, not messaging a person.

2. Choosing the channel and recipient

Open Outbound → Direct send and click Send to one contact. The picker searches contacts by name, phone, or email and lists every reachable identifier on the row (both the phone and the email, when both exist).
  1. Search, then pick a contact.
  2. The picker shows every channel the contact is reachable on — SMS for a phone number, Email for an email address, WhatsApp when a WABA sender is connected — with the exact identifier each would address.
  3. Confirm the channel; the picker routes to that channel’s composer with the recipient prefilled (/messages/sms?to=<phone>&contactId=<id> — the to prefills the To field, contactId records which contact row you came from).
A contact with no reachable identifier is disabled in the list and labeled “no reachable channel” — fix the contact row rather than type an address into the composer, so history attaches to the contact.

3. Template vs ad-hoc body

The composer you land in accepts two body strategies, and which one you pick decides whether next week’s identical message drifts:
  • Stored template — saved once under Outbound → Templates, then picked from the composer. Pick this when the body will recur (order-note updates, appointment reminders, support follow-ups). Reuse stops copy drift: every send renders the same approved version, and edits re-enter any approval the channel needs (WhatsApp templates re-enter Meta review; most other channels stay active).
  • Ad-hoc body — typed in the composer’s freeform field. Pick this when the body is genuinely one-off (a support answer, a one-time correction, a sandbox check). On SMS, the UI flags any unresolved {{variable}} placeholders before send — replace them; shipping literal {{...}} to a customer is an avoidable failure.
Template mechanics (approval states, variable systems, per-channel shapes) are covered in Outbound templates.

4. Approvals and roles

Direct send is a send, gated like every send:
  • Send permission. Firing from the console picker, from a channel composer, or over POST /messages/send requires the owner, admin, or developer role. A viewer/light-seat member can browse the chooser page but the send action rejects.
  • API scope. An API key minted with messages:write is required; read-only keys return 403.
  • The launch gate does not move to direct send. Campaign-scene approval gates (require_approval_default, min_recipients_for_approval) are a campaign property; the dual-control step for a support agent’s one-off note is conversation ownership, not a campaign gate. When the send looks like it needs an approval chain — anything customer-facing at audience scale — it has outgrown direct send; run it as a gated campaign (see Outbound goals, approvals, and direct send).

5. Recording the audit and reference

Every direct send lands in three consumer-readable ledgers — verify after the send, not assume:
  • Message history — the single contact’s conversation gains a message row with its delivery status; contactId links it back to the contact picker selection.
  • Outbound activity — the Outbound → Activity ledger lists the send with its origin (direct send vs campaign) and a per-send reference the reviewer quotes back.
  • Audit log — tenant audit events record the operator who fired the send, the channel, and the approval-chain link when a gated campaign path was involved (for direct send the link is empty by design). Pull the reference from Settings → Audit log when a reviewer asks “who sent this and when.”

6. Failure handling: reading CHANNEL_NOT_CONFIGURED

The most actionable failure a direct send can hit is the platform telling you the chosen channel has no provider credentials yet. CHANNEL_NOT_CONFIGURED surfaces differently depending on the entry point:
  • In the console composer, the send fails with a 503 and the composer surfaces “Provider is not configured — add credentials in Admin → System Config → Credentials.” You only see it if you clicked Send; the message row flips to failed with the reason attached and no wallet credit burns.
  • Over the API (POST /messages/send or /messages/batch), the same failure returns HTTP 503 with error.code: "CHANNEL_NOT_CONFIGURED" and details.channel naming the rejected channel. Do not retry programmatically — it is deterministic against the current credential set: either provision the channel credentials, or fall back to a channel the tenant does have (e.g. email when SMS is unowned).
The distinction matters in an ops loop: treat 503 CHANNEL_NOT_CONFIGURED as “fix setup, then re-send” and treat 4xx validation errors (a 422 on an invalid recipient, a 403 on a read-only key) as “fix the request.” Neither charges the wallet.

Worked example: an order-note update from a support agent

A customer calls support asking whether order 1042 shipped; the agent confirms from the order system that it has, and wants to send a one-off SMS closing the loop.
  1. Open Outbound → Direct send, pick Send to one contact.
  2. Search the customer by phone, pick the SMS channel (the picker labels the exact number it addresses).
  3. The SMS composer opens with the recipient prefilled. Type the ad-hoc body — “Hi Ava, your order 1042 shipped today — tracking in your email.” — or pick the stored order-note-update template to keep copy consistent. On SMS the composer flags unresolved {{variable}} tokens; the agent’s one-off body is clean by construction.
  4. Send. The message lands in the conversation’s message history and the Outbound → Activity ledger with its send reference; the audit log records the agent identity, channel, and timestamp.
  5. If the send returns 503 CHANNEL_NOT_CONFIGURED, the tenant’s SMS sender is not provisioned — the agent hands the setup to an admin, and in the meantime re-sends the note over Email from the same picker (the contact’s email address was a second reachable identifier).
If the same note shape starts shipping forty times a day, that is the signal to promote it from a per-agent habit to a programmatic send — at which point the Direct send reference’s API sections and sandbox magic-number checks apply before production rollout.

See also