# Developer API

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

## Summary

Message send and status contracts for the current TextTree alpha API.

## Source content

# Developer API

The current JSON API is intentionally narrow. It is focused on one production-critical path:
queueing outbound SMS and checking its status.

## Base URL

Local Phoenix development runs at `http://localhost:4001`.

All documented developer endpoints live under `/api/v1`.

## Authentication

Authenticated API calls require a TextTree-issued `txt_...` bearer access token.
The same local identity model is used for the browser app, developer API, and
MCP routes. Legacy `txk_...` API keys are retained as internal records and are
not accepted by `/api/v1/messages`.

```http
Authorization: Bearer <textree_access_token>
Accept: application/json
```

`POST /api/v1/messages` requires `messages:write`. Number inventory endpoints
require `numbers:read` or `numbers:write`. Email, Google, wallet, and agent
identities are normalized into the same local TextTree identity before
authorization.

## Headless account bootstrap

AI and API clients can create an account without email, Google OAuth, or wallet
auth by using username/password bootstrap.

`POST /api/v1/accounts`

This route is public, JSON-only, and limited to one successful account creation
per IP address every five minutes. TextTree stores a hash of the IP address for
the throttle record; the raw IP is not stored.

### Request body

```json
{
  "username": "agent-demo",
  "password": "correct horse battery staple"
}
```

### Success response

```json
{
  "account": {
    "id": "9d7d9df7-58a0-4716-b82e-7ad5e73f7b36",
    "username": "agent-demo"
  },
  "backup_codes": ["AAAA-BBBB-CCCC-DDDD"],
  "token": {
    "type": "Bearer",
    "access_token": "txt_...",
    "scopes": ["mcp:read", "mcp:execute", "messages:write"],
    "expires_at": "2026-06-03T15:30:00Z"
  }
}
```

Save the `txt_...` bearer token and backup codes immediately. The internal
synthetic email, token hash, password, and throttle IP hash are never returned.

### Errors

- `422` with `{"error":"validation_failed","details":...}` for invalid or duplicate usernames
  or invalid passwords
- `429` with `{"error":"account_creation_rate_limited","retry_after_seconds":...}` when the
  caller's IP has already created an account in the last five minutes

## Health

`GET /api/v1/health`

This route is public and is useful for deployment checks and local smoke tests.

## Queue an outbound message

`POST /api/v1/messages`

### Request body

```json
{
  "phone_number": "+15551234567",
  "body": "Your verification code is 482019",
  "estimated_cost_cents": 2,
  "idempotency_key": "msg_2026_04_26_0001",
  "metadata": {
    "campaign": "alpha-invite",
    "source": "api"
  }
}
```

### Fields

- `phone_number`: required E.164 recipient phone number
- `body`: required string, 1 to 1600 chars
- `estimated_cost_cents`: optional positive integer, defaults to the configured SMS cost
- `idempotency_key`: optional string, 8 to 128 chars, scoped per user
- `metadata`: optional JSON object

`phone_number` is the V1 recipient field. Do not send `to` unless a future API version explicitly documents it.
API sends use automatic sender selection. Browser app sends can choose a
connected branded number; that selected sender becomes part of the idempotent
message request.

### 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 verification code is 482019",
    "idempotency_key": "msg_2026_04_26_0001"
  }'
```

### Success responses

Newly queued messages return `202 Accepted`:

```json
{
  "message": {
    "id": "8f3d0f4f-5ab3-4db9-bf6a-92c72bc1f2bb",
    "status": "queued",
    "phone_number": "+15551234567",
    "body": "Your verification code is 482019",
    "provider": "messaging_provider",
    "consent_status": "missing",
    "external_id": null,
    "estimated_cost_cents": 2,
    "idempotency_key": "msg_2026_04_26_0001",
    "metadata": {
      "campaign": "alpha-invite",
      "source": "api"
    },
    "inserted_at": "2026-04-26T20:44:12Z",
    "updated_at": "2026-04-26T20:44:12Z",
    "links": {
      "self": "/api/v1/messages/8f3d0f4f-5ab3-4db9-bf6a-92c72bc1f2bb"
    }
  },
  "replayed": false
}
```

If the same user replays the same `idempotency_key` with the same message
payload and sender selection, TextTree returns `200 OK` with the existing
message and `"replayed": true`.

### Error responses

- `422` with `{"error":"validation_failed","details":...}` when the body is malformed
- `403` with `{"error":"workspace_suppressed"}` when the recipient is blocked by a workspace suppression
- `402` with `{"error":"spend_limit_exceeded"}` when the current spend cap would be exceeded
- `409` with `{"error":"idempotency_conflict"}` when an idempotency key is reused with a different
  recipient, body, cost, or sender
- `422` or provider-bound failure details when a selected app sender is no
  longer available to the workspace

## Fetch message status

`GET /api/v1/messages/:id`

This returns the same `message` envelope used in create responses.

The current message status lifecycle is:

- `queued`
- `dispatching`
- `sent`
- `delivered`
- `failed`
- `blocked`

`external_id` is populated once provider-facing execution has a stable provider identifier.

### Status example

```bash
curl https://api.texttree.ai/api/v1/messages/8f3d0f4f-5ab3-4db9-bf6a-92c72bc1f2bb \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN"
```

```json
{
  "message": {
    "id": "8f3d0f4f-5ab3-4db9-bf6a-92c72bc1f2bb",
    "status": "delivered",
    "phone_number": "+15551234567",
    "estimated_cost_cents": 2,
    "links": {
      "self": "/api/v1/messages/8f3d0f4f-5ab3-4db9-bf6a-92c72bc1f2bb"
    }
  }
}
```

## Before you send

Account creation is self-bootstrapping through `POST /api/v1/accounts` and
`POST /mcp/accounts`, but outbound sending still requires workspace setup.
Before `POST /api/v1/messages` can succeed for a new workspace, the caller or
operator must:

- authenticate through TextTree and include a token with `messages:write`
- confirm the recipient is not under an active workspace suppression
- set an active spend limit in `/app`
- configure a test sender, live sender number, or automatic sender mode

## Numbers API

`GET /api/v1/numbers` requires `numbers:read`.

`POST /api/v1/numbers` requires `numbers:write` and buys a number through
the TextTree messaging provider boundary. Include an `area_code` when the
operator wants a local number. Live buys remain blocked unless the deployment
explicitly enables the live-number-buy guardrail.

```bash
curl https://api.texttree.ai/api/v1/numbers \
  -H "Authorization: Bearer $TEXTREE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "area_code": "415",
    "friendly_name": "Support line"
  }'
```

Successful buys return `201 Created`:

```json
{
  "number": {
    "id": "b86fbe4d-2b92-4664-8e91-d2ed1d37f827",
    "number": "+14155550123",
    "friendly_name": "Support line",
    "capabilities": ["sms"],
    "status": "connected",
    "inbound_webhook_url": "https://app.texttree.ai/webhooks/provider/incoming",
    "compliance_status": "pending"
  },
  "webhook_configured": false,
  "warning": "provider_inbound_webhook_configuration_api_not_documented"
}
```

## Current alpha boundaries

The developer API does **not** currently expose:

- legacy consent/contact metadata CRUD endpoints
- workspace-suppression CRUD endpoints
- spend-limit CRUD endpoints
- direct funding-session creation; use onboarding funding invoices for launch top-ups
