GiftingStack
Sign in Get started

Guide

How to add virtual gifts to Sendbird chat

Your app already chats with Sendbird. GiftingStack adds the gifts: coins, the gift tray, the effects and the ledger. GiftingStack carries no chat of its own, so every gift lands in your Sendbird channel, next to the messages your users already read. This guide shows both ways to get it there, and where the gift button, the tray and the effects go on the screen.

Two ways a gift reaches the chat

Pick one per screen, so a gift never shows twice. Both start from the same setup: a project, a gift catalog and a client token (quickstart).

1. Send the gift with the channel as its reference

For the server path, the webhook has to know which channel the gift belongs to. Give the tray the channel's URL as reference (your own context id, up to 128 characters); it comes back in the gift.sent event.

const tray = createGiftTray({
  container: screen,
  client,
  receiver: otherMemberId,                 // who gets the gift (your user id)
  reference: `chat:${channel.url}`,        // "chat:sendbird_group_channel_4071_9b2e..."
  effects,
  onTopUp: openCoinStore,
});

2. Server: post an admin message on gift.sent

Verify the signature on the raw body first, with the verify function from Webhooks. Then post with Sendbird's server API, the Platform API, using the API token from the Sendbird dashboard. dedup_id is the event id and handled event ids are kept, so a repeated delivery never posts twice.

import express from "express";

const SENDBIRD = `https://api-${process.env.SENDBIRD_APP_ID}.sendbird.com/v3`;

app.post("/webhooks/giftingstack", express.text({ type: "application/json" }), async (req, res) => {
  if (!verify(req.body, req.get("GiftingStack-Signature") ?? "", process.env.GS_WEBHOOK_SECRET))
    return res.sendStatus(400);
  const event = JSON.parse(req.body);
  const gift = event.data;
  if (event.type !== "gift.sent" || !gift.reference?.startsWith("chat:")) return res.sendStatus(200);
  if (await handled.has(event.id)) return res.sendStatus(200);   // your store of handled ids

  const channelUrl = gift.reference.slice("chat:".length);
  const sender = await displayName(gift.sender);                 // from your own users table
  const times = gift.quantity > 1 ? ` x${gift.quantity}` : "";
  const r = await fetch(`${SENDBIRD}/group_channels/${encodeURIComponent(channelUrl)}/messages`, {
    method: "POST",
    headers: { "Api-Token": process.env.SENDBIRD_API_TOKEN, "Content-Type": "application/json" },
    body: JSON.stringify({
      message_type: "ADMM",
      message: `${sender} sent ${gift.gift.name}${times}`,
      custom_type: "gift",
      data: JSON.stringify({ gift_id: gift.gift.id, gift_icon: gift.gift.icon_url, quantity: gift.quantity }),
      dedup_id: event.id,
    }),
  });
  if (!r.ok) return res.sendStatus(500);                         // delivered again later
  await handled.add(event.id);
  res.sendStatus(200);
});

3. App: a live gift row from the gift room

In a live stream, every viewer joins the gift room. The room's onEvent hands over each gift sent into it by someone else; the tray's onSent hands over the viewer's own. Both carry the gift transaction id, so a gift replayed after a reconnect is not added again. The open channel's messages go into the same list.

import { GiftEffectLayer, createGiftTray, joinRoom } from "@giftingstack/sdk/web";
import { OpenChannelHandler } from "@sendbird/chat/openChannel";

const shown = new Set();
function addGiftRow(tx, who, giftName, icon, quantity) {
  if (shown.has(tx)) return;
  shown.add(tx);
  chatList.append(renderGiftRow({ who, giftName, icon, quantity }));   // your row component
}

const effects = new GiftEffectLayer(videoBox);           // effects play over the video
const room = joinRoom({
  room: "live_123",
  getToken,
  client,
  effects,
  onEvent: (e) => addGiftRow(e.tx, e.senderName ?? "Someone", e.gift.name, e.gift.icon, e.quantity),
});
const tray = createGiftTray({
  container: screen,
  client,
  receiver: hostId,
  roomId: "live_123",
  effects,
  anchor: giftButton,
  collapseOnSelect: true,
  scrim: true,
  onTopUp: openCoinStore,
  onSent: (t) => addGiftRow(t.id, "You", t.gift.name, t.gift.icon_url, t.quantity),
});
giftButton.onclick = () => tray.toggle();

// the stream's chat, from Sendbird, into the same list
sb.openChannel.addOpenChannelHandler("live-chat", new OpenChannelHandler({
  onMessageReceived: (channel, message) => {
    if (channel.url === liveChannel.url) chatList.append(renderMessage(message));
  },
}));
// leaving the stream: room.leave(); tray.destroy(); sb.openChannel.removeOpenChannelHandler("live-chat");

React Native: the same two callbacks on the hooks.

const room = useGiftRoom({
  room: "live_123", getToken, client, effects,
  onEvent: (e) => addGiftRow(e.tx, e.senderName ?? "Someone", e.gift.name, e.gift.icon, e.quantity),
});
const tray = useGiftTray({
  client, receiver: hostId, roomId: "live_123", effects,
  onSent: (t) => addGiftRow(t.id, "You", t.gift.name, t.gift.icon_url, t.quantity),
});

Flutter GiftRoom(onEvent: ...) with GiftTrayNotifier(onSent: ...), Android rememberGiftRoom(onEvent = ...) with rememberGiftTrayHolder(onSent = ...), iOS GiftRoomModel(..., onEvent: ...) with GiftTrayModel(..., onSent: ...). Every room event has tx, senderName, gift.name and quantity. The room carries gift events only: no message text, no chat.

4. The screen layout

Colours, texts and motion: Customize. Rooms, tokens and limits: Gift rooms.

Next steps