GiftingStack
Sign in Get started
Docs menu

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.

Checked against the API and the live OpenAPI on 2026-10-21.

Two different tools

Idempotency-Key (header)reference (body)
Protects againstRetrying the same request after a timeout or a dropped connection.Booking the same business event twice, even from two different requests.
Who makes itYour code: a new random UUID or ULID per operation.Your system: the order, store transaction or refund id.
Kept24 hours.Forever.
Same value againReturns 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}/credit and POST /v1/users/{id}/debit
  • POST /v1/gift-transactions and POST /v1/gift-transactions/{id}/reverse
  • DELETE /v1/users/{id}
  • PUT /v1/users/{id}/leaderboard-visibility
  • POST /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

  • reference is 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, so refund_ prefixes are optional.
  • On gift sends, reference is 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.