# TeleTraff Public API (v1)

> Send Telegram messages from your own TeleTraff accounts, read the conversations they hold, and receive replies by webhook -- from your CRM, script or AI agent. JSON over HTTPS, bearer token, English errors with a machine `code` and a `hint`.

## Overview

- Base URL: `https://teletraff.com/api/public/v1`. Every path below is relative to it.
- OpenAPI 3 document: `https://teletraff.com/api/public/v1/openapi.json` (no token needed). This file: `https://teletraff.com/api/public/v1/llms.txt`.
- MCP server for Claude, Cursor and other agents: `https://teletraff.com/api/public/mcp` (see MCP).
- `/v1` only grows: fields and endpoints are added, never removed or renamed. Ignore fields you do not know.
- Times are ISO 8601 in UTC (`2026-10-07T12:30:02Z`). Text is UTF-8.
- Call `GET /me` first: it says what the token may do, its rate limit and the exact numbers of every pace.
- Messages are queued and paced by the server to keep the accounts alive. `POST /messages` answers at once with `scheduled_at`; poll `GET /messages/{message_id}` or take the `message.sent` / `message.failed` webhook.

Quick start:

```sh
curl -s https://teletraff.com/api/public/v1/me -H "Authorization: Bearer $TELETRAFF_API_TOKEN"
curl -s https://teletraff.com/api/public/v1/messages -H "Authorization: Bearer $TELETRAFF_API_TOKEN" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"account_id": "acc_17", "to": {"username": "durov"}, "text": "Hi!", "dry_run": true}'
```

## Authentication

Send `Authorization: Bearer tg_sk_live_...` on every call. Tokens are issued in the panel, Settings -> API; a panel login cookie does not open this API.

Scopes: `public:read`, `public:messages`, `public:campaigns`, `public:webhooks`. `public:read` reads accounts, messages and conversations; `public:messages` sends messages and switches listening; `public:webhooks` manages webhooks; `public:campaigns` is reserved and opens nothing in v1 yet. Each endpoint below names its scope; a missing one is `403 scope_denied` with `details.required_scope`. `GET /me` needs only a valid token.

The API must be enabled for the workspace (closed beta); otherwise every call is `403 api_not_enabled`.

## IDs

Every id is a string with a type prefix, never a bare number, so one kind cannot be passed for another (that is `400 invalid_id`, naming the expected prefix):

- `acc_123` -- account
- `msg_123` -- message
- `conv_123` -- conversation
- `wh_123` -- webhook
- `whd_123` -- webhook delivery
- `cmp_123` -- campaign
- `tok_123` -- token
- `evt_123` -- event

Pass ids back exactly as the API returned them.

## Pagination

List endpoints take `limit` (1-100, default 50) and `cursor`. The answer has `data` and `next_cursor`; pass `next_cursor` back as `cursor` with the same filters until it is `null`. Cursors are opaque -- do not build or edit them (`400 invalid_cursor`). Endpoints with `since` return only what is newer than that time; keep the newest time you saw and poll with it.

## Rate limits

60 requests per 60 seconds per token, in fixed windows, across all endpoints (`openapi.json` and `llms.txt` are not counted). Successful answers carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until the window resets). Over the limit the answer is `429 rate_limited` with the same headers and `Retry-After`: wait that long, never retry in a tight loop.

Separately, one account holds at most 500 queued messages (`429 account_queue_full`), and sending above the pace caps is not refused: the message is queued for the earliest allowed time.

## Idempotency

`POST /messages` requires an `Idempotency-Key` header of 8-128 characters (a UUID works), unique per message and per token:

- Retrying with the same key and the same body returns the same message (`202`), never a second send. After a timeout or a network error, retry with the same key.
- The same key with a different body (account, recipient, text or pace) is `409 idempotency_conflict`.
- Two requests with one key at the same moment: one wins, the other gets `409 idempotency_conflict`; retry it.
- `dry_run: true` writes nothing and needs no key.

Webhook deliveries are at least once: deduplicate by the `X-TeleTraff-Delivery` header (or the event `id`).

## Pace presets

Sending tempo is chosen only by `pace`; **default: `careful`** when `pace` is omitted. Raw delays and caps are not accepted, an unknown name is `422 invalid_pace`.

| pace | protection mode | pause between cold messages, s | cold per account per hour | cold per account per day |
|---|---|---|---|---|
| `careful` (default) | slow | 90-113 | 5 | 20 |
| `balanced` | normal | 45-56 | 10 | 40 |
| `fast` | fast | 20-24 | 20 | 80 |

A **cold** message is first contact (or the account wrote last): it waits for the pause after the account's previous cold message and counts against the hourly and daily caps; above a cap it is queued for later, not refused. A **reply** (the other person wrote last) goes out within seconds and spends no cap. The response's `kind` and `scheduled_at` say which it is and when it goes out.

## Dry run

`POST /messages` with `"dry_run": true` runs every check (ids, recipient, pace, account state, caps) and answers `200` with `would_status`, `kind`, `scheduled_at` and the cold sends left this hour and day -- nothing is written or sent and no Idempotency-Key is needed. Refusals are the same as a real send. Use it to validate a plan before sending.

## Errors

Every refusal has one shape; branch on `code`, show `message`, follow `hint`:

```json
{
  "error": {
    "code": "account_not_eligible",
    "message": "This account cannot send a cold message right now (spamblock).",
    "hint": "This account may not write first while spamblocked; it can still reply.",
    "docs_url": "https://teletraff.com/en/docs/api#account_not_eligible",
    "details": {
      "reason": "spamblock"
    }
  }
}
```

`code` is stable; `message` and `hint` may be reworded. `details` carries what the code says it does. A `5xx` without this shape is a server fault: retry later with the same Idempotency-Key.

### unauthorized

HTTP 401. When: The Authorization header is missing or not `Bearer <token>`, or the token is unknown, revoked or expired, or its operator was removed or deactivated. What to do: Send 'Authorization: Bearer tg_sk_live_...' with a token issued in Settings -> API.

### scope_denied

HTTP 403. When: The token is valid but lacks the scope of this operation; `details.required_scope` names it. What to do: Issue a token that includes details.required_scope in Settings -> API.

### forbidden

HTTP 403. When: The operator who owns the token is blocked, so write requests (POST, PATCH, DELETE) are refused. What to do: Contact TeleTraff support; the token works again once access is restored in the TeleTraff panel.

### payment_required

