TANITARDevelopers
GuidesShop

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.

PolicyMeaningAllowed for
unlimitedNever sells out. stock.remaining reads as null.award, manual
countedA number of units left, taken one per unit soldaward, manual
code-poolOne code per unit, from a pool you load; the number left is the number of unclaimed codescode 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 remaining equals expectedRemaining, 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 with 409 SHOP_STOCK_CONFLICT and 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 }
RuleDetail
Batch size1 to 500 codes per request
Code length1 to 256 characters; surrounding whitespace is trimmed
Blank codesA code that is empty after trimming refuses the batch with 422 SHOP_CODE_INVALID
Duplicates in one batchRefuse the batch with 422 SHOP_CODE_DUPLICATE
Codes already in the poolSkipped and counted in alreadyPresent; resending a batch loads nothing twice
ListingMust 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-code scope discloses it with a reason, which is audited first. See Code disclosure.
  • shop:mutate lets a key load codes but never read one back.

Refusals

StatuserrorCodeCause
404SHOP_LISTING_UNAVAILABLENo listing with that key
422SHOP_RESTOCK_UNSUPPORTEDThe listing does not use code-pool
422SHOP_CODE_INVALIDA code is blank
422SHOP_CODE_DUPLICATEThe batch contains the same code twice
409SHOP_LISTING_TRANSITION_INVALIDThe listing is retired
422SHOP_FULFILLMENT_STOCK_INVALIDA stock edit that does not fit the fulfilment kind, or a counted policy without remaining
422SHOP_LISTING_INVALIDAn edit of a counted listing that keeps it counted without expectedRemaining, or expectedRemaining on a listing that is not counted
409SHOP_STOCK_CONFLICTThe edit changes the count, and a sale moved it since the count you sent in expectedRemaining

On this page