CEEJAYDocs

Buy a shop item

Buy an item from the game's shop with the connected player's coins.

POST /v1/player/purchase · for player session

Use it when the player presses Buy. The coins are deducted, the purchase is made, and the item is delivered or awaits claiming, in one step. This is the only game loop request that takes an idempotency key, and the key is required. Send the player session as the bearer token. The request needs the game loop. For the three conditions (game loop enabled for the game, member consent, server membership) and the error each returns, see What has to be true first.

Body parameters

Prop

Type

Returns

Prop

Type

delivery contains:

Prop

Type

spending contains:

Prop

Type

These are the values of status:

statusWhat it means
claimableBought; the player must claim it with Claim an item before it can be delivered
pendingAwaiting fulfilment. waitingFor identifies who must act
deliveredThe item has been fulfilled. It may still be active or used
refundedThe coins were refunded
rejectedThe community's team rejected the purchase without a refund. A rejection with a refund reads refunded
expiredAn item with a time limit that has run out
failedIt could not be delivered

Treat the list as open: a new value can appear.

waitingForWho must act before a pending purchase completes
teamThe community's team must fulfil or approve it
studioThe game's own server responded that it will finish later
deliveryCEEJAY is delivering it and retries if delivery fails
linkThe player must first link an account on a platform
nullNo one needs to act

A purchase awaiting the community's team reads status: "pending" and waitingFor: "team". An item delivered by the game's own server can read pending with waitingFor: "studio" until that server finishes the delivery: see Deliver shop items from your own platform. A delivery that fails is retried, and the member is refunded if it keeps failing: the balance then reflects the refund, and the locker shows refunded. CEEJAY does not send the member a direct message.

Example

Request
curl -X POST https://ceejay.communitystudios.xyz/api/v1/player/purchase \
  -H "Authorization: Bearer $CEEJAY_PLAYER_SESSION" \
  -H "Content-Type: application/json" \
  -d '{
    "itemId": 7,
    "idempotencyKey": "buy-2026-10-08-gold-skin-1",
    "expectedPrice": 500
  }'

The TypeScript example uses connect.hub.buy from the Connect client in the TypeScript SDK. The player must be connected first: the client manages and refreshes the player session. It returns the data of the response below. It generates an idempotencyKey when you pass none, and sends every attempt with the same one: see Retrying safely.

200 OK, an item the studio's server will finish delivering
{
  "success": true,
  "data": {
    "purchaseId": 4242,
    "itemId": 7,
    "itemName": "Gold skin",
    "pricePaid": 500,
    "balance": 950,
    "status": "pending",
    "waitingFor": "studio",
    "claimNeeded": false,
    "delivery": {
      "state": "pending",
      "message": null
    },
    "duplicate": false,
    "spending": {
      "dailyCap": 5000,
      "spentToday": 500,
      "remainingToday": 4500,
      "resetsAt": "2026-10-09T00:00:00.000Z"
    }
  }
}
200 OK, an item with nothing left to do
{
  "success": true,
  "data": {
    "purchaseId": 4242,
    "itemId": 7,
    "itemName": "Gold skin",
    "pricePaid": 500,
    "balance": 950,
    "status": "delivered",
    "waitingFor": null,
    "claimNeeded": false,
    "delivery": {
      "state": "delivered",
      "message": null
    },
    "duplicate": false,
    "spending": {
      "dailyCap": 5000,
      "spentToday": 500,
      "remainingToday": 4500,
      "resetsAt": "2026-10-09T00:00:00.000Z"
    }
  }
}
200 OK, the same request retried with its key
{
  "success": true,
  "data": {
    "purchaseId": 4242,
    "itemId": 7,
    "itemName": "Gold skin",
    "pricePaid": 500,
    "balance": 950,
    "status": "pending",
    "waitingFor": "studio",
    "claimNeeded": false,
    "delivery": {
      "state": "pending",
      "message": null
    },
    "duplicate": true,
    "spending": {
      "dailyCap": 5000,
      "spentToday": 500,
      "remainingToday": 4500,
      "resetsAt": "2026-10-09T00:00:00.000Z"
    }
  }
}

