Official Kappela SDK for Python — build bots and personal automations
Project description
kappelas-sdk
Official Python SDK for the Kappela messaging platform.
Build bots and personal automations — send messages, handle events, manage chats.
Table of contents
- Prerequisites
- Install
- Quick start
- Pausing automations
- Python autocomplete & type hints
- Events — WebSocket vs Webhook
- API reference
- Keyboards
- Text formatting
- Error handling
- File input
Prerequisites
You need a bot token from BotMother, the official Kappela bot manager.
- Open Kappela and start a conversation with BotMother
- Follow the instructions to create a bot
- BotMother gives you a token — keep it secret, it gives full control over your bot
For personal automation (sending messages as yourself), generate an API key from your Kappela account settings (sk_...).
Install
pip install kappelas-sdk
Requires Python 3.11+.
Quick start
Bot
import asyncio
from kappelas import KappelaBot
bot = KappelaBot('YOUR_BOT_TOKEN')
@bot.on('message')
async def on_message(msg):
await bot.reply(msg, f'Echo: {msg.text}')
@bot.on('callback_query')
async def on_callback(cb):
await bot.messages.send(cb.chat_id, f'You clicked: {cb.callback_data}')
asyncio.run(bot.run())
Personal automation
import asyncio
from kappelas import KappelaUser
me = KappelaUser('sk_your_api_key')
@me.on('message')
async def on_message(msg):
print(f'[{msg.chat_id}] {msg.sender_name}: {msg.text}')
asyncio.run(me.run())
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.
# Personal automation
await me.pause_automations() # → {'automations_paused': True}
await me.resume_automations() # → {'automations_paused': False}
await me.get_automation_status() # → {'automations_paused': bool}
# Bot
await bot.pause() # → {'paused': True}
await bot.resume() # → {'paused': False}
await bot.get_status() # → {'paused': bool}
Python autocomplete & type hints
The SDK is fully typed — every method, parameter, and return value has a type annotation.
Your editor (VS Code, PyCharm, etc.) will show completions and inline documentation automatically.
from kappelas import KappelaBot, InlineKeyboard, InlineKeyboardButton
bot = KappelaBot('YOUR_BOT_TOKEN')
@bot.on('message')
async def on_message(msg): # msg: Message — full autocomplete on msg.chat_id, msg.text, …
result = await bot.messages.send( # → SendResult
msg.chat_id,
'Hello!',
reply_markup=InlineKeyboard(inline_keyboard=[[
InlineKeyboardButton(text='OK', callback_data='ok'),
]]),
)
print(result.message_id) # int — autocomplete works here too
All public types are importable directly from kappelas:
from kappelas import (
Message, CallbackQuery, Chat, Participant,
InlineKeyboard, InlineKeyboardButton,
ReplyKeyboard, ReplyKeyboardButton,
ScrollKeyboard, ScrollKeyboardButton,
CarouselCard, FileData, KappelaError,
ChatMemberInfo, ChatInviteLink, BotGroupEntry,
# … and more
)
Events — WebSocket vs Webhook
| Mode | Method | Best for |
|---|---|---|
| WebSocket | await bot.run() |
Development, local scripts |
| Webhook | await bot.webhooks.set() + bot.handle_webhook() |
Production servers |
The same on('message') and on('callback_query') handlers work in both modes — no code change needed when switching.
WebSocket (development)
bot = KappelaBot('YOUR_BOT_TOKEN')
@bot.on('message')
async def on_message(msg): ...
@bot.on('callback_query')
async def on_callback(cb): ...
asyncio.run(bot.run()) # blocks, auto-reconnects on disconnect
run() blocks until stop() is called. Use start() if you need to connect in the background inside an already-running event loop.
Both KappelaBot and KappelaUser support async with, which automatically closes the WebSocket and HTTP client on exit:
async def main():
async with KappelaBot('YOUR_BOT_TOKEN') as bot:
@bot.on('message')
async def on_message(msg):
await bot.reply(msg, f'Echo: {msg.text}')
await bot.run()
asyncio.run(main())
Webhook (production)
from fastapi import FastAPI, Request
from kappelas import KappelaBot
app = FastAPI()
bot = KappelaBot('YOUR_BOT_TOKEN')
@bot.on('message')
async def on_message(msg):
await bot.reply(msg, f'Echo: {msg.text}')
@bot.on('callback_query')
async def on_callback(cb):
await bot.messages.send(cb.chat_id, f'Clicked: {cb.callback_data}')
@app.on_event('startup')
async def register_webhook():
await bot.webhooks.set('https://your-server.com/kappela-webhook')
@app.post('/kappela-webhook')
async def webhook(request: Request):
bot.handle_webhook(await request.json())
return {'ok': True}
Do not call
bot.run()in webhook mode.
Event reference
| Event | Handler signature | Description |
|---|---|---|
message |
async def handler(msg: Message) |
Incoming message of any type |
callback_query |
async def handler(cb: CallbackQuery) |
Inline button clicked by a user |
connected |
async def handler() |
WebSocket connected or reconnected |
disconnected |
async def handler(code, reason) |
WebSocket disconnected |
error |
async def handler(exc: Exception) |
Connection or handler error |
raw |
async def handler(event: dict) |
Raw { type, data } wire event |
Decorator vs method usage
# Persistent handler — decorator form
@bot.on('message')
async def on_message(msg): ...
# Persistent handler — method form
async def on_message(msg): ...
bot.on('message', on_message)
bot.off('message', on_message) # remove handler
# Fires once then auto-removes
@bot.once('connected')
async def on_first_connect(): ...
bot.reply() — convenience shorthand
reply() sends a text reply without having to repeat chat_id and reply_to_id manually.
bot.reply(msg, text, *, reply_markup=None, delete_previous=False)
- Called with a
Message— setsreply_to_idautomatically (shows a quote banner in the chat).
@bot.on('message')
async def on_message(msg):
# Simple reply
await bot.reply(msg, 'Reçu 👍')
# With an inline keyboard
await bot.reply(msg, 'Choisis une option :', reply_markup=InlineKeyboard(
inline_keyboard=[[
InlineKeyboardButton(text='✅ Oui', callback_data='yes'),
InlineKeyboardButton(text='❌ Non', callback_data='no'),
]]
))
chat_id and reply_to_id are filled automatically — you only need to set extra fields like reply_markup or delete_previous.
Message fields
@bot.on('message')
async def on_message(msg):
msg.id # int — unique message ID
msg.chat_id # int — conversation ID
msg.chat_type # str | None — "private" | "group" | "channel"
msg.sender_id # str | None — UUID of the sender (None for system messages)
msg.type # str — "text" | "image" | "video" | "audio" | "document" | …
msg.text # str | None — text content (None for media-only messages)
msg.media_id # str | None — server-side media ID
msg.extra_data # Any — inline keyboard payload (when attached)
msg.status # str — "sent" | "delivered" | "read"
msg.created_at # int — Unix timestamp (seconds)
msg.edited_at # int | None — last edit time, or None
msg.deleted_at # int | None — deletion time, or None
msg.reply_to_id # int | None — ID of the message being replied to
msg.reply_to_snapshot # ReplySnapshot | None — snapshot of the replied-to message
msg.mentions # list[str] — UUIDs of mentioned users
msg.sender_name # str | None — display name in groups/channels (None in private)
msg.sender_avatar_url # str | None — avatar URL of the sender
msg.expires_at # int | None — expiry time for ephemeral messages
type values
| Value | Description |
|---|---|
"text" |
Plain text message |
"image" |
Photo |
"video" |
Video |
"audio" |
Audio file |
"document" |
Generic file |
"sticker" |
Sticker |
"poll" |
Poll |
"location" |
Location pin |
"contact" |
Contact card |
"system" |
System notification (member joined, etc.) |
CallbackQuery fields
@bot.on('callback_query')
async def on_callback(cb):
cb.chat_id # int — chat where the button was clicked
cb.sender_id # str — UUID of the user who clicked
cb.sender_name # str | None — display name (e.g. "Arnel LAWSON")
cb.sender_username # str | None — username (e.g. "arnell")
cb.callback_data # str — value set on the button
cb.sent_at # int — Unix timestamp (seconds)
Clicks are deduplicated server-side — your handler fires exactly once per click.
API reference
Constructor
KappelaBot(token, *, base_url, max_retries, timeout, ws_max_retries)
| Parameter | Type | Default | Description |
|---|---|---|---|
token |
str |
— | Bot token from BotMother (required) |
base_url |
str |
'https://api.kappelas.com' |
Override API base URL |
max_retries |
int |
2 |
HTTP retry count on 429 / 5xx |
timeout |
float |
30.0 |
Per-request timeout (seconds) |
ws_max_retries |
int |
12 |
Max WebSocket reconnect attempts |
KappelaUser(api_key, *, base_url, max_retries, timeout, ws_max_retries)
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key |
str |
— | Personal API key sk_... (required) |
base_url |
str |
'https://api.kappelas.com' |
Override API base URL |
max_retries |
int |
2 |
HTTP retry count on 429 / 5xx |
timeout |
float |
30.0 |
Per-request timeout (seconds) |
ws_max_retries |
int |
12 |
Max WebSocket reconnect attempts |
messages
messages.send(chat_id, text, *, reply_markup, reply_to_id, delete_previous) → SendResult
result = await bot.messages.send(
chat_id = 42,
text = 'Hello!',
reply_to_id = 123, # optional — shows a quote banner
delete_previous = False,
reply_markup = InlineKeyboard(inline_keyboard=[[
InlineKeyboardButton(text='Yes', callback_data='yes'),
InlineKeyboardButton(text='No', callback_data='no'),
]]),
)
# → SendResult(message_id=..., created_at=...)
messages.send_photo(chat_id, photo, *, caption, reply_to_id, delete_previous, reply_markup) → SendMediaResult
with open('banner.png', 'rb') as f:
result = await bot.messages.send_photo(42, f, caption='Check this out!')
# → SendMediaResult(message_id=..., created_at=..., media_id=...)
messages.send_video / send_document / send_audio → SendMediaResult
Same signature — replace the file parameter with your file input.
messages.send_carousel(chat_id, carousel, *, text, quick_reply_buttons, reply_to_id) → SendCarouselResult
from kappelas import CarouselCard, ScrollKeyboardButton
await bot.messages.send_carousel(
chat_id = 42,
text = 'Pick a product:',
carousel = [
CarouselCard(id='p1', title='Widget A', subtitle='9 990 FCFA', button_text='Buy'),
CarouselCard(id='p2', title='Widget B', subtitle='19 990 FCFA', button_text='Buy'),
],
quick_reply_buttons=[
ScrollKeyboardButton(text='✅ Confirm', callback_data='confirm'),
ScrollKeyboardButton(text='❌ Cancel', callback_data='cancel'),
],
)
When a user clicks a carousel card button, a callback_query fires with callback_data set to the card's id.
messages.edit(chat_id, message_id, *, new_text, new_extra_data) → EditMessageResult
# Edit text only
await bot.messages.edit(42, 123, new_text='Updated!')
# Edit inline keyboard only — keep existing text
from dataclasses import asdict
await bot.messages.edit(42, 123, new_extra_data=asdict(
InlineKeyboard(inline_keyboard=[[
InlineKeyboardButton(text='Done ✅', callback_data='done'),
]])
))
# → EditMessageResult(edited=True, message_id=123)
When new_extra_data is provided, new_text is optional — you can edit the keyboard without touching the text.
messages.send_typing(chat_id, *, is_typing) → TypingResult
await bot.messages.send_typing(42) # show
await bot.messages.send_typing(42, is_typing=False) # hide
messages.delete(chat_id, message_id) → DeleteResult
await bot.messages.delete(42, 123)
# → DeleteResult(deleted=True)
delete_previous
Pass delete_previous=True on any send* call to automatically remove the previous message from this bot in the same chat before sending. Useful for bots that maintain a single "current state" message.
# Send the first message
await bot.messages.send(chat_id, '⏳ Chargement…')
# Replace it — the previous message is deleted automatically
await bot.messages.send(chat_id, '✅ Résultat prêt !', delete_previous=True)
Works on all send methods: send(), send_photo(), send_video(), send_document(), send_audio().
chats
chats.list(*, limit, offset) → ChatsResult
page = await bot.chats.list(limit=20, offset=0)
for chat in page.chats:
print(chat.chat_id, chat.type, chat.title)
print(page.has_more)
chats.iterate(page_size?) → AsyncGenerator[Chat]
async for chat in bot.chats.iterate():
print(chat.chat_id, chat.title, chat.type)
Chat fields
chat.chat_id # int — conversation ID
chat.type # str — "private" | "group" | "channel"
chat.title # str | None — group/channel name (None for private)
chat.participants # list[Participant] — members (private only; empty for large groups)
chat.last_message_at # str | None — ISO 8601 timestamp of last message
chat.created_at # str — ISO 8601 creation timestamp
chat.is_public # bool — public group or channel
chat.only_admins_can_write# bool — only admins can post
chat.description # str | None — group/channel description
chat.avatar_url # str | None — avatar image URL
Groups & channels
Bots work identically in private chats, groups, and channels — same API, same events. The only requirement is that the bot must be a member of the conversation.
Receiving group messages
When a bot is added to a group or channel, it automatically receives every message posted there via the same on('message') handler used for DMs.
@bot.on('message')
async def on_message(msg):
# msg.chat_id — the group's id
# msg.chat_type — "private" | "group" | "channel"
# msg.sender_id — UUID of the user who sent the message
# msg.text — message content (None for media-only)
pass
The
chat_typefield lets you distinguish where a message came from without an extra API call.
Replying to a message
reply_to_id attaches a quote banner to your message. It works identically in private chats, groups, and channels. In groups, always quote the user you're responding to — it makes the context clear to all members.
@bot.on('message')
async def on_message(msg):
name = msg.sender_name or 'ami'
await bot.messages.send(
msg.chat_id,
f'Reçu, {name} 👋',
reply_to_id=msg.id, # quotes the original message
)
Quoting any historical message — reply_to_id accepts any message ID, not just the one that triggered the event:
await bot.messages.send(
msg.chat_id,
'Voici la réponse à ta question précédente :',
reply_to_id=456, # any past message ID
)
Works on all send methods — send_photo(), send_video(), send_document(), send_audio(), and send_carousel() all accept reply_to_id:
await bot.messages.send_carousel(
msg.chat_id,
carousel=[CarouselCard(id='p1', title='Produit A')],
text='Voici nos produits :',
reply_to_id=msg.id, # banner shows above the carousel
)
Getting member IDs in a group
There are three ways to obtain the user_id of members in a group or channel:
1. From incoming messages — the simplest. msg.sender_id is always set on every message event:
@bot.on('message')
async def on_message(msg):
if msg.chat_type == 'group':
print(msg.sender_id) # UUID of the sender
print(msg.sender_name) # display name (None in private chats)
2. From the participants list — chats.list() returns the full member list for each chat:
result = await bot.chats.list(limit=50)
for chat in result.chats:
for member in chat.participants:
print(member.id) # UUID — use as user_id in member calls
print(member.nom) # display name
print(member.is_bot) # True if this participant is a bot
print(member.role) # "admin" | "member" | None (private chats)
3. From get_administrators() — when you only need admin IDs:
from kappelas import GetChatAdministratorsParams
result = await bot.chats.get_administrators(GetChatAdministratorsParams(chat_id=42))
for admin in result.admins:
print(admin.user_id, admin.role) # role is always "admin"
Detecting conversation type
msg.chat_type is available on every incoming message. Use it to adapt bot behaviour per context:
@bot.on('message')
async def on_message(msg):
if msg.chat_type == 'private':
# 1-on-1 chat — show full keyboard, personalise replies
await bot.messages.send(
msg.chat_id,
'De quoi as-tu besoin ?',
reply_markup=ScrollKeyboard(scroll_keyboard=[
ScrollKeyboardButton(text='📦 Commandes', callback_data='menu_orders'),
ScrollKeyboardButton(text='❓ Aide', callback_data='menu_help'),
]),
)
elif msg.chat_type == 'group':
# Multi-user — reply with a quote so context is clear
await bot.messages.send(msg.chat_id, '✅ Noté !', reply_to_id=msg.id)
elif msg.chat_type == 'channel':
# Bot-only posting — no user interaction expected
pass
Full group bot example
A bot that works across private chats, groups, and channels:
import asyncio
from kappelas import KappelaBot, InlineKeyboard, InlineKeyboardButton, KappelaError
from kappelas.types import CreateChatInviteLinkParams
bot = KappelaBot('YOUR_BOT_TOKEN')
@bot.on('message')
async def on_message(msg):
if not msg.text:
return
# /status — works anywhere
if msg.text == '/status':
await bot.messages.send(
msg.chat_id,
'🟢 Bot en ligne',
reply_to_id=msg.id if msg.chat_type == 'group' else None,
)
return
# /invite — admin-only, group/channel only
if msg.text == '/invite' and msg.chat_type != 'private':
try:
link = await bot.chats.create_invite_link(
CreateChatInviteLinkParams(chat_id=msg.chat_id)
)
await bot.messages.send(
msg.chat_id,
f'🔗 Lien d\'invitation : {link.url}',
reply_to_id=msg.id if msg.chat_type == 'group' else None,
)
except KappelaError:
await bot.messages.send(
msg.chat_id,
'❌ J\'ai besoin des droits admin pour créer des liens d\'invitation.',
)
return
# Private only — interactive keyboard
if msg.chat_type == 'private':
await bot.messages.send(
msg.chat_id,
'De quoi as-tu besoin ?',
reply_markup=InlineKeyboard(inline_keyboard=[[
InlineKeyboardButton(text='📦 Commandes', callback_data='orders'),
InlineKeyboardButton(text='❓ Aide', callback_data='help'),
]]),
)
@bot.on('callback_query')
async def on_callback(cb):
await bot.messages.send(cb.chat_id, f'Tu as choisi : {cb.callback_data}')
asyncio.run(bot.run())
Chat member management
All methods below require the bot to be a member of the conversation.
Methods that modify membership (add_member, ban_member, promote_member) additionally require admin rights.
chats.get_administrators(params) → GetChatAdministratorsResult
from kappelas import GetChatAdministratorsParams
result = await bot.chats.get_administrators(GetChatAdministratorsParams(chat_id=42))
for admin in result.admins:
print(admin.user_id, admin.role) # role is always "admin"
chats.get_member(params) → ChatMemberInfo
Returns the role of a specific member. Raises KappelaError(error_code='NOT_FOUND') if the user is not in the conversation.
from kappelas import GetChatMemberParams
member = await bot.chats.get_member(GetChatMemberParams(chat_id=42, user_id='user-uuid'))
print(member.role) # "admin" | "member"
chats.add_member(params) → AddChatMemberResult
from kappelas import AddChatMemberParams
await bot.chats.add_member(AddChatMemberParams(chat_id=42, user_id='user-uuid'))
chats.ban_member(params) → BanChatMemberResult
Removes (kicks) a user. To remove the bot itself, use leave_chat() instead.
from kappelas import BanChatMemberParams
await bot.chats.ban_member(BanChatMemberParams(chat_id=42, user_id='user-uuid'))
chats.promote_member(params) → PromoteChatMemberResult
from kappelas import PromoteChatMemberParams
# Promote to admin
await bot.chats.promote_member(PromoteChatMemberParams(
chat_id=42, user_id='user-uuid', role='admin'
))
# Demote back to member
await bot.chats.promote_member(PromoteChatMemberParams(
chat_id=42, user_id='user-uuid', role='member'
))
chats.leave_chat(params) → LeaveChatResult
from kappelas import LeaveChatParams
await bot.chats.leave_chat(LeaveChatParams(chat_id=42))
Invite links (admin only)
All invite link methods require admin rights in the target group or channel.
chats.create_invite_link(params) → ChatInviteLink
from kappelas import CreateChatInviteLinkParams
# Permanent link, unlimited uses
link = await bot.chats.create_invite_link(CreateChatInviteLinkParams(chat_id=42))
print(link.url) # "https://kappelas.com/invite/aBcD123xyz"
# Max 5 uses, expires in 24 hours
link = await bot.chats.create_invite_link(CreateChatInviteLinkParams(
chat_id=42, max_uses=5, expires_in='24h'
))
expires_in values: "1h" · "24h" · "7d" · "30d" · "never" (default)
chats.create_single_use_invite_link(params) → ChatInviteLink
Shorthand for create_invite_link with max_uses=1.
link = await bot.chats.create_single_use_invite_link(
CreateChatInviteLinkParams(chat_id=42)
)
chats.get_invite_links(params) → GetChatInviteLinksResult
from kappelas import GetChatInviteLinksParams
result = await bot.chats.get_invite_links(GetChatInviteLinksParams(chat_id=42))
for link in result.invite_links:
max_uses = '∞' if link.max_uses == 0 else str(link.max_uses)
print(f'{link.url} — {link.use_count}/{max_uses} uses')
chats.revoke_invite_link(params) → RevokeChatInviteLinkResult
from kappelas import RevokeChatInviteLinkParams
result = await bot.chats.revoke_invite_link(RevokeChatInviteLinkParams(
chat_id=42, code='aBcD123xyz' # link.code from create_invite_link
))
print(result.revoked) # True
getMyGroups
chats.get_my_groups() → GetMyGroupsResult
Returns every group and channel the bot belongs to, with the bot's role in each. Useful to discover which groups the bot can manage.
result = await bot.chats.get_my_groups()
for group in result.groups:
print(group.chat_id, group.type, group.title, group.bot_role)
# Filter to groups where the bot is admin
admin_groups = [g for g in result.groups if g.bot_role == 'admin']
BotGroupEntry fields:
| Field | Type | Description |
|---|---|---|
chat_id |
int |
Conversation ID |
type |
str |
"group" or "channel" (never "private") |
title |
str | None |
Group or channel name |
participant_count |
int |
Total members (including the bot) |
bot_role |
str |
"member" or "admin" |
communities
Manage communities with a bot (same rights as a community admin). A bot administers a community only if it is admin of that community.
⚠️ Distinct scopes. Being admin of a group attached to a community does not make you admin of the community.
Community.roleis the role in the community.
To make someone (a person OR a bot) a community admin, it's two steps — add as member, then promote:
from kappelas import (
AddCommunityMemberParams, PromoteCommunityMemberParams, GetCommunityParams,
CreateCommunityParams, CreateCommunityInviteLinkParams, CommunityRequestActionParams,
)
# 1) add as member 2) promote (same flow for a user or a bot)
await bot.communities.add_member(AddCommunityMemberParams(community_id=7, user_id='<uuid or bot_user_id>', role='member'))
await bot.communities.promote_member(PromoteCommunityMemberParams(community_id=7, user_id='<uuid>', role='admin'))
# Listing
res = await bot.communities.list()
for c in res.communities:
print(c.id, c.name, '→', c.role) # 'member' | 'admin'
admins = await bot.communities.list_admin() # only where the bot is community admin
detail = await bot.communities.get(GetCommunityParams(community_id=7)) # community + groups + members
# CRUD
await bot.communities.create(CreateCommunityParams(name='Devs', requires_approval=True))
await bot.communities.delete(GetCommunityParams(community_id=7)) # admin
r = await bot.communities.join(GetCommunityParams(community_id=7)) # r.pending if approval-required
# Invite links (admin)
inv = await bot.communities.create_invite_link(CreateCommunityInviteLinkParams(community_id=7, max_uses=1, expires_in='24h'))
await bot.communities.get_invite_links(GetCommunityParams(community_id=7))
await bot.communities.preview_invite(CommunityInviteCodeParams(code='aBcD123'))
await bot.communities.accept_invite(CommunityInviteCodeParams(code='aBcD123')) # bot joins via link
# Join requests (admin, when requires_approval)
reqs = await bot.communities.get_join_requests(GetCommunityParams(community_id=7))
await bot.communities.approve_join_request(CommunityRequestActionParams(community_id=7, request_id=3))
# Group requests + linking groups (admin)
await bot.communities.add_group(AddCommunityGroupParams(community_id=7, conversation_id=42))
Other methods: ban_member, leave, update, revoke_invite_link, reject_join_request,
get_group_requests, approve_group_request, reject_group_request, remove_group.
webhooks
webhooks.set(url, *, secret) → WebhookSetResult
await bot.webhooks.set('https://your-server.com/kappela-webhook')
webhooks.get_info() → WebhookInfo
info = await bot.webhooks.get_info()
# → WebhookInfo(active=True, url='https://…', created_at=1234567890)
webhooks.delete() → WebhookDeleteResult
await bot.webhooks.delete()
# → WebhookDeleteResult(active=False)
profile
profile.get() → BotProfile | UserProfile
profile = await bot.profile.get()
# BotProfile → user_id, username, is_bot=True, about, description, avatar_url
# UserProfile → id, username, nom, is_bot=False, is_premium, avatar_url, …
Keyboards
Three keyboard types can be passed as reply_markup on any send* call.
Comparison
| Inline | Reply | Scroll | |
|---|---|---|---|
| Position | Attached to the message | Below the input bar | Horizontal chips above input |
| Stays after tap | ✅ Yes | ❌ Dismissed | ✅ Yes |
Separate callback_data |
✅ Always | ✅ Yes (long form) | ✅ Yes (long form) |
| URL button | ✅ Yes | ❌ No | ❌ No |
| Layout | 2-D grid list[list] |
2-D grid list[list] |
1-D list list |
Inline keyboard — attached to the message
Buttons stay visible after being tapped. Each button fires a callback_query (callback_data) or opens a URL (url).
from kappelas import InlineKeyboard, InlineKeyboardButton
inline = InlineKeyboard(inline_keyboard=[
[
InlineKeyboardButton(text='✅ Confirmer', callback_data='yes'),
InlineKeyboardButton(text='❌ Annuler', callback_data='no'),
],
[
InlineKeyboardButton(text='🌐 Site web', url='https://kappelas.com'),
],
])
Reply keyboard — shown below the input bar
Dismissed after the user taps a button. Buttons trigger a callback_query.
Short form — label and callback value are identical:
from kappelas import ReplyKeyboard, ReplyKeyboardButton
reply = ReplyKeyboard(keyboard=[
[ReplyKeyboardButton('📦 Mes commandes'), ReplyKeyboardButton('❓ Aide')],
[ReplyKeyboardButton('🔙 Retour')],
])
You can also use plain strings as a shorthand when label and callback are the same:
reply = ReplyKeyboard(keyboard=[
['📦 Mes commandes', '❓ Aide'],
['🔙 Retour'],
])
Long form — separate label and callback value:
reply = ReplyKeyboard(keyboard=[
[
ReplyKeyboardButton(text='✅ Confirmer', callback_data='confirm_yes'),
ReplyKeyboardButton(text='❌ Annuler', callback_data='confirm_no'),
],
[ReplyKeyboardButton(text='↩ Retour', callback_data='cancel')],
])
You can mix short and long buttons in the same grid:
reply = ReplyKeyboard(keyboard=[[
ReplyKeyboardButton(text='✅ Confirmer', callback_data='confirm'),
ReplyKeyboardButton(text='❓ Aide'), # short form — callback_data = text
]])
Scroll keyboard — horizontal scrollable chips
A single row of chips, always visible above the input bar.
from kappelas import ScrollKeyboard, ScrollKeyboardButton
# Short form — plain strings or ReplyKeyboardButton without callback_data
scroll = ScrollKeyboard(scroll_keyboard=[
'Petit', 'Moyen', 'Grand', 'XL',
])
# Long form — emoji label, clean callback value
scroll = ScrollKeyboard(scroll_keyboard=[
ScrollKeyboardButton(text='📦 Commandes', callback_data='menu_orders'),
ScrollKeyboardButton(text='❓ Aide', callback_data='menu_help'),
ScrollKeyboardButton(text='⚙️ Réglages', callback_data='menu_settings'),
])
await bot.messages.send(
42,
'Choisis une option :',
reply_markup=inline, # or reply, or scroll
)
Full example — all three in one bot
import asyncio
from kappelas import (
KappelaBot,
InlineKeyboard, InlineKeyboardButton,
ReplyKeyboard, ReplyKeyboardButton,
ScrollKeyboard, ScrollKeyboardButton,
)
bot = KappelaBot('YOUR_BOT_TOKEN')
@bot.on('message')
async def on_message(msg):
if msg.text != '/start':
return
# Persistent navigation chips
await bot.messages.send(
msg.chat_id,
'Bienvenue ! De quoi as-tu besoin ?',
reply_markup=ScrollKeyboard(scroll_keyboard=[
ScrollKeyboardButton(text='📦 Commandes', callback_data='menu_orders'),
ScrollKeyboardButton(text='❓ Aide', callback_data='menu_help'),
]),
)
@bot.on('callback_query')
async def on_callback(cb):
if cb.callback_data == 'menu_orders':
# Inline confirm/cancel buttons
await bot.messages.send(
cb.chat_id,
'Confirmer ta dernière commande ?',
reply_markup=InlineKeyboard(inline_keyboard=[[
InlineKeyboardButton(text='✅ Confirmer', callback_data='order_confirm'),
InlineKeyboardButton(text='❌ Annuler', callback_data='order_cancel'),
]]),
)
elif cb.callback_data == 'menu_help':
# Reply keyboard for topic selection
await bot.messages.send(
cb.chat_id,
'Quel sujet ?',
reply_markup=ReplyKeyboard(keyboard=[
[
ReplyKeyboardButton(text='💳 Facturation', callback_data='help_billing'),
ReplyKeyboardButton(text='🚚 Livraison', callback_data='help_delivery'),
],
[ReplyKeyboardButton(text='↩ Retour au menu', callback_data='menu_back')],
]),
)
asyncio.run(bot.run())
Text formatting
Kappela renders a WhatsApp/Telegram-style subset of Markdown in every message bubble — bot messages, group messages, and private chat messages. All formatting is applied client-side by the Android app; you only need to send the correct markup in the text or caption field.
Inline styles
| Syntax | Result |
|---|---|
**bold** or *bold* |
Bold |
__italic__ or _italic_ |
Italic |
~strikethrough~ |
|
`inline code` |
Monospace with a tinted background |
await bot.messages.send(
42,
'Commande *confirmée* ✅\nTotal : **24 990 FCFA**\nRef : `ORD-2024-001`',
)
Block code
Triple backticks render as a block code card — only when placed on their own line.
| Position | Rendered as |
|---|---|
`code` inline |
Monospace with tinted background |
```code``` on its own line |
Full-width card + copy button |
# Inline — stays in the text flow
await bot.messages.send(42, 'Ta ref est `ORD-2024-001` — garde-la précieusement.')
# Block — must be on its own line for the card to appear
await bot.messages.send(42, 'Ta clé API :\n```\nsk_live_abc123xyz\n```')
The code card collapses to a single line with an ellipsis if the content is too long. Tapping anywhere on the card copies the content to the clipboard.
Blockquote / citation
Prefix a line with > to render it as a citation banner (a ┃ bar on the left, italic, slightly faded):
await bot.messages.send(
42,
'> Question originale ici\n\nVoici ta réponse.',
)
You can combine blockquotes with
reply_to_id— usereply_to_idwhen you want to quote a specific existing message (the app shows a reply banner); use>when you want to render a quote inline within the text itself.
Mentions and commands
@username and /command are auto-detected and rendered as tappable blue links:
# Mention a user by their username
await bot.messages.send(42, 'Merci @arnell, ta commande est prête !')
# Send a command hint
await bot.messages.send(42, 'Tape /help pour voir toutes les commandes disponibles.')
Protection rule:
@and/inside URLs are never formatted.@buy_something_botis treated as a mention, not asbuy+_something_bot(italic).
Auto-detected links
The renderer automatically makes the following clickable without any markup:
| Pattern | Behaviour |
|---|---|
https://… or http://… |
Opens in the in-app browser |
domain.com, domain.io, domain.fr … |
Prefixed with https:// and opened |
email@example.com |
Opens the mail app |
+229 01 62 86 15 71, (229) 0162-861571 |
Opens the dialler |
await bot.messages.send(
42,
'Visitez kappelas.com ou contactez-nous à support@kappelas.com',
)
Supported domain extensions — only the following TLDs are auto-linked:
comorgnetfriodevcomeapptechinfobizxyzeuukderutvccggaibechcaCountry codes like
.bj,.sn,.ciare not auto-detected — use a fullhttps://URL instead:https://kappelas.bj.
Phone format — any sequence of 8+ digits is detected, with spaces, dashes, and parentheses allowed:
+229 01 62 86 15 71,+2290162861571,(229) 0162-861571all open the dialler.
Combining formats
All inline styles can be combined freely:
lines = [
'🛒 *Récapitulatif de commande*',
'',
'> Widget A × 2',
'',
'Total : **49 980 FCFA**',
'Statut : `CONFIRMÉ`',
'',
'Des questions ? Contactez support@kappelas.com ou tapez /help',
]
await bot.messages.send(42, '\n'.join(lines))
Renders as:
🛒 Récapitulatif de commande ← bold
┃ Widget A × 2 ← blockquote (italic, faded)
Total : 49 980 FCFA ← bold amount
Statut : CONFIRMÉ ← monospace badge
Des questions ? Contactez support@kappelas.com ou tapez /help
← email and /help are tappable
Error handling
All API errors raise KappelaError with structured fields:
from kappelas import KappelaError
try:
await bot.messages.send(999, 'Hi')
except KappelaError as e:
e.error_code # 'NOT_FOUND'
e.status # 404
e.hint # 'The requested resource does not exist.'
e.solutions # ['Check the ID is correct', …]
e.request_id # include when contacting support
print(e) # full formatted block with hints and docs link
Error codes
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED |
401 | Token or API key invalid / expired |
FORBIDDEN |
403 | Missing permission or role (bot not in chat, not admin…) |
NOT_FOUND |
404 | Resource does not exist |
MISSING_FIELD |
400 | Required parameter missing |
INVALID_FIELD |
400 | Parameter has wrong type or format |
CONFLICT |
409 | Resource already exists |
METHOD_NOT_ALLOWED |
405 | Wrong HTTP method |
INVALID_PATH |
404 | API path does not exist |
INTERNAL_ERROR |
500 | Unexpected server error |
SERVICE_UNAVAILABLE |
503 | Service temporarily down |
UPSTREAM_ERROR |
502 | Upstream service error |
FORBIDDEN(notNOT_FOUND) is returned when the bot tries to send a message to a chat it has never joined.
File input
Media methods accept files in several forms:
| Type | Example |
|---|---|
bytes |
open('img.jpg', 'rb').read() |
IO[bytes] |
open('img.jpg', 'rb') |
FileData |
FileData(data=b'…', filename='img.jpg', content_type='image/jpeg') |
from kappelas import FileData
# bytes
await bot.messages.send_photo(chat_id, open('photo.jpg', 'rb').read())
# file object
with open('photo.jpg', 'rb') as f:
await bot.messages.send_photo(chat_id, f)
# explicit metadata — recommended for documents and audio
await bot.messages.send_document(
chat_id,
FileData(data=pdf_bytes, filename='report.pdf', content_type='application/pdf'),
caption='Your monthly report',
)
License
MIT © Arnel LAWSON
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kappelas_sdk-0.4.0.tar.gz.
File metadata
- Download URL: kappelas_sdk-0.4.0.tar.gz
- Upload date:
- Size: 55.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f934686b5b1fbcfbeac4ca0b2e0784a6290aa5e5087dedf91dab42084ef6a593
|
|
| MD5 |
1aa76fc95c3bf46043a65629e8cc3c90
|
|
| BLAKE2b-256 |
07eae6a66766d0c88bc893d8d2be6940e16ab63d93207b21a807cd2c32a975d0
|
File details
Details for the file kappelas_sdk-0.4.0-py3-none-any.whl.
File metadata
- Download URL: kappelas_sdk-0.4.0-py3-none-any.whl
- Upload date:
- Size: 43.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03fc9c94189b0e40fca5fee85d9425a937185a8bea84520a0b0c44cba0231a8e
|
|
| MD5 |
02dd31d865cdb0a6fe7baf801a659fda
|
|
| BLAKE2b-256 |
ccbedca77681d164afc1eacf0748bfa4843e6d12dd14e21327068970705dc6cc
|