kappelas/kappelas-sdk-php

Official PHP SDK for the Kappela messaging platform β€” v0.2.2

Maintainers

Package info

github.com/Arnel7/kappelas-sdk-php

pkg:composer/kappelas/kappelas-sdk-php

Transparency log

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.9.1 2026-07-16 07:45 UTC

This package is auto-updated.

Last update: 2026-07-16 07:55:56 UTC


README

Packagist PHP License: MIT

Official PHP SDK for the Kappela messaging platform.
Build bots and personal automations with a clean, typed API.

Table of Contents

Prerequisites

  • PHP 8.1+
  • Composer

Install

composer require kappelas/kappelas-sdk-php

Quick start

<?php
require 'vendor/autoload.php';

use Kappelas\KappelaBot;
use Kappelas\Types\Message;

$bot = new KappelaBot('YOUR_BOT_TOKEN');

$bot->onMessage(function (Message $msg) use ($bot) {
    if ($msg->text === '/start') {
        $bot->messages->send([
            'chat_id' => $msg->chatId,
            'text'    => 'Hello! πŸ‘‹',
        ]);
    }
});

$bot->start(); // blocks β€” WebSocket loop

For a webhook setup, call $bot->handleWebhook($payload) instead of $bot->start():

$payload = json_decode(file_get_contents('php://input'), true);
$bot->handleWebhook($payload);

Personal automation

Authenticate as yourself with a personal API key (sk_...). KappelaUser exposes the same resources as KappelaBot β€” $me->messages, $me->chats (including member management and invite links), $me->communities, $me->profile, and $me->reply(). In addition, KappelaUser has $me->stories (user-only). Collections and Hugging Face credentials are bot-only.

$me = new Kappelas\KappelaUser('sk_...');

$me->reply($msg, 'Got it! πŸ‘‹');
$me->communities->create(['name' => 'Devs', 'requires_approval' => true]);

// User-only: stories
$me->stories->create(['type' => 'text', 'caption' => 'Hello πŸ‘‹']);

Pausing automations

Pausing your personal automation makes your account stop receiving incoming messages over /v1/me, so an AI auto-responder is never triggered, and any send is rejected with AUTOMATIONS_PAUSED β€” until you resume. Pausing a bot makes it stop receiving incoming messages (no WebSocket push, no webhook) and rejects sends with BOT_PAUSED until resumed. This is useful when the human owner wants to take over and stop the AI.

use Kappelas\KappelaUser;
use Kappelas\KappelaBot;

$me = new Kappelas\KappelaUser('sk_...');
$me->pauseAutomations();     // β†’ ['automations_paused' => true]
$me->resumeAutomations();    // β†’ ['automations_paused' => false]
$me->getAutomationStatus();  // β†’ ['automations_paused' => bool]

$bot = new Kappelas\KappelaBot('YOUR_BOT_TOKEN');
$bot->pause();               // β†’ ['paused' => true]
$bot->resume();              // β†’ ['paused' => false]
$bot->getStatus();           // β†’ ['paused' => bool]

To pause only in ONE conversation (e.g. you take over a single chat while the AI keeps handling the rest):

// Personal automation, scoped to one chat
$me->pauseAutomationInChat($chatId);    // β†’ ['done' => true]
$me->resumeAutomationInChat($chatId);   // β†’ ['done' => true]

// Bot, scoped to one chat
$bot->pauseInChat($chatId);             // β†’ ['done' => true]
$bot->resumeInChat($chatId);            // β†’ ['done' => true]

PHP type hints & autocompletion

Every method has full PHPDoc with typed @param shapes and @return types. IDEs (PhpStorm, VS Code + Intelephense) provide autocompletion on all result properties:

$result = $bot->messages->send(['chat_id' => 123, 'text' => 'Hi']);
$result->messageId; // int
$result->createdAt; // int|null

Events β€” WebSocket vs Webhook

Feature WebSocket (start()) Webhook (handleWebhook())
Setup No HTTPS required Requires public HTTPS URL
Connection Persistent TCP Stateless HTTP
Use case Development, VPS bots Serverless, shared hosting

WebSocket:

