GiftingStack
Sign in Get started

Docs

Gift rooms

A room is everyone watching one live stream, audio room or match. Every gift sent into the room plays on every viewer's screen, as fast as the network carries it. Gift events only: no chat, no audio or video, no gift messages.

How it works

1. Your server mints a client token that lists the rooms the user may join.
2. The app joins the room with the SDK: one WebSocket to rt.giftingstack.com.
3. Someone sends a gift with a room_id. The send is booked as usual.
4. The room passes a small gift event to every viewer. The SDK plays the effect.

The room is best effort: a send never waits for the room and never fails because of it. Under heavy load a room groups gifts: above 10 gifts a second viewers get one update every quarter of a second (once a second in a very busy room), with repeated gifts from the same sender added into one quantity. The SDK reconnects, catches up on gifts from the last few seconds and refreshes the token by itself. A viewer who was offline for longer simply does not see the older gifts.

1. The rooms switch

Rooms are on for every project. You can turn Gift rooms off in the project settings of the dashboard. While it is off, joins are refused (4010) and sends with a room_id still succeed but reach no one.

2. Mint tokens with rooms

Rooms are closed by default: a client token without rooms cannot join any room, and a send from it that names a room_id fails with 403 room_not_allowed. List the rooms this user may watch and send into: exact room ids, or prefixes ending in *, at most 8. Add name (1 to 32 characters, no control, zero-width, direction-changing or other invisible characters) for what viewers see as the sender.

curl -X POST https://api.giftingstack.com/v1/client-tokens \
  -H "Authorization: Bearer $GS_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user": "usr_8f3a", "rooms": ["live_123"], "name": "Ava"}'

Keep rooms as narrow as the screen needs: live_123 rather than live_* for a private room. Room ids are 1 to 64 characters of A-Z a-z 0-9 _ . : -, case sensitive.

3. Join with the SDK

Join on the viewer screen with the same getToken you give the client. Room gifts then play in your effect layer; the user's own gifts already played on tap and are not played twice. Web:

import { createGiftingClient } from "@giftingstack/sdk";
import { GiftEffectLayer, joinRoom } from "@giftingstack/sdk/web";

const getToken = async () => (await fetch("/api/gift-token")).text();
const client = createGiftingClient({ getToken });
const effects = new GiftEffectLayer(document.getElementById("stage"));

const room = joinRoom({ room: "live_123", getToken, client, effects });
room.subscribe(() => (watching.textContent = `${room.viewers ?? "-"} watching`));

// Leaving the stream page:
room.leave();

The rooms address must be wss://. A ws:// URL is refused (a programming error, not a GiftingError) unless the host is localhost, 127.0.0.1, ::1 or 10.0.2.2, or you pass allowInsecure to joinRoom (the other SDKs take the same option; <gift-room allow-insecure> for the element).

Without an SDK, a room is a plain WebSocket to wss://rt.giftingstack.com/v1/rooms/{room_id} with the subprotocols gs.v1 and the client token (browsers cannot set headers, so the token never goes in the URL); the protocol is in the SDK spec; contact support@giftingstack.com to get it.

The same on the other platforms: useGiftRoom (React Native), GiftRoom (Flutter), rememberGiftRoom (Android) and GiftRoomModel (iOS). See SDKs. On phones the connection closes when the app goes to the background and resumes when it comes back.

4. Send into the room

Add the room to the send. From the app, pass roomId to the tray or to sendGift; the token's rooms must allow it. From your server, add room_id to POST /v1/gift-transactions; a secret key may name any room of the project. If the sender's token has no name, viewers get the gift without a sender name.

createGiftTray({ container, client, receiver: hostId, effects, roomId: "live_123" });

The gift is booked whether or not the room can show it. A send that names a room_id answers with a GiftingStack-Room-Event header that says what happened to the room event: queued, or skipped; plan_required (live environment on a plan without live gift rooms), or skipped; rooms_disabled (Gift rooms are switched off in the project settings). Use it to find out why nobody watching saw a gift. Test-environment rooms work on every plan, with up to 20 viewers.

A comment bar under the stream

The comments are your app's chat, not ours. To put a gift button in your comment bar, use the SDK's GiftButton and give the tray your comment field and the room: the button closes the keyboard and opens the tray in its place, a tap on the field brings the keyboard back (Gift button in your message bar). Show your own gifts in your comment column from onSent (the room does not send your own gifts back to you) and everybody else's from the room's onEvent.

const button = new GiftButton(commentBar, { theme, filled: true });
createGiftTray({ container: screen, client, receiver: hostId, effects, roomId: "live_123",
  anchor: button.element, input: commentField, onSent: (tx) => addMyGiftLine(tx) });

One gift button for a battle

A battle does not need a Gift button per side. Give the tray the round's sides as receivers: it shows a "Send to" row with a small avatar per side, ringed in the side's colour, and every gift goes to the side that is picked (by default the side this viewer gifted last, else the first). Make the battle bar's sides tappable with onPressSide and open the tray with that side picked. Colour your gift lines with the receiving side's colour so the chat matches the bar.

const sides = [{ id: "mia", name: "Mia" }, { id: "theo", name: "Theo" }];
const tray = createGiftTray({ container: screen, client, receiver: "mia", receivers: sides,
  roomId: "live_123", receiverOf: room.receiverFor, anchor: giftButton.element, input: commentField });
new GiftBattleBar(barSlot, { room, onPressSide: (id) => { tray.selectReceiver(id); tray.open(); } });
// React Native: useGiftTray({ ..., receivers }) and <BattleBar onPressSide={(id) => { tray.selectReceiver(id); tray.open(); }} />