HTTP 402. When: The workspace's trial or subscription has ended: write requests are refused, reads still work. What to do: Renew the subscription in the panel (Billing), then retry the request.

### api_not_enabled

HTTP 403. When: The public API is switched off for this workspace (it is in closed beta). What to do: The API is in closed beta. Ask TeleTraff support to enable it for your workspace.

### rate_limited

HTTP 429. When: This token made more requests in the current fixed window than its limit (GET /me `rate_limit`). The answer carries Retry-After and X-RateLimit-* headers. What to do: Wait Retry-After seconds. Do not retry in a tight loop.

### invalid_request

HTTP 422. When: A field is missing, unknown, of the wrong type or out of range, the JSON does not parse, or the message text was refused. `details.errors` lists `{field, problem}` for each. What to do: Fix the fields listed in details.errors.

### invalid_id

HTTP 400. When: An id is not of the kind this place takes, e.g. an `acc_` id where a `conv_` id belongs. `details.expected_prefix` and `details.value` say which. What to do: Pass the id exactly as the API returned it; ids look like '<prefix>_123'.

### invalid_cursor

HTTP 400. When: `cursor` was not taken from `next_cursor` of the same list (or the filters changed between pages). What to do: Omit cursor to start from the first page, then pass next_cursor back unchanged.

### not_found

HTTP 404. When: No route matches this path. What to do: Check the path against /api/public/v1/openapi.json.

### method_not_allowed

HTTP 405. When: The path exists but not with this HTTP method. What to do: Check the method against /api/public/v1/openapi.json.

### bad_request

HTTP 400. When: Generic answer to an HTTP 400 that no more specific code covers. What to do: Check the request against /api/public/v1/openapi.json.

### conflict

HTTP 409. When: Generic answer to an HTTP 409 that no more specific code covers. What to do: Read the object again and retry with its current state.

### unavailable

HTTP 503. When: The service cannot answer right now, e.g. during maintenance. What to do: Retry later with backoff; honour Retry-After when it is present.

### error

any other HTTP status. When: Catch-all for any other refusal status. Treat it by its HTTP status. What to do: Retry later if the status is 5xx; otherwise check the request against /api/public/v1/openapi.json.

### idempotency_key_required

HTTP 400. When: POST /messages without an `Idempotency-Key` header of 8-128 characters (not needed with dry_run). What to do: Send a unique key per message (a UUID works) and reuse it only to retry the same request.

### idempotency_conflict

HTTP 409. When: The key was already used with a different body, or a parallel request with the same key is still being admitted. What to do: Use a new Idempotency-Key for a new message; to retry, resend exactly the same body.

### invalid_target

HTTP 422. When: `to` gives neither or both of `username` and `conversation_id`, or `username` is not a Telegram username. What to do: Give exactly one of to.username (5-32 characters, letter first, no '@') or to.conversation_id.

### invalid_pace

HTTP 422. When: `pace` is not one of the presets. What to do: Use one of careful, balanced, fast, or omit pace for careful.

### account_not_found

HTTP 404. When: The account id does not exist in this workspace. What to do: List accounts with GET /accounts.

### conversation_not_found

HTTP 404. When: The conversation id does not exist, belongs to another account than `account_id`, or is no longer mirrored. What to do: A conversation_id belongs to one account; list them with GET /conversations?account_id=...

### account_not_eligible

HTTP 422. When: The account's state forbids this action now; `details.reason` says why (see the reasons below). What to do: Pick another account from GET /accounts where can_send_cold (first contact) or can_reply (reply) is true, or fix the reason in the panel.

### account_queue_full

HTTP 429. When: The account already holds `details.limit` queued messages. What to do: Wait for queued messages to go out, or send from another account.

### message_not_found

HTTP 404. When: The message id does not exist in this workspace. What to do: Use the id returned by POST /messages.

### webhook_not_found

HTTP 404. When: The webhook id does not exist in this workspace. What to do: List webhooks with GET /webhooks.

### invalid_webhook_url

HTTP 422. When: The URL is not https, embeds credentials, or its host is localhost, private or link-local; `details.reason` says which. What to do: Use https:// with a public host name; localhost, private and link-local addresses are refused.

### webhook_limit_reached

HTTP 409. When: The workspace already has `details.limit` webhooks. What to do: Delete one with DELETE /webhooks/{webhook_id} first.

### delivery_not_found

HTTP 404. When: The delivery id does not exist or belongs to another webhook. What to do: List deliveries with GET /webhooks/{webhook_id}/deliveries.

### delivery_in_flight

HTTP 409. When: Replay was asked for a delivery that is being sent at this moment. What to do: Wait a minute and read its state again; replay it only once it is delivered or failed.

### account_not_eligible reasons

`details.reason` of `account_not_eligible` (the same word GET /accounts shows as `blocked_reason`). `send` is POST /messages, `listen` is PATCH /accounts/{account_id} with `api_listen: true`.

| reason | applies to | when | what to do |
|---|---|---|---|
| `spamblock` | send | Telegram's spam block: the account may not write first. | It can still reply where the other person wrote last; send cold messages from another account. |
| `cold_hold` | send | A cold-outreach hold after a peer flood: the account may not write first. | It can still reply; send cold messages from another account. |
| `frozen` | send | Telegram froze the account. | Use another account; a frozen account cannot send. |
| `banned` | send | Telegram banned or deleted the account. | Use another account. |
| `blocked` | send, listen | The account is blocked, logged out or awaiting re-authorization. | Fix it in the panel first, or use another account. |
| `quarantine` | send, listen | The panel took the account out of work (quarantine or an operator hold). | Return it to work in the panel first, or use another account. |
| `session_invalid` | send, listen | The account's Telegram session does not open. | Re-authorize the account in the panel. |
| `proxy_failed` | send, listen | The proxy bound to the account failed its last check, so the account cannot connect to Telegram. | Replace or re-check the proxy in the panel, or pick another account from GET /accounts. |
| `no_proxy` | send, listen | No proxy is bound to the account. | Bind a proxy to the account in the panel; the API never uses an account without its proxy. |
| `api_hold` | send | The API holds the account for 24 hours after PeerFlood or a block. | Retry after api_hold_until (GET /accounts, or the `account.limited` webhook), or use another account. |

### Message failure codes

A message that ends `failed` carries `error: {code, message, details}` in GET /messages/{message_id} and in the `message.failed` webhook. The request itself was accepted. `account_unavailable` and `account_busy` mean the account never reached Telegram; `details.console_code` is the panel's own word for the ending (`details.last_retry_cause` too when the retries ran out; quote them to support, branch on `code`).