$bot->onMessage(fn(Message $msg) => ...);
$bot->onCallbackQuery(fn(CallbackQuery $cb) => ...);
$bot->onConnected(fn() => ...);
$bot->onDisconnected(fn(int $code, string $reason) => ...);
$bot->onError(fn(Throwable $e) => ...);
$bot->start();

Webhook:

// In your HTTP handler:
$payload = json_decode(file_get_contents('php://input'), true);
$bot->handleWebhook($payload);
// Same onMessage / onCallbackQuery handlers are called synchronously.

bot->reply()

Reply to a received message in one call β€” reply_to_id is injected automatically:

$bot->onMessage(function (Message $msg) use ($bot) {
    $bot->reply($msg, '↩️ Got your message!');
});

With a keyboard:

$bot->reply($msg, 'Choose an option:', [
    'reply_markup' => [
        'inline_keyboard' => [[
            ['text' => 'βœ… Yes', 'callback_data' => 'yes'],
            ['text' => '❌ No',  'callback_data' => 'no'],
        ]],
    ],
]);

Message fields

$msg->id               // int   β€” message ID
$msg->chatId           // int   β€” chat ID
$msg->senderId         // ?string
$msg->type             // ?string β€” 'text'|'image'|'video'|'audio'|'document'|...
$msg->text             // ?string
$msg->mediaId          // ?string
$msg->extraData        // mixed  β€” inline keyboard definition when received
$msg->status           // string β€” 'sent'|'delivered'|'read'
$msg->editedAt         // ?int   β€” Unix timestamp
$msg->deletedAt        // ?int
$msg->createdAt        // int    β€” Unix timestamp
$msg->replyToId        // ?int
$msg->replyToSnapshot  // ?ReplySnapshot
$msg->mentions         // array
$msg->forwardedFrom    // mixed
$msg->expiresAt        // ?int
$msg->senderName       // ?string
$msg->senderUsername   // ?string
$msg->senderAvatarUrl  // ?string
$msg->clientMsgId      // ?string
$msg->width            // ?int   β€” media width in pixels
$msg->height           // ?int   β€” media height in pixels
$msg->chatType         // ?string β€” 'private'|'group'|'channel'

CallbackQuery fields

$cb->chatId          // int
$cb->senderId        // string
$cb->callbackData    // string
$cb->senderName      // ?string
$cb->senderUsername  // ?string
$cb->sentAt          // ?int

API reference

messages

Recipient β€” chat_id or user_id. Every send / edit / delete / typing method accepts either chat_id (int) or user_id (string UUID). With user_id the message is routed to your 1-to-1 private chat with that user β€” a bot requires the conversation to already exist (FORBIDDEN otherwise); a user creates it automatically (find-or-create). For edit / delete the conversation must exist.

$bot->messages->send(['user_id' => 'f19f2127-…', 'text' => 'Hi']);
$me->messages->sendPhoto(['user_id' => $cb->senderId, 'file' => $file]);
// Send text β€” by chat_id or user_id
$bot->messages->send([
    'chat_id'         => 123,
    'text'            => 'Hello!',
    'reply_markup'    => [...],    // optional keyboard
    'reply_to_id'     => 456,     // optional β€” reply to message ID
    'delete_previous' => true,    // optional
]);
// β†’ SendResult { messageId: int, createdAt: ?int }

// Action button β€” foot-of-bubble copy / link / join button (takes precedence over reply_markup).
// type ∈ copy_text | external_link | internal_link | join
$bot->messages->send([
    'chat_id'       => 123,
    'text'          => 'Your code is 837192',
    'action_button' => ['label' => 'Copy code', 'type' => 'copy_text', 'value' => '837192'],
]);

// Send media
$bot->messages->sendPhoto([
    'chat_id'         => 123,
    'file'            => ['data' => $bytes, 'filename' => 'photo.jpg', 'content_type' => 'image/jpeg'],
    'caption'         => 'Caption text',
    'reply_to_id'     => 456,
    'delete_previous' => true,
    'reply_markup'    => [...],
]);
// sendVideo(), sendDocument(), sendAudio() β€” same signature
// β†’ SendMediaResult { messageId: int, createdAt: ?int, mediaId: string }

