Skip to content
sanalyer API

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.

A product is named by its SKU.

ValueDescription
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:12Premium time of 1, 3 or 12 months
visionaryLifetime 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.

The server computes a price in one place, in whole numbers:

  1. The campaign discount comes off the list price while the campaign has not ended by the server clock.
  2. 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.

FieldTypeDescription
base_try_minorintegerThe list price in kuruş
campaign_percent?integerThe campaign discount, null without a running campaign
campaign_ends_at?ISO8601 timestampWhen the campaign ends, null without a campaign or without an end
sale_try_minorintegerThe price after the campaign in kuruş, equal to base_try_minor without one
premium_try_minor1?integerWhat 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

FieldTypeDescription
premium_sales_openbooleanWhether premium packages and Visionary are on sale
item_sales_openbooleanWhether cosmetic items and bundles are on sale
premium_discount_percentintegerThe member discount on items and bundles for accounts with paid premium (0-95)

Both sales fields read false while no payment provider is configured.

FieldTypeDescription
itemcosmetic item objectThe item
price1?shop price objectThe price of an owned item

1 Null for a premium-availability item, which comes with premium and is not sold

FieldTypeDescription
skustringThe product: premium:1, premium:3 or premium:12
monthsintegerThe premium time the package adds: 1, 3 or 12 months
priceshop price objectThe price
FieldTypeDescription
skustringThe product: bundle:<id>
idsnowflakeThe ID of the bundle
namestringThe display name of the bundle
description?stringA short description of the bundle
items1array[cosmetic item object]The items the bundle gives, in bundle order
priceshop price objectThe 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

FieldTypeDescription
skustringAlways visionary
priceshop price objectThe price, with any running campaign
totalintegerThe number of Visionary numbers the admin set
remaining1integerThe numbers with no owner and no live hold
visiblebooleanWhether 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

FieldTypeDescription
premium_discountbooleanWhether the account pays premium_try_minor for items, which needs paid premium rather than a trial
FieldTypeDescription
settingsshop settings objectThe sales switches and the member discount
itemsarray[shop catalog item object]The priced owned items and the premium items on sale, in catalog order
packagesarray[shop package object]The priced premium packages, in 1, 3 and 12 month order
visionary?shop Visionary objectVisionary, null until the admin sets its price
bundlesarray[shop bundle object]The priced bundles whose items are all on sale, in ID order
server_timeISO8601 timestampThe server clock, for campaign countdowns
viewershop viewer objectWhat applies to the requesting account

Only viewer differs between accounts. The rest is the same for every account.

FieldTypeDescription
skustringThe product
base_try_minorintegerThe list price in kuruş
campaign_percentintegerThe campaign discount applied, 0 without one
premium_percentintegerThe member discount applied, 0 when it does not apply
final_try_minorintegerThe price after both discounts, in kuruş, which an order would charge
currencystringAlways TRY
ValueDescription
pendingThe order is open and waiting for payment
paidThe payment provider confirmed the payment, and delivery has not finished
grantedThe item, the bundle’s items, the premium time or the Visionary number was delivered
failedNothing is delivered, and failure_reason says why
expiredThe order was still unpaid at its last check, and a payment the provider confirms later still completes it
ValueDescription
amount_mismatchThe payment provider reported a payment whose amount or currency differs from the order
account_deletedThe account was deleted before delivery
item_deletedThe item of a cosmetic order was deleted before delivery
provider_errorThe 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.

FieldTypeDescription
idsnowflakeThe ID of the order
skustringThe product
statusstringThe shop order status
failure_reason?stringThe failure reason, null unless the order failed
base_try_minorintegerThe list price in kuruş when the order opened
campaign_percentintegerThe campaign discount applied, 0 without one
premium_percentintegerThe member discount applied, 0 when it does not apply
final_try_minorintegerThe amount charged, in kuruş
currencystringAlways TRY
payment_url?stringThe payment provider’s page, null unless the order is pending
created_atISO8601 timestampWhen the order opened
item_ids?array[snowflake]Bundle orders only: the items the order delivers, frozen when it opened, in bundle order
{
"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"
}
FieldTypeDescription
codestringThe code
redeemedbooleanWhether the code has been redeemed
grantshop code grant objectWhat redeeming the code gives
FieldTypeDescription
typestringpremium for premium time, or cosmetic for an item
duration_type?stringPremium only: the gift duration unit
duration_quantity?integerPremium only: the number of units
item?cosmetic item objectCosmetic only: the item