| code | meaning | what to do |
|---|---|---|
| `peer_flood` | Telegram limited this account for writing to strangers (PeerFlood); the API holds it for 24 hours. | Do not resend from this account; its other queued messages fail with account_limited. |
| `account_blocked` | The account is frozen, banned or deactivated by Telegram. | Send from another account. |
| `username_not_found` | No Telegram user has this username. | Check the username; do not retry it as is. |
| `privacy_restricted` | The recipient's privacy settings do not allow this account to write to them. | Do not retry; this recipient cannot be reached by a stranger. |
| `write_forbidden` | This account may not write to this recipient or chat. | Do not retry from this account. |
| `recipient_deactivated` | Telegram deleted the recipient's account; the message was not sent. | Do not retry; remove this contact from your list. |
| `flood_wait_exhausted` | Telegram kept asking the account to wait longer than the message could be held. | Send again later with a new Idempotency-Key, or from another account. |
| `expired` | The message could not start within five minutes of its scheduled time. | Send again with a new Idempotency-Key. |
| `delivery_unknown` | Telegram did not confirm whether the message arrived; it was not resent to avoid a duplicate. | Check the conversation (GET /conversations/{id}/messages) before sending again. |
| `cancelled` | The message was cancelled before it was sent. | Send again with a new Idempotency-Key if it is still wanted. |
| `account_limited` | The account was put on hold after PeerFlood or a block, so this queued message was not sent. | Send it from another account, or again after the hold ends. |
| `account_unavailable` | The account could not connect (proxy or session problem). Nothing was sent to Telegram. | Check the account's proxy and session in the panel, or pick another account from GET /accounts; then send again with a new Idempotency-Key. |
| `account_busy` | The account was busy with other work until the send deadline passed. Nothing was sent. | Send again later with a new Idempotency-Key, or from another account. |
| `send_failed` | The message was not sent. details.console_code says why. | Send again later with a new Idempotency-Key; if it repeats, use another account and quote details.console_code to TeleTraff support. |

## Endpoints

Refusals in the lists below are `error.code` values; see Errors. Any endpoint can also answer `401 unauthorized`, `403 scope_denied`, `403 api_not_enabled` and `429 rate_limited`; a write (POST, PATCH, DELETE) also `402 payment_required` or `403 forbidden`.

### getMe: GET /me

Describe this token, its limits and the allowed pace presets. Scope: any valid token.

Call this first. It tells what the token may do, how many requests it may make, and the exact limits of every `pace` value.

Responses:

- `200` -> `MeResponse`: Successful Response

Example `200`:

```json
{
  "api_version": "v1",
  "workspace": "default",
  "token": {
    "id": "tok_42",
    "scopes": [
      "public:read",
      "public:messages"
    ]
  },
  "rate_limit": {
    "limit": 60,
    "window_seconds": 60
  },
  "default_pace": "careful",
  "pace_presets": [
    {
      "cold_per_account_day": 20,
      "cold_per_account_hour": 5,
      "delay_seconds_max": 113,
      "delay_seconds_min": 90,
      "name": "careful"
    }
  ],
  "accounts_total": 12,
  "listening_accounts": 2
}
```

### sendMessage: POST /messages

Send a message from one of your accounts. Scope: `public:messages`.

Queue one Telegram message from one of your accounts. Give `to.username` for first contact or `to.conversation_id` to write into an existing conversation of the same account.

A **cold** message (first contact, or the account wrote last) is spaced from the account's previous cold message by the `pace` preset and counted against its hourly and daily caps; `scheduled_at` says when it will go out. A **reply** (the other person wrote last) goes out within seconds and is not counted. `pace` defaults to `careful`; raw delays are refused.

Send a unique `Idempotency-Key` per message and reuse it only to retry: a retry with the same body returns the same message, a different body is `409 idempotency_conflict`. `dry_run: true` writes nothing and answers 200 with what would happen. Poll GET /messages/{id} for the outcome.

Parameters:

- `Idempotency-Key` (in header, optional, string or null): Unique key per message, 8-128 characters (a UUID works). Required unless `dry_run` is true. Example: `7f9c2a5e-3b1d-4f0a-9c8e-1d2b3c4d5e6f`.

Request body: `SendMessageRequest` (fields under Objects). Example:

```json
{
  "account_id": "acc_17",
  "to": {
    "username": "durov"
  },
  "text": "Hi! Saw your post about the meetup, are you still looking for speakers?",
  "pace": "careful",
  "dry_run": false
}
```

Responses:

- `202` -> `MessageResponse`: Accepted and queued (or the earlier answer to the same Idempotency-Key).
- `200` -> `DryRunResponse`: `dry_run: true`: nothing was written.
- `400`: `idempotency_key_required` (missing or not 8-128 characters) or `invalid_id`.
- `404`: `account_not_found` or `conversation_not_found`.
- `409`: `idempotency_conflict`: the key was used with a different body.
- `422`: `invalid_request`, `invalid_target`, `invalid_pace`, or `account_not_eligible` with `details.reason` one of spamblock, cold_hold, frozen, banned, blocked, quarantine, session_invalid, no_proxy, api_hold.
- `429`: `account_queue_full` (500 queued messages on this account) or `rate_limited`.

Example `202`:

```json
{
  "id": "msg_1052",
  "status": "queued",
  "kind": "cold",
  "account_id": "acc_17",
  "conversation_id": "conv_381",
  "to_username": "durov",
  "pace": "careful",
  "scheduled_at": "2026-10-07T12:31:40Z",
  "sent_at": null,
  "error": null,
  "created_at": "2026-10-07T12:30:02Z"
}
```

Example `200`:

```json
{
  "would_status": "queued",
  "kind": "cold",
  "pace": "careful",
  "scheduled_at": "2026-10-07T12:31:40Z",
  "cold_remaining_hour": 4,
  "cold_remaining_day": 19
}
```

### getMessage: GET /messages/{message_id}

Read one message and its outcome. Scope: `public:read`.

The current state of a message sent with POST /messages. `sent` and `failed` are final; a failed message carries `error.code`.

Parameters:

- `message_id` (in path, required, string): Message id returned by POST /messages. Example: `msg_1052`.

Responses:

- `200` -> `MessageResponse`: Successful Response
- `400`: `invalid_id`.
- `404`: `message_not_found`.
- `422`: `invalid_request`.

