Skip to main content

VIBES Client

An async Python wrapper for the VIBES streaming API — REST endpoints, a realtime WebSocket client, a typed command framework, and a plugin (extension) system.

import os
import asyncio
import vibes
from vibes import Client

client = Client(token=os.environ["VIBES_TOKEN"], prefix="!")

@client.event()
async def on_message(message: vibes.Message):
    if message.content == "!ping":
        await message.reply("Pong!")

asyncio.run(client.start())

Table of contents

Features

  • REST API — fetch streams, users and your own application with PassiveClient.
  • Realtime events — a WebSocket connection with automatic reconnects and 30s heartbeats.
  • Typed commands — both classic chat (!command) and native VIBES app commands, with automatic argument parsing, role gating and per-viewer/user/chat cooldowns.
  • Extensions — load and unload self-contained modules of commands and listeners at runtime.
  • Caching — users and messages are cached to cut down on API calls.

Installation

Python 3.11+ is required. Dependencies are httpx and websockets.

pip install vibes-api

Quickstart

Grab an API token from your VIBES dashboard and export it as VIBES_TOKEN (the examples read it from the environment, e.g. with a .env file and python-dotenv).

import os
import asyncio
import vibes
from vibes import Client

client = Client(token=os.environ["VIBES_TOKEN"], prefix="!")

@client.event()
async def on_connect():
    print(f"Logged in as {client.user}")                     # ClientUser
    for stream in await client.fetch_streams(limit=10):
        await client.subscribe_to(stream.channel)            # listen to this stream

@client.event()
async def on_message(message: vibes.Message):
    if message.content.startswith("!"):
        await client.process_commands(message)               # let the command framework run

asyncio.run(client.start())

Client.start() opens the WebSocket and runs the event loop until the connection closes — it is the last thing you await, and it blocks for the lifetime of the bot.

Clients

PassiveClient (REST only)

No WebSocket, no events — just authenticated API calls. Use it for scripts, cron jobs or dashboards.

import vibes

api = vibes.PassiveClient(token=TOKEN)
try:
    app = await api.fetch_client()
    print(app.owner, app.rate_limit)

    streams = await api.fetch_streams(limit=50, category="chit-chat")
    user = await api.fetch_user("some-streamer")   # None if not found
finally:
    await api.close()
