TeleTraff API
Надсилайте повідомлення зі своїх Telegram-акаунтів із CRM, скрипта або ШІ-агента, читайте їхні діалоги й отримуйте відповіді вебхуком. Ключ API випускається в панелі, у розділі «Налаштування → API».
Як це влаштовано
- Закрита бета: API вмикаємо воркспейсу на запит у підтримку.
- Лише текст: одне повідомлення до 4096 символів, без медіа, файлів і кнопок.
- Номери телефонів не підтримуються. Пишіть за username або в діалог, який в акаунта вже є.
- Темп надсилання обирається лише пресетом, за замовчуванням найобережніший. Свої паузи й ліміти API не приймає.
- Акаунти й проксі налаштовуються в панелі. API надсилає лише з акаунтів, до яких прив'язано проксі.
- Відповіді співрозмовників приходять вебхуком з акаунтів, у яких увімкнено «Слухати для API».
Підключити ШІ
Інструкція для ChatGPT і Claude
Вставте її першим повідомленням у чат із ШІ. Ключ покладіть у змінну оточення TELETRAFF_API_TOKEN: в інструкції його немає.
Показати текст
Ти допомагаєш мені працювати з API TeleTraff. Це сервіс, який веде мої Telegram-акаунти: надсилає з них повідомлення, читає діалоги й повідомляє про події вебхуками. Базова адреса API: https://teletraff.com/api/public/v1 Повний опис методів і помилок: https://teletraff.com/api/public/v1/llms.txt. Прочитай його, перш ніж писати код. OpenAPI-схема: https://teletraff.com/api/public/v1/openapi.json Токен лежить у змінній оточення TELETRAFF_API_TOKEN і передається заголовком Authorization: Bearer <токен>. Ніколи не вставляй токен у код і не виводь його. Правила: 1. Кожен запит, який щось змінює, надсилай із заголовком Idempotency-Key: своїм унікальним рядком на одну дію. Повтор із тим самим ключем не надішле повідомлення вдруге. 2. Темп задається лише полем pace: careful, balanced або fast. Типово careful; не змінюй його без моєї прямої вказівки. 3. Спочатку надсилай дію з dry_run: true і показуй мені, що станеться. Справжній запит роби лише після цього. 4. Якщо API повернув помилку, прочитай поля code і hint: hint каже, що робити далі. Не повторюй запит наосліп. 5. На відповідь 429 зачекай стільки секунд, скільки вказано в заголовку Retry-After.
Увесь API одним файлом
llms.txt: довідник англійською, який ШІ читає за посиланням. Схема OpenAPI лежить поруч.
Підключити через MCP
Claude, Cursor та інші агенти отримають інструменти TeleTraff напряму. Замініть tg_sk_live_... на свій ключ.
{
"mcpServers": {
"teletraff": {
"url": "https://teletraff.com/api/public/mcp",
"headers": {
"Authorization": "Bearer tg_sk_live_..."
}
}
}
}{
"mcpServers": {
"teletraff": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://teletraff.com/api/public/mcp",
"--header",
"Authorization:${TELETRAFF_AUTH}"
],
"env": {
"TELETRAFF_AUTH": "Bearer tg_sk_live_..."
}
}
}
}claude mcp add --transport http teletraff https://teletraff.com/api/public/mcp --header "Authorization: Bearer $TELETRAFF_API_TOKEN"Довідник
Довідник англійською: це той самий текст, що в llms.txt.
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 machinecodeand ahint.
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). /v1only 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 /mefirst: 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 /messagesanswers at once withscheduled_at; pollGET /messages/{message_id}or take themessage.sent/message.failedwebhook.
Quick start:
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-- accountmsg_123-- messageconv_123-- conversationwh_123-- webhookwhd_123-- webhook deliverycmp_123-- campaigntok_123-- tokenevt_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: truewrites 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:
{
"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:
{
"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 unlessdry_runis true. Example:7f9c2a5e-3b1d-4f0a-9c8e-1d2b3c4d5e6f.
Request body: SendMessageRequest (fields under Objects). Example:
{
"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) orinvalid_id.404:account_not_foundorconversation_not_found.409:idempotency_conflict: the key was used with a different body.422:invalid_request,invalid_target,invalid_pace, oraccount_not_eligiblewithdetails.reasonone of spamblock, cold_hold, frozen, banned, blocked, quarantine, session_invalid, no_proxy, api_hold.429:account_queue_full(500 queued messages on this account) orrate_limited.
Example 202:
{
"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:
{
"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 Response400:invalid_id.404:message_not_found.422:invalid_request.
Example 200:
{
"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, default50): Page size, 1-100. Example:50.cursor(in query, optional, string or null):next_cursorof the previous page; omit for the first page. Example:acc_17.
Responses:
200->AccountListResponse: Successful Response400:invalid_cursor.422:invalid_request:limitis not 1-100.
Example 200:
{
"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:
{
"api_listen": true
}Responses:
200->AccountInfo: Successful Response400:invalid_id.404:account_not_found.422:account_not_eligible(details.reason:no_proxy,session_invalid,blocked,quarantine) orinvalid_request.
Example 200:
{
"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, default50): Page size, 1-100. Example:50.cursor(in query, optional, string or null):next_cursorof the previous page (with the same filters); omit for the first page. Example:MjAyNi0xMC0wN1QxNDowNDo1OHwzODE.
Responses:
200->ConversationListResponse: Successful Response400:invalid_idorinvalid_cursor.404:account_not_found.422:invalid_request:limitis not 1-100 orsinceis not an ISO 8601 time.
Example 200:
{
"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, default50): Page size, 1-100. Example:50.cursor(in query, optional, string or null):next_cursorof the previous page (with the samesince); omit for the first page. Example:MjAyNi0xMC0wN1QxNDowNDo1OHw0MDIx.
Responses:
200->ConversationMessageListResponse: Successful Response400:invalid_idorinvalid_cursor.404:conversation_not_found.422:invalid_request:limitis not 1-100 orsinceis not an ISO 8601 time.
Example 200:
{
"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:
{
"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:
{
"url": "https://crm.example.com/teletraff/webhook",
"description": "Production CRM"
}Responses:
201->CreatedWebhook: Successful Response409:webhook_limit_reached: the workspace already has 5 webhooks.422:invalid_webhook_url(not https, or a localhost/private/link-local host) orinvalid_request.
Example 201:
{
"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 Response400: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 Response400:invalid_id.404:webhook_not_found.422:invalid_request.
Example 202:
{
"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, default50): Page size, 1-100. Example:50.cursor(in query, optional, string or null):next_cursorof the previous page; omit for the newest page. Example:whd_932.
Responses:
200->DeliveryListResponse: Successful Response400:invalid_idorinvalid_cursor.404:webhook_not_found.422:invalid_request:limitis not 1-100.
Example 200:
{
"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 Response400:invalid_id.404:webhook_not_foundordelivery_not_found.409:delivery_in_flight: the delivery is being sent right now.422:invalid_request.
Example 202:
{
"delivery_id": "whd_981"
}Objects
AccountInfo
id(string, required): Account id to pass asaccount_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 ofspamblock,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 ascursorto get the next page; null on the last page.
ConversationInfo
id(string, required): Conversation id; use it asto.conversation_idin 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 asfrom.peer_idinmessage.received.username(string or null, required): Their Telegram username without '@', when known.title(string, required): Their display name, or@username, orpeer_idwhen 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 ofincoming,outgoingor null, required): Who wrote last:incoming(they did) oroutgoing(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 ascursorto get the next page; null on the last page.
ConversationMessage
telegram_message_id(integer, required): Telegram's id of the message; same astelegram_message_idin webhook events.direction(one ofincoming,outgoing, required):incoming(they wrote) oroutgoing(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 ofapi,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) orcontact(the other person).
ConversationMessageListResponse
data(array of ConversationMessage, required): Messages of this page: oldest first withsince, newest first without it.next_cursor(string or null, required): Pass ascursor(with the samesince) 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 carriesX-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 theX-TeleTraff-Deliveryheader.event_id(string, required): Event id; also theidof the delivered JSON body.event_type(string, required): Event types:message.received(someone wrote to one of your accounts),message.sentandmessage.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) andwebhook.test(sent only by POST /webhooks/{webhook_id}/test).state(one ofpending,claimed,sending,retry,delivered,failed, required):pending(not tried yet),claimed/sending(being sent now),retry(failed, tried again atnext_attempt_at),delivered(your endpoint answered 2xx within 10 s) orfailed(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 ascursorto 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 ofcold,reply, required):coldwhen this is first contact (no conversation yet, or the account wrote last): paced and counted against the cold caps.replywhen the other person wrote last: sent within seconds and not counted.pace(one ofcareful,balanced,fast, required): Sending tempo preset. Optional; the default iscareful.careful,balancedandfastdiffer 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 ofcareful,balanced,fast, required): Pace used when a request does not passpace. Alwayscareful.pace_presets(array of PacePresetInfo, required): Every allowedpacevalue 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 withapi_listenon 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_busyorsend_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 istelegram-retry-exhausted,last_retry_causenames what the retries were waiting on. Quote them to support; they are not a stable vocabulary, branch oncode.
MessageResponse
id(string, required): Message id; poll it with GET /messages/{id}.status(one ofqueued,sending,sent,failed, required):queued(waiting for its scheduled time),sending,sentorfailed(seeerror).sentandfailedare final.kind(one ofcold,reply, required):coldwhen this is first contact (no conversation yet, or the account wrote last): paced and counted against the cold caps.replywhen 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 ofcareful,balanced,fast, required): Sending tempo preset. Optional; the default iscareful.careful,balancedandfastdiffer 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 whenstatusisfailed.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 orconversation_id, not both.conversation_id(string or null): Id of a conversation of this same account (conv_...). Give this orusername, not both.
PacePresetInfo
name(one ofcareful,balanced,fast, required): Value to pass aspace.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 ofusernameorconversation_id.text(string, required, min length 1, max length 4096): Message text, 1-4096 characters, not blank.pace(one ofcareful,balanced,fast, default"careful"): Sending tempo preset. Optional; the default iscareful.careful,balancedandfastdiffer 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, defaultfalse): 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, otherwiseaccount_not_eligiblewithdetails.reasonno_proxy,session_invalid,blockedorquarantine. 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.
{
"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,aifor a Dialogs AI reply,operatorfor the panel),telegram_message_id,text,error({code, message, details}onmessage.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 byPOST /webhooks/{webhook_id}/test.webhook_id,message.
Request headers (header names are case-insensitive; most servers lower-case them):
Content-Type: application/jsonUser-Agent: TeleTraff-Webhooks/1X-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 wholewhsec_...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):
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):
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:
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:
{
"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 aconversation_idthe 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.receivedevents 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_atfails withexpired. - Accounts and proxies are managed in the panel; the API sends only from accounts that already have their proxy bound.