Example `200`:

```json
{
  "id": "msg_1052",
  "status": "queued",
  "kind": "cold",
  "account_id": "acc_17",
  "conversation_id": "conv_381",
  "to_username": "durov",
  "pace": "careful",
  "scheduled_at": "2026-10-07T12:31:40Z",
  "sent_at": null,
  "error": null,
  "created_at": "2026-10-07T12:30:02Z"
}
```

### listAccounts: GET /accounts

List your Telegram accounts and what each may send now. Scope: `public:read`.

Accounts of this workspace, oldest first, with whether each may write first (`can_send_cold`) or reply (`can_reply`), why not (`blocked_reason`), and its cold messages of the last hour and day. Use an `id` as `account_id` in POST /messages. Follow `next_cursor` until it is null.

Parameters:

- `limit` (in query, optional, integer, min 1, max 100, default `50`): Page size, 1-100. Example: `50`.
- `cursor` (in query, optional, string or null): `next_cursor` of the previous page; omit for the first page. Example: `acc_17`.

Responses:

- `200` -> `AccountListResponse`: Successful Response
- `400`: `invalid_cursor`.
- `422`: `invalid_request`: `limit` is not 1-100.

Example `200`:

```json
{
  "data": [
    {
      "api_listen": false,
      "can_reply": true,
      "can_send_cold": true,
      "cold_sent_last_day": 6,
      "cold_sent_last_hour": 1,
      "display_name": "Anna, sales",
      "id": "acc_17",
      "proxy_bound": true,
      "username": "anna_sales"
    }
  ],
  "next_cursor": "acc_17"
}
```

### updateAccount: PATCH /accounts/{account_id}

Turn listening for incoming replies on or off for one account. Scope: `public:messages`.

With `api_listen: true` TeleTraff keeps this account connected so a reply reaches you within seconds instead of when someone opens the panel. Needs a bound proxy, a working session and an account that is not banned or taken out of work (a frozen one is fine). Returns the account as GET /accounts lists it.

Parameters:

- `account_id` (in path, required, string): Account id from GET /accounts. Example: `acc_17`.

Request body: `UpdateAccountRequest` (fields under Objects). Example:

```json
{
  "api_listen": true
}
```

Responses:

- `200` -> `AccountInfo`: Successful Response
- `400`: `invalid_id`.
- `404`: `account_not_found`.
- `422`: `account_not_eligible` (`details.reason`: `no_proxy`, `session_invalid`, `blocked`, `quarantine`) or `invalid_request`.

Example `200`:

```json
{
  "id": "acc_17",
  "display_name": "Anna, sales",
  "username": "anna_sales",
  "can_send_cold": true,
  "can_reply": true,
  "blocked_reason": null,
  "proxy_bound": true,
  "api_hold_until": null,
  "cold_sent_last_hour": 1,
  "cold_sent_last_day": 6,
  "api_listen": false
}
```

### listConversations: GET /conversations

List private conversations of your accounts. Scope: `public:read`.

Private (one-to-one) conversations mirrored for your accounts, most recent message first. With `since`, only conversations that had a message after it -- poll this with the time of your last sync to find where new replies are. Follow `next_cursor` until it is null.

Parameters:

- `account_id` (in query, optional, string or null): Only conversations of this account (`acc_...`); omit for every account. Example: `acc_17`.
- `since` (in query, optional, string (ISO 8601 date-time, UTC) or null): ISO 8601 time; only conversations whose newest message is later. Without a zone it is UTC. Example: `2026-10-07T14:00:00Z`.
- `limit` (in query, optional, integer, min 1, max 100, default `50`): Page size, 1-100. Example: `50`.
- `cursor` (in query, optional, string or null): `next_cursor` of the previous page (with the same filters); omit for the first page. Example: `MjAyNi0xMC0wN1QxNDowNDo1OHwzODE`.

Responses:

- `200` -> `ConversationListResponse`: Successful Response
- `400`: `invalid_id` or `invalid_cursor`.
- `404`: `account_not_found`.
- `422`: `invalid_request`: `limit` is not 1-100 or `since` is not an ISO 8601 time.

Example `200`:

```json
{
  "data": [
    {
      "account_id": "acc_17",
      "id": "conv_381",
      "last_direction": "incoming",
      "last_message_at": "2026-10-07T14:04:58Z",
      "last_message_preview": "Yes, interested",
      "peer_id": "user:123456789",
      "title": "Maria K.",
      "unread_count": 1,
      "username": "lead_name"
    }
  ],
  "next_cursor": "MjAyNi0xMC0wN1QxNDowNDo1OHwzODE"
}
```

### listConversationMessages: GET /conversations/{conversation_id}/messages

Read the messages of one conversation. Scope: `public:read`.

Without `since`: newest first, for showing the history. With `since`: every message sent after it, oldest first -- the same messages webhooks deliver, for catching up after a missed or failed delivery. `actor` says who wrote each one; `message_id` links a message sent with POST /messages.

Parameters:

- `conversation_id` (in path, required, string): Conversation id from GET /conversations. Example: `conv_381`.
- `since` (in query, optional, string (ISO 8601 date-time, UTC) or null): ISO 8601 time; only messages sent after it, oldest first. Without a zone it is UTC. Example: `2026-10-07T14:00:00Z`.
- `limit` (in query, optional, integer, min 1, max 100, default `50`): Page size, 1-100. Example: `50`.
- `cursor` (in query, optional, string or null): `next_cursor` of the previous page (with the same `since`); omit for the first page. Example: `MjAyNi0xMC0wN1QxNDowNDo1OHw0MDIx`.

Responses:

- `200` -> `ConversationMessageListResponse`: Successful Response
- `400`: `invalid_id` or `invalid_cursor`.
- `404`: `conversation_not_found`.
- `422`: `invalid_request`: `limit` is not 1-100 or `since` is not an ISO 8601 time.

Example `200`:

```json
{
  "data": [
    {
      "actor": "contact",
      "direction": "incoming",
      "sent_at": "2026-10-07T14:04:58Z",
      "telegram_message_id": 4021,
      "text": "Yes, interested"
    }
  ],
  "next_cursor": "MjAyNi0xMC0wN1QxNDowNDo1OHw0MDIx"
}
```

### listWebhooks: GET /webhooks

List your webhooks. Scope: `public:webhooks`.

Every webhook of this workspace, oldest first. The signing secret is never listed.

