Bot API

RU EN

Bot API allows you to create bots for the WWChat messenger.

Version: v1.4 · 2026-07-03

New in v1.4: the transferStars method — send stars from the bot's balance to a user (payouts, prizes, cashback).

New in v1.3: location requests — a request_location reply-keyboard button, and incoming messages now carry a structured location object (latitude/longitude). Clients that don't render reply keyboards simply don't show the button, so older versions ignore the request.

New in v1.2: the getFile method and file download by bot token — a bot can now fetch voice messages, photos and documents sent to it (previously downloading required a user JWT the bot doesn't have).

New in v1.1: message_reaction update (member reactions), the getMessageReactions method, and forwarded_from / voice.duration fields on messages.

Getting Started

Base URL

https://api.wwchat.org/bot/v1/{token}/ 

Authentication

The bot authenticates using a token in the URL. The token is issued when creating a bot.

Token format: {user_id}:{random_string}

Example: 550e8400-e29b-41d4-a716-446655440000:aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY3zA5bC7dE9fG1

Creating a Bot

Via @BotMama:

  1. Open a chat with @BotMama
  2. Send the command /newbot
  3. Enter the bot name
  4. Enter username (must end with bot)
  5. Receive your token

Response Format

All methods return JSON:

{
  "ok": true,
  "result": { ... }
}

On error:

{
  "ok": false,
  "error_code": 400,
  "description": "Error message"
}

API Methods

getMe

Get information about the bot.

Request: GET /bot/v1/{token}/getMe

{
  "ok": true,
  "result": {
    "id": "uuid",
    "username": "MyBot",
    "description": "Bot description",
    "is_bot": true,
    "can_join_groups": true,
    "can_read_group_messages": true,
    "can_join_channels": false
  }
}

sendMessage

Send a message.

Request: POST /bot/v1/{token}/sendMessage

ParameterTypeRequiredDescription
chat_idStringYesChat UUID or @username
textStringYesMessage text
parse_modeStringNoFormatting: HTML or Markdown
reply_to_message_idStringNoMessage UUID to reply to
disable_notificationBooleanNoDisable notification
reply_markupReplyMarkupNoInline / reply keyboard or remove

Example with inline keyboard:

{
  "chat_id": "550e8400-e29b-41d4-a716-446655440000",
  "text": "Choose an action:",
  "reply_markup": {
    "inline_keyboard": [
      [
        {"text": "Button 1", "callback_data": "btn1"},
        {"text": "Button 2", "callback_data": "btn2"}
      ],
      [
        {"text": "Open website", "url": "https://example.com"}
      ]
    ]
  }
}

getUpdates

Get updates (long polling).

Request: GET /bot/v1/{token}/getUpdates

ParameterTypeDescription
offsetIntegerID of the first update to return
limitIntegerMax number (1-100)
timeoutIntegerLong polling timeout (max 60 sec)
allowed_updatesArrayFilter update types

Important: After receiving updates, send the next request with offset = last_update_id + 1 to confirm receipt.


editMessageText

Edit message text.

Request: POST /bot/v1/{token}/editMessageText

ParameterTypeRequiredDescription
chat_idStringYesChat UUID
message_idStringYesMessage UUID
textStringYesNew text
reply_markupReplyMarkupNoNew keyboard (inline / reply / remove)

Important: Only messages sent by the bot can be edited.


answerCallbackQuery

Respond to inline button press.

Request: POST /bot/v1/{token}/answerCallbackQuery

ParameterTypeDescription
callback_query_idStringCallback query ID (required)
textStringNotification text (up to 200 chars)
show_alertBooleanShow alert instead of toast
cache_timeIntegerCache time for the response (seconds)

Important: Call this method even if you don't need to show a notification.


getChat

Get information about a chat.

Request: GET/POST /bot/v1/{token}/getChat

ParameterTypeRequiredDescription
chat_idStringYesChat UUID

Response (private chat):

{
  "ok": true,
  "result": {
    "id": "uuid",
    "type": "private",
    "username": "john_doe",
    "first_name": "john_doe",
    "bio": "Hello!",
    "language": "ru",
    "is_bot": false
  }
}

Response (group):

{
  "ok": true,
  "result": {
    "id": "uuid",
    "type": "group",
    "title": "My Group",
    "username": "mygroup",
    "description": "Group description",
    "members_count": 42,
    "is_public": true
  }
}

The language field returns the user's language — useful for localizing bot messages.


getMessageReactions

Get the current reactions on a list of messages (broken down by user). A backfill/reconcile safety net for when realtime message_reaction updates may have been missed. The bot must be a member of the chat.

Request: GET/POST /bot/v1/{token}/getMessageReactions

ParameterTypeRequiredDescription
chat_idStringYesChat UUID
message_idsStringYesComma-separated message UUIDs (max 100). In POST — an array of strings.

Response:

{
  "ok": true,
  "result": [
    {
      "message_id": "uuid",
      "reactions": [
        { "emoji": "🔥", "users": ["uuid", "uuid"] },
        { "emoji": "😀", "users": ["uuid"] }
      ]
    }
  ]
}

Returns one entry per requested message_id (with empty reactions if none). Messages not in this chat are ignored.


Files

uploadFile

Upload a file for later sending.

Request: POST /bot/v1/{token}/uploadFile (multipart/form-data)

ParameterTypeDescription
fileFileFile to upload (max 50 MB)
{
  "ok": true,
  "result": {
    "id": "uuid",
    "file_name": "document.pdf",
    "file_size": 102400,
    "mime_type": "application/pdf",
    "media_type": "document"
  }
}

sendDocument

Send a document.

Request: POST /bot/v1/{token}/sendDocument

ParameterTypeRequiredDescription
chat_idStringYesChat UUID
documentStringYesfile_id of uploaded file
captionStringNoDocument caption
parse_modeStringNoCaption formatting
reply_markupReplyMarkupNoInline / reply keyboard or remove

sendPhoto

Send a photo.

Request: POST /bot/v1/{token}/sendPhoto

ParameterTypeRequiredDescription
chat_idStringYesChat UUID
photoStringYesfile_id of uploaded image
captionStringNoPhoto caption
parse_modeStringNoCaption formatting
reply_markupReplyMarkupNoInline / reply keyboard or remove

sendVoice

Send a voice message.

Request: POST /bot/v1/{token}/sendVoice

ParameterTypeRequiredDescription
chat_idStringYesChat UUID
voiceStringYesfile_id of audio file
captionStringNoCaption
durationIntegerNoDuration in seconds
reply_markupReplyMarkupNoInline / reply keyboard or remove

getFile

Get a file's metadata by file_id (e.g. from an incoming voice message — voice.file_id).

Request: GET /bot/v1/{token}/getFile?file_id={file_id}

ParameterTypeRequiredDescription
file_idStringYesFile UUID from an update (voice / photo / document)
{
  "ok": true,
  "result": {
    "file_id": "abc-...-uuid",
    "file_size": 23456,
    "file_path": "voice/abc-...-uuid.oga"
  }
}

file_path is a <category>/<id>.<ext> path (voice / photos / videos / documents). Append it to the download URL below.


Downloading a file

Download the bytes of a media file (voice, photo, document) a bot received in an update. Authentication is by the bot token in the path — no user JWT required. A bot may download files it owns or that were sent in a chat it belongs to.

Request: GET /bot/v1/{token}/file/{file_path}

Response: a binary stream with the file's Content-Type. Errors: 404 — file not found, 403 — bot has no access.


Star Payments

Bots can accept payments in stars.

Payment Flow

  1. Bot calls sendInvoice — user sees a message with "Pay" button
  2. User clicks "Pay" — client shows a modal with details
  3. User confirms — server sends pre_checkout_query update to the bot
  4. Bot calls answerPreCheckoutQuery (ok: true/false) — within 10 seconds
  5. If ok=true — stars are deducted from user and credited to bot
  6. Bot receives update with successful_payment in message

sendInvoice

Send an invoice for star payment.

Request: POST /bot/v1/{token}/sendInvoice

ParameterTypeRequiredDescription
chat_idStringYesChat UUID or @username
titleStringYesProduct/service name (up to 255 chars)
descriptionStringYesDescription (up to 1000 chars)
amountFloatYesAmount in stars (> 0)
photo_urlStringNoProduct image URL
payloadStringYesBot data (up to 512 chars, not visible to user)

Example:

{
  "chat_id": "550e8400-e29b-41d4-a716-446655440000",
  "title": "Premium Subscription",
  "description": "Access to premium features for 30 days",
  "amount": 10.0,
  "photo_url": "https://example.com/premium.png",
  "payload": "premium_30d_user123"
}

answerPreCheckoutQuery

Respond to a pre-checkout query. The bot must respond within 10 seconds, otherwise the payment will be cancelled.

Request: POST /bot/v1/{token}/answerPreCheckoutQuery

ParameterTypeRequiredDescription
pre_checkout_query_idStringYesUUID from pre_checkout_query update
okBooleanYestrue — confirm, false — decline
error_messageStringNoError message (when ok=false)

Example (confirm):

{
  "pre_checkout_query_id": "550e8400-e29b-41d4-a716-446655440000",
  "ok": true
}

Example (decline):

{
  "pre_checkout_query_id": "550e8400-e29b-41d4-a716-446655440000",
  "ok": false,
  "error_message": "Item out of stock"
}

getStarBalance

Get the bot's star balance.

Request: GET/POST /bot/v1/{token}/getStarBalance

{
  "ok": true,
  "result": {
    "balance": 150.5,
    "pending": 0
  }
}

transferStars

Send stars from the bot's balance to a user — payouts, prizes, cashback, refunds.

Request: POST /bot/v1/{token}/transferStars

ParameterTypeRequiredDescription
user_idStringYesRecipient user UUID or @username
amountFloatYesAmount in stars (minimum 0.01)
descriptionStringNoComment (max 255 chars), visible in both transaction histories. Defaults to "Transfer from @botusername"

Example:

{
  "user_id": "@alice",
  "amount": 5.0,
  "description": "Contest prize"
}

Response:

{
  "ok": true,
  "result": {
    "success": true,
    "transaction_id": "uuid",
    "amount": 5.0,
    "new_balance_stars": 145.5
  }
}

Errors: 400 — insufficient bot balance, recipient is a bot, self-transfer; 404 — user not found.

A bot can only spend its own balance (earned via invoices or topped up by the owner). The owner's personal wallet is not accessible with a bot token.


Webhooks

setWebhook

Set webhook for receiving updates.

Request: POST /bot/v1/{token}/setWebhook

ParameterTypeDescription
urlStringHTTPS webhook URL (required)
secret_tokenStringSecret for X-WWChat-Bot-Api-Secret-Token header
max_connectionsIntegerMax connections (1-100)
allowed_updatesArrayFilter update types

deleteWebhook

Request: POST /bot/v1/{token}/deleteWebhook

getWebhookInfo

Request: GET /bot/v1/{token}/getWebhookInfo

Webhook Request Format

POST https://mybot.example.com/webhook
Content-Type: application/json
X-WWChat-Bot-Api-Secret-Token: my_secret_123

{
  "update_id": 123456789,
  "message": { ... }
}

Your server must return HTTP 200 OK. After 10 consecutive errors, the webhook is automatically disabled.

Important: When a webhook is active, getUpdates is unavailable (returns 409 Conflict).


Text Formatting

Methods sendMessage, editMessageText, sendDocument, sendPhoto support the parse_mode parameter.

HTML

{
  "text": "<b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strikethrough</s>, <code>code</code>, <pre>code block</pre>, <a href=\"url\">link</a>",
  "parse_mode": "HTML"
}

Tags: <b>, <strong>, <i>, <em>, <u>, <s>, <code>, <pre>, <a href="url">

Markdown

{
  "text": "**bold**, *italic*, `code`, ~~strikethrough~~, [link](https://example.com)",
  "parse_mode": "Markdown"
}

Object Types

Update

{
  "update_id": 123456789,
  "message": { ... },            // new message
  "callback_query": { ... },     // inline button press
  "channel_post": { ... },       // new channel post
  "my_chat_member": { ... },     // bot added/removed from chat
  "pre_checkout_query": { ... }, // payment confirmation request
  "message_reaction": { ... }    // reaction added/removed/changed
}

An Update contains one of the event types.

Message

{
  "message_id": "uuid",
  "from": { "id": "uuid", "username": "john", "is_bot": false },
  "chat": { "id": "uuid", "type": "private" },
  "date": 1705123456,
  "text": "Hello",
  "entities": [ ... ],
  "forwarded_from": { "id": "uuid", "username": "..." },  // original author (for forwarded messages)
  "reply_to_message": { ... },
  "reply_markup": { ... },
  "photo": [ ... ],
  "document": { ... },
  "voice": { ... },          // voice.duration — length in seconds
  "location": { ... },       // geo-position (see below); arrives e.g. after tapping a request_location button
  "successful_payment": { ... }
}

Location

The location object is present when the user shares a geo-position (message type location) — including after tapping a reply button with request_location: true. For such updates text is empty and the coordinates are structured:

{
  "latitude": 55.751244,
  "longitude": 37.618423,
  "name": "Kremlin",                  // optional (if the client reverse-geocoded the point)
  "address": "Moscow, Red Square"     // optional
}

CallbackQuery

{
  "id": "unique_callback_id",
  "from": { "id": "uuid", "username": "john" },
  "message": { ... },
  "chat_instance": "uuid",
  "data": "btn1"
}

PreCheckoutQuery

{
  "id": "uuid",
  "from": { "id": "uuid", "username": "john", "is_bot": false },
  "currency": "XTR",
  "total_amount": 1000,
  "invoice_payload": "premium_30d_user123"
}

total_amount is in units (100 units = 1 star). currency is always "XTR".

SuccessfulPayment

Contained in the successful_payment field of a Message object after payment completion.

{
  "currency": "XTR",
  "total_amount": 1000,
  "invoice_payload": "premium_30d_user123",
  "provider_payment_charge_id": "uuid"
}

MyChatMember

Notification about the bot being added or removed from a group/channel.

{
  "chat": { "id": "uuid", "type": "group", "title": "My Group" },
  "from": { "id": "uuid", "username": "admin" },
  "old_chat_member": { "status": "left", "user": { ... } },
  "new_chat_member": { "status": "member", "user": { ... } }
}

Statuses: member, left, kicked.

MessageReaction

Emitted when a member adds, removes, or changes a reaction on a message. Delivered to all bot members of the chat. old_reaction/new_reaction are that user's full reaction set on the message before and after the change (the delta lets you count accurately without double-counting on replace).

{
  "chat": { "id": "uuid", "type": "group" },
  "message_id": "uuid",
  "user": { "id": "uuid", "username": "vasya" },  // who changed the reaction
  "date": 1705123456,
  "old_reaction": ["😀"],   // before (may be empty)
  "new_reaction": ["🔥"]    // after (empty = reaction removed)
}

Reactions on the bot's own messages are delivered too. The update goes into the same durable queue as message — it isn't lost if the bot was offline.

InlineKeyboardMarkup

{
  "inline_keyboard": [
    [
      { "text": "Button", "callback_data": "btn1" }
    ],
    [
      { "text": "Website", "url": "https://example.com" }
    ]
  ]
}

InlineKeyboardButton

FieldTypeDescription
textStringButton text (required)
callback_dataStringCallback data (up to 64 bytes)
urlStringURL to open
payBooleantrue for payment button (invoice only)

reply_markup (one of)

The reply_markup parameter accepts one of: an inline keyboard, a custom reply keyboard, or a keyboard removal. The server stores and returns the value verbatim in message.reply_markup.

ReplyKeyboardMarkup

Buttons are shown over the input field; tapping one sends its text as a regular message.

{
  "keyboard": [
    [ { "text": "📞 Share contact", "request_contact": true } ],
    [ { "text": "Yes" }, { "text": "No" } ]
  ],
  "resize_keyboard": true,
  "one_time_keyboard": true,
  "input_field_placeholder": "Choose an option"
}
FieldTypeDescription
keyboard[[KeyboardButton]]Array of button rows (required)
resize_keyboardBooleanShrink keyboard to fit buttons
one_time_keyboardBooleanHide after first use
is_persistentBooleanAlways keep the keyboard visible
input_field_placeholderStringPlaceholder shown in the input field

Limits: ≤25 rows, ≤12 buttons per row, ≤300 buttons total, button text ≤256 bytes.

KeyboardButton

FieldTypeDescription
textStringButton label, also the text sent on tap (required)
request_contactBooleanAsk the user to share their phone contact
request_locationBooleanAsk the user to share their location

request_contact and request_location cannot both be set.

Requesting a location — full round-trip

  1. The bot sends sendMessage with a reply keyboard whose button has request_location: true (usually a single button + one_time_keyboard: true):
    { "keyboard": [[{ "text": "📍 Send location", "request_location": true }]], "resize_keyboard": true, "one_time_keyboard": true }
  2. A supporting client requests permission on tap, reads the device coordinates and sends a location message.
  3. The bot receives a message update with the location object filled in; text is empty.

A client that can't render custom reply keyboards doesn't show the button — the request is invisible to it and older clients keep working unchanged.

ReplyKeyboardRemove

{ "remove_keyboard": true }

User

FieldTypeDescription
idStringUser UUID
usernameStringUsername
is_botBooleantrue if bot
is_deletedBooleantrue if deleted

Chat

FieldTypeDescription
idStringChat UUID
typeStringprivate, group, channel
titleStringTitle (for groups/channels)
usernameStringGroup/channel username

Bots in Groups and Channels

Bots can be added to groups and channels. When added, the bot receives a my_chat_member update.

Sending to Groups

POST /bot/v1/{token}/sendMessage
{ "chat_id": "@group_username", "text": "Hello group!" }

chat_id supports both UUID and @username.

Sending to Channels

POST /bot/v1/{token}/sendMessage
{ "chat_id": "@channel_username", "text": "New post!" }

When sending to a channel, a full post is created. sendDocument, sendPhoto, sendVoice are also supported.

Receiving Updates


Examples

import requests
import time

TOKEN = "550e8400-...:aB3dE5fG7h..."
BASE_URL = f"https://api.wwchat.org/bot/v1/{TOKEN}"

def get_updates(offset=None):
    params = {"timeout": 30}
    if offset:
        params["offset"] = offset
    resp = requests.get(f"{BASE_URL}/getUpdates", params=params)
    return resp.json()

def send_message(chat_id, text):
    resp = requests.post(f"{BASE_URL}/sendMessage", json={
        "chat_id": chat_id,
        "text": text
    })
    return resp.json()

def main():
    offset = None
    print("Bot started!")

    while True:
        result = get_updates(offset)

        if not result.get("ok"):
            time.sleep(5)
            continue

        for update in result.get("result", []):
            offset = update["update_id"] + 1

            if "message" in update:
                msg = update["message"]
                chat_id = msg["chat"]["id"]
                text = msg.get("text", "")

                if text.startswith("/start"):
                    send_message(chat_id, "Hello! I'm a WWChat bot.")
                elif text.startswith("/help"):
                    send_message(chat_id, "Commands:\n/start\n/help")
                else:
                    send_message(chat_id, f"You wrote: {text}")

if __name__ == "__main__":
    main()
const TOKEN = "550e8400-...:aB3dE5fG7h...";
const BASE_URL = `https://api.wwchat.org/bot/v1/${TOKEN}`;

async function getUpdates(offset) {
  const params = new URLSearchParams({ timeout: "30" });
  if (offset) params.set("offset", offset);

  const resp = await fetch(`${BASE_URL}/getUpdates?${params}`);
  return resp.json();
}

async function sendMessage(chatId, text) {
  const resp = await fetch(`${BASE_URL}/sendMessage`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chat_id: chatId, text }),
  });
  return resp.json();
}

