# Relay documentation

Base URL: https://relay.tinyclaw.online. All requests authenticate with `X-API-Key: rk_...` or `Authorization: Bearer rk_...`.
Responses from `/platform/v1` are `{ "data": ... }`; errors are `{ "error": "message", "code"? }`.
Responses under `/meta/whatsapp/v24.0` match Meta's Cloud API, including Meta-shaped errors.

## Quickstart
1. Sign up at https://relay.tinyclaw.online/app/ — you get a project, an API key and a free sandbox number.
2. Simulate a customer: `POST /platform/v1/whatsapp/sandbox/inbound {"phone_number_id","from","text"}`
3. Reply: `POST /meta/whatsapp/v24.0/{phone_number_id}/messages {"to","type":"text","text":{"body":"Hi"}}`
4. Watch it in the dashboard inbox, or add a webhook.

## Sending messages
`POST /meta/whatsapp/v24.0/{phone_number_id}/messages` takes Meta's body unchanged: text, template, interactive, image, document, location, reaction.
Free-form messages need an open customer service window (24h since the customer's last message). Outside it the API returns error `#131047`; send a template instead.
Sandbox template sends must name an existing template (error `#132001` otherwise). Every sandbox number starts with `hello_world` / `en_US`.

## Sandbox
Sandbox numbers are free, never touch Meta and never message real people.
- `POST /platform/v1/whatsapp/sandbox_numbers {"name"?}` creates one.
- `POST /platform/v1/whatsapp/sandbox/inbound {"phone_number_id","from","text","profile_name"?}` simulates an inbound message.
- Outbound sandbox messages move to `delivered` and then `read` after a short delay, and fire the same webhooks as live numbers.
Sandbox messages do not count toward your plan.

## Phone numbers
- `GET /platform/v1/whatsapp/phone_numbers` lists numbers (`kind`: sandbox | live).
- `POST /platform/v1/whatsapp/phone_numbers {"phone_number_id","access_token","business_account_id"?,"customer_id"?}` connects an existing Cloud API number. The token is verified against Meta.
- `GET /platform/v1/whatsapp/phone_numbers/{id}/health` returns quality rating and messaging limit for live numbers.
- `DELETE /platform/v1/whatsapp/phone_numbers/{id}` disconnects it.

## Customers and setup links
For platforms that connect many businesses. Create a customer, then a setup link; the business opens the link, logs in with Facebook (Meta Embedded Signup) and picks their number. The number then appears under the customer as `live`.
- `POST /platform/v1/customers {"name","external_customer_id"?}`
- `POST /platform/v1/customers/{id}/setup_links {"success_redirect_url"?}` → `{ url, expires_at }` (30 days)

## Conversations, messages, contacts
- `GET /platform/v1/whatsapp/conversations?status=active|ended&per_page&page`
- `GET /platform/v1/whatsapp/conversations/{id}/messages`
- `PATCH /platform/v1/whatsapp/conversations/{id} {"status"?,"assigned_to"?}`
- `GET /platform/v1/whatsapp/messages`, `GET /platform/v1/whatsapp/messages/{wamid}`
- `GET /platform/v1/whatsapp/contacts`

## Templates
- `GET /platform/v1/whatsapp/templates?phone_number_id=...` (or `business_account_id`)
- `POST /platform/v1/whatsapp/templates {"phone_number_id","name","language","category","components"}`
- `DELETE /platform/v1/whatsapp/templates/{name}?phone_number_id=...`
Sandbox templates are approved instantly. Live templates are submitted to Meta and follow Meta review.
The Meta-style path `/meta/whatsapp/v24.0/{waba_id}/message_templates` also works.

## Webhooks
`POST /platform/v1/whatsapp/webhooks {"url","events"?,"phone_number_id"?}` returns a `secret_key` once.
Events: whatsapp.message.received, whatsapp.message.sent, whatsapp.message.delivered, whatsapp.message.read, whatsapp.message.failed, whatsapp.conversation.created, whatsapp.phone_number.created.
Headers: `x-relay-event`, `x-relay-signature` = hex HMAC-SHA256 of the raw body with your secret.
Failed deliveries retry after 10s, 1m, 5m, 30m and 2h (6 attempts). See `GET /platform/v1/webhook_deliveries`, retry with `POST /platform/v1/webhook_deliveries/{id}/retry`, test with `POST /platform/v1/whatsapp/webhooks/{id}/test`.

## Broadcasts
Paid plans. Template-only (Meta rule for business-initiated messages), sent sequentially at a safe rate.
1. `POST /platform/v1/whatsapp/broadcasts {"phone_number_id","name","message":{"type":"template","template":{...}}}`
2. `POST /platform/v1/whatsapp/broadcasts/{id}/recipients {"recipients":["9198...", ...]}`
3. `POST /platform/v1/whatsapp/broadcasts/{id}/send`

## Live events (SSE)
`GET /platform/v1/events/stream?api_key=rk_...` streams every event for the project. Useful for dashboards and `relay inbox watch`.

## MCP server
`POST https://relay.tinyclaw.online/mcp`, streamable HTTP, auth `Authorization: Bearer rk_...`. Tools take `{action, params}`; `action: "help"` lists params.
Read-only keys only see read actions. See https://relay.tinyclaw.online/AGENTS.md for client configs.

## CLI
Install: `curl -fsSL https://relay.tinyclaw.online/install.sh | bash`. Commands: `relay login`, `relay status`, `relay numbers list`, `relay messages send --phone-number-id ID --to 9198... --text "Hi"`, `relay conversations list`, `relay inbox watch`, `relay sandbox inbound`, `relay webhooks create --url ...`, `relay setup --customer "Acme"`, `relay mcp config`, `relay usage`.
In CI set `RELAY_API_KEY` instead of logging in.

## API keys
`POST /platform/v1/api_keys {"name","scope":"full"|"read"}`. Read keys can only make GET requests and read-only MCP actions; give them to agents that only need to look.

## Plans and limits
`GET /platform/v1/plans` (public) and `GET /platform/v1/usage`.
Billable messages = inbound + outbound on live numbers in the calendar month (UTC). Sandbox is free.
- Free: 1,000 messages, 1 live number, 60 requests/min. Sends stop at the limit (HTTP 402, code payment_required).
- Starter ₹999/mo: 25,000 messages, 2 numbers, broadcasts. Overage ₹0.15/message.
- Pro ₹2,499/mo: 100,000 messages, 5 numbers. Overage ₹0.12/message.
- Platform ₹14,999/mo: 1,000,000 messages, 50 numbers, white-label setup pages. Overage ₹0.06/message.
Meta's own conversation charges are billed by Meta to the WhatsApp Business Account, not marked up.
Rate limits return HTTP 429 with `retry-after`; every response carries `x-ratelimit-limit` and `x-ratelimit-remaining`.

## Errors
| HTTP | code | meaning |
|---|---|---|
| 400 | invalid_params | bad body, Meta validation error (#100, #131047, #132001) |
| 401 | unauthorized | missing or wrong API key |
| 402 | payment_required | plan limit reached or feature not in plan |
| 403 | insufficient_scope / forbidden | read-only key, or not your project |
| 404 | not_found | unknown id in this project |
| 409 | conflict | duplicate, or wrong state (e.g. broadcast already sent) |
| 429 | rate_limited | slow down; see retry-after |
| 502 | upstream_error | Meta returned an error |
