Virtual gifting

Gifting gives you three things: a catalogue you curate per project, a ledger of who sent what to whom, and a realtime event that every client in the channel receives so they can play an animation.

AppLooma does not move money

There is no payment processing, no wallet and no payout in this feature. Coins are a number you define. Your server sells them, holds the balances, debits the sender and credits the receiver. AppLooma records the transaction and broadcasts it — nothing more.

coinPrice and coinTotal are your own unit of account. They are stored and reported back to you, and are never converted to a currency.

Create the catalogue#

Gifts belong to one project. Create them in the console, or with the dashboard API using your dashboard login. Each gift has a name, an image, a coin price and a sort order.

FieldTypeRules
namestringRequired. 1–64 characters.
imageUrlstringRequired. Must start with https://. Host the asset yourself.
coinPriceintegerOptional, defaults to 0. Non-negative.
sortOrderintegerOptional, defaults to 0. Lower sorts first.
isActivebooleanSet on update. Inactive gifts disappear from the client catalogue and cannot be sent.

Dashboard API

These endpoints authenticate with your dashboard session token, not with an API key. They are for your admin tooling.

Create a gift
POST https://api.applooma.dev/v1/apps/{appId}/gifts
Authorization: Bearer <dashboard token>
Content-Type: application/json
{
"name": "Rocket",
"imageUrl": "https://cdn.example.com/gifts/rocket.png",
"coinPrice": 500,
"sortOrder": 10
}
→ 201 { "id": "…", "appId": "…", "name": "Rocket", "imageUrl": "…",
"coinPrice": 500, "sortOrder": 10, "isActive": true, "createdAt": "…" }

GET /v1/apps/{appId}/gifts lists every gift including inactive ones. PATCH /v1/apps/{appId}/gifts/{giftId} updates any of the fields above. Retiring a gift is a PATCH with { "isActive": false } — do that rather than deleting, so past transactions still resolve.

Showing the catalogue in your app

GET /v1/gifts returns the active gifts for the project, sorted, with only the fields a client needs. It uses API key auth, so call it from your server and pass the result down, or proxy it.

curl https://api.applooma.dev/v1/gifts \
-H "x-applooma-key: ak_your_key" \
-H "x-applooma-secret: as_your_secret"
# → { "gifts": [
# { "id": "gft_…", "name": "Rocket",
# "imageUrl": "https://cdn.example.com/gifts/rocket.png",
# "coinPrice": 500 }
# ] }

Send a gift from your server#

POST /v1/gifts/send — API key auth.

Never call this from a client

This endpoint needs your API secret, and it does not check any balance — it records the transaction and broadcasts it. Your server owns the coin balance, so your server must debit the sender first and only then call this. A client that could reach this endpoint could send unlimited gifts.

FieldTypeRules
roomNamestringRequired. Must match [a-zA-Z0-9_-]{1,64}.
giftIdstringRequired. Must be an active gift in this project, or the call returns 404.
senderstringRequired. The sender’s identity — the same value you put in their token.
receiverstringRequired. The receiver’s identity.
quantityintegerOptional, defaults to 1. Between 1 and 9999.
server.js
app.post('/api/gifts/send', async (req, res) => {
const user = await authenticate(req);
if (!user) return res.status(401).json({ error: 'Sign in first' });
const gift = await db.gifts.find(req.body.giftId);
const quantity = req.body.quantity ?? 1;
const cost = gift.coinPrice * quantity;
// 1. YOUR economy. Debit first, atomically, and fail closed.
const ok = await db.wallets.debit(user.id, cost);
if (!ok) return res.status(402).json({ error: 'Not enough coins' });
// 2. Record + broadcast.
const r = await fetch('https://api.applooma.dev/v1/gifts/send', {
method: 'POST',
headers: {
'x-applooma-key': process.env.APPLOOMA_API_KEY,
'x-applooma-secret': process.env.APPLOOMA_API_SECRET,
'Content-Type': 'application/json',
},
body: JSON.stringify({
roomName: req.body.channel,
giftId: req.body.giftId,
sender: user.id, // identity, not display name
receiver: req.body.receiver,
quantity,
}),
});
if (!r.ok) {
await db.wallets.credit(user.id, cost); // refund on failure
return res.status(502).json({ error: 'Gift failed' });
}
const { txId, coinTotal, sentAt } = await r.json();
// 3. Credit the receiver in your own ledger.
await db.wallets.credit(req.body.receiver, coinTotal);
res.json({ txId, coinTotal, sentAt });
});

coinTotal is coinPrice × quantity, computed from the catalogue at send time. Use the value the response returns rather than recomputing it, so your ledger and the leaderboard agree.

If the channel has already closed

The transaction is still recorded and still appears in the leaderboard; only the realtime broadcast is skipped. Do not treat a successful 201 as proof that a client saw the animation.

The realtime event clients receive#

A successful send broadcasts a reliable data message to every participant in the channel. It is JSON with a type of applooma.gift.

Payload
{
"type": "applooma.gift",
"txId": "tx_9f3c…",
"gift": {
"id": "gft_rocket",
"name": "Rocket",
"imageUrl": "https://cdn.example.com/gifts/rocket.png",
"coinPrice": 500
},
"sender": "user-42",
"receiver": "user-7",
"quantity": 3,
"sentAt": "2026-01-01T20:15:00.000Z"
}

The SDKs parse this for you and surface it as a dedicated event, so you do not have to inspect raw bytes.

engine.on('gift-received', (g) => {
// g: { txId, gift: { id, name, imageUrl, coinPrice }, sender, receiver, quantity, sentAt }
playAnimation(g.gift.imageUrl, g.quantity);
showToast(`${nameFor(g.sender)} sent ${g.quantity}× ${g.gift.name}`);
});

The event is a notification, not an authority

Clients receive it over the media connection, which means it is not a receipt. Drive balances and any payout logic from your own database and the gift.sent webhook, and use the realtime event only for the animation and the on-screen toast.

Leaderboard#

GET /v1/gifts/leaderboard — API key auth. Optional roomName query parameter. With it, the leaderboard covers one channel; without it, the whole project.

curl "https://api.applooma.dev/v1/gifts/leaderboard?roomName=friday-show" \
-H "x-applooma-key: ak_your_key" \
-H "x-applooma-secret: as_your_secret"
# → {
# "topSenders": [ { "identity": "user-42", "coins": 12500, "gifts": 34 } ],
# "topReceivers": [ { "identity": "user-7", "coins": 98000, "gifts": 210 } ]
# }
  • Both lists are capped at 20 entries, ordered by total coins descending.
  • coins is the summed coinTotal; gifts is the summed quantity.
  • Entries are identities. Map them to display names using your own user table.
  • The totals are all-time for the scope you asked for. There is no date filter — keep your own rollup if you need a weekly board.

Putting it together#

1

Curate the catalogue

Create gifts in the console with hosted https images and your coin prices.

2

Show it in the app

Fetch /v1/gifts through your server and render the picker.

3

Sell and hold coins yourself

Payments, balances and refunds are entirely yours. AppLooma never sees them.

4

Debit, then send, then credit

Debit the sender, call /v1/gifts/send, refund on failure, and credit the receiver with the returned coinTotal.

5

Animate from the realtime event, reconcile from the webhook

Clients react to applooma.gift; your backend reconciles from gift.sent, which carries the same txId.

Next#