# Outbound SMS

Canonical URL: https://texttree.ai/en/docs/outbound-sms/
Markdown URL: https://texttree.ai/en/docs/outbound-sms.md
Page type: docs
Translation status: approved
Legal status: not_required

## Summary

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

## Source content

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

## Request examples

### curl

```bash
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

```js
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

```py
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

```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.

```json
{
  "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."
  }
}
```