// Carousel
$bot->messages->sendCarousel([
    'chat_id'             => 123,
    'text'                => 'Our products:',
    'carousel'            => [
        ['id' => 'p1', 'title' => 'Product A', 'subtitle' => '$9.99', 'button_text' => 'View'],
    ],
    'quick_reply_buttons' => ['See more', ['text' => '❌ Cancel', 'callback_data' => 'cancel']],
    'reply_to_id'         => 456,
]);
// β†’ SendCarouselResult { messageId: int, createdAt: ?int, type: 'carousel' }

// Typing indicator
$bot->messages->sendTyping(['chat_id' => 123]);
$bot->messages->sendTyping(['chat_id' => 123, 'is_typing' => false]);
// β†’ TypingResult { typing: bool }

// Edit β€” texte ET clavier ensemble (ex. un menu qui se coche au clic)
$bot->messages->edit([
    'chat_id'        => 123,
    'message_id'     => 456,
    'new_text'       => 'Tu as choisi : βœ… Oui',
    'new_extra_data' => [
        'inline_keyboard' => [[
            ['text' => 'βœ… Oui βœ“', 'callback_data' => 'yes'],
            ['text' => '❌ Non',   'callback_data' => 'no'],
        ]],
    ],
]);
// β†’ EditMessageResult { edited: bool, messageId: int }

// Clavier seul (on omet 'new_text' β†’ le texte est conservΓ©) :
$bot->messages->edit([
    'chat_id'        => 123,
    'message_id'     => 456,
    'new_extra_data' => ['inline_keyboard' => [[['text' => 'Done βœ…', 'callback_data' => 'done']]]],
]);
// - new_text + new_extra_data β†’ change le texte ET le clavier
// - new_extra_data seul (sans new_text) β†’ change le clavier, garde le texte
// - new_text seul β†’ change le texte, laisse le clavier

// Delete
$bot->messages->delete(['chat_id' => 123, 'message_id' => 456]);
// β†’ DeleteResult { deleted: bool }

// Resolve a media_id β†’ signed download URL + metadata
$info = $bot->messages->getFile($msg->mediaId);
// β†’ FileInfo { mediaId, url, filename, contentType, sizeBytes, expiresIn }

// Or download the raw bytes directly (e.g. a received voice note to transcribe)
$bytes = $bot->messages->downloadFile($msg->mediaId); // binary string

delete_previous

When delete_previous: true, the bot's last message in the chat is deleted before sending the new one. Useful for menus that should replace themselves:

// First send
$bot->messages->send(['chat_id' => $chatId, 'text' => 'Step 1']);

// Next send β€” the "Step 1" message is deleted first
$bot->messages->send([
    'chat_id'         => $chatId,
    'text'            => 'Step 2',
    'delete_previous' => true,
]);

chats

// Paginated list
$result = $bot->chats->list(['limit' => 20, 'offset' => 0]);
// β†’ ChatsResult { chats: Chat[], hasMore: bool }

// Auto-pagination β€” return false from $fn to stop early
$bot->chats->iterate(50, function (Chat $chat): bool {
    echo $chat->title . "\n";
    return true; // continue
});

Chat fields:

$chat->chatId              // int
$chat->id                  // int
$chat->type                // 'private'|'group'|'channel'
$chat->title               // ?string
$chat->participants        // Participant[]
$chat->lastMessageAt       // mixed
$chat->createdAt           // string
$chat->createdBy           // string
$chat->isPinned            // bool
$chat->isPremium           // bool
$chat->isPublic            // bool
$chat->onlyAdminsCanWrite  // bool
$chat->labels              // array
$chat->description         // ?string
$chat->avatarUrl           // ?string

Participant fields:

$p->id         // string
$p->nom        // string
$p->isBot      // bool
$p->isPremium  // bool
$p->avatarUrl  // ?string
$p->role       // ?string β€” 'member'|'admin' (null in private chats)

Groups & channels

Receiving group messages

Group messages arrive via the same onMessage handler:

$bot->onMessage(function (Message $msg) use ($bot) {
    if ($msg->chatType === 'group') {
        // handle group message
    }
});

Replying in a group

$bot->reply($msg, 'Reply to group message');

Getting member IDs

$admins = $bot->chats->getAdministrators(['chat_id' => $groupId]);
foreach ($admins->admins as $admin) {
    echo $admin->userId . ' β€” ' . $admin->role . "\n";
}

