{"openapi":"3.1.0","info":{"title":"GiftingStack API","version":"1.0.0","description":"Balances and user-to-user gifting for your app. GiftingStack never handles real money: you collect payments yourself and credit units through this API."},"servers":[{"url":"https://api.giftingstack.com"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Secret API key (`gs_live_…` or `gs_test_…`). Use it from your backend only."},"clientToken":{"type":"http","scheme":"bearer","description":"Client token (`gs_ct_…`) created by your backend with POST /v1/client-tokens. Safe to put in an app."}},"schemas":{"Health":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"service":{"type":"string"},"checks":{"type":"object","additionalProperties":{"type":"string","enum":["ok"]}},"alerts":{"type":"object","properties":{"open":{"type":"integer"},"oldest_age_seconds":{"type":"integer","nullable":true}},"required":["open","oldest_age_seconds"]},"signals":{"type":"object","properties":{"purge":{"type":"object","properties":{"last_ok_age_hours":{"type":"integer","nullable":true},"overdue_projects":{"type":"integer"},"max_overdue_days":{"type":"integer"}},"required":["last_ok_age_hours","overdue_projects","max_overdue_days"]},"webhooks":{"type":"object","properties":{"pending_over_6h":{"type":"integer"},"dead_lettered_24h":{"type":"integer"}},"required":["pending_over_6h","dead_lettered_24h"]},"creem_usage":{"type":"object","properties":{"pending_accounts":{"type":"integer"}},"required":["pending_accounts"]}},"required":["purge","webhooks","creem_usage"]},"degraded":{"type":"array","items":{"type":"string"}}},"required":["status","service"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","example":"insufficient_balance"},"message":{"type":"string"},"request_id":{"type":"string"},"retryable":{"type":"boolean"},"details":{"type":"object","additionalProperties":{"nullable":true}}},"required":["type","message","request_id","retryable"]}},"required":["error"]},"HealthDegraded":{"type":"object","properties":{"status":{"type":"string","enum":["degraded"]},"service":{"type":"string"},"codes":{"type":"array","items":{"type":"string"}}},"required":["status","service","codes"]},"ReceivedGifts":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"gift":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"icon_url":{"type":"string","nullable":true}},"required":["id","name","icon_url"]},"count":{"type":"integer","description":"How many of this gift the user currently holds. A counter, not a balance."}},"required":["gift","count"]}},"has_more":{"type":"boolean","enum":[false]}},"required":["data","has_more"]},"User":{"type":"object","properties":{"id":{"type":"string"},"external_id":{"type":"string"},"balance":{"type":"integer"},"metadata":{"type":"object","nullable":true,"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"created_at":{"type":"string","format":"date-time"},"deleted_at":{"type":"string","nullable":true,"format":"date-time","description":"Set once the user is deleted; `external_id` is then a `deleted_…` pseudonym."}},"required":["id","external_id","balance","metadata","created_at","deleted_at"]},"DeletedUser":{"type":"object","properties":{"id":{"type":"string","description":"The user's GiftingStack id. The `user.deleted` webhook carries the same id.","example":"usr_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"external_id":{"type":"string","description":"Pseudonym the user's history is kept under. Your external_id is not stored.","example":"deleted_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"deleted":{"type":"boolean","enum":[true]},"forfeited_balance":{"type":"integer","description":"Balance zeroed by a `forfeit` ledger entry (0 if it was already 0)."},"forfeit_entry_id":{"type":"string","nullable":true},"deleted_at":{"type":"string","format":"date-time"}},"required":["id","external_id","deleted","forfeited_balance","forfeit_entry_id","deleted_at"]},"LedgerEntry":{"type":"object","properties":{"id":{"type":"string","example":"led_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"sequence":{"type":"integer"},"user":{"type":"string","example":"user_123"},"type":{"type":"string","enum":["credit","debit","gift_sent","gift_refund","forfeit"]},"amount":{"type":"integer","description":"Signed: negative when the balance decreased."},"balance_after":{"type":"integer"},"reason":{"type":"string"},"reference":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true,"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"price":{"type":"string","nullable":true,"description":"Credits only: what your user paid your store, a decimal string in `currency`; null when not recorded and on every other entry type. Informational only: it records what your user paid your store, for revenue statistics. It never changes the credited `amount` or any balance, is never converted into coins, and GiftingStack never charges, holds or pays out this money.","example":"4.99"},"currency":{"type":"string","nullable":true,"example":"USD"},"price_source":{"type":"string","nullable":true,"enum":["receipt","declared",null],"description":"`receipt`: taken from a verified store receipt; `declared`: sent in the credit request."},"receipt_id":{"type":"string","nullable":true,"description":"The verified receipt (rcp_…) this credit was booked with, if any."},"created_at":{"type":"string","format":"date-time"}},"required":["id","sequence","user","type","amount","balance_after","reason","reference","metadata","price","currency","price_source","receipt_id","created_at"]},"CreditRequest":{"type":"object","properties":{"amount":{"type":"integer","minimum":1,"maximum":1000000000,"example":1000,"description":"Required. Units to add, 1 to 1,000,000,000."},"reason":{"type":"string","pattern":"^[a-z0-9_]{1,64}$","description":"Why the balance changed. Free text in the form ^[a-z0-9_]{1,64}$.","example":"purchase"},"reference":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"Your own id for this movement (e.g. the store transaction id). Unique per operation type, forever: reusing it returns 409.","example":"appstore_2000000123"},"metadata":{"type":"object","additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"price":{"type":"string","pattern":"^(0|[1-9][0-9]{0,9})(\\.[0-9]{1,3})?$","description":"What your user paid your store for these coins, as a decimal string in `currency` (at most the currency's decimals: \"4.99\" USD, \"500\" JPY, \"1.250\" KWD; greater than 0, at most 1000000000). Requires `currency`. Without `receipt_id`: a declared sale outside the app stores (`price_source: \"declared\"`). With `receipt_id` the store's price is used: on live sending `price` or `currency` as well is a 400; on test it must equal the store's. Informational only: it records what your user paid your store, for revenue statistics. It never changes the credited `amount` or any balance, is never converted into coins, and GiftingStack never charges, holds or pays out this money.","example":"4.99"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"ISO 4217 code of `price`, e.g. `USD`. Required with `price`.","example":"USD"},"receipt_id":{"type":"string","pattern":"^rcp_[0-9A-Z]{26}$","description":"The `receipt_id` returned by `POST /v1/receipts/verify` for this purchase (same project and environment): an app store sale. The credit records the store's price (`price_source: \"receipt\"`; none if the store gave none), and each receipt credits once: a second credit with the same receipt is `409 receipt_already_credited`. Retrying with the same `Idempotency-Key` and body replays the first credit.","example":"rcp_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"bonus":{"type":"boolean","description":"`true`: coins with no sale behind them (a reward, compensation, coins given back). Every credit names exactly one kind: `receipt_id` (store sale), `price` + `currency` (web sale) or `bonus: true`; none is a 400 `sale_kind_required`, and `bonus: true` with a receipt or a price is a 400 `invalid_request`.","example":true}},"required":["reason","reference"],"additionalProperties":{"nullable":true}},"DebitRequest":{"type":"object","properties":{"amount":{"type":"integer","minimum":1,"maximum":1000000000,"example":1000},"reason":{"type":"string","pattern":"^[a-z0-9_]{1,64}$","description":"Why the balance changed. Free text in the form ^[a-z0-9_]{1,64}$.","example":"spend"},"reference":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"Your own id for this movement (e.g. the store transaction id). Unique per operation type, forever: reusing it returns 409.","example":"refund_appstore_2000000123"},"metadata":{"type":"object","additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"allow_negative":{"type":"boolean","description":"Accept this debit even if the balance is insufficient (the balance may become negative). Meant for purchase refunds and chargebacks only. Like every balance change, a debit for an `external_id` the project has not seen yet creates that user first: with `allow_negative` the new user starts below zero (for example a chargeback booked before the user's first credit reached us).","example":false}},"required":["amount","reason","reference"]},"RejectedRequestPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RejectedRequest"}},"has_more":{"type":"boolean"}},"required":["data","has_more"]},"RejectedRequest":{"type":"object","properties":{"id":{"type":"string","example":"rej_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"request_id":{"type":"string","description":"The `request_id` of the refused request (its error body and `X-Request-Id`)."},"endpoint":{"type":"string","example":"POST /v1/users/{external_id}/credit"},"status":{"type":"integer","example":409},"error":{"type":"string","description":"The error `type` it was refused with.","example":"duplicate_reference"},"user":{"type":"string","nullable":true,"description":"The user's current external id (a `deleted_…` pseudonym after deletion); null when the user did not exist."},"balance":{"type":"integer","nullable":true,"description":"The user's balance at the moment of the refusal."},"reference":{"type":"string","nullable":true},"receipt_id":{"type":"string","nullable":true},"amount":{"type":"integer","nullable":true,"description":"Credits and debits: the amount asked for."},"gift_id":{"type":"string","nullable":true},"quantity":{"type":"integer","nullable":true},"gift_transaction_id":{"type":"string","nullable":true,"description":"Reversals: the gift transaction."},"source":{"type":"string","enum":["server","client","dashboard"],"description":"`server`: a secret key; `client`: a client token (your app); `dashboard`: a support change."},"actor_id":{"type":"string","description":"The API key (`key_…`) or dashboard account."},"idempotency_key":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"}},"required":["id","request_id","endpoint","status","error","user","balance","reference","receipt_id","amount","gift_id","quantity","gift_transaction_id","source","actor_id","idempotency_key","created_at"]},"LedgerPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LedgerEntry"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true,"description":"Pass as `starting_after` to get the next page; null when `has_more` is false. With a filter, a page looks at no more than 2,000 entries, so it can hold fewer than `limit` items, even none, while `has_more` is true: keep paging with `next_cursor`.","example":null}},"required":["data","has_more","next_cursor"]},"GiftList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Gift"}},"has_more":{"type":"boolean","enum":[false],"description":"Always false: a catalog holds at most 500 gifts and is not paginated."}},"required":["data","has_more"]},"Gift":{"type":"object","properties":{"id":{"type":"string","example":"rose"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"price":{"type":"integer"},"icon_url":{"type":"string","nullable":true},"animation_url":{"type":"string","nullable":true},"sort_order":{"type":"integer"},"active":{"type":"boolean","description":"Archived gifts cannot be sent."},"metadata":{"type":"object","nullable":true,"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","name","description","price","icon_url","animation_url","sort_order","active","metadata","created_at","updated_at"]},"CreateGiftRequest":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$","example":"rose","description":"Your own stable id. Cannot be changed later."},"name":{"type":"string","minLength":1,"maxLength":80,"example":"Rose"},"price":{"type":"integer","minimum":1,"maximum":1000000000,"description":"Price in your currency units. Changing it never affects past transactions.","example":100},"description":{"type":"string","nullable":true,"maxLength":280},"icon_url":{"type":"string","nullable":true,"maxLength":2048,"example":"https://cdn.example.com/rose.png","description":"https URL only."},"animation_url":{"type":"string","nullable":true,"maxLength":2048,"example":"https://cdn.example.com/rose.png","description":"https URL only."},"sort_order":{"type":"integer","description":"Lower first. Default 0."},"active":{"type":"boolean","description":"Default true."},"metadata":{"type":"object","nullable":true,"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}}},"required":["id","name","price"]},"UpdateGiftRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80,"example":"Rose"},"price":{"type":"integer","minimum":1,"maximum":1000000000,"description":"Price in your currency units. Changing it never affects past transactions.","example":100},"description":{"type":"string","nullable":true,"maxLength":280},"icon_url":{"type":"string","nullable":true,"maxLength":2048,"example":"https://cdn.example.com/rose.png"},"animation_url":{"type":"string","nullable":true,"maxLength":2048,"example":"https://cdn.example.com/rose.png"},"sort_order":{"type":"integer"},"active":{"type":"boolean"},"metadata":{"type":"object","nullable":true,"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}}},"description":"Only the fields you send change. Send `null` to clear an optional field."},"GiftTransactionPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/GiftTransaction"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true,"description":"Pass as `starting_after` to get the next page; null when `has_more` is false. With a filter, a page looks at no more than 2,000 entries, so it can hold fewer than `limit` items, even none, while `has_more` is true: keep paging with `next_cursor`.","example":null}},"required":["data","has_more","next_cursor"]},"GiftTransaction":{"type":"object","properties":{"id":{"type":"string"},"sequence":{"type":"integer"},"sender":{"type":"string"},"receiver":{"type":"string"},"gift":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","description":"Name when the gift was sent."},"icon_url":{"type":"string","nullable":true}},"required":["id","name","icon_url"]},"quantity":{"type":"integer"},"unit_price":{"type":"integer","description":"Price when the gift was sent."},"total":{"type":"integer"},"message":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true,"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"status":{"type":"string","enum":["completed","reversed"]},"reversal":{"$ref":"#/components/schemas/GiftReversal"},"created_at":{"type":"string","format":"date-time"}},"required":["id","sequence","sender","receiver","gift","quantity","unit_price","total","message","reference","metadata","status","reversal","created_at"]},"GiftReversal":{"type":"object","nullable":true,"properties":{"id":{"type":"string","example":"grv_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"reason":{"type":"string"},"reference":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true,"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"created_at":{"type":"string","format":"date-time"}},"required":["id","reason","reference","metadata","created_at"]},"GiftTransactionResult":{"allOf":[{"$ref":"#/components/schemas/GiftTransaction"},{"type":"object","properties":{"sender_balance_after":{"type":"integer"}},"required":["sender_balance_after"]}]},"SendGiftRequest":{"type":"object","properties":{"sender":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","example":"user_123"},"receiver":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","example":"user_456"},"gift_id":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$","example":"rose"},"quantity":{"type":"integer","minimum":1,"maximum":1000,"default":1,"example":1},"message":{"type":"string","maxLength":280,"description":"Optional note shown with the gift. User content: do not log it.","example":"Tebrikler!"},"reference":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"Your own context id (a post, a chat, a match). Not required to be unique.","example":"post_99"},"metadata":{"type":"object","additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}},"room_id":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","description":"Gift room to show this gift in. After the gift is sent, everyone connected to the room gets a `gift` event (best effort, never delays or fails the send). Retries with the same Idempotency-Key must name the same room. With a client token the room must be in the token's `rooms` (else 403 `room_not_allowed`); a secret key may name any room of the project. Nothing is published while the project's rooms are switched off.","example":"live_123"}},"required":["sender","receiver","gift_id"]},"ReverseGiftRequest":{"type":"object","properties":{"reason":{"type":"string","pattern":"^[a-z0-9_]{1,64}$","description":"Why the gift is reversed, e.g. fraud, chargeback, refund, moderation.","example":"chargeback"},"reference":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","example":"case_8812"},"metadata":{"type":"object","additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"nullable":true}]},"description":"Up to 20 keys (1-40 characters of A-Z a-z 0-9 _ . -), string values up to 500 characters, 2 KB in total. Do not store personal data here.","example":{"provider":"app_store","product_id":"coins_1000"}}},"required":["reason"]},"EventPage":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","description":"Pass it as `cursor` to get what comes after this page. Always present, also on the last page: keep it and ask again later.","example":"ev1.WzE4NDNd"}},"required":["data","has_more","next_cursor"]},"Event":{"type":"object","properties":{"id":{"type":"string","example":"evt_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"sequence":{"type":"integer","description":"Strictly increasing per project and environment."},"type":{"type":"string","enum":["gift.sent","gift.reversed","balance.credited","balance.debited","user.deleted","purchase.refunded"]},"created_at":{"type":"string","format":"date-time"},"livemode":{"type":"boolean"},"data":{"type":"object","additionalProperties":{"nullable":true},"description":"Same payload as the webhook of this event."}},"required":["id","sequence","type","created_at","livemode","data"]},"Leaderboard":{"type":"object","properties":{"board":{"type":"string","enum":["senders","receivers"]},"period":{"type":"string","enum":["day","week","month","all"]},"metric":{"type":"string","enum":["units","gifts"]},"label":{"type":"string","example":"2026-W41","description":"`2026-10-11`, `2026-W41`, `2026-10` or `all`."},"starts_at":{"type":"string","nullable":true,"format":"date-time","description":"`null` for `all`."},"ends_at":{"type":"string","nullable":true,"format":"date-time","description":"Exclusive. `null` for `all`."},"as_of":{"type":"string","format":"date-time","description":"When the ranking was computed. At most 30 s old for day and all time, 2 min for week and month, 10 min for finished periods."},"data":{"type":"array","items":{"$ref":"#/components/schemas/LeaderboardEntry"},"description":"Ordered by the metric, highest first; ties by account age (older first)."},"has_more":{"type":"boolean","description":"More ranked users exist beyond `limit`."},"me":{"$ref":"#/components/schemas/LeaderboardMe"}},"required":["board","period","metric","label","starts_at","ends_at","as_of","data","has_more","me"]},"LeaderboardEntry":{"type":"object","properties":{"rank":{"type":"integer","description":"Competition rank on the metric: equal scores share a rank (1, 2, 2, 4)."},"user":{"type":"string","description":"The user's external id."},"gifts":{"type":"integer","description":"Number of gifts (sum of quantities)."},"units":{"type":"integer","description":"Value of the gifts in your own coin units. For receivers this is a display counter, never a balance."}},"required":["rank","user","gifts","units"]},"LeaderboardMe":{"type":"object","nullable":true,"properties":{"user":{"type":"string"},"rank":{"type":"integer","nullable":true,"description":"Exact rank when within the top 1,000, else `null` (show \"1,000+\"). Also `null` without a score in the period or when hidden."},"gifts":{"type":"integer"},"units":{"type":"integer"},"hidden":{"type":"boolean","description":"The user opted out of every leaderboard."}},"required":["user","rank","gifts","units","hidden"]},"LeaderboardVisibility":{"type":"object","properties":{"user":{"type":"string"},"hidden":{"type":"boolean"}},"required":["user","hidden"]},"LeaderboardVisibilityRequest":{"type":"object","properties":{"hidden":{"type":"boolean","description":"`true` removes the user from every leaderboard at once (their counters are kept); `false` shows them again."}},"required":["hidden"],"additionalProperties":false},"AwaySummary":{"type":"object","properties":{"user":{"type":"string"},"since":{"type":"string","format":"date-time","description":"Start of the window."},"until":{"type":"string","format":"date-time","description":"When the summary was read."},"cursor":{"type":"string","nullable":true,"description":"Send this to `PUT .../away/cursor` after showing the summary. Unchanged when nothing is new."},"truncated":{"type":"boolean","description":"More than 2,000 gifts arrived in the window: the totals are lower bounds."},"total_gifts":{"type":"integer"},"total_units":{"type":"integer"},"sender_count":{"type":"integer"},"groups":{"type":"array","items":{"type":"object","properties":{"gift":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer","description":"Today's catalog price, so the effect size matches the gift tray."},"icon_url":{"type":"string","nullable":true},"animation_url":{"type":"string","nullable":true}},"required":["id","name","price","icon_url","animation_url"]},"count":{"type":"integer"},"units":{"type":"integer"},"sender_count":{"type":"integer"},"senders":{"type":"array","items":{"type":"object","properties":{"user":{"type":"string","nullable":true,"description":"External id; `null` = a deleted sender (all of them merged into one entry)."},"count":{"type":"integer"}},"required":["user","count"]},"description":"At most 3."}},"required":["gift","count","units","sender_count","senders"]},"description":"Biggest first (by units), at most `max_groups`."},"top_senders":{"type":"array","items":{"type":"object","properties":{"user":{"type":"string","nullable":true,"description":"External id; `null` = a deleted sender (all of them merged into one entry)."},"count":{"type":"integer"},"units":{"type":"integer"}},"required":["user","count","units"]},"description":"At most 5, by gift count."}},"required":["user","since","until","cursor","truncated","total_gifts","total_units","sender_count","groups","top_senders"]},"Inbox":{"type":"object","properties":{"user":{"type":"string"},"since":{"type":"string","format":"date-time","description":"Start of the window (24 hours ago)."},"until":{"type":"string","format":"date-time","description":"When the page was read."},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The gift transaction (`gtx_...`)."},"created_at":{"type":"string","format":"date-time"},"sender":{"type":"string","nullable":true,"description":"External id of the sender; `null` = a deleted sender."},"gift":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"price":{"type":"integer","description":"Today's catalog price, so the effect size matches the gift tray."},"icon_url":{"type":"string","nullable":true},"animation_url":{"type":"string","nullable":true}},"required":["id","name","price","icon_url","animation_url"]},"quantity":{"type":"integer"},"units":{"type":"integer","description":"Value of the gift (price x quantity) when it was sent."},"status":{"type":"string","enum":["completed","reversed"],"description":"A reversed gift stays in the list with `reversed`; the SDKs hide it."}},"required":["id","created_at","sender","gift","quantity","units","status"]},"description":"Newest first. Never the gift's message, metadata or reference."},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true,"description":"Send it as `cursor` for the next (older) page; `null` on the last page."}},"required":["user","since","until","data","has_more","next_cursor"]},"AwayCursorRequest":{"type":"object","properties":{"cursor":{"type":"string","minLength":1,"maxLength":128,"description":"The `cursor` of the summary the person has seen.","example":"aw1.MTcyODYzMTgwMjUxMS40MjE3"}},"required":["cursor"],"additionalProperties":false},"RevokedClientToken":{"type":"object","properties":{"revoked":{"type":"string","enum":["token","user"]},"user":{"type":"string","description":"Echo of `user` when revoking by user."},"effective_within_seconds":{"type":"integer","description":"Verification caches results for this long, so the revocation reaches every edge location (and the rooms service) within 30 seconds.","example":30}},"required":["revoked","effective_within_seconds"]},"RevokeClientTokenRequest":{"type":"object","properties":{"token":{"type":"string","minLength":1,"maxLength":4096,"description":"One client token (`gs_ct_...`) minted with this key's project and environment. Tokens minted before revocation existed carry no id and can only be revoked by `user`."},"user":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Revoke every token of this end user minted up to now (including the current second). Tokens minted afterwards work.","example":"user_123"}},"additionalProperties":false},"ClientToken":{"type":"object","properties":{"token":{"type":"string","description":"Give this to your app. Starts with `gs_ct_`."},"user":{"type":"string"},"expires_at":{"type":"string","format":"date-time"}},"required":["token","user","expires_at"]},"CreateClientTokenRequest":{"type":"object","properties":{"user":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"The end user the token acts for. The app can only send gifts as this user.","example":"user_123"},"ttl_seconds":{"type":"integer","minimum":60,"maximum":86400,"default":3600,"description":"Lifetime in seconds, 60 to 86400. Default 3600.","example":3600},"rooms":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,63}\\*$|^[A-Za-z0-9_.:-]{1,64}$"},"minItems":1,"maxItems":8,"description":"Gift rooms this token may join and send into (`room_id`): room ids, or prefixes ending in `*` (`match_*`), at most 8. Required for rooms: without it the token has no room access at all.","example":["live_123"]},"name":{"type":"string","pattern":"^[^\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]{1,32}$/u","description":"Display name shown to gift room viewers with this user's gifts. Without it apps show their own name for the user, or a placeholder.","example":"Ayse"}},"required":["user"],"additionalProperties":false},"ReceiptVerification":{"type":"object","properties":{"valid":{"type":"boolean","description":"True only for an authentic, paid, not refunded purchase. Grant coins only when this is true **and** `already_seen` is false."},"status":{"type":"string","enum":["purchased","revoked","pending","cancelled"]},"receipt_id":{"type":"string","nullable":true,"description":"`rcp_…`, the stored verification. Null when `valid` is false."},"platform":{"type":"string","enum":["apple","google"]},"environment":{"type":"string","enum":["live","test"]},"transaction_id":{"type":"string","description":"Apple `transactionId`; Google `orderId` (or `token:` + a hash of the purchase token when Google gives no order id)."},"original_transaction_id":{"type":"string","nullable":true},"product_id":{"type":"string"},"purchased_at":{"type":"string","format":"date-time"},"price":{"type":"string","nullable":true,"description":"What the user paid, as a decimal string in major units (`\"4.99\"`). Apple: from the signed transaction (null on older payloads). Google: the order's total from the Play Developer API (`orders.get`, all units together; null for promo or rewarded purchases and when the order cannot be read, see `price_missing`). Always the store's price, never one you enter.","example":"4.99"},"currency":{"type":"string","nullable":true,"description":"ISO 4217 code.","example":"USD"},"price_source":{"type":"string","nullable":true,"enum":["store",null],"description":"`store` when `price` is set; null otherwise."},"price_missing":{"type":"string","nullable":true,"enum":["permission","not_found","unavailable",null],"description":"Why a paid Google purchase has no price: `permission` (grant the service account 'View financial data' in Play Console), `not_found` (Google has no order for it) or `unavailable` (Google did not answer). The receipt is still valid; verifying it again later fills the price in. Null when priced or free."},"quantity":{"type":"integer"},"already_seen":{"type":"boolean","description":"True when this purchase was verified before in this project and environment. Do not grant it again."},"first_seen":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time"},"same_user":{"type":"boolean","nullable":true,"description":"Whether `user` matches the first verification's. Null if either had none."}},"required":["at","same_user"]},"google":{"type":"object","nullable":true,"properties":{"order_id":{"type":"string","nullable":true},"acknowledged":{"type":"boolean"},"consumed":{"type":"boolean"},"purchase_type":{"type":"string","nullable":true,"enum":["test","promo","rewarded",null]}},"required":["order_id","acknowledged","consumed","purchase_type"],"description":"Google Play only."}},"required":["valid","status","receipt_id","platform","environment","transaction_id","original_transaction_id","product_id","purchased_at","price","currency","price_source","price_missing","quantity","already_seen","first_seen","google"]},"ReceiptVerifyRequest":{"oneOf":[{"$ref":"#/components/schemas/AppleReceiptVerifyRequest"},{"$ref":"#/components/schemas/GoogleReceiptVerifyRequest"}],"discriminator":{"propertyName":"platform","mapping":{"apple":"#/components/schemas/AppleReceiptVerifyRequest","google":"#/components/schemas/GoogleReceiptVerifyRequest"}}},"AppleReceiptVerifyRequest":{"type":"object","properties":{"platform":{"type":"string","enum":["apple"]},"signed_transaction":{"type":"string","minLength":1,"maxLength":12000,"description":"The JWS of the transaction: StoreKit 2 `Transaction.jwsRepresentation` from the app, or `signedTransactionInfo` from the App Store Server API or a notification."},"product_id":{"type":"string","pattern":"^[\\x21-\\x7E]{1,200}$","description":"Optional: refuse the receipt if it is for another product."},"user":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"The end user you intend to credit for this purchase. Only a hash is stored; when the same purchase is verified again, `first_seen.same_user` tells you whether it was for the same user.","example":"user_123"}},"required":["platform","signed_transaction"],"additionalProperties":false},"GoogleReceiptVerifyRequest":{"type":"object","properties":{"platform":{"type":"string","enum":["google"]},"purchase_token":{"type":"string","pattern":"^[A-Za-z0-9._-]{10,1000}$","description":"`Purchase.getPurchaseToken()` from Google Play Billing."},"product_id":{"type":"string","pattern":"^[a-z0-9][a-z0-9._]{0,138}$","description":"The product id the token was bought for.","example":"coins_500"},"user":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"The end user you intend to credit for this purchase. Only a hash is stored; when the same purchase is verified again, `first_seen.same_user` tells you whether it was for the same user.","example":"user_123"}},"required":["platform","purchase_token","product_id"],"additionalProperties":false},"Lookup":{"type":"object","properties":{"receipts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string","enum":["apple","google"]},"transaction_id":{"type":"string"},"product_id":{"type":"string"},"purchased_at":{"type":"string","format":"date-time"},"verified_at":{"type":"string","format":"date-time"},"price":{"type":"string","nullable":true},"currency":{"type":"string","nullable":true},"quantity":{"type":"integer"},"duplicate_attempts":{"type":"integer"},"last_duplicate_at":{"type":"string","nullable":true,"format":"date-time"},"credited_at":{"type":"string","nullable":true,"format":"date-time","description":"When a credit claimed this receipt (receipt_id on POST /v1/users/{id}/credit)."},"credit_entry_id":{"type":"string","nullable":true},"revoked_at":{"type":"string","nullable":true,"format":"date-time","description":"When a later verification found the purchase refunded or revoked."}},"required":["id","platform","transaction_id","product_id","purchased_at","verified_at","price","currency","quantity","duplicate_attempts","last_duplicate_at","credited_at","credit_entry_id","revoked_at"]},"description":"Verified store receipts: the one asked for, or those the entries were credited with."},"entries":{"type":"array","items":{"$ref":"#/components/schemas/LedgerEntry"},"description":"Ledger entries, oldest first: the one asked for, or every entry with this reference."},"webhook_deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDeliveryRecord"},"description":"Deliveries of the entries' events to your endpoints (kept 90 days)."},"rejected_requests":{"type":"array","items":{"$ref":"#/components/schemas/RejectedRequest"},"description":"Refused requests for the same reference, receipt or request, newest first (kept 180 days, at most 100)."},"has_more_rejected":{"type":"boolean"}},"required":["receipts","entries","webhook_deliveries","rejected_requests","has_more_rejected"]},"WebhookDeliveryRecord":{"type":"object","properties":{"event_id":{"type":"string"},"event_type":{"type":"string","example":"balance.credited"},"endpoint_id":{"type":"string"},"status":{"type":"string","enum":["pending","succeeded","failed","cancelled"]},"attempts":{"type":"integer"},"last_status_code":{"type":"integer","nullable":true,"description":"The HTTP status your endpoint answered last."},"first_attempt_at":{"type":"string","nullable":true,"format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["event_id","event_type","endpoint_id","status","attempts","last_status_code","first_attempt_at","updated_at"]},"BattleResponse":{"type":"object","properties":{"battle":{"$ref":"#/components/schemas/Battle"},"replayed":{"type":"boolean","description":"true: the start named the current round; nothing was reset"}},"required":["battle"]},"Battle":{"type":"object","nullable":true,"properties":{"round":{"type":"string","example":"match_42_r1"},"started":{"type":"integer","description":"Epoch ms","example":1760180000000},"ends":{"type":"integer","description":"Epoch ms. Gifts committed at or after this no longer count.","example":1760180300000},"scores":{"type":"array","items":{"$ref":"#/components/schemas/BattleScore"}}},"required":["round","started","ends","scores"],"description":"null: no round in this room"},"BattleScore":{"type":"object","properties":{"id":{"type":"string","description":"Receiver (your user id)","example":"ava"},"units":{"type":"integer","description":"Sum of unit price x quantity of the gifts counted for this receiver","example":1250},"gifts":{"type":"integer","description":"Number of gifts counted","example":9}},"required":["id","units","gifts"]},"BattleStart":{"type":"object","properties":{"round":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","description":"Your id for this round; it makes retries safe. A start that names the room's current round changes nothing (`replayed: true`); one that names an earlier round of the room's last 16 is refused with 409 `round_superseded`. Use a new id for each round.","example":"match_42_r1"},"receivers":{"type":"array","items":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"minItems":1,"maxItems":8,"description":"The users who compete (1 to 8 distinct ids), shown at 0 from the start in this order. Only gifts to them count: gifts to anyone else in the room still play but never take a place in the round.","example":["ava","mateo"]},"duration_s":{"type":"integer","minimum":1,"maximum":86400,"description":"Seconds until the round stops counting (default and maximum 86,400). End it sooner with the end endpoint.","example":300}},"required":["round","receivers"],"additionalProperties":false},"ClientMe":{"type":"object","properties":{"user":{"type":"string"},"balance":{"type":"integer","description":"0 for a user who was never credited."},"livemode":{"type":"boolean","description":"false: the token was made with a test key (the test environment allows 50 users and 5,000 operations a month). The SDK trays show a \"Test mode\" badge."}},"required":["user","balance","livemode"]},"ClientGiftList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ClientGift"}},"has_more":{"type":"boolean","enum":[false]}},"required":["data","has_more"]},"ClientGift":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"price":{"type":"integer"},"icon_url":{"type":"string","nullable":true},"animation_url":{"type":"string","nullable":true},"sort_order":{"type":"integer"}},"required":["id","name","description","price","icon_url","animation_url","sort_order"]},"ClientGiftTransaction":{"type":"object","properties":{"id":{"type":"string"},"sender":{"type":"string"},"receiver":{"type":"string"},"gift":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"icon_url":{"type":"string","nullable":true}},"required":["id","name","icon_url"]},"quantity":{"type":"integer"},"unit_price":{"type":"integer"},"total":{"type":"integer"},"message":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true},"status":{"type":"string","enum":["completed","reversed"]},"created_at":{"type":"string","format":"date-time"},"sender_balance_after":{"type":"integer"}},"required":["id","sender","receiver","gift","quantity","unit_price","total","message","reference","status","created_at","sender_balance_after"]},"ClientSendGiftRequest":{"type":"object","properties":{"receiver":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","example":"user_456"},"gift_id":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$","example":"rose"},"quantity":{"type":"integer","minimum":1,"maximum":1000,"default":1,"example":1},"message":{"type":"string","maxLength":280,"example":"Congrats!"},"reference":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"Your own context id, for example the post the gift is on.","example":"post_99"},"room_id":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","description":"Gift room to show this gift in. After the gift is sent, everyone connected to the room gets a `gift` event (best effort, never delays or fails the send). Retries with the same Idempotency-Key must name the same room. With a client token the room must be in the token's `rooms` (else 403 `room_not_allowed`); a secret key may name any room of the project. Nothing is published while the project's rooms are switched off.","example":"live_123"}},"required":["receiver","gift_id"],"additionalProperties":false},"GiftWallList":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"receiver":{"type":"string"},"reference":{"type":"string","nullable":true},"gifts":{"type":"array","items":{"type":"object","properties":{"gift":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"icon_url":{"type":"string","nullable":true}},"required":["id","name","icon_url"]},"count":{"type":"integer"}},"required":["gift","count"]}}},"required":["receiver","reference","gifts"]}}},"required":["data"]},"GiftWallQueryRequest":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"receiver":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"reference":{"type":"string","nullable":true,"pattern":"^[\\x21-\\x7E]{1,128}$","description":"Absent or `null`: the receiver's whole profile wall.","example":"post_99"}},"required":["receiver"]},"minItems":1,"maxItems":25}},"required":["items"],"additionalProperties":false}},"parameters":{}},"paths":{"/v1/health":{"get":{"tags":["System"],"summary":"Service health check","description":"Public. Does not require an API key. With `deep=1` it also reads the database, checks that the rooms bindings exist and adds operational numbers (rate limited per IP, 429 with `Retry-After`): `checks`, `alerts` (open system alerts: a count and the age of the oldest), `signals` (the daily purge job's age and overdue projects; webhook deliveries pending over 6 hours and dead-lettered in the last 24 hours; accounts whose closed-month Creem usage share is still not fixed after day 3) and `degraded`, a short list of codes for signals beyond their limit. `status` stays `ok` while `degraded` is not empty: it is a warning, not an outage. A failed dependency answers 503 with codes only. Numbers only, never customer data.","parameters":[{"schema":{"type":"string","enum":["0","1"]},"required":false,"name":"deep","in":"query"}],"responses":{"200":{"description":"The service is running","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"429":{"description":"`rate_limited` (deep check only): too many deep checks from this IP","headers":{"Retry-After":{"schema":{"type":"string","example":"10"},"required":true}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"A dependency failed (deep check only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthDegraded"}}}}}}},"/v1/users/{external_id}/received-gifts":{"get":{"tags":["Users"],"summary":"List the gifts a user has received","description":"A counter per gift. Received gifts are never spendable and never add to the user's balance. With `reference`, only the gifts sent with that reference (a post's wall) are counted; an unknown user then has an empty list.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"Count only the gifts sent with this `reference` (a post's wall).","example":"post_99"},"required":false,"description":"Count only the gifts sent with this `reference` (a post's wall).","name":"reference","in":"query"}],"responses":{"200":{"description":"Gifts the user currently holds","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceivedGifts"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}":{"get":{"tags":["Users"],"summary":"Get a user and balance","description":"Users are created automatically by their first credit.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"}],"responses":{"200":{"description":"The user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Users"],"summary":"Delete a user (GDPR)","description":"Removes the user's personal data and keeps the economic history under a pseudonym: a non-zero balance is zeroed by a `forfeit` ledger entry, `external_id` becomes `deleted_…`, metadata and gift messages are erased, store receipts verified with this user lose their link to them (verifying one again answers `first_seen.same_user: null`), and a `user.deleted` webhook is sent. Your external_id is not stored afterwards; using it again creates a new user. Store the returned `id` if you need to match the webhook.\n\n`Idempotency-Key` is required. Deletion is never refused for a suspended project or for one scheduled for deletion (until the purge, then 410 `project_deleted`) and is not counted against the test environment's caps. A deleted user cannot be deleted again (422 `user_deleted`) and every other change to them is refused with the same error. Events already in the webhook queue keep their original payload; `GET /v1/events` and resends show the redacted copy.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]+$","description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","example":"7f1c2e0a-4d1e-4c2b-9a53-0f4c8a1b2c3d"},"required":true,"description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","name":"idempotency-key","in":"header"}],"responses":{"200":{"description":"Deleted. A replayed request returns the original response with `Idempotent-Replayed: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletedUser"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted` (already deleted), or `idempotency_key_reused` (the key was used for a different request)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/credit":{"post":{"tags":["Balance"],"summary":"Add units to a user's balance","description":"Call this from your backend after your own payment system confirms a purchase. `reference` makes the credit permanently unique (409 `duplicate_reference`); `Idempotency-Key` is required and makes retries safe. The user is created by their first credit. `amount` is 1 to 1,000,000,000, and a balance never leaves the allowed range (422 `balance_limit_exceeded`).\n\n**Every credit names exactly one kind, on live and test** (decision 2026-10-23): a store sale (`receipt_id`), a web sale (`price` + `currency`) or a bonus (`bonus: true`). None of them is a 400 `sale_kind_required`; `bonus: true` with a `receipt_id` or a `price` is a 400 `invalid_request`. Prices are informational: they never change `amount` or any balance and no money moves through GiftingStack. They measure the sales GiftingStack's fee is computed from.\n- **App Store / Google Play sale:** send only the `receipt_id` from `POST /v1/receipts/verify`. The price and currency come from the store (`price_source: \"receipt\"`), never from the request: on live, `price` or `currency` next to `receipt_id` is a 400 `invalid_request`. On live, a Google Play receipt whose price could not be read is a 422 `price_missing` (give the service account the View financial data permission and verify the receipt again); a purchase the store gives no price for at all (a free promo) is credited without one, as on test.\n- **Sale outside the app stores (web shop):** send `price` + `currency` without `receipt_id` (`price_source: \"declared\"`). It counts in full as a declared web sale. On live the currency must be USD, EUR or a currency of the European Central Bank reference rates (400 `currency_not_supported`).\n- **No sale** (bonus, reward, coins given back): send `bonus: true` and neither a receipt nor a price. See the dashboard's Billing page for how bonus coins far above sold coins are counted.\n\n**Receipts:** the receipt must belong to this project and environment, be valid and not used before. Each receipt credits at most once, however many requests race. If the request fails before the ledger answers, retry with the same `Idempotency-Key` and body: the receipt stays reserved for exactly that request. In the TEST environment a `price` sent with a receipt must equal the store's (422 `receipt_price_mismatch`), and a receipt without a store price takes the declared one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]+$","description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","example":"7f1c2e0a-4d1e-4c2b-9a53-0f4c8a1b2c3d"},"required":true,"description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditRequest"}}}},"responses":{"201":{"description":"Credited. A replayed request returns the original response with `Idempotent-Replayed: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LedgerEntry"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one. `sale_kind_required`: the credit names no kind: send `receipt_id`, `price` + `currency`, or `bonus: true`. `currency_not_supported`: a live web sale in a currency without a European Central Bank rate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`test_limit_reached` (test keys only): the test environment allows 50 users and 5,000 operations a month (details: `limit` = `users` or `monthly_operations`, `max`). Live keys have no such caps.; or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`receipt_not_found`: no verified receipt with this id in this project and environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`duplicate_reference`, or `receipt_already_credited` (details: `credited_at`, `ledger_entry_id`); or `project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`, `balance_limit_exceeded`, `idempotency_key_reused`, `receipt_revoked` (the store reported a refund), `receipt_price_mismatch` (details: `receipt_price`, `receipt_currency`), or `price_missing` (live: the Google Play price of the receipt could not be read)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/debit":{"post":{"tags":["Balance"],"summary":"Remove units from a user's balance","description":"Fails with 422 `insufficient_balance` when the balance is insufficient, unless `allow_negative` is true. Use `allow_negative` only to book purchase refunds and chargebacks. `reference` is unique forever per operation type (409 `duplicate_reference`) and `Idempotency-Key` is required.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]+$","description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","example":"7f1c2e0a-4d1e-4c2b-9a53-0f4c8a1b2c3d"},"required":true,"description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DebitRequest"}}}},"responses":{"201":{"description":"Debited. A replayed request returns the original response with `Idempotent-Replayed: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LedgerEntry"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`test_limit_reached` (test keys only): the test environment allows 50 users and 5,000 operations a month (details: `limit` = `users` or `monthly_operations`, `max`). Live keys have no such caps.; or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`duplicate_reference`; or `project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`insufficient_balance`, `balance_limit_exceeded`, `user_deleted`, or `idempotency_key_reused` (the key was used for a different request)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/rejected-requests":{"get":{"tags":["Support"],"summary":"List a user's refused requests","description":"Credits, debits, gift sends and reversals for this user that were refused, newest first: what was asked, the error, and the user's balance at that moment. Kept 180 days. A refusal before the user's first credit has no user yet: find it with `GET /v1/lookup` by its `reference` or `request_id`. Page with `starting_after`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":100,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor: the id of the last refusal of the previous page."},"required":false,"description":"Cursor: the id of the last refusal of the previous page.","name":"starting_after","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Inclusive, ISO 8601."},"required":false,"description":"Inclusive, ISO 8601.","name":"from","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Exclusive, ISO 8601."},"required":false,"description":"Exclusive, ISO 8601.","name":"to","in":"query"}],"responses":{"200":{"description":"A page of refused requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectedRequestPage"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/ledger":{"get":{"tags":["Balance"],"summary":"List a user's ledger entries","description":"Newest first. Page with `starting_after`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":100,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Cursor: the id of the last entry of the previous page."},"required":false,"description":"Cursor: the id of the last entry of the previous page.","name":"starting_after","in":"query"},{"schema":{"type":"string","enum":["credit","debit","gift_sent","gift_refund","forfeit"]},"required":false,"name":"type","in":"query"},{"schema":{"type":"string","pattern":"^[a-z0-9_]{1,64}$"},"required":false,"name":"reason","in":"query"},{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$"},"required":false,"name":"reference","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Inclusive, ISO 8601."},"required":false,"description":"Inclusive, ISO 8601.","name":"from","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Exclusive, ISO 8601."},"required":false,"description":"Exclusive, ISO 8601.","name":"to","in":"query"}],"responses":{"200":{"description":"A page of entries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LedgerPage"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/gifts":{"get":{"tags":["Gift catalog"],"summary":"List the gift catalog","description":"Ordered by `sort_order`, then `id`. Includes archived gifts unless filtered.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"Filter by archived state. Omit for all gifts."},"required":false,"description":"Filter by archived state. Omit for all gifts.","name":"active","in":"query"}],"responses":{"200":{"description":"The catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftList"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Gift catalog"],"summary":"Create a gift","description":"The `id` is yours and permanent. Creating is not idempotent (no `Idempotency-Key`): a repeat returns 409 `gift_exists`. Catalog edits are not counted towards your plan. A catalog holds at most 500 gifts (422 `catalog_full` beyond that); `icon_url` and `animation_url` must be https URLs.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGiftRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gift"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`gift_exists`; or `project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`catalog_full`: the catalog already holds 500 gifts (`details.limit`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/gifts/{gift_id}":{"get":{"tags":["Gift catalog"],"summary":"Get a gift","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$","example":"rose"},"required":true,"name":"gift_id","in":"path"}],"responses":{"200":{"description":"The gift","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gift"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such gift","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Gift catalog"],"summary":"Update a gift","description":"Partial update. A price change applies to future sends only; past transactions keep the price they were sent at.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$","example":"rose"},"required":true,"name":"gift_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGiftRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gift"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such gift","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Gift catalog"],"summary":"Archive a gift","description":"Gifts are never deleted because past transactions reference them. Archiving sets `active` to false, so the gift can no longer be sent. Re-enable it with PATCH `{ \"active\": true }`.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$","example":"rose"},"required":true,"name":"gift_id","in":"path"}],"responses":{"200":{"description":"The archived gift","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gift"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such gift","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/gift-transactions":{"get":{"tags":["Gifts"],"summary":"List gift transactions","description":"Newest first. Filter by `sender`, `receiver`, `gift_id` or `status`. Without a `sender` or `receiver`, `from` and `to` are required and may span at most 31 days.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$"},"required":false,"name":"sender","in":"query"},{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$"},"required":false,"name":"receiver","in":"query"},{"schema":{"type":"string","pattern":"^[a-z0-9_-]{1,64}$"},"required":false,"name":"gift_id","in":"query"},{"schema":{"type":"string","enum":["completed","reversed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Inclusive, ISO 8601. Required together with `to` (at most 31 days) when neither `sender` nor `receiver` is given."},"required":false,"description":"Inclusive, ISO 8601. Required together with `to` (at most 31 days) when neither `sender` nor `receiver` is given.","name":"from","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Exclusive, ISO 8601."},"required":false,"description":"Exclusive, ISO 8601.","name":"to","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","pattern":"^gtx_[0-9A-Z]{26}$","description":"Cursor: the id of the last transaction of the previous page."},"required":false,"description":"Cursor: the id of the last transaction of the previous page.","name":"starting_after","in":"query"}],"responses":{"200":{"description":"A page of transactions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftTransactionPage"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Gifts"],"summary":"Send a gift","description":"Debits `price × quantity` from the sender and adds the gift to the receiver's received gifts. The receiver never gets a spendable balance. `Idempotency-Key` is required. Use the response to update your UI immediately; webhooks are for background processing and reconciliation.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]+$","description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","example":"7f1c2e0a-4d1e-4c2b-9a53-0f4c8a1b2c3d"},"required":true,"description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendGiftRequest"}}}},"responses":{"201":{"description":"Gift sent. A replayed request returns the original response with `Idempotent-Replayed: true`. When the request names a `room_id`, the response carries `GiftingStack-Room-Event`: `queued`, or `skipped; plan_required` (live gift rooms need a paid plan), `skipped; rooms_disabled` (Gift rooms are off for the project). The gift is booked either way.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftTransactionResult"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`test_limit_reached` (test keys only): the test environment allows 50 users and 5,000 operations a month (details: `limit` = `users` or `monthly_operations`, `max`). Live keys have no such caps.; or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such gift (`details.gift_id`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`insufficient_balance`, `balance_limit_exceeded`, `gift_inactive`, `self_gift_not_allowed`, `user_deleted`, or `idempotency_key_reused`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/gift-transactions/{id}":{"get":{"tags":["Gifts"],"summary":"Get a gift transaction","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^gtx_[0-9A-Z]{26}$","example":"gtx_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The transaction, including its reversal if any","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftTransaction"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Gift transaction not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/gift-transactions/{id}/reverse":{"post":{"tags":["Gifts"],"summary":"Reverse a gift","description":"Refunds the sender in full and removes the gift from the receiver's counter. A gift can be reversed once; partial reversals are not supported. To claw back a refunded coin purchase, reverse the affected gifts and then debit the purchase with `allow_negative`. A deleted sender's gift can be reversed too: the refund is forfeited in the same step, so their balance stays 0 (`sender_balance_after` 0) and no metadata is stored. `Idempotency-Key` is required.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^gtx_[0-9A-Z]{26}$","example":"gtx_01JA0Q8X2M4N6P8R0T2V4W6Y8Z"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]+$","description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","example":"7f1c2e0a-4d1e-4c2b-9a53-0f4c8a1b2c3d"},"required":true,"description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReverseGiftRequest"}}}},"responses":{"200":{"description":"Gift reversed. A replayed request returns the original response with `Idempotent-Replayed: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftTransactionResult"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`test_limit_reached` (test keys only): the test environment allows 50 users and 5,000 operations a month (details: `limit` = `users` or `monthly_operations`, `max`). Live keys have no such caps.; or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such gift transaction","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`already_reversed`; or `project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`idempotency_key_reused`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/events":{"get":{"tags":["Events"],"summary":"List recent events","description":"Every webhook event of the last 30 days, oldest first. Use it to catch up after missed webhooks. Two ways to page, both until `has_more` is false: pass the `next_cursor` of the page before as `cursor` (works on every project; keep the last `next_cursor` to ask again later), or pass the last `sequence` you processed as `after_sequence`. A project whose ledger is sharded (high-volume projects) pages by `cursor` only: its events are numbered per shard, so `sequence` orders the events of one user, not of the whole project, and `after_sequence` answers 400. Payloads of deleted users are redacted. Events older than 30 days are no longer returned; the `webhook.test` events of the dashboard's test button are not stored here.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"integer","nullable":true,"minimum":0,"description":"Return events after this sequence (the last one you processed). Default 0.","example":1843},"required":false,"description":"Return events after this sequence (the last one you processed). Default 0.","name":"after_sequence","in":"query"},{"schema":{"type":"string","maxLength":4000,"pattern":"^ev1\\.[A-Za-z0-9_-]+$","description":"The `next_cursor` of an earlier page: return the events after it. Opaque; do not build or change it. Without `cursor` and `after_sequence` the list starts at the oldest kept event. Not together with `after_sequence`. Works on every project; projects with a sharded ledger page by `cursor` only.","example":"ev1.WzE4NDNd"},"required":false,"description":"The `next_cursor` of an earlier page: return the events after it. Opaque; do not build or change it. Without `cursor` and `after_sequence` the list starts at the oldest kept event. Not together with `after_sequence`. Works on every project; projects with a sharded ledger page by `cursor` only.","name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"example":50},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"A page of events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventPage"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/leaderboards/{board}":{"get":{"tags":["Leaderboards"],"summary":"Get a leaderboard","description":"Top senders or top receivers for a UTC day, week, month or all time, by the value of the gifts (`units`) or by their number (`gifts`). Reversed gifts never count; deleted users and users who opted out never appear. The numbers are display counters, never balances. Reads are not counted towards your plan.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","enum":["senders","receivers"],"description":"`senders` ranks who sent gifts, `receivers` who received them.","example":"senders"},"required":true,"description":"`senders` ranks who sent gifts, `receivers` who received them.","name":"board","in":"path"},{"schema":{"type":"string","enum":["day","week","month","all"],"description":"UTC calendar periods; `week` is Monday to Sunday. Default `week`. Kept: today and 7 days back, this week and 4 before, this month and 12 before."},"required":false,"description":"UTC calendar periods; `week` is Monday to Sunday. Default `week`. Kept: today and 7 days back, this week and 4 before, this month and 12 before.","name":"period","in":"query"},{"schema":{"type":"string","enum":["units","gifts"],"description":"`units` = value of the gifts in your own coin units, `gifts` = number of gifts. Default `units`."},"required":false,"description":"`units` = value of the gifts in your own coin units, `gifts` = number of gifts. Default `units`.","name":"metric","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Any day inside the wanted period (UTC). Default today. Ignored for `all`. A day outside the kept range is a 400 `period_out_of_range`.","example":"2026-10-11"},"required":false,"description":"Any day inside the wanted period (UTC). Default today. Ignored for `all`. A day outside the kept range is a 400 `period_out_of_range`.","name":"at","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Rows to return, 1 to 100. Default 10. The board is the top 100: there is no page 2.","example":10},"required":false,"description":"Rows to return, 1 to 100. Default 10. The board is the top 100: there is no page 2.","name":"limit","in":"query"},{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Adds a `me` block for this user (their score and, if in the top 1,000, rank).","example":"user_123"},"required":false,"description":"Adds a `me` block for this user (their score and, if in the top 1,000, rank).","name":"user","in":"query"}],"responses":{"200":{"description":"The leaderboard","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Leaderboard"}}}},"400":{"description":"`invalid_request`, or `period_out_of_range` (`at` is outside the retained history or in the future)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`leaderboard_building`: the one-time count of older gifts is still running. Retry after the `Retry-After` header (30 s). Client token endpoints can also answer `temporarily_unavailable`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/leaderboard-visibility":{"put":{"tags":["Leaderboards"],"summary":"Hide a user from leaderboards (or show them again)","description":"For users who asked not to be ranked. Takes effect at once on every leaderboard; the user's counters are kept. Creates the user if it does not exist yet (a test key counts that user towards the test environment's 50-user cap). `Idempotency-Key` is required. Not counted towards your plan.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]+$","description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","example":"7f1c2e0a-4d1e-4c2b-9a53-0f4c8a1b2c3d"},"required":true,"description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","name":"idempotency-key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardVisibilityRequest"}}}},"responses":{"200":{"description":"The new setting. A replayed request returns the original response with `Idempotent-Replayed: true`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeaderboardVisibility"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`test_limit_reached` (test keys only): the test environment allows 50 users and 5,000 operations a month (details: `limit` = `users` or `monthly_operations`, `max`). Live keys have no such caps.; or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`, or `idempotency_key_reused` (the key was used for a different request)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/away":{"get":{"tags":["While you were away"],"summary":"Get what a user received since they last looked","description":"Gifts a user received since they last looked (at most the last 7 days), grouped by gift, for a \"while you were away\" card and a short replay of the effects. Reading changes nothing: after the person has seen it, send the `cursor` to the cursor endpoint. Nothing is real time. An unknown user gets an empty summary.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":128,"description":"An opaque `cursor` from an earlier response. Overrides the stored cursor for this read only."},"required":false,"description":"An opaque `cursor` from an earlier response. Overrides the stored cursor for this read only.","name":"since","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":10,"description":"Most gift groups to return, 1 to 10. Default 5.","example":5},"required":false,"description":"Most gift groups to return, 1 to 10. Default 5.","name":"max_groups","in":"query"}],"responses":{"200":{"description":"The summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AwaySummary"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/inbox":{"get":{"tags":["Inbox"],"summary":"Get the gifts a user received in the last 24 hours","description":"The gifts a user received in the last 24 hours, one row per gift, newest first: who sent which gift and when, for an inbox or activity list. Pages of up to 100 with `next_cursor`. Reading changes nothing. Messages, metadata and references are never included. Not counted towards your plan. An unknown user gets an empty list.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":128,"description":"The `next_cursor` of the previous page."},"required":false,"description":"The `next_cursor` of the previous page.","name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page, 1 to 100. Default 20.","example":20},"required":false,"description":"Rows per page, 1 to 100. Default 20.","name":"limit","in":"query"}],"responses":{"200":{"description":"One page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Inbox"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/users/{external_id}/away/cursor":{"put":{"tags":["While you were away"],"summary":"Mark a summary as seen","description":"Stores the cursor. It only moves forward: an older or equal cursor changes nothing, so repeating the call is safe. No Idempotency-Key needed. Not counted towards your plan.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AwayCursorRequest"}}}},"responses":{"204":{"description":"Stored"},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client-tokens/revoke":{"post":{"tags":["Client tokens"],"summary":"Revoke client tokens","description":"Call this from your backend with your secret key (a client token cannot). Send exactly one of `token` (that single token) or `user` (every token of that end user minted up to now). Tokens are verified with a 30-second cache, so **a revocation takes effect within 30 seconds**, here and for gift room joins. Revoking twice is fine. Revoking the API key that minted the tokens still ends all of them. A token from another project or environment answers 404; an already expired token is accepted and does nothing.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeClientTokenRequest"}}}},"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokedClientToken"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: the token was not minted for this project and environment, or is not a token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`token_not_revocable`: the token has no id (minted before revocation existed): revoke by `user` instead","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client-tokens":{"post":{"tags":["Client tokens"],"summary":"Create a client token","description":"Call this from your backend with your secret key, then hand the token to your app. The app uses it on the `/v1/client` endpoints to read the catalog and its own balance and to send gifts as this user only. It cannot credit, debit or act for anyone else. Gift rooms need `rooms`: a token without it can neither join a room nor send with `room_id`. Revoking the API key ends its tokens within 30 seconds; `POST /v1/client-tokens/revoke` ends one token or all tokens of one user. Tokens live 60 seconds to 24 hours (default one hour). No `Idempotency-Key` is needed: every call mints a new token. `name` is shown to room viewers: 1 to 32 characters, no control, invisible (format) or direction-changing characters.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateClientTokenRequest"}}}},"responses":{"201":{"description":"Token created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientToken"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/receipts/verify":{"post":{"tags":["Receipts"],"summary":"Verify an App Store or Google Play purchase","description":"Checks that an in-app purchase is real before you credit coins for it. Call it from your backend with your secret key when the app reports a purchase.\n\n**Apple:** send the StoreKit 2 `jwsRepresentation` (or `signedTransactionInfo`). It is verified offline: the certificate chain up to Apple Root CA - G3, Apple's certificate markers, the ES256 signature, certificate validity at signing time, your bundle ids and the environment. Certificate revocation (OCSP) is not checked.\n\n**Google Play:** send the purchase token and product id. GiftingStack asks the Google Play Developer API with the service account you uploaded in the dashboard. On live this is how a store sale is recorded (credit it with the returned `receipt_id`); for the sale's price the service account also needs the View financial data permission in Play Console.\n\nVerification has its own rate limit per project and environment (429 `rate_limited`, `Retry-After`). No `Idempotency-Key` is needed: repeating a verification is safe.\n\n**Environments:** a test key accepts only sandbox (Apple) or license-tester (Google) purchases, a live key only real ones.\n\n**Each purchase counts once:** the first verification is recorded; any later one, with any key of the same environment, returns `already_seen: true`. Grant coins only when `valid` is true and `already_seen` is false. This endpoint never changes a balance: credit the user yourself afterwards.\n\nErrors carry `details.reason`: `receipt_invalid` (malformed, unsupported_algorithm, untrusted_root, certificate_chain_invalid, signature_invalid, certificate_expired, signed_in_future), `app_mismatch`, `environment_mismatch`, `product_mismatch`, `receipt_not_found`, `receipts_not_configured`, `store_credentials_rejected`, `store_unavailable` (retry).","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptVerifyRequest"}}}},"responses":{"200":{"description":"The store vouches for the purchase. Check `valid` (refunded, pending and cancelled purchases are authentic but not valid) and `already_seen`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptVerification"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`receipts_not_configured`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"receipt_invalid, app_mismatch, environment_mismatch, product_mismatch, receipt_not_found or store_credentials_rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`store_unavailable` (retry after the `Retry-After` header), or `temporarily_unavailable` (receipt verification is not available)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/lookup":{"get":{"tags":["Support"],"summary":"Look up a purchase or request","description":"Everything recorded about one purchase or request, for support claims such as \"I paid and got no coins\": the verified store receipt, the ledger entries, the webhook deliveries of those entries and every refused request for the same reference, receipt or request id (with why it was refused and the user's balance at that moment). Give exactly one of `reference`, `receipt_id`, `transaction_id`, `request_id` or `entry_id`.\n\nLedger entries are kept forever, refused requests 180 days (at most 5,000 a day per project and environment), webhook deliveries 90 days. A successful request is found by its `reference` or entry id; `request_id` finds refused ones. Never changes anything.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"The `reference` of a credit, debit or reversal (your order or refund id).","example":"order_8812"},"required":false,"description":"The `reference` of a credit, debit or reversal (your order or refund id).","name":"reference","in":"query"},{"schema":{"type":"string","pattern":"^rcp_[0-9A-Z]{26}$","description":"A verified receipt (`rcp_…`)."},"required":false,"description":"A verified receipt (`rcp_…`).","name":"receipt_id","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":256,"description":"The store's transaction id of a verified purchase (`transaction_id` of `POST /v1/receipts/verify`)."},"required":false,"description":"The store's transaction id of a verified purchase (`transaction_id` of `POST /v1/receipts/verify`).","name":"transaction_id","in":"query"},{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"A refused request's `request_id`."},"required":false,"description":"A refused request's `request_id`.","name":"request_id","in":"query"},{"schema":{"type":"string","pattern":"^led_[0-9A-Z]{26}$","description":"A ledger entry (`led_…`)."},"required":false,"description":"A ledger entry (`led_…`).","name":"entry_id","in":"query"}],"responses":{"200":{"description":"What was found; empty lists when nothing matches","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Lookup"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/rooms/{room_id}/battle":{"post":{"tags":["Gift rooms"],"summary":"Start a battle round","description":"Battle (versus) scores of a gift room: while a round is open, the room adds up, per receiver, the value (`units` = unit price x quantity) and number of the gifts sent into it, and pushes the scores to every viewer (`scores` frame, at most 4 per second). Counters for display only, never balances. Needs a paid plan for live rooms (test rooms on every plan). Starting sets every score back to 0. Name the round to make retries safe: `round` is required, a start that names the current round changes nothing (`replayed: true`), and a late retry of an earlier round (one of the room's last 16) answers 409 `round_superseded` instead of resetting the newer one.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","example":"live_123"},"required":true,"name":"room_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BattleStart"}}}},"responses":{"200":{"description":"The room's round now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BattleResponse"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`rooms_disabled` (Gift rooms switched off for the project) or `plan_required` (live rooms need a paid plan; test rooms work on every plan); or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`round_superseded`: `round` names an earlier round of this room; nothing was changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`rooms_unavailable`: the room could not be reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Gift rooms"],"summary":"Remove the battle round","description":"Removes the round and its scores; viewers get `battle: null`. Safe to repeat.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","example":"live_123"},"required":true,"name":"room_id","in":"path"}],"responses":{"200":{"description":"The room's round now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BattleResponse"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`rooms_disabled` (Gift rooms switched off for the project) or `plan_required` (live rooms need a paid plan; test rooms work on every plan); or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`rooms_unavailable`: the room could not be reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"tags":["Gift rooms"],"summary":"Get the battle round","description":"The round and its scores, for example to record the result when it ends.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","example":"live_123"},"required":true,"name":"room_id","in":"path"}],"responses":{"200":{"description":"The room's round now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BattleResponse"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`rooms_disabled` (Gift rooms switched off for the project) or `plan_required` (live rooms need a paid plan; test rooms work on every plan)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`rooms_unavailable`: the room could not be reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/rooms/{room_id}/battle/end":{"post":{"tags":["Gift rooms"],"summary":"End the battle round now","description":"Stops counting now; the scores stay visible. Safe to repeat.","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,64}$","example":"live_123"},"required":true,"name":"room_id","in":"path"}],"responses":{"200":{"description":"The room's round now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BattleResponse"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed (`details.issues` lists each one; a rule the ledger enforces names its `details.field`), including a missing `Idempotency-Key` on an endpoint that requires one","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_api_key`: the key is missing, malformed, unknown, revoked or expired, or its project is scheduled for deletion (its keys stop working at once). Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`rooms_disabled` (Gift rooms switched off for the project) or `plan_required` (live rooms need a paid plan; test rooms work on every plan); or `project_suspended`: live is suspended for this project (`details.reason` says why). Reads (GET) and `DELETE /v1/users/{external_id}` still work; every other write is refused. Test keys are never suspended","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted and its data purged (only a key still in the 30-second key cache sees this; after that the key answers 401)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: over the plan's limit for this key (or, after failed authentication, over the per-IP limit). The `Retry-After` header says how many seconds to wait. A valid key is never limited by its IP address","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry, and quote `request_id` to support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`rooms_unavailable`: the room could not be reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/me":{"get":{"tags":["Client"],"summary":"The token user's balance","security":[{"clientToken":[]}],"responses":{"200":{"description":"Balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientMe"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now, or the project's ledger is momentarily overloaded; wait `Retry-After` seconds when present and retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/gifts":{"get":{"tags":["Client"],"summary":"Gifts the app can show in its gift tray","description":"Active gifts only, in your sort order. Metadata is never exposed to apps.","security":[{"clientToken":[]}],"responses":{"200":{"description":"Active gifts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientGiftList"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now, or the project's ledger is momentarily overloaded; wait `Retry-After` seconds when present and retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/users/{external_id}/received-gifts":{"get":{"tags":["Client"],"summary":"Gifts a user has received","description":"Counters per gift, for showing on profiles and posts. Senders and messages are not included. An unknown user has an empty list. With `reference`, only the gifts sent with that reference (for example one post) are counted.","security":[{"clientToken":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,128}$","description":"Your own user identifier. GiftingStack never sees credentials or profile data.","example":"user_123"},"required":true,"description":"Your own user identifier. GiftingStack never sees credentials or profile data.","name":"external_id","in":"path"},{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,128}$","description":"Count only the gifts sent with this `reference` (a post's wall).","example":"post_99"},"required":false,"description":"Count only the gifts sent with this `reference` (a post's wall).","name":"reference","in":"query"}],"responses":{"200":{"description":"Counters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceivedGifts"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now, or the project's ledger is momentarily overloaded; wait `Retry-After` seconds when present and retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/gift-transactions":{"post":{"tags":["Client"],"summary":"Send a gift as the token user","description":"The sender is always the user the token was created for. `Idempotency-Key` is required; retrying with the same key and body returns the original result. The key space is the token user's own, separate from your backend's. A receiver id that does not exist yet is created by the gift, so one user can bring in at most 200 new receivers per hour (429 `rate_limited`, `details.limit` `new_receivers_per_hour`, `details.retry_after` in seconds); existing receivers are never counted. Show receiver ids from the client as untrusted text.","security":[{"clientToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]+$","description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","example":"7f1c2e0a-4d1e-4c2b-9a53-0f4c8a1b2c3d"},"required":true,"description":"Required on this endpoint (a missing or malformed key is 400 `invalid_request`). A new random UUID or ULID per logical operation; reuse it only to retry that operation. Retrying with the same key and body returns the original response (`Idempotent-Replayed: true`); the same key with another endpoint or body is `422 idempotency_key_reused`. Keys are kept 24 hours. Only successful (2xx) responses are stored; a failed request can be retried with the same key. Keys are scoped by caller: requests with your secret key (and the dashboard) share one key space per project and environment; each client-token user has their own. The same key in another space is an unrelated request. Do not derive keys from guessable values such as order numbers: put those in `reference`, which is what makes a credit or debit permanently unique.","name":"idempotency-key","in":"header"},{"schema":{"type":"string","description":"Where your app runs: `web`, `ios`, `android`, `flutter` or `react-native`. Optional and informational: your dashboard shows which platforms your gifts are sent from. Any other value counts as `unknown`; it never changes or refuses the request. The web and React Native SDKs set it for you."},"required":false,"description":"Where your app runs: `web`, `ios`, `android`, `flutter` or `react-native`. Optional and informational: your dashboard shows which platforms your gifts are sent from. Any other value counts as `unknown`; it never changes or refuses the request. The web and React Native SDKs set it for you.","name":"x-giftingstack-platform","in":"header"},{"schema":{"type":"string","description":"The phone's operating system, `ios` or `android`, for apps built with Flutter or React Native (native apps say it with their platform). Optional and informational; any other value is ignored and it never changes or refuses the request. The React Native SDK sets it."},"required":false,"description":"The phone's operating system, `ios` or `android`, for apps built with Flutter or React Native (native apps say it with their platform). Optional and informational; any other value is ignored and it never changes or refuses the request. The React Native SDK sets it.","name":"x-giftingstack-os","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientSendGiftRequest"}}}},"responses":{"201":{"description":"Gift sent. A replayed request returns the original response with `Idempotent-Replayed: true`. When the request names a `room_id`, the response carries `GiftingStack-Room-Event`: `queued`, or `skipped; plan_required` (live gift rooms need a paid plan), `skipped; rooms_disabled` (Gift rooms are off for the project). The gift is booked either way.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientGiftTransaction"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`room_not_allowed`: `room_id` is not covered by the token's `rooms`; or `test_limit_reached` (test keys only): the test environment allows 50 users and 5,000 operations a month (details: `limit` = `users` or `monthly_operations`, `max`). Live keys have no such caps. Suspended project: `project_suspended` with the generic message \"This app is temporarily unavailable.\" (no `details.reason`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such gift (`details.gift_id`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`insufficient_balance`, `balance_limit_exceeded`, `gift_inactive`, `self_gift_not_allowed`, `user_deleted` or `idempotency_key_reused`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now, or the project's ledger is momentarily overloaded; wait `Retry-After` seconds when present and retry; or `temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/leaderboards/{board}":{"get":{"tags":["Client"],"summary":"Get a leaderboard","description":"Top senders or top receivers for a UTC day, week, month or all time, by the value of the gifts (`units`) or by their number (`gifts`). Reversed gifts never count; deleted users and users who opted out never appear. The numbers are display counters, never balances. Reads are not counted towards your plan. The `me` block is the token user's own score and rank (leave it out with `include_me=false`).","security":[{"clientToken":[]}],"parameters":[{"schema":{"type":"string","enum":["senders","receivers"],"description":"`senders` ranks who sent gifts, `receivers` who received them.","example":"senders"},"required":true,"description":"`senders` ranks who sent gifts, `receivers` who received them.","name":"board","in":"path"},{"schema":{"type":"string","enum":["day","week","month","all"],"description":"UTC calendar periods; `week` is Monday to Sunday. Default `week`. Kept: today and 7 days back, this week and 4 before, this month and 12 before."},"required":false,"description":"UTC calendar periods; `week` is Monday to Sunday. Default `week`. Kept: today and 7 days back, this week and 4 before, this month and 12 before.","name":"period","in":"query"},{"schema":{"type":"string","enum":["units","gifts"],"description":"`units` = value of the gifts in your own coin units, `gifts` = number of gifts. Default `units`."},"required":false,"description":"`units` = value of the gifts in your own coin units, `gifts` = number of gifts. Default `units`.","name":"metric","in":"query"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Any day inside the wanted period (UTC). Default today. Ignored for `all`. A day outside the kept range is a 400 `period_out_of_range`.","example":"2026-10-11"},"required":false,"description":"Any day inside the wanted period (UTC). Default today. Ignored for `all`. A day outside the kept range is a 400 `period_out_of_range`.","name":"at","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Rows to return, 1 to 100. Default 10. The board is the top 100: there is no page 2.","example":10},"required":false,"description":"Rows to return, 1 to 100. Default 10. The board is the top 100: there is no page 2.","name":"limit","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"`false` leaves out the `me` block (the token user). Default `true`."},"required":false,"description":"`false` leaves out the `me` block (the token user). Default `true`.","name":"include_me","in":"query"}],"responses":{"200":{"description":"The leaderboard","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Leaderboard"}}}},"400":{"description":"`invalid_request`, or `period_out_of_range` (`at` is outside the retained history or in the future)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`leaderboard_building`: the one-time count of older gifts is still running. Retry after the `Retry-After` header (30 s). Client token endpoints can also answer `temporarily_unavailable`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/me/away":{"get":{"tags":["Client"],"summary":"What the token user received since they last looked","description":"Gifts a user received since they last looked (at most the last 7 days), grouped by gift, for a \"while you were away\" card and a short replay of the effects. Reading changes nothing: after the person has seen it, send the `cursor` to the cursor endpoint. Nothing is real time.","security":[{"clientToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":128,"description":"An opaque `cursor` from an earlier response. Overrides the stored cursor for this read only."},"required":false,"description":"An opaque `cursor` from an earlier response. Overrides the stored cursor for this read only.","name":"since","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":10,"description":"Most gift groups to return, 1 to 10. Default 5.","example":5},"required":false,"description":"Most gift groups to return, 1 to 10. Default 5.","name":"max_groups","in":"query"}],"responses":{"200":{"description":"The summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AwaySummary"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now, or the project's ledger is momentarily overloaded; wait `Retry-After` seconds when present and retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/me/inbox":{"get":{"tags":["Client"],"summary":"The gifts the token user received in the last 24 hours","description":"The gifts a user received in the last 24 hours, one row per gift, newest first: who sent which gift and when, for an inbox or activity list. Pages of up to 100 with `next_cursor`. Reading changes nothing. Messages, metadata and references are never included. Not counted towards your plan.","security":[{"clientToken":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":128,"description":"The `next_cursor` of the previous page."},"required":false,"description":"The `next_cursor` of the previous page.","name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page, 1 to 100. Default 20.","example":20},"required":false,"description":"Rows per page, 1 to 100. Default 20.","name":"limit","in":"query"}],"responses":{"200":{"description":"One page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Inbox"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now, or the project's ledger is momentarily overloaded; wait `Retry-After` seconds when present and retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/me/away/cursor":{"put":{"tags":["Client"],"summary":"Mark the summary as seen","description":"Stores the cursor for the token user. It only moves forward, so repeating the call is safe.","security":[{"clientToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AwayCursorRequest"}}}},"responses":{"204":{"description":"Stored"},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found`: no such user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`project_deletion_pending`: the project is scheduled for deletion and takes no more writes (its keys already answer 401; only a key in the 30-second key cache sees this)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`user_deleted`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`temporarily_unavailable`: client tokens are not available right now, or the project's ledger is momentarily overloaded; wait `Retry-After` seconds when present and retry; or `temporarily_unavailable`: the project's ledger is receiving more requests than it can queue (roughly 1,000 a second for one project and environment). Nothing was applied; wait `Retry-After` seconds and retry with the same `Idempotency-Key`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/client/gift-walls/query":{"post":{"tags":["Client"],"summary":"Gift walls for several users or posts at once","description":"For feeds: up to 25 `{ receiver, reference }` pairs in one request, answered in the same order. Without `reference` you get the receiver's whole profile wall. Unknown users have an empty list. This is a read: it needs no Idempotency-Key and is not counted towards your plan.","security":[{"clientToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftWallQueryRequest"}}}},"responses":{"200":{"description":"One wall per item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GiftWallList"}}}},"400":{"description":"`invalid_request`: a field is missing or malformed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_client_token`: the token is missing, malformed, expired, revoked, or its API key is no longer valid. A secret key is refused here. Failed authentication also counts against a per-IP limit (429)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`project_suspended`: live is suspended for this project. End users get the generic message \"This app is temporarily unavailable.\" with no `details.reason`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`project_deleted`: the project was deleted (only a token in the key cache)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`payload_too_large`: the request body is over 16 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: this end user is over the per-user limit. The `Retry-After` header says how many seconds to wait","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds to wait before retrying.","example":"10"},"required":true,"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal_error`: unexpected failure; retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`leaderboard_building`: counters per `reference` are still being built (retry after the `Retry-After` header, 30 s); or `temporarily_unavailable`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}