Responses:

- `200` -> `WebhookListResponse`: Successful Response

Example `200`:

```json
{
  "data": [
    {
      "created_at": "2026-10-07T12:30:02Z",
      "description": "Production CRM",
      "enabled": true,
      "id": "wh_3",
      "url": "https://crm.example.com/teletraff/webhook"
    }
  ]
}
```

### createWebhook: POST /webhooks

Register an HTTPS endpoint for events. Scope: `public:webhooks`.

Registers an endpoint (at most 5 per workspace) and returns its signing `secret` -- the only time the secret is shown. Each event is POSTed as JSON `{id, type, created_at, data}` with headers `X-TeleTraff-Event`, `X-TeleTraff-Delivery`, `X-TeleTraff-Timestamp` and `X-TeleTraff-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>`. Answer 2xx within 10 seconds; otherwise it is retried after 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h and 8 h, then marked `failed`. Delivery is at least once: deduplicate by `X-TeleTraff-Delivery` or the event `id`. Redirects are not followed. Event types: `message.received` (someone wrote to one of your accounts), `message.sent` and `message.failed` (outcome of a message from the API, a Dialogs AI reply or the panel), `account.limited` (the API holds an account after PeerFlood or a block) and `webhook.test` (sent only by POST /webhooks/{webhook_id}/test).

Request body: `CreateWebhookRequest` (fields under Objects). Example:

```json
{
  "url": "https://crm.example.com/teletraff/webhook",
  "description": "Production CRM"
}
```

Responses:

- `201` -> `CreatedWebhook`: Successful Response
- `409`: `webhook_limit_reached`: the workspace already has 5 webhooks.
- `422`: `invalid_webhook_url` (not https, or a localhost/private/link-local host) or `invalid_request`.

Example `201`:

```json
{
  "id": "wh_3",
  "url": "https://crm.example.com/teletraff/webhook",
  "enabled": true,
  "description": "Production CRM",
  "created_at": "2026-10-07T12:30:02Z",
  "secret": "whsec_8VQf3m1yJqzK0cT9pWb2xR7nLd4sHa6uEo5iYg1kMvc"
}
```

### deleteWebhook: DELETE /webhooks/{webhook_id}

Delete a webhook. Scope: `public:webhooks`.

Stops deliveries to this endpoint at once and removes its delivery log.

Parameters:

- `webhook_id` (in path, required, string): Webhook id from GET /webhooks. Example: `wh_3`.

Responses:

- `204`: Successful Response
- `400`: `invalid_id`.
- `404`: `webhook_not_found`.
- `422`: `invalid_request`.

### sendTestEvent: POST /webhooks/{webhook_id}/test

Send a test event to one webhook. Scope: `public:webhooks`.

Queues one `webhook.test` event to this webhook only (no other webhook receives it); it goes out within seconds, signed like every other event. `data` is `{webhook_id, message}`.

Parameters:

- `webhook_id` (in path, required, string): Webhook id from GET /webhooks. Example: `wh_3`.

Responses:

- `202` -> `DeliveryAccepted`: Successful Response
- `400`: `invalid_id`.
- `404`: `webhook_not_found`.
- `422`: `invalid_request`.

Example `202`:

```json
{
  "delivery_id": "whd_981"
}
```

### listWebhookDeliveries: GET /webhooks/{webhook_id}/deliveries

Read the delivery log of a webhook. Scope: `public:webhooks`.

Deliveries to this webhook, newest first, 50 per page by default, with the last status code and error.

Parameters:

- `webhook_id` (in path, required, string): Webhook id from GET /webhooks. Example: `wh_3`.
- `limit` (in query, optional, integer, min 1, max 100, default `50`): Page size, 1-100. Example: `50`.
- `cursor` (in query, optional, string or null): `next_cursor` of the previous page; omit for the newest page. Example: `whd_932`.

Responses:

- `200` -> `DeliveryListResponse`: Successful Response
- `400`: `invalid_id` or `invalid_cursor`.
- `404`: `webhook_not_found`.
- `422`: `invalid_request`: `limit` is not 1-100.

Example `200`:

```json
{
  "data": [
    {
      "attempt_count": 1,
      "created_at": "2026-10-07T12:30:02Z",
      "delivered_at": "2026-10-07T12:30:03Z",
      "event_id": "evt_5512",
      "event_type": "message.received",
      "id": "whd_981",
      "last_status_code": 200,
      "state": "delivered"
    }
  ],
  "next_cursor": "whd_932"
}
```

### replayWebhookDelivery: POST /webhooks/{webhook_id}/deliveries/{delivery_id}/replay

Send a delivery again. Scope: `public:webhooks`.

Puts the delivery back on the schedule from scratch (state `pending`, attempts reset) with the same event body and `X-TeleTraff-Delivery` id. Refused while it is being sent.

Parameters:

- `webhook_id` (in path, required, string): Webhook id from GET /webhooks. Example: `wh_3`.
- `delivery_id` (in path, required, string): Delivery id from the delivery log. Example: `whd_981`.

Responses:

- `202` -> `DeliveryAccepted`: Successful Response
- `400`: `invalid_id`.
- `404`: `webhook_not_found` or `delivery_not_found`.
- `409`: `delivery_in_flight`: the delivery is being sent right now.
- `422`: `invalid_request`.

Example `202`:

```json
{
  "delivery_id": "whd_981"
}
```

## Objects

### AccountInfo

- `id` (string, required): Account id to pass as `account_id`.
- `display_name` (string, required): Name of the account in the panel.
- `username` (string or null, required): The account's own Telegram username, if it has one.
- `can_send_cold` (boolean, required): Whether the account may write first (cold message) right now.
- `can_reply` (boolean, required): Whether the account may reply where the other person wrote last. A spamblocked account can often still reply.
- `blocked_reason` (string or null, required): Why the account cannot send cold messages; null when it can. One of `spamblock`, `cold_hold`, `frozen`, `banned`, `blocked`, `quarantine`, `proxy_failed`, `session_invalid`, `no_proxy`, `api_hold`.
- `proxy_bound` (boolean, required): Whether a proxy is bound; an account without one cannot send.
- `api_hold_until` (string (ISO 8601 date-time, UTC) or null, required): Until when the API holds this account after PeerFlood or a block (UTC); null when not held.
- `cold_sent_last_hour` (integer, required): Cold messages this account sent through the API in the last hour.
- `cold_sent_last_day` (integer, required): Cold messages this account sent through the API in the last 24 hours.
- `api_listen` (boolean, required): Whether TeleTraff keeps listening on this account so incoming replies arrive within seconds (switch with PATCH /accounts/{account_id}). A flagged account that loses its proxy or session, or is banned or taken out of work, keeps the flag but is not listened to until that is fixed.

