Shop
The shop sells owned cosmetic items, bundles of them, premium packages, Visionary, the boost slot and username cards through Payriff, as Shop describes. These routes set the sales switches, the member discount, the rate, the prices and the campaigns, manage the bundles, look up an order or an account’s premium subscription, manage the Visionary numbers, and manage the username cards. Every route on this page requires shop:manage and records an Admin audit entry, reads included. Mark shop order refunded also requires user:update:username and username:reserve; Mark Payriff boost order refunded needs shop:manage alone. 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, the member discount or the rate 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 cent or below 0.10 AZN 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, Visionary and the boost slot |
| 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) |
| usd_to_azn_rate_micro | integer | Manat per US dollar in millionths, so a rate of 1.70 is 1700000 |
| try_to_azn_rate_micro | ?integer | Last accepted CBAR rate, in millionths of AZN per TRY; display only |
| try_rate_date | ?date | Bulletin date from CBAR’s XML, in YYYY-MM-DD format |
| try_rate_fetched_at | ?ISO8601 timestamp | Last successful rate refresh |
| try_to_azn_rate_override_micro | ?integer | Manual display rate; null uses the automatic rate |
| try_rate_override_at | ?ISO8601 timestamp | When the current override was set; null when cleared |
| payriff_configured1 | boolean | Whether the deployment has a Payriff configuration |
| 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 |
| payment_default_route? | payment route | The route of a country without its own row |
| payment_country_routes? | object | The route of each listed country, by ISO 3166-1 alpha-2 code (at most 250) |
| payment_providers? | object | payriff and paddle, each {orders_open}: whether the provider takes new orders |
| paddle_configured? | boolean | Whether Paddle is switched on with a key, a webhook secret and products |
| paddle_environment? | ?string | sandbox or production; null when Paddle is not configured |
| payriff_last_order_at? | ?ISO8601 timestamp | When the last Payriff order opened; keep the Payriff key at least 7 days after it |
| payment_route_warnings? | array[route warning] | The routes that sell nothing now, and why, as payment routes describes |
| paddle_test_user_ids? | array[snowflake] | The accounts that buy through Paddle whatever their route and the Paddle switch say |
| paddle_test_currency? | string | USD or TRY: what Paddle charges a test account in |
| paddle_trial_days? | integer | Free days (0-30) before a new Paddle Plus subscription’s first charge, once an account; 0 offers none |
| username_card_sale_1 | boolean | The switch for the 1 character username card |
| username_card_sale_2 | boolean | The switch for the 2 character username card |
| username_card_sale_3 | boolean | The switch for the 3 character username card |
| username_card_asset_1 | ?string | The image hash of the 1 character card, null without an image |
| username_card_asset_2 | ?string | The image hash of the 2 character card, null without an image |
| username_card_asset_3 | ?string | The image hash of the 3 character card, null without an image |
1 False keeps every sale closed whatever the switches say. It comes from the deployment configuration, and no route changes it. A Micup 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, early_plus_opened_once and the three username card switches start off, early_plus_badge_id starts null, the card images start null, the member discount starts at 0, and the rate starts at 1700000. The settings live in the instance configuration under the shop_config key.
Payriff charges AZN; users see TL (CBAR daily rate) and the AZN amount. The price anchor remains the stored US-cent price and usd_to_azn_rate_micro. Changing either TRY rate never changes a quote, an order or the Payriff charge. Legacy dollar fields remain in the API for older clients.
The effective display rate is the override, or the automatic rate if its bulletin is no more than 10 days old. Without an effective rate, the client shows AZN alone and sales continue. The worker checks CBAR every six hours, and queues an initial refresh when no automatic rate is stored. Catalog requests use the existing shared cache and never fetch CBAR themselves.
The refresh reads only the TRY entry, divides Value by Nominal, and uses the XML bulletin date even when the requested day is a weekend. It rejects malformed XML, DOCTYPE declarations, older bulletins, rates outside 0.001–1.0 AZN per TRY, and changes greater than 25% from the previous automatic rate. A rejected refresh logs a warning and preserves the last accepted rate and admin settings. Requests have a timeout, a response-size limit and no redirects.
Payment routes
Section titled “Payment routes”A payment route says who sells to a country and in which currency. It is an object with a provider of paddle, payriff or off.
| Provider | Currency | Meaning |
|---|---|---|
| paddle | USD or TRY | Paddle charges in this currency |
| payriff | TRY or AZN | The price is shown in this currency, and Payriff always charges AZN. TRY falls back to AZN without a TL rate |
| off | none | Nobody sells in the country |
The country comes from GeoIP. A country missing from payment_country_routes, or an unknown country, takes payment_default_route. The default route starts as payriff with TRY, the country table starts empty, Payriff orders start open, and Paddle orders start closed. A closed provider still checks and delivers the orders it already has.
A route warning holds country, null for the default route and for geoip_unavailable, an optional provider, and a reason:
not_configured: the provider is not configured, so the route sells nothing.orders_closed: the provider’s switch is off.try_rate_missing: the route needs a TL rate that is not known.geoip_unavailable: no country is known, so every buyer takes the default route.
Shop admin price object
Section titled “Shop admin price object”| Field | Type | Description |
|---|---|---|
| sku | string | The product |
| base_usd_cents | integer | The list price in US cents |
| 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 |
| base_try_minor? | ?integer | The list price in kuruş where a route charges TRY; null converts from US cents |
| base_azn_minor? | ?integer | The list price in qəpik where a route charges AZN; null converts from US cents |
| 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_usd_cents? | ?integer | List price in US cents (1-10000000); null takes the product off the shop; absent keeps the stored price (400 if none) |
| base_try_minor? | ?integer | The list price in kuruş (1-1000000000) where a route charges TRY; null clears it, absent keeps it |
| base_azn_minor? | ?integer | The list price in qəpik (1-1000000000) where a route charges AZN; null clears it, absent keeps it |
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 |
| payriff_backing_off | boolean | Whether Payriff answered 429 in the last 30 seconds |
Payriff request budget
Section titled “Payriff 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 Payriff, 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.
A username card order first spends a share of its own, about 0.15 per second with at most 3 at once, then holds its username, and only then spends a request from the opening bucket, so a rush for short usernames cannot take the whole opening budget, and an order that loses the username spends no opening request. A refusal from either returns 503 SHOP_BUSY and counts in busy_refusals_last_hour too, and a refusal from the opening bucket frees the username the order held.
Grow the budget when any of these appears:
busy_refusals_last_hourabove 0.reconcile_lag_secondsabove 900, which is 15 minutes.payriff_backing_offtrue, after the first 429 from Payriff.- Daily orders reaching half of the limit Payriff approved.
The first step is a higher Payriff 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 Payriff 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 |
| billing_mode? | string | one_time, or recurring for the first payment of a premium subscription |
| interval_months? | ?integer | Recurring only: the period, 1, 3 or 12 months |
| terms_version? | ?string | Recurring only: the subscription terms the buyer accepted |
| base_usd_cents | integer | The list price in US cents 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_usd_cents | integer | The US dollar price charged |
| amount_azn_minor | integer | The price in qəpik: what Payriff charged, and the AZN equivalent for Paddle |
| usd_to_azn_rate | string | The rate used, with four decimal places |
| currency | string | The currency the provider charged: AZN for Payriff, USD or TRY for Paddle |
| display_currency? | ?string | Currency of the price shown in the shop when the order opened: TRY or AZN; null on legacy orders |
| display_amount_minor? | ?integer | That shown price in minor units (kuruş or qəpik), frozen on the order; null on legacy orders |
| display_rate_micro? | ?integer | Frozen AZN per TRY in millionths used for the shown TRY price; null for AZN or legacy orders |
| provider? | string | Who took the payment, payriff or paddle; absent means Payriff |
| charge_amount_minor? | integer | What the provider charged, in the minor unit of currency; absent means amount_azn_minor |
| provider_order_id | ?string | The Payriff order ID or the Paddle transaction ID, null until the provider opened the order |
| provider_status | ?string | The last payment status the provider reported |
| country_code? | ?string | The GeoIP country of the order, null when unknown |
| provider_price_id? | ?string | The Paddle price ID of the order’s one line, null for Payriff |
| provider_adjustment? | ?string | The last Paddle adjustment as action:status, such as refund:approved |
| adjusted_at? | ?ISO8601 timestamp | When that adjustment was recorded |
| 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 |
| username? | ?string | Username card orders only: the username the order buys, null once the account is deleted |
Older Paddle orders may carry the buyer’s consent to immediate delivery; new orders record none.
| Field | Type | Description |
|---|---|---|
| immediate_performance_accepted_at? | ?ISO8601 timestamp | When the buyer accepted immediate delivery, null without consent |
| immediate_performance_terms_version? | ?string | The version of the consent text the buyer accepted |
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 Micup code writes, is never handed out and never counts here
Shop admin username card object
Section titled “Shop admin username card object”| Field | Type | Description |
|---|---|---|
| length | integer | The username length the card sells: 1, 2 or 3 |
| sku | string | The product username:<length> |
| tier | string | The username policy tier of the length: locked, for_sale or public |
| on_sale | boolean | The switch of the length, username_card_sale_<length> |
| asset | ?string | The image hash, null without an image |
| base_usd_cents | ?integer | The list price in US cents, null without a price |
| sold | integer | The delivered orders of the card, less the refunded ones |
| free1 | ?integer | The sellable usernames of the length that nobody has, holds or embargoes, null before the first count |
| sellable1 | ?integer | The usernames of the length that the dictionary and the username rules allow, null before the first count |
| counted_at | ?ISO8601 timestamp | When the hourly count last ran, null before the first |
1 From the hourly count, which runs at minute 17 of every hour. It makes one point read for each allowed username of the three lengths, at most 47,988, and never scans the table. A username whose read fails counts sellable but not free, so free is never too high. More than 16 failed reads in one length fail the count: that length and the ones after it keep their last count and counted_at
Shop admin username cards object
Section titled “Shop admin username cards object”| Field | Type | Description |
|---|---|---|
| migration_state | string | The username migration state: legacy, preparing or selecting |
| cards | array[shop admin username card object] | The cards of lengths 1, 2 and 3, in that order |
| refunds_due | username refunds due object | The paid username card orders waiting for a refund |
Username refunds due object
Section titled “Username refunds due object”| Field | Type | Description |
|---|---|---|
| total | integer | How many paid username card orders wait for a refund |
| items | array[username refund due object] | The oldest of them first, at most 100 |
Username refund due object
Section titled “Username refund due object”| Field | Type | Description |
|---|---|---|
| order_id | snowflake | The ID of the order |
| user_id | snowflake | The account that bought |
| sku | string | The product username:<length> |
| amount_azn_minor | integer | The amount charged, in minor units of currency. For amount_mismatch, the amount Payriff reported |
| currency | string | The currency of amount_azn_minor: AZN unless Payriff reported another |
| failure_reason | string | The failure reason: a username reason, account_deleted or amount_mismatch |
| failed_at | ISO8601 timestamp | When the order failed |
Shop admin refund object
Section titled “Shop admin refund object”| Field | Type | Description |
|---|---|---|
| order | shop admin order object | The order, now refunded |
| name_action | string | The username refund name action |
Username refund name actions
Section titled “Username refund name actions”| Value | Description |
|---|---|
| none | No name step: nothing was delivered, or the username changed hands or is held by another order |
| held | The username went into an admin hold |
| reselected | The buyer must pick a new username of 4 or more characters |
| reselected_and_held | Both: the buyer must pick a new username, and the username went into an admin hold |
Shop admin user Paddle subscription object
Section titled “Shop admin user Paddle subscription object”| Field | Type | Description |
|---|---|---|
| subscription | ?shop admin Paddle subscription object | The account’s latest Paddle subscription, null when it never had one |
Shop admin Paddle subscription object
Section titled “Shop admin Paddle subscription object”| Field | Type | Description |
|---|---|---|
| subscription_id | string | The Paddle subscription ID |
| customer_id | string | The Paddle customer ID |
| order_id | snowflake | The shop order of the first payment |
| status | string | The Paddle subscription status |
| interval_months | integer | The period: 1, 3 or 12 months |
| amount_minor | integer | What each period charges, in the minor unit of currency |
| currency | string | USD or TRY |
| paid_until | ?ISO8601 timestamp | The end of the last paid period |
| next_billed_at | ?ISO8601 timestamp | When Paddle charges next, as Paddle last reported |
| cancel_at | ?ISO8601 timestamp | The cancel scheduled for the end of the paid period |
| pending_cancel | ?string | Why Micup still owes Paddle a cancel, such as a refund, a chargeback or the account’s deletion; null when none waits |
| review_reason | ?string | Why the subscription needs support, null when it does not |
| created_at | ISO8601 timestamp | When the first payment bound the subscription |
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 US cents, such as
premium:1:500→450, 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.
Username cards
Section titled “Username cards”A username card sells a username of 1, 2 or 3 characters, as Username cards describes. Each length is a product, username:1, username:2 or username:3, priced with Set shop prices and discounted with Apply shop campaign. The member discount never applies. Get username cards shows the cards, their counts and the refund list, and Set username card image sets a card’s image.
A card is sold while item sales are open, Payriff is configured, username_card_sale_<length> is on, and the card has a price, and while the username policy tier of its length is for_sale and the username migration is selecting. The switches belong to the shop settings, so turning one on never changes the username policy.
Refunds
Section titled “Refunds”A paid username card order that fails with username_taken, username_not_allowed, username_already_owned, username_delivery_stalled, account_deleted or amount_mismatch waits in refunds_due until it is marked refunded. Refund the amount and currency the list shows: for amount_mismatch, that is what Payriff reported, not the order’s price. An order that a later delivery try still delivers leaves refunds_due by itself. Micup sends no refund through Payriff:
- Refund the payment in the Payriff panel.
- Mark the order with Mark shop order refunded. It becomes
refundedand leavesrefunds_due, and agrantedorder no longer counts insold. - For a
grantedorder whose buyer still has the username, the route takes it back: the buyer must pick a new username of 4 or more characters, even while suspended, and the username goes into an admin hold with the noterefund <order_id>.
A username that changed hands after delivery, or that another buyer’s open order holds, is left alone, and the refund still completes. name_action in the response reports the name step.
A username_delivery_stalled order was paid, but the username store could not deliver it within a day of the order opening and an hour of its payment. Its delivery can still finish later, so the route takes the username back the same way from every username card order whose delivery reached the buyer, granted or not, and leaves alone a username the order never delivered. A purchase from the order that is still open is repaired first: one already committed is finished, and one past its time is aborted. While it is still settling after that, the route marks the order refunded and returns 503 USERNAME_UNAVAILABLE: repeat it once the purchase settles, and the repeat finishes the name step.
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, the rate, the Visionary settings and Early Plus. Returns the shop admin settings object.
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| try_to_azn_rate_override? | ?string | Manual TL display rate in AZN per TRY, "0.001" through "1" with up to six decimals; null resumes the automatic CBAR rate |
The payment fields are optional too, and are payment routes and Paddle test accounts.
| Field | Type | Description |
|---|---|---|
| payment_default_route? | payment route | The route of a country without its own row |
| payment_country_routes? | object | Replaces the whole country table; {} clears it |
| payment_providers? | object | payriff and paddle, each {orders_open}; both are written together |
| paddle_test_user_ids? | array[snowflake] | Replaces the Paddle test accounts (at most 20); [] clears the list |
| paddle_test_currency? | string | USD or TRY: what Paddle charges a test account in |
| paddle_trial_days? | integer | Free days (0-30) before a new Paddle Plus subscription’s first charge, once an account; 0 offers none |
The sales switches, premium_discount_percent, usd_to_azn_rate_micro, visionary_on_sale, visionary_reward_item_ids, early_plus_badge_id, early_plus_open and the username card switches username_card_sale_1 to username_card_sale_3 are optional. An omitted field stays as it is. CBAR fields, override timestamps and the card images are read-only here, and Set username card image sets an image.
premium_discount_percentis 0 through 95.usd_to_azn_rate_microis 1000000 through 3000000, which catches a rate entered as 0.017 or as 17.try_to_azn_rate_overrideis a decimal string from"0.001"through"1", with up to six decimal places, in AZN per TRY, or null to resume the automatic rate. It only changes the informational TL display.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.payment_default_route,payment_country_routesandpayment_providersset the payment routes. A country code is two upper case letters.paddle_test_user_idsneedsgift_codes:generateas well. A test account buys through Paddle inpaddle_test_currencywhatever its route and the Paddle switch say. With sandbox test cards it gets real premium and items, a free grant like a gift code. While Paddle runs on a sandbox key, only these accounts can buy through Paddle, unless the deployment setsFLUXER_PADDLE_SANDBOX_SELLS_ALL.early_plus_openset to true needsbadges:manageas well, and a badge. Closing needs onlyshop:manage.- A username card switch sells its length only while the username policy tier of that length is
for_sale. Writing it never changes the username policy or its revision.
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: badges:manage is missing for Early Plus, or gift_codes:generate for paddle_test_user_ids |
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.
Refresh TRY display rate
Section titled “Refresh TRY display rate”POST/v1/admin/shop/settings/refresh-try-rateQueues the existing worker task to fetch the current CBAR bulletin. Requires shop:manage; the admin interface also verifies CSRF. No payment or order is created. The automatic-rate fields are updated only after a valid response, without replacing other shop settings. A manual override stays in force.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The refresh was queued |
The route uses the shop management rate limit and records an admin write audit entry. A queued refresh may fail or be rejected; check the last-success timestamp and bulletin date to confirm a new rate. The catalog can retain the previous display rate for up to 60 seconds.
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 and boost:30 products are 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 Payriff panel shows the order ID after Micup # in the order description, followed by the product. The description contains no US-dollar suffix.
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 user premium subscription
Section titled “Get user premium subscription”GET/v1/admin/shop/users/{user_id}/paddle-subscriptionReturns the account’s latest Paddle subscription for support, as a shop admin user Paddle subscription object. The route only reads.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| user_id | snowflake | The ID of the account |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin user Paddle subscription object | The subscription, or null, was returned |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action view_user_paddle_subscription, target type user, the user ID as target ID, and metadata key has_subscription.
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.
Get username cards
Section titled “Get username cards”GET/v1/admin/shop/username-cardsReturns the shop admin username cards object: the username migration state, each card’s tier, switch, image, price and counts, and the paid username card orders waiting for a refund.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin username cards object | The cards were returned |
Side effects
Section titled “Side effects”The operation records one Admin audit entry with action view_shop_username_cards, target type shop, target ID 0, and metadata key result_count, the number of orders waiting for a refund.
Rate limit
Section titled “Rate limit”60 requests per minute for each authenticated user, on the admin:shop:view bucket.
Set username card image
Section titled “Set username card image”PUT/v1/admin/shop/username-cards/{length}/imageUploads the image of one username card, or removes it, and returns an empty 204 response.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| length | string | The card length: 1, 2 or 3 |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| asset | ?string | A base64 data URL of a PNG, APNG or WebP image of up to 512 KB and 96-480 pixels, or null to remove it |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | empty | The image was saved or removed |
| 400 | error response | The image is refused, returned as INVALID_FORM_BODY |
Side effects
Section titled “Side effects”The image is stored under the owner cosmetic-username_card-<length>, and its hash replaces username_card_asset_<length> in the shop settings. When the settings cannot be saved, the upload is removed again. The catalog shows the change within 60 seconds.
The operation records one Admin audit entry with action set_username_card_image, target type shop, target ID 0, and metadata keys length, asset_old and asset_new. A hash that is null is left out.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Mark shop order refunded
Section titled “Mark shop order refunded”POST/v1/admin/shop/orders/{order_id}/refundMarks a username card order refunded once its payment was refunded in the Payriff panel, takes the username back from a buyer who still has it, and returns the shop admin refund object. Requires shop:manage, user:update:username and username:reserve, and an audit reason. Refunds describes the steps.
The order must be a username card order that is granted, that failed after payment with a reason listed in refunds_due, or that is already refunded. The route is safe to repeat: a repeat finishes a name step that did not finish and counts nothing twice.
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 refund object | The order is refunded |
| 400 | error response | The order cannot be refunded here, or the audit reason is missing, returned as INVALID_FORM_BODY |
| 403 | error response | ACL evaluation denies the request |
| 404 | error response | No order has this ID |
| 503 | error response | USERNAME_UNAVAILABLE: the order is refunded, and the name step has to be repeated |
Side effects
Section titled “Side effects”The order becomes refunded with a compare-and-set and leaves refunds_due, and a granted order no longer counts in sold. When the buyer still has the username, as delivered by this order, the buyer must pick a new username of 4 or more characters. The username then goes into an admin hold, unless another buyer’s open order holds it.
The operation records one Admin audit entry with action refund_shop_order, target type shop_order, the order ID as target ID, and metadata keys sku, status_old, name_action and force_username_reselect, which is true when the buyer had to pick a new username. A name step that fails still records the entry before the error answers, with name_action set to failed and force_username_reselect set to true when the new username pick was attempted.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.
Mark Payriff boost order refunded
Section titled “Mark Payriff boost order refunded”POST/v1/admin/shop/orders/{order_id}/boost-refundMarks a delivered Payriff boost order refunded once its payment was refunded in the Payriff panel, or taken back by the bank as a chargeback, and returns the shop admin order object. Requires shop:manage and an audit reason. Payriff reports no refund or chargeback, so this route is the only way such a boost slot ends early.
The order must be a boost:30 order that is granted, or already refunded. A Paddle order is refused: its refunds and chargebacks end the slot by themselves. The route is safe to repeat.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| order_id | snowflake | The ID of the order |
JSON body
Section titled “JSON body”The body is optional.
| Field | Type | Description |
|---|---|---|
| chargeback? | boolean | true when the bank took the payment back: the buyer’s purchases are disabled too |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | shop admin order object | The order is refunded |
| 400 | error response | The order cannot be refunded here, or the audit reason is missing, returned as INVALID_FORM_BODY |
| 403 | error response | ACL evaluation denies the request |
| 404 | error response | No order has this ID |
Side effects
Section titled “Side effects”The order’s bought boost slot ends first, and the boost it is on comes off its guild; a boost that races it fails with 409 CONFLICT. A slot that ended this way is kept, so a late delivery of the order cannot add it again. With chargeback, the buyer gets the PURCHASE_DISABLED premium flag, as an approved Paddle chargeback gives it, and later orders return 403 PREMIUM_PURCHASE_BLOCKED; lift it by hand if the chargeback is reversed. Then the order becomes refunded with a compare-and-set, so a refunded boost order never keeps its slot.
The operation records one Admin audit entry with action refund_shop_order, target type shop_order, the order ID as target ID, and metadata keys sku, status_old, user_id and chargeback. A step that fails after the slot ended still records the entry before the error answers, with status_step set to failed; a repeat finishes the order.
Rate limit
Section titled “Rate limit”30 requests per minute for each authenticated user, on the admin:shop:manage bucket.