Shop
The shop sells four kinds of product, priced and charged in Turkish lira (TRY): owned cosmetic items, bundles of such items, premium packages of 1, 3 or 12 months, and Visionary. Every amount is in kuruş, so ₺1 is 100, and no amount is ever converted to another currency. The server computes every amount, and a client sends only the product. The Admin API sets the prices, campaigns and sales switches.
Every route on this page is user-only, except Get code preview and the two payment callback routes, which take no credential. The catalog, quote and order routes answer only on a self-hosted instance in the mirror premium mode. Any other instance returns 403 FEATURE_NOT_AVAILABLE_SELF_HOSTED.
Shop products
Section titled “Shop products”A product is named by its SKU.
| Value | Description |
|---|---|
cosmetic:<item_id> | An owned-availability cosmetic item, by its ID |
bundle:<bundle_id> | A bundle of 2-10 owned-availability items, by its ID |
premium:1, premium:3, premium:12 | Premium time of 1, 3 or 12 months |
visionary | Lifetime premium with a Visionary number |
An item or bundle ID is a positive integer within the signed 64-bit range. A value of any other shape fails validation with 400 INVALID_FORM_BODY.
Prices
Section titled “Prices”The server computes a price in one place, in whole numbers:
- The campaign discount comes off the list price while the campaign has not ended by the server clock.
- The premium member discount comes off that, for cosmetic items and bundles only, and only for an account with paid premium. An account in its free trial gets no member discount. Lifetime premium counts as paid.
Each step rounds half up, and the result is the amount charged. A list price of ₺500 (50000) with a 20% campaign and a 30% member discount comes to ₺400 (40000), then ₺280 (28000). A product whose final price would fall below ₺1 (100) is not sold.
Premium packages and Visionary take campaigns, and never take the member discount.
Each API process keeps the catalog for up to 60 seconds, and the quote reads a price list kept for up to 5 seconds, so both are for display. Create shop order reads the stored price and settings directly, and the amount it records is the amount charged.
Shop price object
Section titled “Shop price object”| Field | Type | Description |
|---|---|---|
| base_try_minor | integer | The list price in kuruş |
| campaign_percent | ?integer | The campaign discount, null without a running campaign |
| campaign_ends_at | ?ISO8601 timestamp | When the campaign ends, null without a campaign or without an end |
| sale_try_minor | integer | The price after the campaign in kuruş, equal to base_try_minor without one |
| premium_try_minor1 | ?integer | What an account with paid premium pays for an item or a bundle, in kuruş |
1 Null while the member discount is 0, and always null for premium packages and Visionary
Shop settings object
Section titled “Shop settings object”| Field | Type | Description |
|---|---|---|
| premium_sales_open | boolean | Whether premium packages and Visionary are on sale |
| item_sales_open | boolean | Whether cosmetic items and bundles are on sale |
| premium_discount_percent | integer | The member discount on items and bundles for accounts with paid premium (0-95) |
Both sales fields read false while no payment provider is configured.
Shop catalog item object
Section titled “Shop catalog item object”| Field | Type | Description |
|---|---|---|
| item | cosmetic item object | The item |
| price1 | ?shop price object | The price of an owned item |
1 Null for a premium-availability item, which comes with premium and is not sold
Shop package object
Section titled “Shop package object”| Field | Type | Description |
|---|---|---|
| sku | string | The product: premium:1, premium:3 or premium:12 |
| months | integer | The premium time the package adds: 1, 3 or 12 months |
| price | shop price object | The price |
Shop bundle object
Section titled “Shop bundle object”| Field | Type | Description |
|---|---|---|
| sku | string | The product: bundle:<id> |
| id | snowflake | The ID of the bundle |
| name | string | The display name of the bundle |
| description | ?string | A short description of the bundle |
| items1 | array[cosmetic item object] | The items the bundle gives, in bundle order |
| price | shop price object | The price of the bundle, with any running campaign |
1 Embedded whole, because an item sold only in bundles has no price of its own and is not in the catalog items
Shop Visionary object
Section titled “Shop Visionary object”| Field | Type | Description |
|---|---|---|
| sku | string | Always visionary |
| price | shop price object | The price, with any running campaign |
| total | integer | The number of Visionary numbers the admin set |
| remaining1 | integer | The numbers with no owner and no live hold |
| visible | boolean | Whether Visionary is on sale: premium sales open, Visionary switched on, and a payment provider configured |
1 For display only. Every API process reads one count shared for 60 seconds and keeps its catalog for up to 60 seconds more, so the value can be up to 2 minutes old. Create shop order decides whether a number is free
Shop viewer object
Section titled “Shop viewer object”| Field | Type | Description |
|---|---|---|
| premium_discount | boolean | Whether the account pays premium_try_minor for items, which needs paid premium rather than a trial |
Shop catalog object
Section titled “Shop catalog object”| Field | Type | Description |
|---|---|---|
| settings | shop settings object | The sales switches and the member discount |
| items | array[shop catalog item object] | The priced owned items and the premium items on sale, in catalog order |
| packages | array[shop package object] | The priced premium packages, in 1, 3 and 12 month order |
| visionary | ?shop Visionary object | Visionary, null until the admin sets its price |
| bundles | array[shop bundle object] | The priced bundles whose items are all on sale, in ID order |
| server_time | ISO8601 timestamp | The server clock, for campaign countdowns |
| viewer | shop viewer object | What applies to the requesting account |
Only viewer differs between accounts. The rest is the same for every account.
Shop quote object
Section titled “Shop quote object”| Field | Type | Description |
|---|---|---|
| sku | string | The product |
| base_try_minor | integer | The list price in kuruş |
| campaign_percent | integer | The campaign discount applied, 0 without one |
| premium_percent | integer | The member discount applied, 0 when it does not apply |
| final_try_minor | integer | The price after both discounts, in kuruş, which an order would charge |
| currency | string | Always TRY |
Shop order statuses
Section titled “Shop order statuses”| Value | Description |
|---|---|
| pending | The order is open and waiting for payment |
| paid | The payment provider confirmed the payment, and delivery has not finished |
| granted | The item, the bundle’s items, the premium time or the Visionary number was delivered |
| failed | Nothing is delivered, and failure_reason says why |
| expired | The order was still unpaid at its last check, and a payment the provider confirms later still completes it |
Shop order failure reasons
Section titled “Shop order failure reasons”| Value | Description |
|---|---|
| amount_mismatch | The payment provider reported a payment whose amount or currency differs from the order |
| account_deleted | The account was deleted before delivery |
| item_deleted | The item of a cosmetic order was deleted before delivery |
| provider_error | The payment provider did not open the order |
sanalyer sends no refund through the payment provider. An admin refunds a failed payment by hand in the provider’s panel.
Shop order object
Section titled “Shop order object”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the order |
| sku | string | The product |
| status | string | The shop order status |
| failure_reason | ?string | The failure reason, null unless the order failed |
| base_try_minor | integer | The list price in kuruş when the order opened |
| campaign_percent | integer | The campaign discount applied, 0 without one |
| premium_percent | integer | The member discount applied, 0 when it does not apply |
| final_try_minor | integer | The amount charged, in kuruş |
| currency | string | Always TRY |
| payment_url | ?string | The payment provider’s page, null unless the order is pending |
| created_at | ISO8601 timestamp | When the order opened |
| item_ids? | array[snowflake] | Bundle orders only: the items the order delivers, frozen when it opened, in bundle order |
Example
Section titled “Example”{ "id": "1501314428688998182", "sku": "premium:3", "status": "pending", "failure_reason": null, "base_try_minor": 40000, "campaign_percent": 25, "premium_percent": 0, "final_try_minor": 30000, "currency": "TRY", "payment_url": "https://payment.example.com/pay/abc123", "created_at": "2026-09-28T20:15:00.000Z"}Shop code object
Section titled “Shop code object”| Field | Type | Description |
|---|---|---|
| code | string | The code |
| redeemed | boolean | Whether the code has been redeemed |
| grant | shop code grant object | What redeeming the code gives |
Shop code grant object
Section titled “Shop code grant object”| Field | Type | Description |
|---|---|---|
| type | string | premium for premium time, or cosmetic for an item |
| duration_type? | string | Premium only: the gift duration unit |
| duration_quantity? | integer | Premium only: the number of units |
| item? | cosmetic item object | Cosmetic only: the item |
Shop refusals
Section titled “Shop refusals”The quote and order routes refuse a product with these codes:
- 403
SHOP_SALES_CLOSEDwhen sales of the product’s kind are closed or no payment provider is configured. Premium packages and Visionary follow the premium sales switch, and cosmetic items and bundles follow the item sales switch. - 404
SHOP_ITEM_UNAVAILABLEwhen the product has no price or no longer exists, when an item, or any item of a bundle, is not an owned-availability item on sale, when the final price would fall below ₺1, when an account with lifetime premium asks for a package or Visionary, or when Visionary is switched off. An order also receives it when no Visionary number is free. - 409
COSMETIC_ALREADY_OWNEDwhen the account already owns the item, or any item of a bundle. A bundle is never sold in part. - 409
SHOP_TOO_MANY_OPEN_ORDERS, on an order only, when the account already has three open orders. An open order is unpaid, less than 15 minutes old, and not declined, cancelled or expired at the payment provider. - 503
SHOP_BUSY, on an order only, when the shared payment provider request budget is spent, when the provider is backing off after a 429 or did not open the order, or while another order of the same account is being opened. Retrying after a few seconds is safe.
Payment and delivery
Section titled “Payment and delivery”- Create shop order returns an order with
payment_url, the payment provider’s page, which a client opens in the browser. - The provider calls Receive Payriff callback when the payment changes, and the browser comes back through Return from Payriff. Neither is trusted. A matching callback token only queues a check, in which the server reads the order from the provider.
- The check accepts a payment only when the provider reports it
APPROVEDorPAIDfor a purchase, in the order’s currency, for exactly the amount of the order. The order becomespaid, is delivered, and becomesgranted. - A client follows the order with Get shop order, which reads only the server’s record and never reaches the provider.
Apart from callbacks, the server checks every unpaid order with the provider 2 minutes, 15 minutes, 3 hours and 24 hours after it opened. An order still unpaid at the fourth check expires, and so does an order the provider reports expired. An order that has no provider order ID 10 minutes after it opened fails with provider_error.
Scheduled checks leave an expired order alone, so only a callback or a return finds a payment made after the order expired. The check that finds it makes the order paid and schedules one more check 2 minutes later, so the delivery still completes when the callback check gives up. A paid order whose delivery has to wait keeps its scheduled check until the delivery finishes.
Delivery depends on the product:
- A cosmetic item is added to the account and worn in the slot of its type. Deliveries of one item to one account run one at a time, and a delivery that waits more than about 5 seconds is tried again later. That sends User Update, and for an avatar decoration, ultra decoration or nameplate also Guild Member Update, as Wear or take off cosmetics does.
- A bundle delivers each item in the order’s
item_idsas its own delivery, the way a cosmetic item is delivered, and wears only the first item of each type in that list. The other items are added without being worn. A delivery that stops part way is tried again later and gives only the items still missing. - A premium package adds its months to the account’s premium and sends User Update.
- Visionary is delivered as Visionary describes.
A payment for an item the account already owns when the delivery runs grants no second copy, and neither does a second paid order for the same item delivered at the same time. The order becomes granted, and an admin refunds the payment by hand.
A paid cosmetic order whose item was deleted before delivery fails with item_deleted, and an admin refunds it by hand too.
A bundle order skips an item deleted before delivery and an item the account already owns, delivers the rest, and becomes granted. An admin refunds it by hand, as Bundles in the Admin API describes.
Order records
Section titled “Order records”An order is a financial record. Deleting the account keeps its orders, which hold the account ID, the product, the items of a bundle order, the amounts in kuruş, the currency, the payment provider and its order ID, and the times, and no email address, name or IP address. The data harvest lists every order of the account, newest first, without the payment page, whether or not a payment provider is configured.
Visionary
Section titled “Visionary”Visionary is lifetime premium with a number. The admin sets the price, the number of numbers, and whether Visionary is on sale. The catalog carries a shop Visionary object once the price is set. It stays in the catalog when every number is taken, with remaining at 0.
The visionary product takes campaigns, and never takes the member discount. It follows the premium sales switch, so closed premium sales return 403 SHOP_SALES_CLOSED. A switched-off Visionary, a missing price, or an account that already has lifetime premium, whether as a Visionary or not, returns 404 SHOP_ITEM_UNAVAILABLE.
Numbers are handed out as follows:
- Create shop order holds the lowest free number for 15 minutes before it asks the payment provider. When no number is free, it returns 404
SHOP_ITEM_UNAVAILABLEand the provider is never called. - An order that fails or expires unpaid frees its number.
- A payment that arrives after the hold ended still gets a number: the held one if it is still free, otherwise the next free one, otherwise one above the total. The Admin API counts a number above the total in
over_cap_grants. - An account that is already a Visionary when its order is delivered gets no second number. The order still becomes
granted, the number it held is freed because it was never any account’s number, and an admin refunds the payment by hand. Two orders of one account delivered together give one number. - A number is never given twice. A number an admin takes back, or one whose account is deleted, stays retired and is never sold again. An order whose number an admin took back never gives it again, even when its delivery is retried.
Delivery gives the account lifetime premium, with the number as its premium lifetime sequence, and the reward items the admin chose, each added and worn. It runs under the account’s premium lock, as Make user a Visionary does. While another premium change holds that lock, the delivery is tried again later. The account’s sessions receive User Update. The shop joins no Visionary guild.
In the mirror premium mode, only an account with lifetime premium can take the discriminator 0000. An account that already wears 0000 keeps it until it changes its username, and must then pick another discriminator. Otherwise the change fails with 400 INVALID_FORM_BODY and the validation code VISIONARY_REQUIRED_FOR_DISCRIMINATOR.
Get shop catalog
Section titled “Get shop catalog”GET/v1/shop/catalogReturns the shop catalog object: the priced owned items, the premium items, the priced bundles, the premium packages, Visionary, and the sales settings.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop catalog object | The catalog was returned |
| 403 | error response | FEATURE_NOT_AVAILABLE_SELF_HOSTED: the instance is not self-hosted in the mirror premium mode |
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the shop:catalog bucket.
Get shop quote
Section titled “Get shop quote”GET/v1/shop/quoteReturns the shop quote object: what the account would pay for one product now. A quote reserves nothing and binds nothing. The order records the amount charged.
Query parameters
Section titled “Query parameters”| Field | Type | Description |
|---|---|---|
| sku | string | The product |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop quote object | The quote was returned |
| 400 | error response | The SKU fails validation, returned as INVALID_FORM_BODY |
| 403 | error response | Sales are closed, or the instance is not self-hosted in the mirror premium mode |
| 404 | error response | The product cannot be bought now, returned as SHOP_ITEM_UNAVAILABLE |
| 409 | error response | The account already owns the item or an item of the bundle, returned as COSMETIC_ALREADY_OWNED |
Shop refusals lists the conditions behind each code.
Rate limit
Section titled “Rate limit”60 requests per minute for each authenticated user, on the shop:quote bucket.
Create shop order
Section titled “Create shop order”POST/v1/shop/ordersOpens a payment provider order for one product at the price the server computes now, and returns the shop order object with its payment page. The order is delivered once the provider confirms the payment, as Payment and delivery describes.
An open order of the account for the same product at the same amount, and for a bundle with the same items in the same order, is returned instead of a new one, so repeating the request opens no second payment.
Limitations
Section titled “Limitations”- The account must be claimed, and an unclaimed account returns 400
UNCLAIMED_ACCOUNT_CANNOT_MAKE_PURCHASES. - The account must have a verified email address, and an unverified address returns 403
PURCHASE_EMAIL_VERIFICATION_REQUIRED. - The account must not have the purchase-disabled premium flag, which returns 403
PREMIUM_PURCHASE_BLOCKED. - Every shop refusal applies.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| sku | string | The product to buy |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop order object | The order was opened, or an open order for the same product and amount was returned |
| 400 | error response | The SKU fails validation, or the account is unclaimed |
| 403 | error response | Sales are closed, purchases are not allowed for the account, or the instance has no shop |
| 404 | error response | The product cannot be bought now, returned as SHOP_ITEM_UNAVAILABLE |
| 409 | error response | The account owns the item or an item of the bundle, or already has three open orders |
| 503 | error response | The server cannot open a payment provider order now, returned as SHOP_BUSY |
Side effects
Section titled “Side effects”The order is written as pending with the amounts of the one price computation, and its first scheduled check is set. A Visionary order holds a number before the payment provider is asked. A bundle order records the bundle’s items in item_ids, and a later change to the bundle, or its deletion, leaves them as they are. The catalog can be up to 60 seconds old, so a client compares item_ids with the items it showed before it opens the payment page. The order description sent to the provider names the product, the price in lira and the order ID, so an admin can find the order with Get shop order in the Admin API. When the provider does not open the order, the order fails with provider_error, its Visionary number is freed, and the request returns 503 SHOP_BUSY.
Rate limit
Section titled “Rate limit”3 requests per minute for each authenticated user, on the shop:order:create bucket. 20 requests per day for each authenticated user, on the shop:order:create:day bucket.
Get shop order
Section titled “Get shop order”GET/v1/shop/orders/{order_id}Returns one order of the account as the server last recorded it, as a shop order object. The route never asks the payment provider, so it is the one to poll while a payment is open.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| order_id | snowflake | The ID of the order |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop order object | The order was returned |
| 403 | error response | FEATURE_NOT_AVAILABLE_SELF_HOSTED: the instance is not self-hosted in the mirror premium mode |
| 404 | error response | NOT_FOUND: no order has this ID, or the order belongs to another account |
Rate limit
Section titled “Rate limit”60 requests per minute for each authenticated user, on the shop:order:get bucket.
Get code preview
Section titled “Get code preview”GET/v1/shop/codes/{code}Returns what a gift code gives, premium time or a cosmetic item, as a shop code object. It is the only route that shows an item code, because Get gift answers an item code with 404. Redeem gift redeems both kinds.
Every deployment serves the route. A self-hosted instance outside the mirror premium mode returns 403 FEATURE_NOT_AVAILABLE_SELF_HOSTED.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| code | string | The code to look up (1-32 characters) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop code object | The code was returned |
| 403 | error response | FEATURE_NOT_AVAILABLE_SELF_HOSTED: the instance is self-hosted outside the mirror premium mode |
| 404 | error response | UNKNOWN_GIFT_CODE: no code exists, the code was revoked, or its item no longer exists |
Rate limit
Section titled “Rate limit”The shop:gift:get bucket takes the limit of the gift:get bucket of Get gift: 60 requests per 10 seconds for each authenticated user or client IP address. The two buckets count separately, and a supplied credential only keys the bucket.
Return from Payriff
Section titled “Return from Payriff”GET/v1/payriff/callback/{order_id}/{token}The browser lands here from the payment provider’s page. The route redirects it to the premium callback page of the web app with a status and no order detail. It never answers with an error.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| order_id | string | The ID of the order |
| token1 | string | The callback token of the order |
1 32 random bytes in base64url, created with the order. It exists only in the callback address handed to the provider. The server keeps its SHA-256 hash and compares in constant time, and the request and error logs write the path with the token masked
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 302 | empty | Always, with Location set to /premium-callback?status= on the web app and one of the statuses below |
successwhen the order is granted, or paid and waiting for delivery.cancelwhen the order failed.pendingin every other case: an unpaid or expired order, a token that does not match, an unknown or malformed order ID, or a server error.
Side effects
Section titled “Side effects”A matching token queues a check of the order with the provider, as Payment and delivery describes. Repeats within 30 seconds queue one check, and each order takes at most 10 callback checks an hour, so replaying a return link cannot spend the provider request budget. The scheduled checks run whatever the callbacks do. Anything else queues nothing.
Rate limit
Section titled “Rate limit”600 requests per minute for each client IP address, or for the authenticated user when a credential happens to be supplied, on the payriff:callback bucket.
Receive Payriff callback
Section titled “Receive Payriff callback”POST/v1/payriff/callback/{order_id}/{token}Payriff calls this when a payment changes. Payriff signs nothing, so the body is never read. Only the order ID and the token in the path count, with the same path parameters and side effects as Return from Payriff. Returns a webhook received object.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | webhook received object | Always, unless the caller accepts text/html |
| 303 | empty | The caller accepts text/html, and is sent where Return from Payriff would send it |
Rate limit
Section titled “Rate limit”600 requests per minute for each client IP address, or for the authenticated user when a credential happens to be supplied, on the payriff:callback bucket.