Skip to main content

blackbirdtalk

The official Python library for Blackbird bots.

pip install blackbirdtalk

Python 3.11 or newer. One dependency (httpx).

from blackbirdtalk import Bot, Button

bot = Bot()  # reads BLACKBIRD_TOKEN; or Bot("bb_...")

@bot.command("start")
async def start(msg):
    await msg.answer("Welcome!", buttons=[[Button("Pay ₹49", callback="pay_49", style="main", icon="card")]])

@bot.button("pay_49")
async def pay(tap):
    await tap.answer("Payment started")

bot.run()

Get the token in the Blackbird app: Settings › Bot Studio › your bot › Overview. Set it in your terminal so it stays out of your code:

macOS / Linux export BLACKBIRD_TOKEN=bb_...
Windows PowerShell $env:BLACKBIRD_TOKEN="bb_..."
Windows cmd set BLACKBIRD_TOKEN=bb_...

Or start from a ready project: blackbirdtalk new mybot. Check a token, its webhook and commands any time with blackbirdtalk check.

Contents

Handlers

The first handler that matches an update runs, in the order you wrote them. Messages from one chat are handled one at a time, in order; different chats run in parallel.

@bot.command("quiz", "q")                 # /quiz physics -> msg.args == "physics"
async def quiz(msg): ...

@bot.message(regex=r"(\d+) apples")       # msg.match[1] == "3"
async def apples(msg): ...

@bot.message(text=["hi", "hello"])
async def hi(msg): ...

@bot.message(kind="image")                # image, video, voice, doc, text
async def photo(msg): ...

@bot.message(chat="group", when=lambda m: len(m.text or "") > 500)
async def long_text(msg): ...

@bot.message()                            # everything else
async def fallback(msg): ...

@bot.on_error
async def oops(error, update):
    print("handler failed:", error)

A message has msg.answer() (same chat), msg.reply() (quoting it), msg.answer_file(), msg.edit(), msg.delete(), msg.pin(), msg.typing(), msg.ask() and msg.download().

Buttons

Blackbird buttons have styles, icons, colours, sizes, a confirm step and copy-to-clipboard. The library checks every rule the server checks before it sends, so a mistake fails on your machine with the button's row and position.

from blackbirdtalk import Button, Buttons

menu = Buttons(
    [Button("Buy ₹49", callback="buy", style="success", icon="card", confirm="Pay ₹49?")],
    [Button("Coupon", copy="SAVE20", icon="copy"), Button("Site", url="https://example.com", icon="link")],
    [Button("Brand colour", callback="x", color="#5B3FD9", size="pill")],
)
print(menu.preview())          # see it in the terminal
await msg.answer("Pick one", buttons=menu)

@bot.button(prefix="buy")      # also: exact data, a list, regex=, when=
async def buy(tap):
    await tap.answer("Done!")              # a short note (alert=True for a dialog)
    await tap.edit("Paid ✓", buttons=[])   # change the message in place

style: main, normal, success, warning, danger. size: pill, normal, full. icon: one of 40 (blackbirdtalk.BUTTON_ICONS). Up to 8 rows, 8 per row. Taps you don't answer are answered for you, so the spinner never hangs.

Keyboard buttons (under the text box) send their text; they can also ask for the person's location or contact:

from blackbirdtalk import Keyboard, RemoveKeyboard

await msg.answer("When?", keyboard=Keyboard([["Today", "Tomorrow"],
                                             [Button("Share location", request="location")]], one_time=True))
await msg.answer("Thanks", reply_markup=RemoveKeyboard())

Questions with ask()

Write a conversation top to bottom. Each person's answers come back to their own question, even with many people at once.

from blackbirdtalk import AskTimeout

@bot.command("register")
async def register(msg):
    try:
        name = await msg.ask("Your name?", placeholder="Full name")
        tap = await msg.ask("Class?", buttons=[[Button("10", callback="10"), Button("12", callback="12")]])
    except AskTimeout:                    # default 5 minutes; timeout=...
        return await msg.answer("Stopped. Send /register again.")
    await msg.answer(f"Saved {name.text}, class {tap.data}")

A text answer comes back as a Message, a tap as a Callback.

Files

await msg.answer_file("report.pdf", caption="Your report")        # a path
await msg.answer_file(InputFile(data, filename="chart.png"))      # bytes made in memory
await bot.send_document(chat_id, file_id=old.file.file_id)        # send again, no upload

@bot.message(kind="doc")
async def got(msg):
    data = await msg.download()            # bytes; or msg.download("saved.pdf")

Groups

Add the bot to a group and give it the powers it needs (approve, remove, delete, pin ...); msg.chat.my_powers lists what it has.

@bot.on_join_request()
async def check(req):
    await (req.approve() if "blackbird" in (req.answer or "").lower() else req.decline())