### AccountListResponse

- `data` (array of AccountInfo, required): Accounts of this page, oldest first.
- `next_cursor` (string or null, required): Pass as `cursor` to get the next page; null on the last page.

### ConversationInfo

- `id` (string, required): Conversation id; use it as `to.conversation_id` in POST /messages.
- `account_id` (string, required): Account this conversation belongs to.
- `peer_id` (string, required): The other person as Telegram knows them, `user:<id>`; same as `from.peer_id` in `message.received`.
- `username` (string or null, required): Their Telegram username without '@', when known.
- `title` (string, required): Their display name, or `@username`, or `peer_id` when nothing else is known.
- `last_message_at` (string (ISO 8601 date-time, UTC) or null, required): When the newest message of the conversation was sent (UTC); null when unknown.
- `last_message_preview` (string or null, required): Start of the newest message, or a word for an attachment.
- `last_direction` (one of `incoming`, `outgoing` or null, required): Who wrote last: `incoming` (they did) or `outgoing` (your account); null when unknown.
- `unread_count` (integer, required): Incoming messages the account has not read.

### ConversationListResponse

- `data` (array of ConversationInfo, required): Conversations of this page, most recent message first.
- `next_cursor` (string or null, required): Pass as `cursor` to get the next page; null on the last page.

### ConversationMessage

- `telegram_message_id` (integer, required): Telegram's id of the message; same as `telegram_message_id` in webhook events.
- `direction` (one of `incoming`, `outgoing`, required): `incoming` (they wrote) or `outgoing` (your account wrote).
- `text` (string or null, required): Message text; null for an attachment without a caption.
- `sent_at` (string (ISO 8601 date-time, UTC) or null, required): When the message was sent (UTC).
- `message_id` (string or null, required): Id of the API message (`msg_...`) when it was sent with POST /messages; null otherwise.
- `actor` (one of `api`, `ai`, `operator`, `contact`, required): Who wrote it: `api` (POST /messages), `ai` (a Dialogs AI reply), `operator` (anyone else writing from your account: the panel, a mailing, a phone) or `contact` (the other person).

### ConversationMessageListResponse

- `data` (array of ConversationMessage, required): Messages of this page: oldest first with `since`, newest first without it.
- `next_cursor` (string or null, required): Pass as `cursor` (with the same `since`) to get the next page; null on the last page.

### CreateWebhookRequest

- `url` (string, required, min length 1, max length 2048): HTTPS endpoint that receives a POST per event. Must be reachable on the public internet: http, localhost, private and link-local addresses are refused.
- `description` (string or null): Your own note about this endpoint, up to 255 characters.

### CreatedWebhook

- `id` (string, required): Webhook id.
- `url` (string, required): Endpoint events are POSTed to.
- `enabled` (boolean, required): Whether events are delivered to this endpoint.
- `description` (string or null, required): Your note about this endpoint.
- `created_at` (string (ISO 8601 date-time, UTC), required): When the webhook was created (UTC).
- `secret` (string, required): Signing secret, shown only in this response; store it now. Every delivery carries `X-TeleTraff-Signature: sha256=<hex HMAC-SHA256(secret, "<X-TeleTraff-Timestamp>.<raw body>")>`.

### DeliveryAccepted

- `delivery_id` (string, required): Delivery that will be attempted within seconds; follow it in GET /webhooks/{webhook_id}/deliveries.

### DeliveryInfo

- `id` (string, required): Delivery id; also sent as the `X-TeleTraff-Delivery` header.
- `event_id` (string, required): Event id; also the `id` of the delivered JSON body.
- `event_type` (string, required): Event types: `message.received` (someone wrote to one of your accounts), `message.sent` and `message.failed` (outcome of a message from the API, a Dialogs AI reply or the panel), `account.limited` (the API holds an account after PeerFlood or a block) and `webhook.test` (sent only by POST /webhooks/{webhook_id}/test).
- `state` (one of `pending`, `claimed`, `sending`, `retry`, `delivered`, `failed`, required): `pending` (not tried yet), `claimed` / `sending` (being sent now), `retry` (failed, tried again at `next_attempt_at`), `delivered` (your endpoint answered 2xx within 10 s) or `failed` (10 attempts over about 23.7 hours failed, or the endpoint was refused).
- `attempt_count` (integer, required): Attempts made so far.
- `last_status_code` (integer or null, required): HTTP status of the last attempt; null when there was no answer.
- `last_error` (string or null, required): Why the last attempt failed, e.g. `http_500`, `timeout`, `connection_failed`, `tls_failed`, `redirect_not_followed`, `url_blocked`, `webhook_disabled`; null after a success.
- `next_attempt_at` (string (ISO 8601 date-time, UTC) or null, required): When the next attempt is due (UTC); null once delivered or failed.
- `delivered_at` (string (ISO 8601 date-time, UTC) or null, required): When your endpoint accepted it (UTC); null until delivered.
- `created_at` (string (ISO 8601 date-time, UTC), required): When the delivery was created (UTC).

### DeliveryListResponse

- `data` (array of DeliveryInfo, required): Deliveries to this webhook, newest first.
- `next_cursor` (string or null, required): Pass as `cursor` to get older deliveries; null on the last page.

### DryRunResponse

- `would_status` (string, required): The status the message would get. A message that would be refused gets the same error a real send gets.
- `kind` (one of `cold`, `reply`, required): `cold` when this is first contact (no conversation yet, or the account wrote last): paced and counted against the cold caps. `reply` when the other person wrote last: sent within seconds and not counted.
- `pace` (one of `careful`, `balanced`, `fast`, required): Sending tempo preset. Optional; the default is `careful`. `careful`, `balanced` and `fast` differ in the pause between two cold messages from one account and in the hourly and daily cold caps (see GET /me). Raw delays or caps are not accepted.
- `scheduled_at` (string (ISO 8601 date-time, UTC), required): When the message would go out (UTC).
- `cold_remaining_hour` (integer, required): Cold messages this account could still schedule in the current rolling hour, counting this one as sent.
- `cold_remaining_day` (integer, required): Cold messages this account could still schedule in the current rolling day, counting this one as sent.

