Skip to main content

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 -> string
  • int -> integer
  • float -> number
  • bool -> boolean
  • parameters with defaults are optional
  • str | None is 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 speech recognition off locally. Luma forwards text transcripts, not raw microphone audio, to the bot.

@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.

Download files

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

Source Distribution

luma_bot-1.7.0.tar.gz (67.6 kB view details)

Uploaded Source

Built Distribution

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

luma_bot-1.7.0-py3-none-any.whl (49.7 kB view details)

Uploaded Python 3

File details

Details for the file luma_bot-1.7.0.tar.gz.

File metadata

  • Download URL: luma_bot-1.7.0.tar.gz
  • Upload date:
  • Size: 67.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.12

File hashes

Hashes for luma_bot-1.7.0.tar.gz
Algorithm Hash digest
SHA256 0c7731fa28b32a21aed5ee062973678e6e163ac05d01eff743cb44657988441b
MD5 4560361ee54f452b3e37376262437d7f
BLAKE2b-256 3d42cbb0d8459dcc625ceb2adb293b5826de96e3729851d66bff854e02b5c077

See more details on using hashes here.

File details

Details for the file luma_bot-1.7.0-py3-none-any.whl.

File metadata

  • Download URL: luma_bot-1.7.0-py3-none-any.whl
  • Upload date:
  • Size: 49.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.12

File hashes

Hashes for luma_bot-1.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6e73e952f3caadcd5ec0dca717e4ae225269096df97ab786787a310a0b4c44cf
MD5 4602658b74aa65cf1f5ac779428ddf35
BLAKE2b-256 67c27f705fc4b3704db954712729965f2d96bdc400df1d625136ab92ad92510c

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 Sentry Error logging StatusPage Status page