@bot.on_member(lambda m: m.event == "joined")
async def welcome(change):
    await change.chat.send("Welcome " + ", ".join(u.name for u in change.users))

@bot.on_my_membership(lambda m: m.status == "added")
async def added(change): ...

Private bots

Choose who can use the bot (everyone, owner, or chosen people) from the code or from Bot Studio. People can ask for access; you decide.

await bot.set_access(audience="chosen", message="For our class only", allow=[user_id])

@bot.on_access_request()
async def asked(req):
    await req.approve()        # or req.decline()

Webhooks

Polling (bot.run()) needs nothing but the token. On a server with HTTPS you can use a webhook instead:

app = bot.webhook_app(secret="long-random-text", path="/hook")   # ASGI: uvicorn mybot:app
await bot.set_webhook("https://your.domain/hook", secret_token="long-random-text")   # once

FastAPI, aiohttp or anything else:

@api.post("/hook")
async def hook(request: Request):
    status = await bot.handle_webhook(await request.body(), request.headers, secret=SECRET)
    return Response(status_code=status)

SyncBot.wsgi_app() does the same for gunicorn, Flask and Django. The secret header is checked in constant time; requests without it get 401.

No async: SyncBot

Same handlers and helpers with plain functions:

from blackbirdtalk import SyncBot

bot = SyncBot()

@bot.command("start")
def start(msg):
    msg.answer("Hi!")

bot.run()

Testing your bot

FakeBlackbird stands in for the server: no token, no network.

from blackbirdtalk.testing import FakeBlackbird

async def test_pay():
    fake = FakeBlackbird(bot)
    await fake.send("/start")
    assert fake.last("sendMessage")["text"] == "Welcome!"
    await fake.tap(fake.last_message(), "pay_49")
    assert fake.last("answerCallback")["text"] == "Payment started"

Errors and retries

Every error is a BlackbirdError with .code, .description and .method: BadRequest, Unauthorized (token wrong or replaced), Forbidden (the person hasn't started the bot, or blocked it), NotFound, Conflict (another copy is polling with this token), RateLimited (.retry_after), ServerError, NetworkError, and MarkupError for a button mistake caught before sending.

"Too many requests" is waited out by itself (up to max_retry_wait), and server errors and lost connections are retried with back-off. Retries never send a message twice: each send carries an id the server uses to drop repeats.

Every method

All 25 Bot API methods are on the bot with snake_case names (send_message, edit_message, answer_callback, set_commands, get_chat, remove_member, set_access ...), typed, with docs in your editor. Telegram-style names are accepted by the server too, so code ported from a Telegram bot mostly works.

New fields from the server show up on objects at once (obj["new_field"], or obj.to_dict()), and a method or parameter newer than your library can still be used: await bot.send_message(chat_id, "hi", new_param=1). The full reference: https://blackbirdtalk.com/bots.

Updates and versions

The library prints one line a day when a newer version is out (turn it off with Bot(check_updates=False) or BLACKBIRD_NO_UPDATE_CHECK=1). Update with pip install -U blackbirdtalk. Versions: a new minor (1.2.0) adds things, a patch (1.2.1) fixes things; your code keeps working across both. The JavaScript library @blackbirdtalk/bot has the same version number and features.

MIT licence.

Metadata

Release files for blackbirdtalk 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for blackbirdtalk 1.1.0
File Size Uploaded Provenance Actions
blackbirdtalk-1.1.0.tar.gz 52.3 kB With provenance Details for blackbirdtalk-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for blackbirdtalk 1.1.0
File Interpreter ABI Platform Actions
blackbirdtalk-1.1.0-py3-none-any.whl Python 3 none any Details for blackbirdtalk-1.1.0-py3-none-any.whl

Total release size: 100.4 kB

Release files / blackbirdtalk-1.1.0.tar.gz

Download URL blackbirdtalk-1.1.0.tar.gz
Size 52.3 kB
Tags Source
SHA-256 checksum
How to use checksums
70926360656d34f4d583adeaf6edd511c1c8257b6a9ca9a9be7f142279b2cbcf
BLAKE2b-256 checksum
How to use checksums
a4b23454939ee5eb0b6517a17b8cf9b9f7989debcdc5b5137187d91358b74dce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Release files / blackbirdtalk-1.1.0-py3-none-any.whl

Download URL blackbirdtalk-1.1.0-py3-none-any.whl
Size 48.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
85ddad5ea822792fe26385d2e7cd0eed87b37a96514f65e00efd47ddb57f981f
BLAKE2b-256 checksum
How to use checksums
6ec64446ee62ec1d28dd5ace45380698cbcdbce40cf27c1b9dbe7eac8b149193
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Security for blackbirdtalk 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for blackbirdtalk-1.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page