Detecting conversation type

$bot->onMessage(function (Message $msg) use ($bot) {
    $context = match($msg->chatType) {
        'private' => 'private chat',
        'group'   => 'group',
        'channel' => 'channel',
        default   => 'unknown',
    };
    $bot->messages->send(['chat_id' => $msg->chatId, 'text' => "You're in a $context"]);
});

Full group bot example

<?php
require 'vendor/autoload.php';

use Kappelas\KappelaBot;
use Kappelas\Types\Message;
use Kappelas\Types\CallbackQuery;

$bot = new KappelaBot('YOUR_BOT_TOKEN');

// Get groups the bot belongs to
$groups = $bot->chats->getMyGroups();
foreach ($groups->groups as $g) {
    echo "Group: {$g->title} ({$g->type}) β€” bot role: {$g->botRole}\n";
}

$bot->onMessage(function (Message $msg) use ($bot) {
    if ($msg->chatType !== 'group') return;

    if ($msg->text === '/members') {
        $admins = $bot->chats->getAdministrators(['chat_id' => $msg->chatId]);
        $list = implode(', ', array_map(fn($a) => $a->userId, $admins->admins));
        $bot->reply($msg, "Admins: $list");
    }
});

$bot->onCallbackQuery(function (CallbackQuery $cb) use ($bot) {
    $bot->messages->send([
        'chat_id' => $cb->chatId,
        'text'    => 'You clicked: ' . $cb->callbackData,
    ]);
});

$bot->start();

Chat member management

Admin-only operations.

// Add a member
$bot->chats->addMember(['chat_id' => 123, 'user_id' => 'abc456']);
// β†’ AddChatMemberResult { description: string }

// Ban a member
$bot->chats->banMember(['chat_id' => 123, 'user_id' => 'abc456']);
// β†’ BanChatMemberResult { description: string }

// Leave a chat
$bot->chats->leaveChat(['chat_id' => 123]);
// β†’ LeaveChatResult { description: string }

// Promote / demote
$bot->chats->promoteMember(['chat_id' => 123, 'user_id' => 'abc456', 'role' => 'admin']);
$bot->chats->promoteMember(['chat_id' => 123, 'user_id' => 'abc456', 'role' => 'member']);
// β†’ PromoteChatMemberResult { userId: string, role: string }

// Get all admins
$result = $bot->chats->getAdministrators(['chat_id' => 123]);
// β†’ GetChatAdministratorsResult { admins: ChatMemberInfo[] }

// Get one member
$info = $bot->chats->getMember(['chat_id' => 123, 'user_id' => 'abc456']);
// β†’ ChatMemberInfo { userId: string, role: string }

Invite links (admin only)

// Create a permanent link (no limit)
$link = $bot->chats->createInviteLink(['chat_id' => 123]);

// Create with options
$link = $bot->chats->createInviteLink([
    'chat_id'    => 123,
    'max_uses'   => 10,
    'expires_in' => 86400, // seconds
]);

// Single-use shorthand (max_uses=1)
$link = $bot->chats->createSingleUseInviteLink(['chat_id' => 123]);

// β†’ ChatInviteLink { code, url, maxUses, useCount, expiresAt, createdAt }

// List active links
$result = $bot->chats->getInviteLinks(['chat_id' => 123]);
// β†’ GetChatInviteLinksResult { inviteLinks: ChatInviteLink[] }

// Revoke a link
$bot->chats->revokeInviteLink(['chat_id' => 123, 'code' => $link->code]);
// β†’ RevokeChatInviteLinkResult { revoked: bool, code: string }

getMyGroups

$result = $bot->chats->getMyGroups();
// β†’ GetMyGroupsResult { groups: BotGroupEntry[] }

foreach ($result->groups as $group) {
    echo "{$group->title} β€” {$group->type} β€” {$group->participantCount} members β€” bot: {$group->botRole}\n";
}

BotGroupEntry fields: chatId, type, title, participantCount, botRole

communities

Manage communities a bot belongs to: CRUD, members & roles, invite links, join requests, and group requests. A bot can only administer a community where it is an admin. Note: the community role (member/admin) is distinct from a group role.

use Kappelas\KappelaBot;

$bot = new KappelaBot('YOUR_BOT_TOKEN');