### MeResponse

- `api_version` (string, required): Version of this API.
- `workspace` (string, required): Workspace the token belongs to.
- `token` (TokenInfo, required): The token making this request and its scopes.
- `rate_limit` (RateLimitInfo, required): How many requests this token may make per window.
- `default_pace` (one of `careful`, `balanced`, `fast`, required): Pace used when a request does not pass `pace`. Always `careful`.
- `pace_presets` (array of PacePresetInfo, required): Every allowed `pace` value with its exact limits. Replies into a conversation where the other person wrote last do not count against these caps.
- `accounts_total` (integer, required): Telegram accounts in this workspace; GET /accounts lists them.
- `listening_accounts` (integer, required): Accounts with `api_listen` on that are eligible to listen now (proxy bound, session works, not banned or taken out of work); at most 8 are connected at once per console runner process, the rest wait for a slot.

### MessageError

- `code` (string, required): Why the message was not sent: `peer_flood`, `account_blocked`, `username_not_found`, `privacy_restricted`, `write_forbidden`, `recipient_deactivated`, `flood_wait_exhausted`, `expired`, `delivery_unknown`, `cancelled`, `account_limited`, `account_unavailable`, `account_busy` or `send_failed`.
- `message` (string, required): What happened, in English.
- `details` (object or null): `console_code`: the panel's own word for how the send ended (e.g. `proxy-unavailable`); when it is `telegram-retry-exhausted`, `last_retry_cause` names what the retries were waiting on. Quote them to support; they are not a stable vocabulary, branch on `code`.

### MessageResponse

- `id` (string, required): Message id; poll it with GET /messages/{id}.
- `status` (one of `queued`, `sending`, `sent`, `failed`, required): `queued` (waiting for its scheduled time), `sending`, `sent` or `failed` (see `error`). `sent` and `failed` are final.
- `kind` (one of `cold`, `reply`, required): `cold` when this is first contact (no conversation yet, or the account wrote last): paced and counted against the cold caps. `reply` when the other person wrote last: sent within seconds and not counted.
- `account_id` (string, required): Account the message is sent from.
- `conversation_id` (string or null, required): Conversation the message belongs to; null for a cold message until it is sent.
- `to_username` (string or null, required): Recipient username, when known.
- `pace` (one of `careful`, `balanced`, `fast`, required): Sending tempo preset. Optional; the default is `careful`. `careful`, `balanced` and `fast` differ in the pause between two cold messages from one account and in the hourly and daily cold caps (see GET /me). Raw delays or caps are not accepted.
- `scheduled_at` (string (ISO 8601 date-time, UTC), required): When the message is due to go out (UTC). Cold messages are spaced by the pace preset.
- `sent_at` (string (ISO 8601 date-time, UTC) or null, required): When Telegram accepted the message (UTC); null until sent.
- `error` (MessageError or null, required): Set only when `status` is `failed`.
- `created_at` (string (ISO 8601 date-time, UTC), required): When the request was accepted (UTC).

### MessageTarget

- `username` (string or null): Telegram username to write to, without '@' (5-32 characters: a letter, then letters, digits or underscores). Give this or `conversation_id`, not both.
- `conversation_id` (string or null): Id of a conversation of this same account (`conv_...`). Give this or `username`, not both.

### PacePresetInfo

- `name` (one of `careful`, `balanced`, `fast`, required): Value to pass as `pace`.
- `delay_seconds_min` (number, required): Shortest pause between two cold messages from one account.
- `delay_seconds_max` (number, required): Longest pause between two cold messages from one account.
- `cold_per_account_hour` (integer, required): Cold messages one account may send per hour.
- `cold_per_account_day` (integer, required): Cold messages one account may send per day.

### RateLimitInfo

- `limit` (integer, required): Requests allowed per window for this token.
- `window_seconds` (integer, required): Window length in seconds.

### SendMessageRequest

- `account_id` (string, required): Account to send from (`acc_...`, see GET /accounts).
- `to` (MessageTarget, required): Recipient: exactly one of `username` or `conversation_id`.
- `text` (string, required, min length 1, max length 4096): Message text, 1-4096 characters, not blank.
- `pace` (one of `careful`, `balanced`, `fast`, default `"careful"`): Sending tempo preset. Optional; the default is `careful`. `careful`, `balanced` and `fast` differ in the pause between two cold messages from one account and in the hourly and daily cold caps (see GET /me). Raw delays or caps are not accepted.
- `dry_run` (boolean, default `false`): When true nothing is written or sent: the answer says whether the message would be accepted and when it would go out. No Idempotency-Key needed.

### TokenInfo

- `id` (string, required): Id of the token making this request.
- `scopes` (array of string, required): Scopes this token holds.

### UpdateAccountRequest

- `api_listen` (boolean, required): true: keep a listener on this account so replies arrive within seconds; false: stop (always allowed). Turning it on needs a bound proxy, a working session and an account that is neither banned nor taken out of work, otherwise `account_not_eligible` with `details.reason` `no_proxy`, `session_invalid`, `blocked` or `quarantine`. A frozen account can be listened to.

### WebhookInfo

- `id` (string, required): Webhook id.
- `url` (string, required): Endpoint events are POSTed to.
- `enabled` (boolean, required): Whether events are delivered to this endpoint.
- `description` (string or null, required): Your note about this endpoint.
- `created_at` (string (ISO 8601 date-time, UTC), required): When the webhook was created (UTC).

### WebhookListResponse

- `data` (array of WebhookInfo, required): Every webhook of this workspace (at most 5), oldest first. Secrets are never listed.

## Webhooks

Register up to 5 HTTPS endpoints per workspace with `POST /webhooks`; the answer carries the signing `secret` (`whsec_...`) once -- store it. Each event is POSTed as JSON to every enabled webhook. Events: `message.received`, `message.sent`, `message.failed`, `account.limited`, `webhook.test`.

```json
{
  "id": "evt_5512",
  "type": "message.received",
  "created_at": "2026-10-07T12:30:02Z",
  "data": {
    "account_id": "acc_17",
    "conversation_id": "conv_381",
    "telegram_message_id": 4411,
    "from": {
      "username": "durov",
      "peer_id": "user:1"
    },
    "text": "Sounds good, call me tomorrow",
    "sent_at": "2026-10-07T12:30:01Z"
  }
}
```

`data` by event type:

