Skip to main content

Official Kappela SDK for Python — build bots and personal automations

Project description

kappelas-sdk

PyPI version Python version License: MIT GitHub

Official Python SDK for the Kappela messaging platform.
Build bots and personal automations — send messages, handle events, manage chats.


Table of contents


Prerequisites

You need a bot token from BotMother, the official Kappela bot manager.

  1. Open Kappela and start a conversation with BotMother
  2. Follow the instructions to create a bot
  3. 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.messages.send(msg.chat_id, 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())

Async-first design

Every method that touches the network is a coroutine — await it:

result = await bot.messages.send(chat_id, 'Hello!')

Use asyncio.run() as the entry point for standalone scripts, or integrate into any async framework (FastAPI, aiohttp, etc.).

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.messages.send(msg.chat_id, f'Echo: {msg.text}')

        await bot.run()

asyncio.run(main())

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.

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.messages.send(msg.chat_id, 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

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_nom       # 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.

Decorator vs method usage

# Decorator (persistent)
@bot.on('message')
async def on_message(msg): ...

# Method call (persistent)
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(): ...

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 — reply to a message
    delete_previous = False,       # optional
    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:
    await bot.messages.send_photo(chat_id, f, caption='Check this out!')
# → SendMediaResult(message_id=..., created_at=..., media_id=...)

messages.send_video / send_document / send_audioSendMediaResult

Same signature — replace the file parameter (video, document, audio) with your file.

messages.send_carousel(chat_id, carousel, *, text, quick_reply_buttons)SendCarouselResult

from kappelas import CarouselCard

await bot.messages.send_carousel(
    chat_id  = 42,
    text     = 'Pick a product:',
    carousel = [
        CarouselCard(id='p1', title='Widget A', subtitle='$9.99',  button_text='Buy'),
        CarouselCard(id='p2', title='Widget B', subtitle='$19.99', button_text='Buy'),
    ],
    quick_reply_buttons=['See more', 'Cancel'],
)

messages.edit(chat_id, message_id, *, new_text, new_extra_data)EditMessageResult

# Edit text
await bot.messages.edit(42, 123, new_text='Updated!')

# Edit inline keyboard only (no text change)
await bot.messages.edit(42, 123, new_extra_data={
    'inline_keyboard': [[{'text': 'Done ✅', 'callback_data': 'done'}]]
})
# → EditMessageResult(edited=True, message_id=...)

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)

chats

chats.list(*, limit, offset)ChatsResult

page = await bot.chats.list(limit=20, offset=0)
print(page.chats, page.has_more)

chats.iterate(page_size?)AsyncGenerator[Chat]

async for chat in bot.chats.iterate():
    print(chat.chat_id, chat.title, chat.type)

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=...)

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 types of keyboard can be passed as reply_markup on any send* call:

from kappelas import InlineKeyboard, InlineKeyboardButton, ReplyKeyboard, ScrollKeyboard

# Inline buttons — attached to the message
inline = InlineKeyboard(inline_keyboard=[
    [
        InlineKeyboardButton(text='Yes', callback_data='yes'),
        InlineKeyboardButton(text='No',  callback_data='no'),
    ],
    [
        InlineKeyboardButton(text='Open website', url='https://kappelas.com'),
    ],
])

# Reply keyboard — shown below the input bar
reply = ReplyKeyboard(keyboard=[
    ['Option A', 'Option B'],
    ['Cancel'],
])

# Scroll keyboard — horizontal scrollable chips
scroll = ScrollKeyboard(scroll_keyboard=['Small', 'Medium', 'Large'])

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

Error codes

Code HTTP Meaning
UNAUTHORIZED 401 Token or API key invalid / expired
FORBIDDEN 403 Missing permission or role
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

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
await bot.messages.send_document(
    chat_id,
    FileData(data=pdf_bytes, filename='report.pdf', content_type='application/pdf'),
)

License

MIT © Arnel LAWSON

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kappelas_sdk-0.1.2.tar.gz (21.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kappelas_sdk-0.1.2-py3-none-any.whl (26.4 kB view details)

Uploaded Python 3

File details

Details for the file kappelas_sdk-0.1.2.tar.gz.

File metadata

  • Download URL: kappelas_sdk-0.1.2.tar.gz
  • Upload date:
  • Size: 21.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for kappelas_sdk-0.1.2.tar.gz
Algorithm Hash digest
SHA256 62b9a8519e9bf8cbbf32b13e6941638ffd944ea336784b9be45871bc29497ca8
MD5 29c88afc0ade9e4db9c2ca6f6372cad8
BLAKE2b-256 87d4921c9bf3bbb6423cd775ab234dadd40d2cb457b5e8b80cda0ca4326dc60b

See more details on using hashes here.

File details

Details for the file kappelas_sdk-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: kappelas_sdk-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 26.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.11

File hashes

Hashes for kappelas_sdk-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 ae0c51be3ee8faa7a0213f3596a41d99755c537fef89a85d1713e202c07a927a
MD5 fabe211e565124b4adfb11661d402477
BLAKE2b-256 8ac09beb3e828639189e6718bea3933d0c53081603823e4397fe08e6b32866e6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page