Luma Bot SDK (luma_bot)
A Python SDK for creating bots on Luma.
Install
pip install luma-bot
Minimal bot
import os
import luma_bot
from luma_bot.ext import commands
bot = commands.Bot()
@bot.event
async def on_ready():
print(f"Logged in as {bot.user}")
@bot.command(description="Say hello")
async def hello(ctx: luma_bot.Context):
await ctx.send(f"Hello, {ctx.author.display_name}!")
bot.run(os.environ["LUMA_BOT_TOKEN"])
commands.Bot() automatically connects to the official Luma API and realtime gateway. Normal bot developers do not configure a base URL or LUMA_URL; the endpoint is built into the SDK. Only self-hosted/test deployments need to pass base_url= explicitly.
Quick start
Create a bot in Luma → Developer Console, copy the token shown once, then:
python -m pip install --upgrade luma-bot
export LUMA_BOT_TOKEN="luma_..." # Linux/macOS
# Windows PowerShell: $env:LUMA_BOT_TOKEN="luma_..."
python your_bot.py
Your code only needs commands.Bot(); the official Luma service address is already part of the SDK.
Included examples
examples/minimal_bot.py— smallest token + slash-command starter.examples/basic_bot.py— commands, events, timers, UI, server data, voice, speech recognition, and TTS.examples/components_bot.py— buttons, views, and rich cards.examples/voice_assistant.py— speech transcript → TTS assistant.examples/voice_bot.py— voice join/playback basics.
Every normal example uses commands.Bot() with the official Luma connection built in.
Authentication
bot.run(token) validates the token through GET /api/v1/bot/me before opening the Socket.IO gateway. Send bot tokens as Authorization: Bearer luma_...; the platform also accepts the older Bot prefix while installations upgrade. User IDs, application IDs, account sessions, revoked tokens, disabled bots, and banned bots are rejected.
Supported bot REST API
The SDK is intentionally limited to routes protected by the botToken security scheme in Luma OpenAPI:
| Method | Route | SDK behavior |
|---|---|---|
GET |
/api/v1/bot/me |
Token validation and bot identity |
PATCH |
/api/v1/bot/presence |
bot.change_presence() |
PUT |
/api/v1/bot/commands |
Automatic slash-command synchronization |
POST |
/api/v1/bot/channels/{channelId}/messages |
ctx.send() and bot.send_message() |
POST |
/api/v1/bot/messages/{messageId}/reactions |
ctx.react() and bot.add_reaction() |
DELETE |
/api/v1/bot/messages/{messageId}/reactions |
ctx.remove_reaction() and bot.remove_reaction() |
GET |
/api/v1/bot/communities |
bot.fetch_communities() (installed servers only) |
GET |
/api/v1/bot/communities/{communityId} |
bot.fetch_community() (installed servers only) |
GET |
/api/v1/bot/communities/{communityId}/members |
bot.fetch_members() / bot.fetch_users() |
GET |
/api/v1/bot/communities/{communityId}/members/{memberId} |
bot.fetch_member() |
GET |
/api/v1/bot/communities/{communityId}/users/{userId} |
bot.fetch_user() (only in that server) |
GET |
/api/v1/bot/communities/{communityId}/channels |
bot.fetch_channels() |
GET |
/api/v1/bot/communities/{communityId}/roles |
bot.fetch_roles() |
POST |
/api/v1/bot/communities/{communityId}/invites |
bot.create_invite() |
PUT / DELETE |
/api/v1/bot/communities/{communityId}/members/{memberId}/roles/{roleId} |
ctx.add_role(), bot.add_role(), bot.remove_role() |
PATCH |
/api/v1/bot/communities/{communityId}/members/{memberId}/timeout |
ctx.timeout() / bot.timeout_member() |
DELETE |
/api/v1/bot/communities/{communityId}/members/{memberId} |
ctx.kick() / bot.kick_member() |
PUT |
/api/v1/bot/communities/{communityId}/members/{memberId}/ban |
ctx.ban() / bot.ban_member() |
Public diagnostics are also available:
live = await bot.is_service_live()
ready = await bot.is_service_ready()
The SDK does not use user-session, CSRF-protected, developer-portal, or admin routes.
Reactions
@bot.command(description="Celebrate this message")
async def celebrate(ctx):
await ctx.react("🎉")
Bots can only react in servers where they are installed and have the Add Reactions permission.
Commands
@bot.command(description="Add two numbers")
@commands.describe(first="First number", second="Second number")
async def add(ctx, first: int, second: int):
await ctx.send(str(first + second))
Type annotations become command option types:
str->stringint->integerfloat->numberbool->boolean- parameters with defaults are optional
str | Noneis optional
discord.py-style command tree
Luma uses slash commands, so bot.tree is the familiar way to declare
and synchronise them. commands.Bot.command() and bot.tree.command() both
register the same command type; automatic syncing is enabled by default.
from luma_bot.ext import commands
bot = commands.Bot()
@bot.tree.command(description="Check whether the bot is online")
async def ping(ctx):
await ctx.reply("Pong!")
@bot.event
async def on_ready():
# Optional: use this when commands are changed while the process is running.
await bot.tree.sync()
The client also provides bot.add_command(), bot.get_command(), and
bot.remove_command() for dynamic command registration.
Typing indicators
Make the bot visibly type while it is preparing a response. A single await shows a short indicator; an async context keeps it visible until the work is done and clears it immediately afterwards.
@bot.tree.command(description="Show server information")
async def server(ctx):
async with ctx.typing():
data = await bot.fetch_community(ctx.community.id)
await ctx.reply(f"{data['community']['name']} is ready.")
# Or show a brief indicator from any background task:
await bot.typing(channel_id)
The Luma client groups simultaneous typers into a clear “Multiple people are typing…” line, including bots.
UI components
from luma_bot import ui
class Menu(ui.View):
@ui.button(label="Click", custom_id="menu:click", style=ui.ButtonStyle.primary)
async def click(self, interaction, button):
await interaction.respond("Clicked!")
@bot.command(description="Show UI")
async def menu(ctx):
await ctx.send("Choose:", view=Menu())
Use bot.add_view(Menu()) during on_ready to restore a reusable view at
startup. In callbacks, both await interaction.respond("…") and the familiar
await interaction.response.send_message("…") are supported.
Rich embeds, images, video, and player previews
Use ui.Embed for an announcement or profile-style card, then combine it with
normal interactive controls or external ui.LinkButton actions. Images, direct
MP4/WebM/Ogg video, and allowlisted YouTube, Vimeo, or Twitch player URLs are
rendered safely in the client.
from luma_bot import ui
@bot.command(description="Show the community profile card")
async def profile(ctx):
card = ui.Embed(
title="BlackBull310",
description="Community profile and live status.",
color="#2f80ed",
image_url="https://cdn.example.com/profile-card.png",
image_alt="BlackBull310 profile card",
footer="Luma profile service",
).add_field("Level", "5 / 100", inline=True).add_field("Created", "2021-05-18", inline=True)
links = ui.View().add_item(ui.LinkButton(label="Twitch", url="https://twitch.tv/example"))
await ctx.send("", components=[card.to_dict()], view=links)
For an iframe player, set iframe_url to an HTTPS YouTube, Vimeo, or Twitch
embed URL. For uploaded platform media, image_url, thumbnail_url,
video_url, and video_poster_url can use a Luma /uploads/... path.
Replying to messages
User replies and bot replies share the same quoted-message display. In a bot
command, call ctx.reply() to quote the message that invoked the command.
Interaction responses automatically quote the message containing the clicked
button or select menu.
@bot.command(description="Reply in context")
async def hello(ctx):
await ctx.reply("Hey! I am replying directly to your message.")
Voice gateway
await bot.voice.join(channel_id)
await bot.voice.play(channel_id, audio_url, title="Music")
await bot.voice.say(channel_id, "Hello from Luma!", language="en-US")
await bot.voice.leave(channel_id)
Voice operations use the realtime gateway rather than a REST route. audio_url
must be an HTTPS URL or a Luma /uploads/... file that each listener's
browser can reach. For TTS, Luma's clients use their native speech engine, so
there is no temporary MP3 to host.
Voice recognition / voice assistants
Enable Voice recognition and Text-to-Speech for the application in
Developer Console, then add the voice_transcript event intent. When the bot is
inside a voice or stage channel, members see a clear bot listening indicator
and can turn voice-bot recognition off locally. Luma forwards short consented PCM speech utterances to the connected bot SDK; luma_bot 1.8+ transcribes them and dispatches on_voice_transcript. Luma does not persist the recognition audio.
@bot.event
async def on_voice_transcript(event):
if not event.get("final"):
return
heard = event.get("text", "").strip().lower()
channel_id = event["channel"]["id"]
speaker = event["speaker"]["display_name"]
if heard == "hello":
await bot.voice.say(channel_id, f"Hello {speaker}!")
Call await bot.voice.listen(channel_id, enabled=False) to pause transcript
delivery without leaving the channel.
Events
@bot.event
async def on_connect():
print("Gateway connected")
@bot.event
async def on_ready():
print("Bot ready")
@bot.event
async def on_command_error(ctx, error):
await ctx.send(f"Error: {error}")
@bot.event
async def on_interaction(interaction):
print(interaction.custom_id)
Installed bots also receive message events without polling:
@bot.event
async def on_message(message):
if message.author.bot:
return
print(message.channel_id, message.author.username, message.content)
For future platform events not yet represented by a typed callback, use
on_raw_event(event_type, payload). The SDK dispatches on_message_create in
addition to on_message for message-created events.
For background tasks that must not run before the gateway is ready, use the same lifecycle pattern as discord.py:
@my_task.before_loop
async def wait_for_gateway():
await bot.wait_until_ready()
Timers and scheduled jobs
from luma_bot import tasks
@tasks.loop(minutes=15)
async def refresh_status():
await bot.change_presence(
status="online",
activity_type="watching",
activity_text="new community activity",
activity_color="#725cff",
)
@bot.event
async def on_ready():
if not refresh_status.is_running():
refresh_status.start()
Set status to online, idle, dnd, invisible, or offline. Supported
activity types are playing, streaming, listening, watching, and
custom.
Server data and roles
Bots can only inspect a server where they are installed. They cannot read data from other servers, even if they know an ID.
@bot.command(description="Show this server's member total")
async def stats(ctx):
server = await bot.fetch_community(ctx.community.id)
await ctx.reply(f"{server['stats']['members']} members")
@bot.command(description="Give a member a role")
async def give_role(ctx, member_id: str, role_id: str):
await ctx.add_role(member_id, role_id)
await ctx.reply("Role updated.")
ctx.add_role() requires the bot to have the server's Manage Roles
permission. fetch_users() returns ordinary dictionaries, so integrations can
use for user in await bot.fetch_users(server_id): print(user["username"]).
Invites and moderation
Bots can create an invite when they have Create Invites:
invite = await bot.create_invite(ctx.community.id, expires_in_hours=24, max_uses=25)
await ctx.reply(f"Invite created: {invite['token']}")
timeout_member, kick_member, ban_member, and role updates require the
matching server permission. They can only target members and roles below the
bot's highest assigned role; bots cannot act on the server owner or themselves.
Sign in with Luma
Create an OAuth application in Developer Console, add each exact callback URL, and keep the client secret only on your website's server. Luma uses OAuth 2.1 authorization code flow with mandatory PKCE (S256), short-lived access tokens, rotating refresh tokens, and a consent page.
from luma_bot import OAuthClient
oauth = OAuthClient(
client_id="luma_client_...",
client_secret="luma_client_secret_...",
redirect_uri="https://your-app.example/auth/luma/callback",
)
# Start login: store request.state and request.code_verifier in the user's
# temporary server-side session, then redirect them to request.url.
request = oauth.create_authorization_request(scopes=("identity", "email"))
# Callback: verify `state` first, then exchange the code using the verifier.
tokens = await oauth.exchange_code(code, request.code_verifier)
identity = await oauth.fetch_identity(tokens.access_token)
print(identity["display_name"])
Use await oauth.refresh(tokens.refresh_token) to renew an expired access
token and await oauth.revoke(tokens.refresh_token) when a user disconnects
their Luma account.
Documentation: https://luma.blackbullnetwork.eu/app/developers?tab=docs
Complete Luma developer API
This SDK README documents the 19 bot-token REST operations the Python bot runtime can call directly. Luma's complete supported developer contract contains 56 REST operations and is documented in the main project at:
docs/DEVELOPER_API.md— human-readable route, authentication, gateway, webhook, OAuth, App Directory, and voice/TTS reference.docs/openapi.yaml— machine-readable OpenAPI 3.1 contract (v0.8.0).- Developer Console → API Reference — searchable in-app copy of the same supported route catalog.
- Help & Support → API documentation — searchable platform developer reference.
The additional operations cover OAuth, developer application management, App Directory installation/reviews, incoming webhooks, Luma client bot interactions, and service health. They are not exposed as ordinary Bot methods when they require a human session, OAuth client credential, or secret webhook URL.
After changing backend developer routes, run:
python tools/audit_developer_docs.py
The audit fails when the backend-supported developer contract, Developer Console route catalog, or OpenAPI developer markers drift apart.
Voice transcript diagnostics
The bundled examples/basic_bot.py and examples/voice_assistant.py print every final speech transcript to the bot terminal. A successful event looks like:
[VOICE] #General Voice | Robin: hello luma (language=en-US, confidence=92%)
Voice gateway methods (join, listen, say, play, and leave) wait for a server acknowledgement. Configuration errors therefore raise immediately instead of failing silently.
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 luma_bot-1.8.0.tar.gz.
File metadata
- Download URL: luma_bot-1.8.0.tar.gz
- Upload date:
- Size: 72.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0832d3033eff722a71bb8de3f28dc86293c1ceb20e9d55c778db89cd5dc7e0c3
|
|
| MD5 |
f018441379e2983e287242eb4a6eed65
|
|
| BLAKE2b-256 |
97c667647ce3fb66c697d65accb12346fc7368ad8376e67813001d9fa1edb59b
|
File details
Details for the file luma_bot-1.8.0-py3-none-any.whl.
File metadata
- Download URL: luma_bot-1.8.0-py3-none-any.whl
- Upload date:
- Size: 51.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
86d17ddd977595b632bfd46899aa2bdcc3b2b59d509a1ca7dc23f5b63b8fbbd1
|
|
| MD5 |
bf2f82eddce154feb930745e1d2aaf8c
|
|
| BLAKE2b-256 |
c7dc3c3fdfc666545aa3c17d7c8f6b2534bf3241e503dc72cf61fb9fc539b366
|