async function main() {
  let offset = null;
  console.log("Bot started!");

  while (true) {
    const result = await getUpdates(offset);

    if (!result.ok) {
      await new Promise((r) => setTimeout(r, 5000));
      continue;
    }

    for (const update of result.result) {
      offset = update.update_id + 1;

      if (update.message) {
        const chatId = update.message.chat.id;
        const text = update.message.text || "";

        if (text.startsWith("/start")) {
          await sendMessage(chatId, "Hello! I'm a WWChat bot.");
        } else if (text.startsWith("/help")) {
          await sendMessage(chatId, "Commands:\n/start\n/help");
        } else {
          await sendMessage(chatId, `You wrote: ${text}`);
        }
      }
    }
  }
}

main();
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"
)

const token = "550e8400-...:aB3dE5fG7h..."
const baseURL = "https://api.wwchat.org/bot/v1/" + token

type Update struct {
	UpdateID int     `json:"update_id"`
	Message  *Message `json:"message"`
}

type Message struct {
	MessageID string `json:"message_id"`
	From      struct {
		ID       string `json:"id"`
		Username string `json:"username"`
	} `json:"from"`
	Chat struct {
		ID   string `json:"id"`
		Type string `json:"type"`
	} `json:"chat"`
	Text string `json:"text"`
}