Method Returns Notes
fetch_client() Application Your own app: owner, key, rate limit, webhook.
fetch_streams(*, limit=100, category=None, mature=True) list[Stream] category accepts a slug or a StreamCategory. mature=True requests 18+ streams (403 if your account isn't 18+).
fetch_user(slug) User | None Accepts a username or ID. Results are cached.
send_request(coro, *args, raise_for_status=True, **kwargs) httpx.Response The low-level call every method is built on.
close() None Closes the underlying httpx.AsyncClient.
clear_cache() None Empties the user and message caches.

send_request gives you rate-limit handling and typed errors for free:

  • 429 responses are retried automatically after the Retry-After header (default 60s).
  • 404 raises vibes.errors.NotFound.
  • 403 raises vibes.errors.Forbidden.
  • Any other non-OK status raises httpx.HTTPStatusError.

Pass raise_for_status=False to receive the raw response instead.

Client (WebSocket)

Client extends PassiveClient with the realtime connection, event listeners and the command framework.

client = Client(token=TOKEN, prefix="!")
  • prefix is required if you use text commands; it can't be empty or contain spaces.
  • client.user is a ClientUser, available after on_connect.
  • client.is_running tells you whether the socket is open.
Member Description
start() Connect and run forever.
close() Disconnect once started (replaces the passive close()).
event(name=None) Decorator registering an event listener.
subscribe_to(user) / unsubscribe_from(user) Follow a streamer's events. Requires a running client and a live user.
process_commands(message) Parse a Message as a command.
sync_commands() Push your app commands to VIBES. Call it in on_connect.
load_extension(name) / unload_extension(name) See Extensions.
commands / app_commands The registered Command / AppCommand objects.

Events

Register listeners with @client.event(). The event name is taken from the function name (on_connect → connected), or you can pass one explicitly: @client.event("chat_message").

@client.event()
async def on_stream_online(stream: vibes.Stream):
    print(f"{stream.channel} is live: {stream.title}")
Listener Arguments Fires when
on_connect — The socket is connected and client.user is populated.
on_message message: Message A chat message is sent.
on_message_delete message: Message A cached message is deleted.
on_viewer_join stream: Stream, viewer: Viewer Someone starts watching a subscribed stream.
on_viewer_leave stream: Stream, viewer: Viewer Someone stops watching.
on_new_follower streamer: User, viewer: User A streamer you subscribe to gains a follower.
on_stream_like stream: Stream, viewer: Viewer Someone likes a stream.
on_stream_online stream: Stream A subscribed stream goes live.
on_stream_offline stream: Stream A subscribed stream ends.
on_donation donation: Donation A donation arrives.
on_command_error error: CommandError | Exception A command fails (see Error handling).

Listeners run as independent asyncio tasks — a slow listener doesn't block the others. Exceptions raised inside a listener are logged, not propagated.

There is also an internal _debug event that receives every raw WebSocket frame:

@client.event()
async def _debug(raw: dict):
    ...

Commands

VIBES supports two kinds of commands. Both accept role and cooldown options.

Text commands

Plain chat messages starting with your prefix. You must wire process_commands into on_message:

from vibes import commands
from vibes.commands import Context

@commands.command()                     # invoked as "!hello Ada"
async def hello(ctx: Context, name: str = "world"):
    await ctx.message.reply(f"Hello, {name}!")

@client.event()
async def on_message(message: vibes.Message):
    if message.author and not message.author.bot and message.content.startswith(client.prefix):
        await client.process_commands(message)

ctx is always the first parameter and is a Context:

Attribute Description
ctx.message The triggering Message. Use await ctx.message.reply(...) to respond.
ctx.author The Viewer who sent the message.
ctx.stream / ctx.chat The Stream and its StreamChat (await ctx.chat.send(...)).
ctx.command The Command being run.
ctx.client The Client.
ctx.invocation_id The invocation ID (message ID for text commands).

Parameters

Parameter types come from annotations. Text commands accept str, int and float; app commands also accept Viewer. A parameter with a default is optional, and when a message contains more words than the command takes parameters, the leftovers are merged into the last one — so !hello Ada Lovelace passes the full name as a single string, and !donate 10 Have a good one passes message="Have a good one".

@commands.command()
async def donate(ctx: Context, amount: int, message: str = "No message"):
    ...

If the user writes something that can't be converted (!donate abc), the command is skipped and a BadArgument error is raised.

Roles

Gate a command behind a minimum role — Role.VIEWER (default), Role.MOD or Role.STREAMER. Roles are comparable, so the framework checks ctx.author.role < command.required_role.

@commands.command(role=vibes.Role.MOD)
async def purge(ctx: Context):
    ...

Cooldowns

Cooldown(period, max_uses=1, target=Cooldown.VIEWER) limits how often a command can run.

Target Cooldown applies to
Cooldown.VIEWER One viewer in one stream chat (default).
Cooldown.USER One user across all streams.
Cooldown.CHAT The entire chat, shared by everybody.
@commands.command(cooldown=commands.Cooldown(period=10, max_uses=2, target=commands.Cooldown.USER))
async def coinflip(ctx: Context):
    ...

App commands

Native VIBES commands, registered with the API instead of parsed from chat. Use describe to give each option a description shown in the VIBES UI, and put the command description in the docstring. Call client.sync_commands() in on_connect to publish them.

@commands.app_command()
@commands.describe(name="Your name", age="Your age")
async def intro(ctx: Context, name: str, age: int):
    """Tells everyone who you are."""
    await ctx.message.reply(f"{ctx.author} is {name}, {age} years old!")

App command handlers receive parsed options as keyword arguments, and ctx.invocation_id is the invocation ID supplied by the server.

Error handling

Command errors are never raised out of process_commands — they're dispatched to a single on_command_error listener, which receives either a CommandError subclass or an unexpected Exception from your callback.

Error Extra attributes
CommandNotFound —
NoPermission —
BadArgument .parameter (CommandParameter)
MissingArguments —
OnCooldown .cooldown, .available_after (seconds)

Every CommandError carries .context (the Context).

from vibes.commands import CommandError, BadArgument, MissingArguments, NoPermission, OnCooldown

@client.event()
async def on_command_error(error: CommandError):
    if not isinstance(error, CommandError):
        logging.exception("Unexpected command error", exc_info=error)
        return

    ctx = error.context
    if isinstance(error, BadArgument):
        await ctx.message.reply(f"Invalid value for `{error.parameter.name}`.")
    elif isinstance(error, MissingArguments):
        await ctx.message.reply("Missing required argument(s).")
    elif isinstance(error, NoPermission):
        await ctx.message.reply("You don't have permission to do that.")
    elif isinstance(error, OnCooldown):
        await ctx.message.reply(f"Try again in {error.available_after:.1f}s.")

If no on_command_error listener is registered, user-input errors are swallowed silently and unexpected exceptions are logged.

Extensions

An extension is a module that exposes a setup(client) coroutine. Commands and listeners defined inside it can be removed cleanly at runtime.

my_extension.py:

import vibes
from vibes import commands
from vibes.commands import Context
from vibes.ext import Extension, event_listener


class MyExtension(Extension):
    def __init__(self, client):
        self.client: vibes.Client = client

    async def ext_load(self):
        print("Extension loaded")

    async def ext_unload(self):
        print("Extension unloaded")

    @event_listener()                       # events inside an extension use this decorator
    async def on_connect(self):
        print("Connected!")

    @commands.command(role=vibes.Role.MOD)
    async def ping(self, ctx: Context):
        await ctx.message.reply("Pong!")


async def setup(client):                    # the entry point
    await client.add_ext(MyExtension(client))

main.py:

await client.load_extension("my_extension")   # without ".py"
...
await client.unload_extension("my_extension")
await client.sync_commands()                  # push removed app commands to VIBES

The extension class must live in an importable module — it can't be defined in __main__.

load_extension raises one of ExtensionNotFound, ExtensionAlreadyLoaded, NoEntryPointError (from vibes.ext.errors) or ExtensionFailed.

Models reference

Type Key attributes
User id, username, mention, avatar_url, bio, followers, created_at, url, is_live, stream, premium
ClientUser Everything on User, plus name, visibility, scopes, channels
Viewer Everything on User, plus display_name, role, bot, viewer_id
Role name, level; predefined as Role.VIEWER, Role.MOD, Role.STREAMER
Stream id, title, channel, category, viewers, likes, mature, thumbnail_url, url, started_at, duration, chat
StreamChannel username, avatar_url, url, premium
StreamCategory slug, name
StreamChat await chat.send(content)
Message id, content, author, streamer, stream, reference, created_at, system, await message.reply(content)
MessageReference id, author, content
Donation streamer, user, amount, amount_usd, currency, message
Application url, key, owner, rate_limit, utm_source, mature_allowed, webhook
APIKey name, created_at
ApplicationOwner username, url
ClientWebhook url, scope, active
PremiumSubscription name, level; reached via user.premium / channel.premium

All datetime attributes (created_at, started_at) are timezone-aware datetime.datetime objects.

User and Viewer compare by ID, and str(user) gives the username.

Examples

Runnable examples live in examples/:

File Shows
quickstart.py The smallest useful bot: connect, subscribe, reply.
events.py Every event listener and the arguments it receives.
rest_api.py PassiveClient for REST-only scripts.
text_commands.py Text commands, typed parameters, roles and cooldowns.
app_commands.py Native app commands, describe and sync_commands.
extensions.py Loading and unloading an extension.
example_extension.py The extension itself.

License

MIT — see LICENSE.

Metadata

Release files for vibes-client 0.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 vibes-client 0.1.0
File Size Uploaded
vibes_client-0.1.0.tar.gz 28.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vibes-client 0.1.0
File Interpreter ABI Platform
vibes_client-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 59.0 kB

Release files / vibes_client-0.1.0.tar.gz

Download URL vibes_client-0.1.0.tar.gz
Size 28.7 kB
Tags Source
SHA-256 checksum
How to use checksums
cfd3fab9a14ee41fe72b22d8889dd06af8e1e17be90f5fb236ab8c7c5a816549
BLAKE2b-256 checksum
How to use checksums
304e698d3eb52cbee149fd4f1f2bff55596d1feb07bdddc68877943d85786d81
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / vibes_client-0.1.0-py3-none-any.whl

Download URL vibes_client-0.1.0-py3-none-any.whl
Size 30.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
412ec37d90526bc2caf00e01c57f1384afdd9440d838a587f97e4ae33cc12284
BLAKE2b-256 checksum
How to use checksums
4f9430c99f028cabd5b1d7079afd8fb78060a8a748bd0d4b36c21f5a9422569b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

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