The quote and order routes refuse a product with these codes:

  • 403 SHOP_SALES_CLOSED when 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_UNAVAILABLE when 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_OWNED when 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.
  1. Create shop order returns an order with payment_url, the payment provider’s page, which a client opens in the browser.
  2. 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.
  3. The check accepts a payment only when the provider reports it APPROVED or PAID for a purchase, in the order’s currency, for exactly the amount of the order. The order becomes paid, is delivered, and becomes granted.
  4. 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_ids as 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.

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 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_UNAVAILABLE and 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/v1/shop/catalog

Returns the shop catalog object: the priced owned items, the premium items, the priced bundles, the premium packages, Visionary, and the sales settings.

StatusBodyCondition
200shop catalog objectThe catalog was returned
403error responseFEATURE_NOT_AVAILABLE_SELF_HOSTED: the instance is not self-hosted in the mirror premium mode

30 requests per minute for each authenticated user, on the shop:catalog bucket.

GET/v1/shop/quote

Returns 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.

FieldTypeDescription
skustringThe product
StatusBodyCondition
200shop quote objectThe quote was returned
400error responseThe SKU fails validation, returned as INVALID_FORM_BODY
403error responseSales are closed, or the instance is not self-hosted in the mirror premium mode
404error responseThe product cannot be bought now, returned as SHOP_ITEM_UNAVAILABLE
409error responseThe account already owns the item or an item of the bundle, returned as COSMETIC_ALREADY_OWNED

Shop refusals lists the conditions behind each code.

60 requests per minute for each authenticated user, on the shop:quote bucket.

POST/v1/shop/orders

Opens 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.

  • 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.
FieldTypeDescription
skustringThe product to buy
StatusBodyCondition
200shop order objectThe order was opened, or an open order for the same product and amount was returned
400error responseThe SKU fails validation, or the account is unclaimed
403error responseSales are closed, purchases are not allowed for the account, or the instance has no shop
404error responseThe product cannot be bought now, returned as SHOP_ITEM_UNAVAILABLE
409error responseThe account owns the item or an item of the bundle, or already has three open orders
503error responseThe server cannot open a payment provider order now, returned as SHOP_BUSY

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.

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/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.

FieldTypeDescription
order_idsnowflakeThe ID of the order
StatusBodyCondition
200shop order objectThe order was returned
403error responseFEATURE_NOT_AVAILABLE_SELF_HOSTED: the instance is not self-hosted in the mirror premium mode
404error responseNOT_FOUND: no order has this ID, or the order belongs to another account

60 requests per minute for each authenticated user, on the shop:order:get bucket.

GET/v1/shop/codes/{code}Unauthenticated

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.

FieldTypeDescription
codestringThe code to look up (1-32 characters)
StatusBodyCondition
200shop code objectThe code was returned
403error responseFEATURE_NOT_AVAILABLE_SELF_HOSTED: the instance is self-hosted outside the mirror premium mode
404error responseUNKNOWN_GIFT_CODE: no code exists, the code was revoked, or its item no longer exists

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.

GET/v1/payriff/callback/{order_id}/{token}Unauthenticated

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.

FieldTypeDescription
order_idstringThe ID of the order
token1stringThe 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

StatusBodyCondition
302emptyAlways, with Location set to /premium-callback?status= on the web app and one of the statuses below
  • success when the order is granted, or paid and waiting for delivery.
  • cancel when the order failed.
  • pending in every other case: an unpaid or expired order, a token that does not match, an unknown or malformed order ID, or a server error.

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.

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.

POST/v1/payriff/callback/{order_id}/{token}Unauthenticated

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.

StatusBodyCondition
200webhook received objectAlways, unless the caller accepts text/html
303emptyThe caller accepts text/html, and is sent where Return from Payriff would send it

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.