type APIResponse struct {
	OK     bool     `json:"ok"`
	Result []Update `json:"result"`
}

func getUpdates(offset int) (*APIResponse, error) {
	url := fmt.Sprintf("%s/getUpdates?timeout=30", baseURL)
	if offset > 0 {
		url += fmt.Sprintf("&offset=%d", offset)
	}
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result APIResponse
	json.NewDecoder(resp.Body).Decode(&result)
	return &result, nil
}

func sendMessage(chatID, text string) {
	body, _ := json.Marshal(map[string]string{
		"chat_id": chatID,
		"text":    text,
	})
	http.Post(baseURL+"/sendMessage", "application/json",
		bytes.NewReader(body))
}

func main() {
	offset := 0
	fmt.Println("Bot started!")

	for {
		result, err := getUpdates(offset)
		if err != nil || !result.OK {
			time.Sleep(5 * time.Second)
			continue
		}

		for _, update := range result.Result {
			offset = update.UpdateID + 1

			if update.Message == nil {
				continue
			}

			chatID := update.Message.Chat.ID
			text := update.Message.Text

			switch {
			case strings.HasPrefix(text, "/start"):
				sendMessage(chatID, "Hello! I'm a WWChat bot.")
			case strings.HasPrefix(text, "/help"):
				sendMessage(chatID, "Commands:\n/start\n/help")
			default:
				sendMessage(chatID, "You wrote: "+text)
			}
		}
	}
}
using System.Net.Http.Json;
using System.Text.Json;