What a purchase needs

In addition to what every game loop request needs, a purchase spends the member's coins, so it also requires:

  • The economy enabled. Otherwise 409 with the code economy_disabled.
  • Proof, when the game asks for it. A device that connected without proof returns 403 with the code verified_proof_required. Reading, quests and the daily reward need none.
  • The device's spending hold has ended. A new connection or a new device is held before it can spend: 409 with the code spend_on_hold, holdUntil and retryAfter. The time that counts is spending.holdUntil in Get the balance.
  • Within the daily spending limit. Each player can spend a set number of coins a day through the game: beyond it, 409 with the code daily_spend_cap_reached, with cap, spentToday, price, resetsAt and retryAfter. An item priced above the limit can never be bought through the game.
  • An item on the game's allow list. An item that is not on the list, is disabled or does not exist returns 404 with the code item_not_found, and the three cases cannot be distinguished.

The game loop explains each rule.

The price is exact

Send the price the player saw as expectedPrice. When the price has changed, nothing is bought and the response is 409 with the code price_changed and the current price: show the shop again. A member with an insufficient balance gets insufficient_balance with price and balance.

409 Conflict, the price changed
{
  "error": "The price of this item has changed. Read the shop again",
  "code": "price_changed",
  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
  "price": 600,
  "expectedPrice": 500
}

An item that is not on sale, that the member cannot buy, that is sold out or whose purchase limit the member has reached returns purchase_refused, with a reason and a message the game can show:

reasonWhat it means
not_on_saleThe item is not on sale right now
not_eligibleThe member does not meet a condition. missingPlatform names the account the member must link in Discord, when that is the condition
unavailableThe item cannot be bought now
out_of_stockThe item is sold out
limit_reachedThe member has bought the maximum allowed. availableAt gives when they can buy again, if known
409 Conflict, the device is on hold
{
  "error": "This device cannot spend yet. New connections and devices cannot buy until the hold ends",
  "code": "spend_on_hold",
  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
  "holdUntil": "2026-10-09T18:06:40.000Z",
  "retryAfter": 3600
}

Retrying safely

Generate a key when the player presses Buy, store it, and retry the same request with the same key after a lost response, a 429, a 503 or a 504. Nothing is bought twice: the response is the original response with duplicate: true and the header Idempotent-Replayed: true. A game in a browser cannot read that header, so read duplicate.

The responseWhat it meansWhat to do
409 idempotency_key_reusedThe key was used for another item or other values. Nothing was boughtUse a new key for each new purchase
409 idempotency_in_progressThe first request with this key is still being processed, for up to a minuteWait retryAfter seconds and retry with the same key
409 idempotency_outcome_unknownTwo concurrent requests used the same key, and CEEJAY cannot tell whether the first took effectCheck the locker before buying again with a new key
503 idempotency_unavailableThis instance does not support idempotency keys yet. Nothing was boughtRetrying does not change this response

When the first request never finished, because it timed out or its response was lost, a retry within the first minute returns idempotency_in_progress. After that minute the request is processed, and CEEJAY finds the first purchase by its key instead of buying again. On a 504, retry the same request with the same key. A key whose request was rejected can be reused once the cause is fixed.

A key is scoped to the member, the game and the connection: a player who moves to another player of the game, or connects again, starts a new set of keys, and another member's key is never treated as this one. Keys are retained for 30 days. Two purchases of the same item in a row need two keys.

Limits

20 writes a minute for one player session, and 60 buys and claims together an hour for one member in one game. The 20 writes are shared by Start, Verify and Answer a quest task, Buy a shop item, Claim an item and Check in. The three X-RateLimit headers report that count. Every request made with a player session also counts toward 140 a minute. See Rate limits. Over a limit the response is 429 with the code rate_limited, retryAfter and a Retry-After header. A retry served from a key counts toward neither limit.