// --- CRUD ---
$c = $bot->communities->create(['name' => 'Devs', 'description' => 'Notre commu', 'requires_approval' => true]);
$all   = $bot->communities->list();        // Community[] (each with ->role)
$admin = $bot->communities->listAdmin();   // only those where the bot is admin
$one   = $bot->communities->get(['community_id' => $c->id]);   // CommunityDetail (with members)
$bot->communities->update(['community_id' => $c->id, 'description' => 'Nouvelle desc']); // only sent fields change
$bot->communities->delete(['community_id' => $c->id]);
$bot->communities->join(['community_id' => 42]); // ->pending === true if approval required

// --- Members & roles ---
// To make someone (person OR bot) admin: add them as member, then promote.
$bot->communities->addMember(['community_id' => $c->id, 'user_id' => 'uuid', 'role' => 'member']);
$bot->communities->promoteMember(['community_id' => $c->id, 'user_id' => 'uuid', 'role' => 'admin']);
$bot->communities->banMember(['community_id' => $c->id, 'user_id' => 'uuid']); // remove a member
$bot->communities->leave(['community_id' => $c->id]);

// --- Invite links ---
$inv  = $bot->communities->createInviteLink(['community_id' => $c->id, 'max_uses' => 10, 'expires_in' => '24h']);
$list = $bot->communities->getInviteLinks(['community_id' => $c->id]); // CommunityInvite[]
$bot->communities->revokeInviteLink(['community_id' => $c->id, 'code' => $inv->code]);
$preview = $bot->communities->previewInvite(['code' => $inv->code]); // CommunityInvitePreview (no auth needed)
$communityId = $bot->communities->acceptInvite(['code' => $inv->code]); // bot joins via code

// --- Join requests (user -> community) ---
$reqs = $bot->communities->getJoinRequests(['community_id' => $c->id]); // CommunityJoinRequest[]
$bot->communities->approveJoinRequest(['community_id' => $c->id, 'request_id' => $reqs[0]->id]);
$bot->communities->rejectJoinRequest(['community_id' => $c->id, 'request_id' => $reqs[0]->id]);

// --- Group requests + linking groups ---
$greqs = $bot->communities->getGroupRequests(['community_id' => $c->id]); // CommunityGroupRequest[]
$bot->communities->approveGroupRequest(['community_id' => $c->id, 'request_id' => $greqs[0]->id]);
$bot->communities->rejectGroupRequest(['community_id' => $c->id, 'request_id' => $greqs[0]->id]);
$bot->communities->addGroup(['community_id' => $c->id, 'conversation_id' => 123]);
$bot->communities->removeGroup(['community_id' => $c->id, 'conversation_id' => 123]);

Community fields: id, name, description, avatarUrl, createdBy, announcementChannelId, requiresApproval, createdAt, role (only in list()). CommunityDetail adds members (each with userId, name, avatarUrl, role).

webhooks

// Register
$bot->webhooks->set(['url' => 'https://yourserver.com/webhook']);
// β†’ WebhookSetResult { url: string, active: bool }

// Get info
$info = $bot->webhooks->getInfo();
// β†’ WebhookInfo { active: bool, url: ?string, createdAt: mixed }

// Remove
$bot->webhooks->delete();
// β†’ WebhookDeleteResult { active: bool }

profile

// Bot profile
$profile = $bot->profile->get();
// β†’ BotProfile { userId, username, isBot, about, description, avatarUrl }

// User profile (KappelaUser only)
$profile = $user->profile->get();
// β†’ UserProfile { id, username, nom, isBot, isPremium, avatarUrl, about }

stories (KappelaUser only)

Create and manage stories (ephemeral, 24 h) via $me->stories. Available on KappelaUser only β€” their audience is based on your private-conversation contacts.

For image/video stories, pass media (a ['data', 'filename', 'content_type'] array or a file path) β€” the SDK uploads it automatically and uses the resulting media id. For text/poll stories, no upload is needed. You can also pass a pre-uploaded media_id.

// Image story β€” SDK uploads the file, then creates the story
$story = $me->stories->create([
    'type'    => 'image',
    'media'   => ['data' => $bytes, 'filename' => 'photo.jpg', 'content_type' => 'image/jpeg'],
    // or simply: 'media' => '/path/to/photo.jpg',
    'caption' => 'Sunset πŸŒ‡',
    'audience' => 'all', // 'all' (default) | 'selected' | 'excluded'
]);

