Documentation

Outbound SMS

Send production SMS through suppression, spend, sender, and provider checks.

Message debugging
msg_123 Delivered
  1. API request received
  2. Message queued
  3. Sent to carrier
  4. Delivered
  5. Webhook delivered

Outbound sends move through the same safety path whether they originate from the API, message composer, campaign builder, or workflow engine.

Request examples

curl

curl https://api.texttree.ai/api/v1/messages \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+15551234567",
    "body": "Your appointment is tomorrow at 9 AM.",
    "metadata": {
      "workflow": "appointment-reminder"
    }
  }'

JavaScript

const response = await fetch("https://api.texttree.ai/api/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TEXTREE_ACCESS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    phone_number: "+15551234567",
    body: "Your appointment is tomorrow at 9 AM.",
    metadata: { workflow: "appointment-reminder" },
  }),
});

const result = await response.json();

Python

import os
import requests

result = requests.post(
    "https://api.texttree.ai/api/v1/messages",
    headers={"Authorization": f"Bearer {os.environ['TEXTREE_ACCESS_TOKEN']}"},
    json={
        "phone_number": "+15551234567",
        "body": "Your appointment is tomorrow at 9 AM.",
        "metadata": {"workflow": "appointment-reminder"},
    },
    timeout=10,
).json()

Elixir

Mix.install([:req])

result =
  Req.post!(
    "https://api.texttree.ai/api/v1/messages",
    auth: {:bearer, System.fetch_env!("TEXTREE_ACCESS_TOKEN")},
    json: %{
      phone_number: "+15551234567",
      body: "Your appointment is tomorrow at 9 AM.",
      metadata: %{workflow: "appointment-reminder"}
    }
  ).body

Every send is checked for workspace suppressions, spend limits, ledger reservation, sender availability, payload validation, and provider readiness before provider execution. Campaign sends also check campaign-specific suppressions before they enqueue a TextTree message and again if a campaign suppression is added before final dispatch.

The dashboard quick-send form and the Send page use the same queueing path as the API. Operators can choose a connected branded sender number in the app, or leave sender selection automatic. A selected sender must still be connected to the workspace when the worker dispatches the message; if it is unavailable, the job ends with sender_number_unavailable and does not fall back to another sender.

Message idempotency includes the selected sender. Reusing an idempotency_key with a different recipient, body, cost, or sender is an idempotency conflict.

Failure behavior

If a send is blocked, the API returns a structured failure and the message log shows the same reason in the timeline.

{
  "error": {
    "code": "workspace_suppressed",
    "message": "Recipient is suppressed for this workspace.",
    "fix": "Respect the suppression or remove it only after your compliance process allows SMS again."
  }
}