- `message.received`: someone wrote to a listening account (PATCH /accounts/{account_id} `api_listen: true`). `account_id`, `conversation_id`, `telegram_message_id`, `from` (`username`, `peer_id`), `text`, `sent_at`. Private conversations only.
- `message.sent` / `message.failed`: outcome of a message. `message_id` (null when not sent through the API), `account_id`, `conversation_id`, `actor` (`api`, `ai` for a Dialogs AI reply, `operator` for the panel), `telegram_message_id`, `text`, `error` (`{code, message, details}` on `message.failed`, see Message failure codes).
- `account.limited`: the API holds an account after PeerFlood or a block. `account_id`, `reason`, `held_until`.
- `webhook.test`: sent only by `POST /webhooks/{webhook_id}/test`. `webhook_id`, `message`.

Request headers (header names are case-insensitive; most servers lower-case them):

- `Content-Type: application/json`
- `User-Agent: TeleTraff-Webhooks/1`
- `X-TeleTraff-Event`: the event type.
- `X-TeleTraff-Delivery`: delivery id (`whd_...`); the same on every retry of this delivery -- dedupe by it.
- `X-TeleTraff-Timestamp`: Unix seconds when this attempt was signed.
- `X-TeleTraff-Signature`: `sha256=` + hex HMAC-SHA256, key = the whole `whsec_...` secret, message = `<X-TeleTraff-Timestamp>.<raw body bytes>`.

Answer any `2xx` within 10 seconds; do slow work after answering. Anything else -- another status, a timeout, a redirect (never followed) -- is retried after 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h, 8 h; after 10 attempts the delivery is `failed`. `GET /webhooks/{webhook_id}/deliveries` shows every attempt and `POST .../replay` sends one again.

### Verifying the signature

Compute the HMAC over the raw body bytes as received (parsing and re-serializing the JSON changes them), compare in constant time, and reject a timestamp more than 300 seconds from your clock.

Python (standard library only):

```python
import hashlib
import hmac
import time

def verify_teletraff_signature(secret, headers, raw_body, tolerance_seconds=300):
    """True when this delivery was signed with your webhook secret and is fresh.

    secret: the whole whsec_... string. headers: the request headers (any mapping).
    raw_body: the request body as bytes, exactly as received -- not re-serialized JSON.
    """
    lowered = {str(name).lower(): str(value) for name, value in headers.items()}
    timestamp = lowered.get("x-teletraff-timestamp", "")
    signature = lowered.get("x-teletraff-signature", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance_seconds:
        return False
    signed = timestamp.encode("ascii") + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask:   verify_teletraff_signature(SECRET, request.headers, request.get_data())
# FastAPI: verify_teletraff_signature(SECRET, request.headers, await request.body())
```

Node.js (built-in `crypto` only):

```js
const crypto = require("node:crypto");

// secret: the whole whsec_... string. headers: the request headers.
// rawBody: the request body as a Buffer, exactly as received -- not re-serialized JSON.
function verifyTeletraffSignature(secret, headers, rawBody, toleranceSeconds = 300) {
  const lowered = {};
  for (const [name, value] of Object.entries(headers)) lowered[name.toLowerCase()] = String(value);
  const timestamp = lowered["x-teletraff-timestamp"] || "";
  const signature = lowered["x-teletraff-signature"] || "";
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
    return false;
  }
  const expected = "sha256=" + crypto.createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(timestamp + ".", "ascii"), Buffer.from(rawBody)]))
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: app.post("/teletraff/webhook", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyTeletraffSignature(SECRET, req.headers, req.body)) return res.sendStatus(401);
//   const event = JSON.parse(req.body); res.sendStatus(200); /* handle event after answering */ });
module.exports = { verifyTeletraffSignature };
```

## MCP

`https://teletraff.com/api/public/mcp` is a Model Context Protocol server (Streamable HTTP: JSON-RPC 2.0 over POST, answers in `application/json`, protocol version `2025-06-18`). It takes the same bearer token, and each tool call is the REST call named below -- same scopes, rate limit, idempotency, pace and `dry_run`. A refused call returns `isError: true` with the text `code: message — hint`, then the error's `details` and `Retry-After` when there are any; `structuredContent.error` is the REST error object.

Every tool call is one REST request and counts against the token's rate limit (60 per 60 s); `initialize`, `tools/list` and `ping` do not. The server is stateless: no `Mcp-Session-Id`, and `GET` answers 405 (no server-sent stream).

| tool | calls | arguments / returns |
|---|---|---|
| `get_me` | `GET /me` | token scopes, rate limit, pace presets with their numbers, account counts |
| `list_accounts` | `GET /accounts` | accounts and whether each may send now; `limit`, `cursor` |
| `send_message` | `POST /messages` | `account_id`, `to`, `text`, `pace?`, `dry_run?`, `idempotency_key?`. Without `idempotency_key` the tool derives one from account_id, to, text and pace, so repeating the same call never sends twice |
| `get_message` | `GET /messages/{message_id}` | status and outcome of one message |
| `list_conversations` | `GET /conversations` | `account_id?`, `since?`, `limit`, `cursor` |
| `get_conversation_messages` | `GET /conversations/{conversation_id}/messages` | `since?`, `limit`, `cursor` |
| `list_webhooks` | `GET /webhooks` | registered webhooks (secrets are never listed) |

Claude Code:

```sh
claude mcp add --transport http teletraff https://teletraff.com/api/public/mcp --header "Authorization: Bearer $TELETRAFF_API_TOKEN"
```

Cursor (`~/.cursor/mcp.json`) and other clients that take a URL with headers:

```json
{
  "mcpServers": {
    "teletraff": {
      "url": "https://teletraff.com/api/public/mcp",
      "headers": {
        "Authorization": "Bearer tg_sk_live_..."
      }
    }
  }
}
```

## Limits

- Text only: one message is 1-4096 characters of plain text. No media, files, buttons or formatting.
- Phone numbers are not supported: write to a Telegram `username`, or into a `conversation_id` the account already has.
- Conversations are private one-to-one chats; groups and channels are not listed.
- Listening: at most 8 listening accounts are connected at once per console runner process; accounts beyond that wait for a free slot, so their `message.received` events arrive later. Listening also counts against your plan's stream limit.
- 500 queued messages per account; 5 webhooks per workspace; 60 requests per 60 s per token.
- A queued message that cannot start within five minutes of its `scheduled_at` fails with `expired`.
- Accounts and proxies are managed in the panel; the API sends only from accounts that already have their proxy bound.