// Text story β€” no media
$me->stories->create(['type' => 'text', 'caption' => 'Good morning β˜€οΈ']);

// Restricted audience
$me->stories->create(['type' => 'text', 'caption' => 'PrivΓ©', 'audience' => 'selected', 'audience_user_ids' => ['uuid']]);

// Clickable CTA link over the story (text or image)
$me->stories->create(['type' => 'text', 'caption' => 'New drop', 'link' => 'https://shop.example.com', 'link_label' => 'Shop now']);

Link (CTA) β€” link (+ optional link_label) adds a clickable link shown over the story in the Kappela apps. The SDK carries it inside the caption as a JSON envelope ({text, link, linkLabel}); without a link the caption stays plain text.

Method Returns Description
stories->create($params) Story Create a story (uploads media automatically for image/video).
stories->uploadMedia($file) StoryMediaUpload Upload story media manually and get a media id (usually unnecessary).
stories->list() Story[] Feed of your contacts' active stories.
stories->listMine() Story[] Your own stories.
stories->get($storyId) Story A single story (audience-checked server-side).
stories->delete($storyId) StoryActionResult Delete one of your stories.
stories->view($storyId) StoryActionResult Mark a story as viewed.
stories->getViewers($storyId) StoryView[] Who viewed your story (owner only).
stories->getPreferences() StoryPreferences Your default audience preference.
stories->setPreferences($audience, $audienceUserIds = []) StoryActionResult Set your default audience preference.

Pause β€” while automations are paused, story reads still work but creating/deleting/viewing stories is rejected with AUTOMATIONS_PAUSED.

Keyboards

Comparison

Type Usage Rendered
Inline keyboard Buttons attached to a message Below the message
Reply keyboard Grid of buttons (replaces input bar) Bottom of screen
Scroll keyboard Horizontal scrollable buttons Above input bar

Inline keyboard

Buttons are passed as a 2D array β€” rows Γ— columns.

// Short form: text = callback_data
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Choose:',
    'reply_markup' => [
        'inline_keyboard' => [[
            ['text' => 'βœ… Yes', 'callback_data' => 'yes'],
            ['text' => '❌ No',  'callback_data' => 'no'],
        ]],
    ],
]);

// Long form: separate text and callback_data
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Choose action:',
    'reply_markup' => [
        'inline_keyboard' => [
            [['text' => 'πŸ“¦ Orders',    'callback_data' => 'action_orders']],
            [['text' => 'βš™οΈ Settings',  'callback_data' => 'action_settings']],
        ],
    ],
]);

Reply keyboard

Grid of buttons shown at the bottom. Each item can be a plain string or an array with text + callback_data.

// Short form β€” text is also the callback_data
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Pick a size:',
    'reply_markup' => ['keyboard' => [['S', 'M'], ['L', 'XL']]],
]);

// Long form β€” separate text and callback_data
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Confirm?',
    'reply_markup' => [
        'keyboard' => [[
            ['text' => 'βœ… Yes', 'callback_data' => 'confirm'],
            ['text' => '❌ No',  'callback_data' => 'cancel'],
        ]],
    ],
]);

// Mixed β€” strings and objects in the same row
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Mixed keyboard:',
    'reply_markup' => [
        'keyboard' => [
            [['text' => 'βœ… Yes', 'callback_data' => 'yes'], 'No'],
        ],
    ],
]);

Scroll keyboard

Flat horizontal list. Items can be plain strings or arrays with text + callback_data.

// Short form
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Filter by:',
    'reply_markup' => ['scroll_keyboard' => ['All', 'Active', 'Closed']],
]);

// Long form
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Menu:',
    'reply_markup' => [
        'scroll_keyboard' => [
            ['text' => 'πŸ“¦ Orders',   'callback_data' => 'menu_orders'],
            ['text' => '❓ Help',     'callback_data' => 'menu_help'],
            ['text' => 'βš™οΈ Settings', 'callback_data' => 'menu_settings'],
        ],
    ],
]);

