Skip to content
sanalyer API

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.

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.

FieldTypeDescription
premium_sales_openbooleanThe switch for premium packages and Visionary
item_sales_openbooleanThe switch for owned items and bundles
premium_discount_percentintegerThe member discount on items and bundles for accounts with paid premium (0-95)
payment_provider_configured1booleanWhether the deployment has a payment provider configured
visionary_on_salebooleanWhether Visionary is shown and sold, or hidden
visionary_reward_item_idsarray[snowflake]The items delivered and worn with every Visionary number
early_plus_badge_id?snowflakeThe catalog badge that paid premium and Visionary orders get while Early Plus is open
early_plus_openbooleanWhether paid premium and Visionary orders get the Early Plus badge
early_plus_opened_oncebooleanWhether 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.

FieldTypeDescription
skustringThe product
base_try_minorintegerThe list price in kuruş
campaign_percent?integerThe campaign discount as stored, including a campaign that has ended
campaign_ends_at?ISO8601 timestampWhen the campaign ends, null for a campaign with no end
nowshop price objectWhat the shop shows for the product now
FieldTypeDescription
skustringThe product
base_try_minor?integerThe list price in kuruş (100-10000000, ₺1 to ₺100,000), or null to take the product out of the shop
FieldTypeDescription
idsnowflakeThe ID of the bundle
skustringThe product bundle:<id>, priced with Set shop prices
namestringThe display name of the bundle (1-64 characters)
description?stringA short description (1-256 characters)
item_idsarray[snowflake]The items the bundle gives, in bundle order (2-10)
FieldTypeDescription
busy_refusals_last_hourintegerThe orders refused with SHOP_BUSY in the current and the previous clock hour
reconcile_lag_secondsintegerThe age of the oldest scheduled check still waiting, 0 while checks are current
provider_backing_offbooleanWhether the payment provider answered 429 in the last 30 seconds

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_hour above 0.
  • reconcile_lag_seconds above 900, which is 15 minutes.
  • provider_backing_off true, 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.

FieldTypeDescription
settingsshop admin settings objectThe stored settings
pricesarray[shop admin price object]Every price row, in SKU order
healthshop health objectThe payment provider budget triggers
bundlesarray[shop admin bundle object]Every bundle, priced or not, in ID order
FieldTypeDescription
idsnowflakeThe ID of the order
user_idsnowflakeThe account that bought
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 did not apply
final_try_minorintegerThe amount charged, in kuruş
currencystringAlways TRY
provider_order_id?stringThe payment provider’s order ID, null until the provider opened the order
provider_status?stringThe last payment status the provider reported
created_atISO8601 timestampWhen the order opened
paid_at?ISO8601 timestampWhen the payment was confirmed
granted_at?ISO8601 timestampWhen delivery finished
check_countintegerThe scheduled checks made so far (0-4)
item_ids?array[snowflake]Bundle orders only: the items frozen when the order opened, in bundle order
FieldTypeDescription
totalintegerThe number of Visionary numbers
soldintegerThe numbers with an owner, including taken-back numbers, which are never sold again
heldintegerThe numbers a pending order holds while its 15 minute hold lasts
remaining1integerThe numbers with no owner and no live hold
over_cap_grantsintegerThe 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

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, and none stands for no price.
  • A campaign reads as its percentage, such as 20%, or with its end as 20%@2026-10-01T00:00:00.000Z, and none stands for no campaign.

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_ids when 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.

To roll the bundle release back:

  1. Give every bundle: product a null price with Set shop prices, so no bundle order opens.
  2. Wait 24 hours after the last bundle order opened, the time of its fourth scheduled check.
  3. Check that reconcile_lag_seconds in Get shop is 0.
  4. Roll the code back. The schema only adds, so it stays.
  5. For a bundle order still paid, give each of its item_ids with Give user a cosmetic item. No route closes an order: it stays paid and holds the reconcile cursor until its check row expires after 7 days.
GET/v1/admin/shopAudit reason

Returns the shop admin object. The settings, prices and bundles are read from storage, not from the caches the shop pages use.

StatusBodyCondition
200shop admin objectThe shop was returned

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.

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

PATCH/v1/admin/shop/settingsAudit reason

Opens or closes sales, and sets the member discount and the Visionary settings. Returns the shop admin settings object.

FieldTypeDescription
premium_sales_open?booleanThe switch for premium packages and Visionary
item_sales_open?booleanThe switch for owned items and bundles
premium_discount_percent?integerThe member discount on items and bundles (0-95)
visionary_on_sale?booleanWhether 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_ids replaces 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_id is a badge from the catalog whose text has no {n}, or null. It needs badges:manage as 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_open set to true needs badges:manage as well, and a badge. Closing needs only shop:manage.
StatusBodyCondition
200shop admin settings objectThe settings were saved
400error responseA value is refused, or a product would fall below the minimum price
403error responseMISSING_ACL, because the request chooses the Early Plus badge or opens Early Plus without badges:manage

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.

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

PUT/v1/admin/shop/pricesAudit reason

Sets the list price of up to 200 products, and returns the shop admin object. The visionary product is priced here too.

FieldTypeDescription
pricesarray[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.
StatusBodyCondition
200shop admin objectThe prices were set
400error responseAn entry is refused, or a product would fall below the minimum price

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.

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

POST/v1/admin/shop/campaignsAudit reason

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

FieldTypeDescription
skusarray[string]The products, each with a price (1-200 entries)
percentintegerThe campaign discount (1-95)
ends_at?ISO8601 timestampWhen 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.

StatusBodyCondition
200shop admin objectThe campaign was applied
400error responseA SKU has no price, the end is in the past, or a product would fall below the minimum price

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.

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

POST/v1/admin/shop/campaigns/removeAudit reason

Ends the campaign of up to 200 products now, and returns the shop admin object. A SKU with no price is skipped.

FieldTypeDescription
skusarray[string]The products (1-200 entries)
StatusBodyCondition
200shop admin objectThe campaigns were removed
400error responseThe body fails validation

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.

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

POST/v1/admin/shop/bundlesAudit reason

Creates a bundle, and returns the shop admin bundle object. The bundle is not in the shop until Set shop prices gives it a price.

FieldTypeDescription
namestringThe display name of the bundle (1-64 characters)
description??stringA short description (1-256 characters)
item_idsarray[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.
StatusBodyCondition
200shop admin bundle objectThe bundle was created
400error responseAn item is refused, or the shop already holds 200 bundles

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.

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

PATCH/v1/admin/shop/bundles/{bundle_id}Audit reason

Changes the name, description or items of a bundle, and returns the updated shop admin bundle object.

FieldTypeDescription
bundle_idsnowflakeThe ID of the bundle

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.

StatusBodyCondition
200shop admin bundle objectThe bundle was updated
400error responseAn item is refused
404error responseNOT_FOUND: no bundle has this ID

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.

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

DELETE/v1/admin/shop/bundles/{bundle_id}Audit reason

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.

FieldTypeDescription
bundle_idsnowflakeThe ID of the bundle
StatusBodyCondition
204emptyThe bundle and its price were deleted
404error responseNOT_FOUND: no bundle has this ID

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.

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

GET/v1/admin/shop/orders/{order_id}Audit reason

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.

FieldTypeDescription
order_idsnowflakeThe ID of the order
StatusBodyCondition
200shop admin order objectThe order was returned
404error responseNOT_FOUND: no order has this ID

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.

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

GET/v1/admin/shop/visionaryAudit reason

Returns the shop admin Visionary object: how many numbers exist, and how many are sold, held and free.

StatusBodyCondition
200shop admin Visionary objectThe numbers were counted

The operation records one Admin audit entry with action view_shop_visionary, target type shop, target ID 0, and metadata key total.

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

PUT/v1/admin/shop/visionary/slotsAudit reason

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

FieldTypeDescription
totalintegerThe number of Visionary numbers (0-100000)
StatusBodyCondition
200shop admin Visionary objectThe count was set
400error responseA removed number has an owner or a live hold, returned as CANNOT_SHRINK_RESERVED_SLOTS

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.

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