Battles (versus)

Two to eight hosts share one live room, for example Ava and Mateo. Viewers tap a side to gift that host, and everyone sees a live score bar. The room adds up the gifts for you, so you do not count anything yourself.

Your server starts the round with the secret key. Client tokens cannot start, end or reset a round. Starting sets both scores to 0.

curl -X POST https://api.giftingstack.com/v1/rooms/live_123/battle \
  -H "Authorization: Bearer $GS_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"round": "match_42_r1", "receivers": ["ava", "mateo"], "duration_s": 300}'

The viewer picks the side by choosing the receiver of the gift. Use a tray per host, or change the tray's receiver when the viewer taps a side. Scores are the gifts' value: price times quantity, plus a gift count.

Scores arrive by themselves. A viewer who joins late gets the current scores at once. After that, updates come at most 4 times a second (once a second in a very busy room). The same gift is never counted twice. On web and React Native you can let the SDK draw the bar (below); this is the data it draws from, on web:

const room = joinRoom({ room: "live_123", getToken, client, effects });

room.onBattle(() => {
  const view = room.battleView(); // null when there is no round
  if (!view) return hideBar();
  const [ava, mateo] = view.sides; // in the order your server gave
  bar.style.setProperty("--left", `${ava.share * 100}%`);
  leftScore.textContent = ava.units;
  rightScore.textContent = mateo.units;
  timer.textContent = view.open ? `${Math.ceil(view.remainingMs / 1000)} s` : "Final";
});

tapAva.onclick = () => client.sendGift({ receiver: "ava", giftId: "pink-tulip", roomId: "live_123" });
tapMateo.onclick = () => client.sendGift({ receiver: "mateo", giftId: "pink-tulip", roomId: "live_123" });

React Native: useBattleScore(useGiftRoom({ ... }).controller) returns the same view and keeps the countdown moving. Flutter, Android and iOS have the same headless client: GiftRoom.battleView and onBattle (Flutter), GiftRoom.battle, battleView() and rememberGiftRoom(onBattle = ...) (Android), GiftRoomModel with battle, battleView() and onBattle (iOS). All five SDKs also draw the bar (see below), or you draw your own from the view. Battles follow the same plan rules as rooms: live rooms need a paid plan, and test rooms work on every plan.

The bar, drawn for you (web, React Native, Flutter, Android and iOS). The receivers with their scores, a countdown, and a split bar whose boundary slides as gifts arrive; the side that just got a gift pulses, and when the round ends the winner is marked. A round can have up to eight receivers (the API limit): two draw the classic versus, three or more a row of cells with a bar of proportional segments. Colours come from the SDK theme (the accent, the coin colour, then teal and pink, light or dark; or pass sideColors, one colour per side), and reduced motion turns the animation off. The bar only shows what battleView says; your server still starts and ends rounds. The gift notice of a battle says who got the gift ("sent Rose to Mia") and borders its pill in that side's colour; give the room the same sideColors as the bar (Customize).

// web: pass the room you joined; it follows the battle by itself
new GiftBattleBar(container, { room, resolveUsers });

<gift-battle-bar>  // el.room = roomHandle; el.resolveUsers = ...

// React Native
const { controller } = useGiftRoom({ room: "live_123", getToken });
const view = useBattleScore(controller);
<BattleBar view={view} users={{ ava: { name: "Ava" }, mateo: { name: "Mateo" } }} />

// Flutter:  GiftBattleBar(room: room, users: {"ava": BattleBarUser(name: "Ava"), ...})
// Android:  GiftBattleBar(room, users = mapOf("ava" to BattleBarUser("Ava"), ...))
// iOS:      GiftBattleBar(room: room, users: ["ava": BattleBarUser(name: "Ava"), ...])

Limits

Close codes and what the SDKs do. These end the session: the SDK reports the code and does not retry.

These the SDK handles by itself:

reconnectNow() after retries_exhausted or 4011 starts again at once, asking for a token first. During an ordinary back-off wait it skips the wait. It never skips a 4008 or 4009 wait: the platform said when to come back. It does nothing after a final close (4003, 4004, 4010, 4400, 4009 replaced) or after leave().

A room rechecks its open connections every few minutes (within about 5 minutes 30 seconds): if the key that minted a token was revoked, the project is scheduled for deletion or switched off for rooms, or the live environment was suspended, the connection is closed with 4003 (key_invalid), 4010 or 4011, with a generic reason. Revoking a single user's tokens still applies to new joins only.

Suspension: 4011 (reason project_suspended) closes a live join, and open live connections, while the project's live environment is suspended. The SDKs do not retry it: the room state is closed with code 4011. Your app decides: call reconnectNow() when the suspension is over, or stop the room itself (room.leave() on web).

Privacy: what viewers see

Everyone in the room receives, for each gift:

Never sent to a room: balances, the gift message, metadata or reference. Totals go out only while a battle round exists: the score of each competing receiver (value and gift count). Use opaque user ids (not phone numbers, emails or usernames) and room ids that do not name people, because every viewer sees them.

Display names come from your server and are shown to other people as they are. You are responsible for what they say: check names before you put them in tokens and act on reports from your users. See the Terms and the Privacy Policy.

Rooms store no gift events. Gift events stay in memory for about 15 seconds so a reconnecting viewer can catch up. During a battle the room keeps the round's scores (receiver ids and totals) until you start a new round or remove it. Our logs never contain room ids, user ids or display names.