Stock
Choose a stock policy for each listing, top up counted stock, and load codes into a code pool.
Every listing has exactly one stock policy, set in its stock field when you create or edit it.
| Policy | Meaning | Allowed for |
|---|---|---|
unlimited | Never sells out. stock.remaining reads as null. | award, manual |
counted | A number of units left, taken one per unit sold | award, manual |
code-pool | One code per unit, from a pool you load; the number left is the number of unclaimed codes | code only |
Stock is claimed inside the purchase transaction with a conditional write, so two players can never
both take the last unit. When a published listing's last unit goes, Tanitar publishes
shop.stock.depleted, and the listing shows availability: "sold-out" to players until you add
more. A purchase that finds no stock is refused with SHOP_SOLD_OUT.
Reversing or cancelling an order does not put its units back. If you want a unit back on sale, add it yourself as described below.
Counted stock
A counted listing carries the number of units it has left:
{ "stock": { "policy": "counted", "remaining": 250 } }To top it up, read the listing (GET /v1/shop/listings/:listingKey), then edit it with the new
number of units left in remaining and the count you read in expectedRemaining:
curl -X PATCH "$REWARD_API_URL/v1/shop/listings/gold-frame" \
-H "Authorization: Bearer $TANITAR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "stock": { "policy": "counted", "remaining": 500, "expectedRemaining": 250 } }'expectedRemaining is what keeps a sale from being lost. Players keep buying while you edit, and
remaining is an absolute count, so the service compares the two values you send:
- If
remainingequalsexpectedRemaining, you did not change the count. The edit leaves the stored count alone, so units sold since your read stay sold. Send this when you edit a counted listing for any other reason. - If they differ, the edit sets the new count only while the stored count still equals
expectedRemaining. If a unit sold in between, the edit is refused with409 SHOP_STOCK_CONFLICTand nothing changes. Read the listing again and resend with the count you now see.
Every edit that sends a counted stock for a listing that is already counted must include
expectedRemaining; without it the edit is refused with 422 SHOP_LISTING_INVALID. Leave it out
when the listing is not counted yet, including when the edit switches it to counted, and when you
switch a counted listing to another policy. An edit that omits stock entirely leaves the count
untouched. On a published listing the edit emits shop.listing.published with the new stock.
Code pools
A code listing sells codes you supply: gift-card codes, game keys, voucher codes. You load them
in batches with POST /v1/shop/listings/:listingKey/stock/codes (scope shop:mutate,
Idempotency-Key required):
curl -X POST "$REWARD_API_URL/v1/shop/listings/steam-key-10/stock/codes" \
-H "Authorization: Bearer $TANITAR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "codes": ["AAAA-BBBB-CCCC", "DDDD-EEEE-FFFF", "GGGG-HHHH-IIII"] }'{ "listingKey": "steam-key-10", "loaded": 3, "alreadyPresent": 0, "remaining": 3 }| Rule | Detail |
|---|---|
| Batch size | 1 to 500 codes per request |
| Code length | 1 to 256 characters; surrounding whitespace is trimmed |
| Blank codes | A code that is empty after trimming refuses the batch with 422 SHOP_CODE_INVALID |
| Duplicates in one batch | Refuse the batch with 422 SHOP_CODE_DUPLICATE |
| Codes already in the pool | Skipped and counted in alreadyPresent; resending a batch loads nothing twice |
| Listing | Must use the code-pool policy (422 SHOP_RESTOCK_UNSUPPORTED otherwise) and must not be retired (409 SHOP_LISTING_TRANSITION_INVALID) |
You can load codes into a draft listing before you publish it, and top up a published one at any time.
How codes are protected
- Each code is encrypted individually before it is stored. The load response contains only counts, never a code.
- No catalog read, order read, event or webhook contains a code. An order reports only how many codes
it took (
codeCount). - A code is decrypted only when the buyer re-reads their own order's codes, or when someone holding
the separate
shop:disclose-codescope discloses it with a reason, which is audited first. See Code disclosure. shop:mutatelets a key load codes but never read one back.
Refusals
| Status | errorCode | Cause |
|---|---|---|
404 | SHOP_LISTING_UNAVAILABLE | No listing with that key |
422 | SHOP_RESTOCK_UNSUPPORTED | The listing does not use code-pool |
422 | SHOP_CODE_INVALID | A code is blank |
422 | SHOP_CODE_DUPLICATE | The batch contains the same code twice |
409 | SHOP_LISTING_TRANSITION_INVALID | The listing is retired |
422 | SHOP_FULFILLMENT_STOCK_INVALID | A stock edit that does not fit the fulfilment kind, or a counted policy without remaining |
422 | SHOP_LISTING_INVALID | An edit of a counted listing that keeps it counted without expectedRemaining, or expectedRemaining on a listing that is not counted |
409 | SHOP_STOCK_CONFLICT | The edit changes the count, and a sale moved it since the count you sent in expectedRemaining |