const string token = "550e8400-...:aB3dE5fG7h...";
const string baseUrl = $"https://api.wwchat.org/bot/v1/{token}";

using var http = new HttpClient();
int? offset = null;
Console.WriteLine("Bot started!");

while (true)
{
    var url = $"{baseUrl}/getUpdates?timeout=30";
    if (offset.HasValue) url += $"&offset={offset}";

    try
    {
        var resp = await http.GetFromJsonAsync<JsonElement>(url);
        if (!resp.GetProperty("ok").GetBoolean())
        {
            await Task.Delay(5000);
            continue;
        }

        foreach (var update in resp.GetProperty("result").EnumerateArray())
        {
            offset = update.GetProperty("update_id").GetInt32() + 1;

            if (!update.TryGetProperty("message", out var msg))
                continue;

            var chatId = msg.GetProperty("chat").GetProperty("id").GetString()!;
            var text = msg.TryGetProperty("text", out var t) ? t.GetString() ?? "" : "";

            var reply = text switch
            {
                _ when text.StartsWith("/start") => "Hello! I'm a WWChat bot.",
                _ when text.StartsWith("/help") => "Commands:\n/start\n/help",
                _ => $"You wrote: {text}"
            };

            await http.PostAsJsonAsync($"{baseUrl}/sendMessage", new
            {
                chat_id = chatId,
                text = reply
            });
        }
    }
    catch
    {
        await Task.Delay(5000);
    }
}
<?php
$token = "550e8400-...:aB3dE5fG7h...";
$baseUrl = "https://api.wwchat.org/bot/v1/$token";