Errors

StatuscodeerrorWhen
400bad_requestThe request body must be a JSON objectThe body is JSON but not an object: null, text, a number or a list. A body that is not JSON at all returns bad_request with the message The request body must be JSON
400bad_requestitemId must be a whole number from 1 to 2147483647itemId is missing, is not a whole number, or is outside 1 to 2,147,483,647
400bad_requestidempotencyKey must be a string of 1-128 charactersidempotencyKey is missing, is not text, or is not 1 to 128 characters
400bad_requestexpectedPrice must be a whole number from 0 to 2147483647expectedPrice is missing, is not a number, or is outside 0 to 2,147,483,647
400bad_requestThis item asks a question. Answer it firstThe item asks a question and no answer was sent. The body includes question
400bad_requestanswer cannot be longer than 200 charactersanswer is longer than 200 characters
401connection_endedThis connection has endedThe connection or this device was ended since the player session was made. Discard the stored tokens and show the player as disconnected. See Renew a player session for the reasons
403verified_proof_requiredThis requires a verified player. This device connected without verificationThis device connected without proof, and the game only lets proven players spend
404item_not_foundShop item not foundThe item does not exist, is disabled, is not a type the game may sell, or is not on the game's allow list
409hub_not_enabledThis game does not show the community's quests, shop and rankThe game loop is disabled for the game. Do not show the community's quests, shop or rank in the game
409sharing_neededThe member has not consented to sharing their data with this gameThe member has not consented to share their quests, rewards, shop and rank with the game, or must consent again. The body includes manageUrl: send the player there
409membership_requiredThe member has to be in the server to use thisThe Discord account is not in the server. The body includes membership (pending, left or not_member) and, for left and not_member, joinUrl, the URL to send the player to
409spend_on_holdThis device cannot spend yet. New connections and devices cannot buy until the hold endsA new connection or device is still on hold. The body includes holdUntil and retryAfter
409daily_spend_cap_reachedThe daily spending limit for this member in this game has been reachedThe price would exceed the daily spending limit. The body includes cap, spentToday, price, resetsAt and retryAfter
409price_changedThe price of this item has changed. Read the shop againThe item costs something else now. The body includes price and expectedPrice
409insufficient_balanceInsufficient balanceThe member's balance is too low. The body includes price and balance
409purchase_refusedThis item cannot be bought right nowThe item cannot be bought now. The body includes reason, as in the table above, and message
409economy_disabledThe community has switched its economy offThe community has disabled its economy
409idempotency_key_reusedThis idempotencyKey was already used for a different requestThe key was used for another item or other values
409idempotency_in_progressThe first request with this idempotencyKey is still running. Try again in a few seconds.The first request with this key is still being processed. The body includes retryAfter
409idempotency_outcome_unknownAn earlier request with this idempotencyKey may or may not have been applied. It was not sent again. Use a new key to send it again.An earlier request with this key may or may not have been applied. It was not repeated
413body_too_largeThe request body is larger than 16 KBThe body is larger than 16 KB. Nothing was changed
503bot_unavailableThis could not be done right now and nothing was changed. Try again in a few secondsCEEJAY's bot could not be reached or was too busy, and nothing was changed. The body includes retryAfter, in seconds: retry after that many seconds
503idempotency_unavailableidempotencyKey cannot be used on this community yet, so nothing was changedThis instance does not support idempotency keys yet. Nothing was bought
504bot_timeoutThe answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeatThe request timed out, so it is not known whether the purchase was made. Retry the same request with the same key: it is safe to repeat

Every error body also includes requestId, and a 400 with the code bad_request identifies the invalid field in field. Match on code; message text can change.

The errors every request of a game shares (the key, the state of the game, the origin, 405, 429, 500) are on the Errors page, and the errors specific to the game loop are under Codes of the game loop.

On this page

Community StudiosPrivacyTerms