Docs
Quickstart: send your first gift
You will create a project, put gifts in it, give a user some coins, create a client token and send a gift with it. Everything here uses the test environment: no real users, no real data. Not sure whether you need the API or the SDK? Start here.
What the test environment is for. Test keys (gs_test_…) are
for building and trying: a test project allows up to 50 users and
5,000 operations a month. An operation is a successful credit, debit,
gift send or gift reversal; a replay of the same Idempotency-Key does not
count, and catalog edits, token creation and user deletion are never refused. Users are
counted once they exist, receivers included, and a deleted user still counts until you
reset the test data. Past a limit, those writes fail with
403 test_limit_reached. "Reset test data" in the dashboard clears the test
users, so the user limit starts again, but the monthly operation count is not lowered. A
live key is needed to run an app for real users.
1. Create a project and a test key
- Sign in at app.giftingstack.com with your email. We send a sign-in link, there is no password.
- Create a project. Pick the region closest to your server (it cannot be changed later) and name your currency, for example "coin".
-
Open the project, create an API key and choose Test. The key
(
gs_test_…) is shown once: copy it now.
Keep it on your server, for example as an environment variable. Never put it in an app.
export GS_KEY=gs_test_your_key_here
export API=https://api.giftingstack.com
# a fresh random Idempotency-Key for each request below ($(idem)); works on macOS, Linux
# and Git Bash for Windows. (uuidgen also works where it is installed.)
idem() { node -e "console.log(crypto.randomUUID())"; }
2. Add gifts
In the project, open Gifts. A new project starts empty: click
Choose your gifts, then Add this set on the Starter set:
15 gifts with pictures, from small to grand. Or use
Browse the gift library to pick your own, and set your own prices. You
can change any price later. Each gift has an id, for example pink-tulip (the
first Starter gift); your server sends gifts by that id. If you picked other gifts, read
their ids from your catalog:
curl $API/v1/gifts -H "Authorization: Bearer $GS_KEY".
3. Give a user some coins
Users do not need to be created: a user exists once you credit them. In your real app you do this after your own payment system confirms a purchase. Coins cannot be paid out or cashed out.
curl -X POST $API/v1/users/ava/credit \
-H "Authorization: Bearer $GS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(idem)" \
-d '{"amount": 500, "reason": "purchase", "reference": "order_1001",
"price": "4.99", "currency": "USD"}'
reference makes the credit unique for good: crediting the same order twice is
refused (409 duplicate_reference), which is why the next example uses a new
reference. Idempotency-Key makes a retry after a network error safe: you get
the first answer back and nothing is added twice. The answer shows the new balance:
"balance_after": 500.
What the credit is decides how your fee is measured (How your fee is measured). The example above is a web sale: price and
currency say what your user paid your own checkout (a decimal string and an
ISO 4217 code). They are informational only: they never change the balance, and
GiftingStack never charges, holds or pays out that money.
A store sale (App Store or Google Play) is credited with a verified
receipt. Verify the purchase first (Receipt validation),
then pass the receipt_id it returned and no price: the price comes from the
store, and the receipt can credit only once. On live this is how store sales are recorded,
and price or currency next to receipt_id is
refused:
curl -X POST $API/v1/users/ava/credit \
-H "Authorization: Bearer $GS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(idem)" \
-d '{"amount": 500, "reason": "purchase", "reference": "appstore_2000000123",
"receipt_id": "rcp_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"}'
A bonus (a welcome gift, a reward) is a credit with
"bonus": true and neither a price nor a receipt. It is not a sale; up to 20%
of the coins you sell each month can be given away without counting:
curl -X POST $API/v1/users/ava/credit \
-H "Authorization: Bearer $GS_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(idem)" \
-d '{"amount": 100, "reason": "welcome_bonus", "reference": "welcome_ava", "bonus": true}'
Every credit names one of these three kinds, on test and live: a credit with none of them
is refused with 400 sale_kind_required.
Your fee is measured from live sales only: a sales share on the month's verified store sales, web sales, bonus excess and web spending floor: nothing on the first $2,000, 1.5% from $2,000 to $500,000 and 1% above $500,000 (the same on every paid plan). The monthly plan fee is always due on a paid plan. Plans and prices: Pricing.
Generate a new random UUID (or ULID) as the Idempotency-Key for each credit,
keep it with the order, and send that same key again only when you retry that request. Do
not use the order number as the key: order numbers belong in reference. Keys
are kept for 24 hours and are scoped by caller: your server (secret key) has its own key
space, and every app user (client token) has their own, so the same key from an app never
touches your server's requests.
4. Create a client token for that user
The token is what your app uses. Your server creates it for the signed-in user. It lasts one hour by default (between a minute and 24 hours).
curl -X POST $API/v1/client-tokens \
-H "Authorization: Bearer $GS_KEY" \
-H "Content-Type: application/json" \
-d '{"user": "ava"}'
# {"token":"gs_ct_…","user":"ava","expires_at":"2026-10-10T19:32:22.000Z"}
In your backend this is one endpoint your app calls after sign-in:
// GET /api/gift-token (Node, any framework)
// requireLogin and req.user are yours: req.user.id is the id you use for this person
// in credit calls (the "user" value), not a GiftingStack id.
app.get("/api/gift-token", requireLogin, async (req, res) => {
const r = await fetch("https://api.giftingstack.com/v1/client-tokens", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
user: req.user.id,
// name: req.user.displayName, // optional: shown to gift room viewers
// rooms: ["live_123"], // only for gift rooms: the rooms this user may join
}),
});
if (!r.ok) return res.status(502).send("could not create a gift token");
res.type("text/plain").send((await r.json()).token);
});
name and rooms are only needed for
gift rooms; a token without rooms cannot join one.
5. Send a gift as that user
This is what the app does, with the token instead of the secret key. The sender is always the user the token belongs to.
export TOKEN=gs_ct_the_token_from_step_4
curl $API/v1/client/me -H "Authorization: Bearer $TOKEN"
# {"user":"ava","balance":500,"livemode":false}
curl $API/v1/client/gifts -H "Authorization: Bearer $TOKEN"
curl -X POST $API/v1/client/gift-transactions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(idem)" \
-d '{"receiver": "bora", "gift_id": "pink-tulip"}'
The answer has the gift (id, name, icon), quantity,
unit_price, total and sender_balance_after. Send
with the same Idempotency-Key again and you get the same answer, not a second
gift. If the balance is too low you get 422 insufficient_balance: show your
coin store.
Or send from your server
When your server decides who sends what (a bot, a reward, a purchase flow), use the secret key and name the sender yourself:
curl -X POST $API/v1/gift-transactions -H "Authorization: Bearer $GS_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: $(idem)" -d '{"sender": "ava", "receiver": "mateo", "gift_id": "pink-tulip"}'
The sender pays the price. The receiver gets the gift in their received-gifts counter, never coins they can spend (here Mateo gets one pink tulip, not coins).
6. Show the tray and the effects in your app
For React Native and Expo the SDK gives you the tray, the effects, the combo counter and
the received-gifts data. It asks your server for the token from step 4 whenever it needs
one, and gets a new one when it expires. Install it with
npm i @giftingstack/sdk. post, openCoinStore and
the views are placeholders for your own app:
import { useRef } from "react";
import { View } from "react-native";
import { createGiftingClient } from "@giftingstack/sdk";
import {
GiftEffectLayer,
GiftTray,
useGiftTray,
} from "@giftingstack/sdk/react-native";
const client = createGiftingClient({
getToken: async () => (await fetch("https://your.app/api/gift-token")).text(),
});
function PostScreen({ post }) {
const effects = useRef(null);
const tray = useGiftTray({ client, receiver: post.authorId, effects });
return (
<View style={{ flex: 1 }}>
{/* your screen */}
<GiftTray tray={tray} onTopUp={openCoinStore} />
<GiftEffectLayer ref={effects} />
</View>
);
}
For the browser, the same client plays the same effects without a framework (post
and openCoinStore
are your own):
import { createGiftingClient } from "@giftingstack/sdk";
import { GiftEffectLayer, createGiftTray } from "@giftingstack/sdk/web";
const client = createGiftingClient({
getToken: async () => (await fetch("/api/gift-token")).text(),
});
const screen = document.querySelector("#screen");
const effects = new GiftEffectLayer(screen);
createGiftTray({ container: screen, client, receiver: post.authorId, effects, onTopUp: openCoinStore });
Flutter, Android and iOS SDKs work the same way: see SDKs. Or call the same endpoints from step 5 and draw the gifts yourself (the pictures come with each gift).
7. React to gifts on your server (webhooks)
In the project open Webhooks, choose Test, and add the
https address of your server. The signing secret is shown once. Use
Send test event to try it. Each request has a
GiftingStack-Signature header (t=…,v1=…): check it before you
trust the body.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a string), not parsed JSON.
function verify(rawBody, header, secret) {
const fields = header.split(",").map((p) => p.trim().split("="));
const t = Number(fields.find(([k]) => k === "t")?.[1]);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return fields.some(
([k, v]) => k === "v1" && v?.length === expected.length &&
timingSafeEqual(Buffer.from(v), Buffer.from(expected)),
);
}
A sent gift arrives as a gift.sent event. Every event has the same envelope:
id, sequence, type, created_at,
livemode (false for test events) and data.
POST https://your.app/webhooks/giftingstack
GiftingStack-Signature: t=1760000000,v1=…
{"id":"evt_…","sequence":1843,"type":"gift.sent","created_at":"2026-10-10T19:32:22.000Z",
"livemode":false,"data":{ … the gift transaction … }}
Answer with any 2xx. If your server fails or times out we try again, first after about a minute and then less often for several days. The same event can arrive twice, so handle an event id only once. The dashboard lists every delivery and has a button to send one again.
In plain words: what GiftingStack is not
- It never handles real money: no payments from your users, no payouts, no refunds.
- Coins are your in-app currency. They cannot be turned into cash and you cannot send coins from one user to another.
- A receiver gets a counter of received gifts, never spendable balance. There is nothing to withdraw.
8. Go live
- Add your gifts to the Live environment too (it has its own catalog).
-
Create a
gs_live_…key and use it in your production server. Test and live data never mix. Live keys need a paid plan (Starter or above). - Add a webhook address for Live.
The full reference for every endpoint, field and error is in the API reference.
Things you always do yourself: sell the coins and take the money, and call
credit from your server when a purchase is confirmed.