function getUpdates(string $baseUrl, ?int $offset = null): array {
    $url = "$baseUrl/getUpdates?timeout=30";
    if ($offset !== null) $url .= "&offset=$offset";

    $resp = file_get_contents($url);
    return json_decode($resp, true);
}

function sendMessage(string $baseUrl, string $chatId, string $text): void {
    $opts = [
        'http' => [
            'method'  => 'POST',
            'header'  => 'Content-Type: application/json',
            'content' => json_encode([
                'chat_id' => $chatId,
                'text'    => $text,
            ]),
        ],
    ];
    file_get_contents($baseUrl . "/sendMessage",
        false, stream_context_create($opts));
}

$offset = null;
echo "Bot started!\n";

while (true) {
    $result = getUpdates($baseUrl, $offset);

    if (!($result['ok'] ?? false)) {
        sleep(5);
        continue;
    }

    foreach ($result['result'] as $update) {
        $offset = $update['update_id'] + 1;

        if (!isset($update['message'])) continue;

        $chatId = $update['message']['chat']['id'];
        $text = $update['message']['text'] ?? '';

        if (str_starts_with($text, '/start')) {
            sendMessage($baseUrl, $chatId, "Hello! I'm a WWChat bot.");
        } elseif (str_starts_with($text, '/help')) {
            sendMessage($baseUrl, $chatId, "Commands:\n/start\n/help");
        } else {
            sendMessage($baseUrl, $chatId, "You wrote: $text");
        }
    }
}

Limits

Rate Limits

LimitValue
Requests per second (all methods)30 per bot
Messages per second per chat1 per bot
Messages per minute per chat20 per bot

When rate limit is exceeded, 429 Too Many Requests is returned with a Retry-After: 1 header.

Per-chat message limits apply to: sendMessage, sendDocument, sendPhoto, sendVoice, sendInvoice.

Inline Keyboard

LimitValue
Max rows25
Max buttons per row8
Max buttons total100
Button text256 bytes
callback_data64 bytes

Messages

LimitValue
Message text4096 characters
Invoice title255 characters
Invoice description1000 characters
Invoice payload512 characters

Other

System Bots

@BotMama

System bot for creating and managing bots: