Shop
The shop sells owned cosmetic items, bundles of them, premium packages and Visionary in Turkish lira, as Shop describes. Every amount on this page is in kuruş, so ₺1 is 100. These routes set the sales switches, the member discount, the prices and the campaigns, manage the bundles, look up an order, and manage the Visionary numbers. Every route on this page requires shop:manage and records an Admin audit entry, reads included. The routes answer in every premium mode, so an admin can prepare the shop before opening it.
A value the shop cannot accept returns 400 INVALID_FORM_BODY, with one element in errors that names the field and carries an English message. Make user a Visionary and Take Visionary from user are on the Admin users page.
Minimum price
Section titled “Minimum price”Every write that changes a price, a campaign or the member discount is checked against the worst case, which is the campaign together with the member discount on an item. A write that would leave any product below ₺1 (100) returns 400 INVALID_FORM_BODY, and nothing is written. Create shop order checks the final price again.
Shop admin settings object
Section titled “Shop admin settings object”| Field | Type | Description |
|---|---|---|
| premium_sales_open | boolean | The switch for premium packages and Visionary |
| item_sales_open | boolean | The switch for owned items and bundles |
| premium_discount_percent | integer | The member discount on items and bundles for accounts with paid premium (0-95) |
| payment_provider_configured1 | boolean | Whether the deployment has a payment provider configured |
| visionary_on_sale | boolean | Whether Visionary is shown and sold, or hidden |
| visionary_reward_item_ids | array[snowflake] | The items delivered and worn with every Visionary number |
| early_plus_badge_id | ?snowflake | The catalog badge that paid premium and Visionary orders get while Early Plus is open |
| early_plus_open | boolean | Whether paid premium and Visionary orders get the Early Plus badge |
| early_plus_opened_once | boolean | Whether Early Plus has opened at least once, after which its badge can no longer change |
1 False keeps every sale closed whatever the switches say. It comes from the deployment configuration, and no route changes it. The one integration today is Payriff, which stays off unless the deployment enables it. A sanalyer process refuses to start while FLUXER_PAYRIFF_BASE_URL is not an https URL, even with Payriff switched off
Both sales switches, visionary_on_sale, early_plus_open and early_plus_opened_once start off, early_plus_badge_id starts null, and the member discount starts at 0. The settings live in the instance configuration under the shop_config key.
While no payment provider is configured, give items and premium time with gift codes and admin grants such as Give user a cosmetic item.
Shop admin price object
Section titled “Shop admin price object”| Field | Type | Description |
|---|---|---|
| sku | string | The product |
| base_try_minor | integer | The list price in kuruş |
| campaign_percent | ?integer | The campaign discount as stored, including a campaign that has ended |
| campaign_ends_at | ?ISO8601 timestamp | When the campaign ends, null for a campaign with no end |
| now | shop price object | What the shop shows for the product now |
Shop price entry object
Section titled “Shop price entry object”| Field | Type | Description |
|---|---|---|
| sku | string | The product |
| base_try_minor | ?integer | The list price in kuruş (100-10000000, ₺1 to ₺100,000), or null to take the product out of the shop |
Shop admin bundle object
Section titled “Shop admin bundle object”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the bundle |
| sku | string | The product bundle:<id>, priced with Set shop prices |
| name | string | The display name of the bundle (1-64 characters) |
| description | ?string | A short description (1-256 characters) |
| item_ids | array[snowflake] | The items the bundle gives, in bundle order (2-10) |
Shop health object
Section titled “Shop health object”| Field | Type | Description |
|---|---|---|
| busy_refusals_last_hour | integer | The orders refused with SHOP_BUSY in the current and the previous clock hour |
| reconcile_lag_seconds | integer | The age of the oldest scheduled check still waiting, 0 while checks are current |
| provider_backing_off | boolean | Whether the payment provider answered 429 in the last 30 seconds |
Payment provider request budget
Section titled “Payment provider request budget”The budget is a temporary limit sized for the shop’s traffic today. Every API and worker process shares about 2 requests per second to the payment provider, split into three buckets so that none can starve another. Opening orders has about 0.3 per second, callback checks about 0.5 per second, and scheduled checks about 1.2 per second. The shop therefore opens about 0.3 orders per second, roughly 18 a minute, and refuses the rest with 503 SHOP_BUSY. The API logs such a refusal at debug level, as expected load shedding, and counts it in busy_refusals_last_hour.
The three buckets hold at most 6, 10 and 12 requests, 28 together, which stays under the burst of 30 observed from Payriff. The reconcile job runs once a minute or more often, and each run spends only what the reconcile bucket holds, 12 requests when it is full. While reconcile_lag_seconds is above 900, every reconcile run also logs a warning.
Grow the budget when any of these appears:
busy_refusals_last_hourabove 0.reconcile_lag_secondsabove 900, which is 15 minutes.provider_backing_offtrue, after the first 429 from the provider.- Daily orders reaching half of the limit the provider approved.
The first step is a higher provider quota, with the three buckets grown together in the same 1 : 1.7 : 4 ratio. The next is a server-side order queue in place of SHOP_BUSY. The last is a second payment provider, for which every order already records its provider.
Shop admin object
Section titled “Shop admin object”| Field | Type | Description |
|---|---|---|
| settings | shop admin settings object | The stored settings |
| prices | array[shop admin price object] | Every price row, in SKU order |
| health | shop health object | The payment provider budget triggers |
| bundles | array[shop admin bundle object] | Every bundle, priced or not, in ID order |
Shop admin order object
Section titled “Shop admin order object”| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the order |
| user_id | snowflake | The account that bought |
| 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 did not apply |
| final_try_minor | integer | The amount charged, in kuruş |
| currency | string | Always TRY |
| provider_order_id | ?string | The payment provider’s order ID, null until the provider opened the order |
| provider_status | ?string | The last payment status the provider reported |
| created_at | ISO8601 timestamp | When the order opened |
| paid_at | ?ISO8601 timestamp | When the payment was confirmed |
| granted_at | ?ISO8601 timestamp | When delivery finished |
| check_count | integer | The scheduled checks made so far (0-4) |
| item_ids? | array[snowflake] | Bundle orders only: the items frozen when the order opened, in bundle order |
Shop admin Visionary object
Section titled “Shop admin Visionary object”| Field | Type | Description |
|---|---|---|
| total | integer | The number of Visionary numbers |
| sold | integer | The numbers with an owner, including taken-back numbers, which are never sold again |
| held | integer | The numbers a pending order holds while its 15 minute hold lasts |
| remaining1 | integer | The numbers with no owner and no live hold |
| over_cap_grants | integer | The numbers given above total because a payment arrived after its hold ended |
1 Read fresh on every request. A number row without a version, which only upstream code writes, is never handed out and never counts here
Shop audit changes
Section titled “Shop audit changes”The price and campaign operations record what they changed in the changes metadata key. It lists each product whose price or campaign changed as <sku>:<old>→<new>, joined with commas, and leaves out a product that stayed the same. The key is absent when nothing changed.
- A price reads as kuruş, such as
premium:1:50000→45000, andnonestands for no price. - A campaign reads as its percentage, such as
20%, or with its end as20%@2026-10-01T00:00:00.000Z, andnonestands for no campaign.
Bundles
Section titled “Bundles”A bundle sells 2-10 owned-availability items as one product, bundle:<id>. Create shop bundle makes it, and it enters the shop once Set shop prices gives it a price. Campaigns and the member discount apply as they do to an item. An item sold only in bundles is an owned-availability item with no price of its own.
- A null price takes a bundle out of the shop and keeps it. Delete shop bundle removes the bundle and its price.
- A bundle leaves the catalog while any of its items is off sale, and cannot be bought until the item is back or the bundle changes.
- A Visionary reward cannot be in a bundle, and an item in a bundle cannot become a Visionary reward.
- An account that owns any item of a bundle cannot buy the bundle, so an item bought alone keeps its buyer from every bundle that holds it.
- An order freezes the bundle’s items in
item_idswhen it opens. Open and paid orders deliver those items even after the bundle changes or is deleted.
Delivery gives each item as its own delivery, keyed by the order and the item, then wears the first item of each type it gave in one user update. An item deleted before delivery is skipped, the rest are delivered, the order becomes granted, and the API logs a warning with the order and item IDs. Refund by hand.
Deploying and rolling back bundles
Section titled “Deploying and rolling back bundles”To roll the bundle release back:
- Give every
bundle:product a null price with Set shop prices, so no bundle order opens. - Wait 24 hours after the last bundle order opened, the time of its fourth scheduled check.
- Check that
reconcile_lag_secondsin Get shop is 0. - Roll the code back. The schema only adds, so it stays.
- For a bundle order still
paid, give each of itsitem_idswith Give user a cosmetic item. No route closes an order: it stayspaidand holds the reconcile cursor until its check row expires after 7 days.
Get shop
Section titled “Get shop”GET/v1/admin/shopReturns the shop admin object. The settings, prices and bundles are read from storage, not from the caches the shop pages use.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin object | The shop was returned |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action view_shop, target type shop, target ID 0, and metadata key result_count, the number of price rows.
Rate limit
Section titled “Rate limit”60 requests per minute for each authenticated user, on the admin:shop:view bucket.
Update shop settings
Section titled “Update shop settings”PATCH/v1/admin/shop/settingsOpens or closes sales, and sets the member discount and the Visionary settings. Returns the shop admin settings object.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| premium_sales_open? | boolean | The switch for premium packages and Visionary |
| item_sales_open? | boolean | The switch for owned items and bundles |
| premium_discount_percent? | integer | The member discount on items and bundles (0-95) |
| visionary_on_sale? | boolean | Whether Visionary is shown and sold |
| visionary_reward_item_ids? | array[snowflake] | The items delivered and worn with every Visionary number |
An omitted field stays as it is. payment_provider_configured and early_plus_opened_once are read-only.
visionary_reward_item_idsreplaces the list and holds at most 10 IDs. Each must be an owned-availability item without a shop price and in no bundle, and a repeated ID counts once.early_plus_badge_idis a badge from the catalog whose text has no{n}, or null. It needsbadges:manageas well. Once Early Plus has opened, the badge is fixed for good. Choose a badge made only for Early Plus, because a take-back removes that badge even when an admin gave it by hand.early_plus_openset to true needsbadges:manageas well, and a badge. Closing needs onlyshop:manage.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin settings object | The settings were saved |
| 400 | error response | A value is refused, or a product would fall below the minimum price |
| 403 | error response | MISSING_ACL, because the request chooses the Early Plus badge or opens Early Plus without badges:manage |
Side effects
Section titled “Side effects”The settings are saved with a compare-and-set, and every API process picks them up through the instance configuration refresh. The shop catalog can show the old values for up to 60 seconds. A new reward item reaches only numbers delivered after the change.
While Early Plus is open, every delivered premium or Visionary order gives its buyer the Early Plus badge. Gift codes, admin grants, referral rewards and trials do not, and neither does an order support refunds by hand: a premium order paid by a Visionary, or a Visionary order paid by an account that already is one. Every request that sets early_plus_open to true queues a backfillEarlyPlusBadges job, at most one a minute, shown on the jobs page. The job gives the badge to the buyer of every premium order delivered so far, and of every Visionary order whose buyer still is a Visionary. It skips deleted accounts and accounts with the EARLY_PLUS_REFUNDED premium flag. Its result holds granted, scanned and duration_ms, and a cancel on the jobs page stops it. Closing stops new badges. Badges already given stay.
Ending paid premium and taking Visionary back both take the badge away and set EARLY_PLUS_REFUNDED, and the next delivered premium or Visionary order clears the flag. A refund made before Early Plus existed has no flag, so its badge must be taken away by hand.
The operation records one Admin audit entry with action update_shop_settings, target type shop, and target ID 0. For every field the request changed, the metadata holds <field>_old and <field>_new.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Set shop prices
Section titled “Set shop prices”PUT/v1/admin/shop/pricesSets the list price of up to 200 products, and returns the shop admin object. The visionary product is priced here too.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| prices | array[shop price entry object] | The prices to set (1-200 entries) |
- A cosmetic SKU must name an owned-availability item, and a Visionary reward item cannot take a price.
- A bundle SKU must name an existing bundle. A null price is accepted for any bundle SKU, which clears a price left behind by a deleted bundle.
- A new price keeps the product’s campaign. A null price removes the product and its campaign.
- A SKU repeated in the list takes its last value.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin object | The prices were set |
| 400 | error response | An entry is refused, or a product would fall below the minimum price |
Side effects
Section titled “Side effects”A quote shows a new price within 5 seconds and the catalog within 60 seconds. An order opened after the write is charged the new price.
The operation records one Admin audit entry with action set_shop_prices, target type shop, target ID 0, and metadata keys count, skus, the SKUs joined with commas, and changes, as Shop audit changes describes.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Apply shop campaign
Section titled “Apply shop campaign”POST/v1/admin/shop/campaignsPuts one campaign discount on up to 200 priced products, until the given time or with no end, and returns the shop admin object. Visionary takes campaigns too. It never takes the member discount, which items still take on top of the campaign.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| skus | array[string] | The products, each with a price (1-200 entries) |
| percent | integer | The campaign discount (1-95) |
| ends_at | ?ISO8601 timestamp | When the campaign ends, in the future, or null for no end |
A campaign replaces the product’s previous one, and a repeated SKU counts once. The campaign ends by the server clock. Its row keeps the percentage until it is removed or replaced, and now shows the price without it.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin object | The campaign was applied |
| 400 | error response | A SKU has no price, the end is in the past, or a product would fall below the minimum price |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action apply_shop_campaign, target type shop, target ID 0, and metadata keys count, percent, ends_at and changes, as Shop audit changes describes.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Remove shop campaign
Section titled “Remove shop campaign”POST/v1/admin/shop/campaigns/removeEnds the campaign of up to 200 products now, and returns the shop admin object. A SKU with no price is skipped.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| skus | array[string] | The products (1-200 entries) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin object | The campaigns were removed |
| 400 | error response | The body fails validation |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action remove_shop_campaign, target type shop, target ID 0, and metadata keys count and changes, as Shop audit changes describes.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Create shop bundle
Section titled “Create shop bundle”POST/v1/admin/shop/bundlesCreates a bundle, and returns the shop admin bundle object. The bundle is not in the shop until Set shop prices gives it a price.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| name | string | The display name of the bundle (1-64 characters) |
| description? | ?string | A short description (1-256 characters) |
| item_ids | array[snowflake] | The items, in bundle order (2-10) |
- A repeated item counts once, in its first place, and 2 to 10 items must remain.
- Each item must be an owned-availability item on sale, with or without a price of its own, and not a Visionary reward.
- The shop holds at most 200 bundles.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin bundle object | The bundle was created |
| 400 | error response | An item is refused, or the shop already holds 200 bundles |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action create_shop_bundle, target type shop_bundle, the bundle ID as target ID, and metadata keys name and item_ids, the item IDs joined with commas.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Update shop bundle
Section titled “Update shop bundle”PATCH/v1/admin/shop/bundles/{bundle_id}Changes the name, description or items of a bundle, and returns the updated shop admin bundle object.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| bundle_id | snowflake | The ID of the bundle |
JSON body
Section titled “JSON body”Every field of Create shop bundle, each optional. An omitted field stays as it is, and a null description clears it. New items are checked as on create.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin bundle object | The bundle was updated |
| 400 | error response | An item is refused |
| 404 | error response | NOT_FOUND: no bundle has this ID |
Side effects
Section titled “Side effects”A quote shows the change within 5 seconds and the catalog within 60 seconds. An order opened after the write gets the new items, and orders opened before keep theirs.
The operation records one Admin audit entry with action update_shop_bundle, target type shop_bundle, and the bundle ID as target ID. For every field the request changed, the metadata holds <field>_old and <field>_new, with item IDs joined with commas.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Delete shop bundle
Section titled “Delete shop bundle”DELETE/v1/admin/shop/bundles/{bundle_id}Deletes a bundle together with its price. To take a bundle out of the shop and keep it, give it a null price with Set shop prices instead.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| bundle_id | snowflake | The ID of the bundle |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The bundle and its price were deleted |
| 404 | error response | NOT_FOUND: no bundle has this ID |
Side effects
Section titled “Side effects”Open and paid orders still deliver the items they were opened with. The operation records one Admin audit entry with action delete_shop_bundle, target type shop_bundle, the bundle ID as target ID, and metadata key name.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Get shop order
Section titled “Get shop order”GET/v1/admin/shop/orders/{order_id}Returns one order as a shop admin order object, for support and refunds. A bundle order lists its items in item_ids. The order description sent to the payment provider names the product and the price in lira, and ends with Sipariş # and the order ID, so the provider’s panel leads back to the order.
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 admin order object | The order was returned |
| 404 | error response | NOT_FOUND: no order has this ID |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action view_shop_order, target type shop_order, the order ID as target ID, and metadata key order_id.
Rate limit
Section titled “Rate limit”60 requests per minute for each authenticated user, on the admin:shop:view bucket.
Get Visionary numbers
Section titled “Get Visionary numbers”GET/v1/admin/shop/visionaryReturns the shop admin Visionary object: how many numbers exist, and how many are sold, held and free.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin Visionary object | The numbers were counted |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action view_shop_visionary, target type shop, target ID 0, and metadata key total.
Rate limit
Section titled “Rate limit”60 requests per minute for each authenticated user, on the admin:shop:view bucket.
Set Visionary numbers
Section titled “Set Visionary numbers”PUT/v1/admin/shop/visionary/slotsSets how many Visionary numbers exist, and returns the shop admin Visionary object. Growing adds free numbers after the highest one. Shrinking removes numbers from the top.
A sold number stays counted in total and is never sold again, so selling more numbers means raising total by as many.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| total | integer | The number of Visionary numbers (0-100000) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin Visionary object | The count was set |
| 400 | error response | A removed number has an owner or a live hold, returned as CANNOT_SHRINK_RESERVED_SLOTS |
Side effects
Section titled “Side effects”A number with an owner or a live hold is never removed, and a shrink that races with a sale is refused. Read Get Visionary numbers to see how many are sold and held.
The operation records one Admin audit entry with action set_visionary_slots, target type shop, target ID 0, and metadata keys total_old and total_new.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.