@houla/live-connector
v0.5.0
Published
Receive Hou.la live events (gifts, hearts, chat and more) in real time and wire them into anything: OBS overlays, games, lights, bots.
Downloads
130
Maintainers
Readme
@houla/live-connector
Receive your Hou.la live events (gifts, hearts, chat and more) in real time, and do whatever you want with them: show an alert on an OBS overlay, spawn a mob in Minecraft, flash your lights, run a bot.
If you have used tiktok-live-connector before, this will feel familiar. You create a connection, you listen for events, that is it.
const { HoulaLiveConnection } = require('@houla/live-connector');
const conn = new HoulaLiveConnection({ token: 'hle_your_key_here' });
conn.on('gift', (gift) => {
console.log(`${gift.sender.name} sent ${gift.gift.name}`);
});
conn.connect();Install
npm install @houla/live-connectorNode 18 or newer.
Get a key
The connection needs a key, not your password. The key is read only. It can receive events, nothing else. It cannot post, spend, or change anything on your account.
- Open the Hou.la studio and go to Event keys.
- Create a key, pick the events you want (gifts, hearts, and so on).
- Copy it. It is shown once. If you lose it, rotate it and the old one stops working.
A key belongs to a workspace and only ever receives that workspace's own live events. You cannot listen to someone else's live.
Events
Listen with .on(type, handler).
Connection events:
| Event | Fires when |
| --- | --- |
| connected | the key is accepted and the stream is open |
| disconnected | the connection drops |
| error | the key is rejected or the socket fails |
Live events — pick which ones a key receives when you create it:
| Event | Fires when |
| --- | --- |
| gift | a gift is received on one of your lives |
| hearts | the live's heart total updates |
| comment | a comment is approved in your chat |
| viewer | a viewer joins, shares, opens your shop or adds to cart |
| poll | a poll starts, its tally updates, or it closes (switch on poll.phase) |
| gift_goal | a gift-goal's progress advances |
| follow | a NEW follower during your live — deduped + throttled server-side, so an unfollow/re-follow loop can't spam your effect |
All events are fully typed. There is also a catch all event that fires for everything, handy for logging:
conn.on('event', (envelope) => {
console.log(envelope.type, envelope.data);
});Every payload carries only public display identity — a workspace's name, avatar, handle and verified badge — never private account data. Each live event includes live: { roomId, workspaceId }.
Who did it — followers & moderators
The person behind an event also comes with two relationship flags, isFollower and isModerator, so you can gate what an effect does. They sit on sender (gifts), author (comments), viewer (viewer events) and follower (follows):
conn.on('gift', (gift) => {
if (!gift.sender.isFollower) return; // effects reserved for followers
if (gift.sender.isModerator) doModThing(); // mods get something extra
fireEffect(gift);
});Gift payload
{
transactionId: string | null,
live: { roomId: string, workspaceId: string },
gift: {
id: string,
slug: string, // stable id, good for mapping rules
name: string,
category: string | null,
coinCost: number, // price of one unit
quantity: number, // how many were sent at once
totalCoins: number, // what was actually spent
totalStars: number,
thumbnailUrl: string | null,
animationDurationMs: number | null
},
sender: {
workspaceId: string | null,
name: string | null,
avatarUrl: string | null
}
}Other event payloads
live is { roomId, workspaceId } on every event. Full TypeScript types ship with the package.
// hearts
{ live, totalHearts: number }
// comment
{ live,
comment: { id, content, parentId, serverSequence, createdAt },
author: { workspaceId, name, avatarUrl, slug, isVerified } }
// viewer
{ live,
kind: 'join' | 'share' | 'shop_view' | 'cart_add',
viewer: { workspaceId, name, avatarUrl, isVerified } }
// poll — switch on poll.phase
{ live, poll: { phase: 'started', id, sessionId, question,
options: [{ id, label }], anonymous, tally, totalVotes,
expiresAt, startedAt } }
{ live, poll: { phase: 'tally', sessionId, tally, totalVotes } }
{ live, poll: { phase: 'closed', sessionId, tally, totalVotes, status } }
// gift_goal
{ live, goal: { id, label, isCompleted,
items: [{ giftId, giftName, giftThumbnailUrl,
coinCost, targetQuantity, currentQuantity }] } }De-duplicate with transactionId
The same gift can reach you more than once, for example right after a reconnect. If your handler does something you cannot take back, like spawning a mob or firing a payout, skip anything you have already seen:
const seen = new Set();
conn.on('gift', (gift) => {
if (gift.transactionId && seen.has(gift.transactionId)) return;
if (gift.transactionId) seen.add(gift.transactionId);
// safe to act now
});Gift reference
To map a gift to an action you need its slug. The full, always-current gift catalogue is public, no key required:
GET https://api.hou.la/api/giftsEvery active gift, with the fields that matter for an integration:
{
"slug": "flame",
"name": "Flame",
"coinCost": 3,
"category": "chat_png",
"thumbnailUrl": "https://.../gifts/.../flame.png"
}slugis the stable id. Match on it, not onname(display text, can change or be localized).coinCostis the value in coins, handy for "bigger gift, bigger reaction" logic.thumbnailUrlis a PNG on the CDN, ready to drop into an overlay.
New gifts show up here on their own, so fetch it at startup rather than hard-coding a list.
Bundles → effects (loadPreset / applyPreset)
For interactive gifts, instead of a hand-written slug→command switch you can load a
bundle and let the connector wire it up — with a per-slot cooldown and safe
placeholder substitution. A bundle is the same bundle.json shape used by the
community bundles repo: each reserved slot
(ix_slot_01…ix_slot_30) carries an effect.
const { HoulaLiveConnection, loadPreset, applyPreset } = require('@houla/live-connector');
const preset = loadPreset('./gaming-fx.bundle.json'); // path OR object OR { slug: {command} } map
const conn = new HoulaLiveConnection({ token: 'hle_...' });
applyPreset(conn, preset, {
cooldownMs: 1500, // default per-slot throttle
vars: { player: 'Steve' }, // fills {player} in commands
onCommand: (command, gift) => rcon.send(command), // YOUR executor
});
conn.connect();Placeholders resolved in commands: {sender}, {quantity}, {coins}, {name}, plus any
vars you pass (e.g. {player}). Untrusted values (the sender name) are sanitized. Gifts are
de-duplicated on transactionId by default, and each slot honours its own cooldownMs.
Dry-run without a live — simulateGift
Test your effects offline (no live, no coins, no connection). It fires the exact same path as a real gift:
conn.simulateGift({ slug: 'ix_slot_09', senderName: 'Alice' }); // → your onCommand runsNo terminal — the app (for streamers)
Don't want to touch npm or a terminal? Download the zip for your OS from the Releases page (built by CI) — Windows, macOS (Intel + Apple Silicon), Linux:
| OS | Download | Run |
|----|----------|-----|
| Windows | houla-connector-windows-x64.zip | double-click houla-connector.exe (SmartScreen → More info → Run anyway) |
| macOS (Apple Silicon / M-series) | houla-connector-macos-apple-silicon.zip | right-click houla-connector → Open (Gatekeeper → Open anyway), or chmod +x houla-connector && ./houla-connector |
| macOS (Intel) | houla-connector-macos-intel.zip | same as above |
| Linux | houla-connector-linux-x64.zip | chmod +x houla-connector && ./houla-connector |
Each zip contains the binary + houla.config.json + the Minecraft bundle.json + QUICKSTART.md. Then:
- Open
houla.config.jsonand paste your key (created in the Studio → Connecteur live):
The{ "key": "hle_your_key", "preset": "bundle.json", "vars": { "player": "YourMinecraftName" }, "rcon": { "host": "127.0.0.1", "port": 25575, "password": "your_rcon_password" } }rconblock is optional — leave it out to just see gifts logged; add it and gifts fire Minecraft commands.bundle.jsonsupplies the gift→command mapping. - Run it (see the table). First run without a config? It asks for your key and saves it. Leave it running during your live — set-and-forget.
Full step-by-step: QUICKSTART.md. The connector is optional: run it only if
you want gifts to trigger real effects.
Build it yourself with Bun: bun build bin/houla-connector.js --compile
--target=bun-windows-x64|bun-linux-x64|bun-darwin-x64|bun-darwin-arm64 --outfile <name>. CI
(release-exe.yml) builds all four on every version tag.
Examples
See the examples folder.
log.jsprints every gift.minecraft-rcon.jsturns a gift into a Minecraft command over RCON — a hand-written slug→command switch.bundle-effects.jsloads a bundle preset, applies it with per-slot cooldown, and runs offline viasimulateGiftwhen no key is set.
Options
new HoulaLiveConnection({
token: 'hle_...', // required
url: 'https://api.hou.la', // optional, override for local dev only
reconnect: true, // optional, on by default
});How it works
The connector opens a WebSocket to Hou.la and joins a room for each event type you selected. Events are pushed to you as they happen. There is no polling and no public endpoint to expose on your side, so it works from a laptop behind a router without any setup.
Connections are rate limited and capped per key. If you open too many at once, or reconnect in a tight loop, some will be refused. Keep one connection per process and let it reconnect on its own.
Contributing
This repository is published for you to read, audit, fork and build on — but it is maintained in-house and does not accept pull requests. Incoming PRs are closed automatically.
Issues are very welcome, and they are the fastest way to get something changed: bug reports, a bridge that misbehaves, a missing event field, an idea for a new integration. See CONTRIBUTING.md.
Want to share a gift pack (artwork + effect mapping) rather than connector code? That has its own repo and it is open to pull requests: Hou-la/houla-bundles.
License
Apache License 2.0. Use it, fork it, ship it commercially — the only things it asks are that you keep the notices and that you don't use the Hou.la name or logo to pass your fork off as ours.
