Docs
Idempotency and references
Networks fail. An Idempotency-Key lets you retry any request that moves coins without ever doing it twice. A reference makes a purchase unique for good.
Two different tools
Idempotency-Key (header) | reference (body) | |
|---|---|---|
| Protects against | Retrying the same request after a timeout or a dropped connection. | Booking the same business event twice, even from two different requests. |
| Who makes it | Your code: a new random UUID or ULID per operation. | Your system: the order, store transaction or refund id. |
| Kept | 24 hours. | Forever. |
| Same value again | Returns the first answer. | 409 duplicate_reference. |
Which requests need a key
The header is required (a missing one is 400 invalid_request) on every request that
changes balances or user data:
POST /v1/users/{id}/creditandPOST /v1/users/{id}/debitPOST /v1/gift-transactionsandPOST /v1/gift-transactions/{id}/reverseDELETE /v1/users/{id}PUT /v1/users/{id}/leaderboard-visibilityPOST /v1/client/gift-transactions(from an app)
Other requests (reads, catalog edits, client tokens, receipt verification) do not take one.
The rules
- Format. 1 to 255 printable ASCII characters (no spaces). Use a random UUID or ULID. Do not use an order number or anything guessable: that belongs in
reference. - One key, one operation. Make a new key for each new operation. Reuse a key only to retry that same request.
- Replay. The same key with the same endpoint and body returns the stored response with the same status and the header
Idempotent-Replayed: true. Nothing is changed again. Key order inside the JSON body does not matter. - Conflict. The same key with another endpoint or another body is
422 idempotency_key_reused. - Only successes are stored. A request that failed (any non-2xx) can be retried with the same key and is processed fresh. There is no "in progress" state: if you send the same key in parallel, one request does the work and the others get its answer.
- Retention. A key is honoured for 24 hours after its first successful use. After that the same key is a new request, so do not retry that late.
- Scopes. Keys live in separate spaces per project and environment. Requests with your secret key (and the dashboard) share one space; each client-token user has their own. The same string in another space is an unrelated request.
- Deleted users. After a user is deleted, stored responses that name them are redacted, so a replay cannot bring their data back.
References for credits and debits
referenceis required on credit and debit: 1 to 128 printable ASCII characters (no spaces), for example the store transaction id.- It is unique per operation type, forever. Reusing one on a credit is
409 duplicate_reference. A credit and a debit may share a reference, sorefund_prefixes are optional. - On gift sends,
referenceis only your own context (a post, a match). It does not have to be unique.
A safe retry
Make the key once, keep it with the order, and send the same key on every attempt. Real credit call in Node (18 or newer):
import { randomUUID } from "node:crypto";
const API = "https://api.giftingstack.com";
// A web shop sale: `price` and `currency` are what your user paid (a store sale sends
// `receipt_id` instead, a bonus `bonus: true`).
export async function creditPurchase(userId, orderId, coins, price, currency) {
const idempotencyKey = randomUUID(); // store it with the order if you may retry later
for (let attempt = 1; attempt <= 5; attempt++) {
try {
const res = await fetch(`${API}/v1/users/${encodeURIComponent(userId)}/credit`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GS_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ amount: coins, reason: "purchase", reference: orderId, price, currency }),
});
if (res.ok) return await res.json(); // 201 the first time, the same answer on a replay
if (res.status === 409) {
const { error } = await res.json();
if (error.type === "duplicate_reference") return null; // this order is already booked
throw new Error(`credit refused: ${error.type}`);
}
if (res.status !== 429 && res.status < 500) throw new Error(`credit refused: ${res.status}`);
} catch (err) {
if (!(err instanceof TypeError)) throw err; // fetch network errors are TypeErrors
}
await new Promise((r) => setTimeout(r, 2 ** attempt * 250));
}
throw new Error("credit did not complete; retry later with the same idempotency key");
}
If the loop gives up, keep the key and the order id: a later retry with the same key (inside 24
hours) either completes the credit or returns the first answer. Past 24 hours the
reference still prevents a double credit: a second attempt is
409 duplicate_reference.
Questions or something unclear? support@giftingstack.com.