// Mixed
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'Mixed scroll:',
    'reply_markup' => [
        'scroll_keyboard' => [
            ['text' => 'πŸ“¦ Orders', 'callback_data' => 'menu_orders'],
            '❓ Help',
        ],
    ],
]);

Full example

<?php
require 'vendor/autoload.php';

use Kappelas\KappelaBot;
use Kappelas\Types\CallbackQuery;
use Kappelas\Types\Message;

$bot = new KappelaBot('YOUR_BOT_TOKEN');
$chatId = 123;

// Show a menu with inline keyboard
$bot->messages->send([
    'chat_id'      => $chatId,
    'text'         => 'πŸ—‚ What do you need?',
    'reply_markup' => [
        'inline_keyboard' => [
            [['text' => 'πŸ“¦ Orders',    'callback_data' => 'menu_orders']],
            [['text' => '❓ Help',      'callback_data' => 'menu_help']],
            [['text' => 'βš™οΈ Settings',  'callback_data' => 'menu_settings']],
        ],
    ],
]);

// Handle button clicks
$bot->onCallbackQuery(function (CallbackQuery $cb) use ($bot) {
    $response = match($cb->callbackData) {
        'menu_orders'   => 'πŸ“¦ Here are your orders...',
        'menu_help'     => '❓ How can I help you?',
        'menu_settings' => 'βš™οΈ Opening settings...',
        default         => 'Unknown option',
    };
    $bot->messages->send(['chat_id' => $cb->chatId, 'text' => $response]);
});

$bot->start();

Text formatting

Inline styles

*bold*           β†’ **bold**
__italic__       β†’ _italic_
~strikethrough~  β†’ ~~strikethrough~~
`inline code`    β†’ `code`
$bot->messages->send([
    'chat_id' => $chatId,
    'text'    => '*bold*  __italic__  ~strikethrough~  `code`',
]);

Block code

Wrap with triple backticks. Optionally specify a language:

$bot->messages->send([
    'chat_id' => $chatId,
    'text'    => "Your API key:\n```\nsk_live_abc123\n```",
]);

Blockquote / citation

Lines starting with > are rendered as a blockquote:

$bot->messages->send([
    'chat_id' => $chatId,
    'text'    => "> Original question\n\nDetailed answer here.",
]);

Mentions and commands

$bot->messages->send([
    'chat_id' => $chatId,
    'text'    => 'Thanks @alice! Type /help for available commands.',
]);

Auto-detected links

Plain URLs and bare domains are automatically made clickable:

$bot->messages->send([
    'chat_id' => $chatId,
    'text'    => 'Visit kappelas.com or https://kappelas.com/docs',
]);

Error handling

All API errors throw KappelaError. Catch it for structured error info:

use Kappelas\KappelaError;

try {
    $bot->messages->send(['chat_id' => $chatId, 'text' => 'Hi']);
} catch (KappelaError $e) {
    echo $e->errorCode;     // 'NOT_FOUND', 'FORBIDDEN', ...
    echo $e->errorMessage;  // human-readable message from the API
    echo $e->status;        // HTTP status code (int)
    echo $e->requestId;     // trace ID (include in bug reports)
}
errorCode HTTP Meaning
UNAUTHORIZED 401 Invalid or expired token
FORBIDDEN 403 Missing permission for this action
NOT_FOUND 404 Resource doesn't exist
MISSING_FIELD 400 Required parameter missing
INVALID_FIELD 400 Parameter has wrong type/format
CONFLICT 409 Resource already exists
INTERNAL_ERROR 500 Unexpected server error
SERVICE_UNAVAILABLE 503 Platform temporarily unavailable

File input

Pass file content as a raw string plus metadata:

// From bytes in memory
$bot->messages->sendPhoto([
    'chat_id' => 123,
    'file'    => [
        'data'         => file_get_contents('/path/to/photo.jpg'),
        'filename'     => 'photo.jpg',
        'content_type' => 'image/jpeg',
    ],
    'caption' => 'My photo',
]);

// From a file path (the SDK reads it automatically)
$bot->messages->sendDocument([
    'chat_id' => 123,
    'file'    => '/path/to/document.pdf',
    'caption' => 'My document',
]);

Supported methods: sendPhoto, sendVideo, sendDocument, sendAudio.