{"openapi":"3.1.0","info":{"title":"SellerSideKick Shop API","version":"1.2.0","description":"Read and update one SellerSideKick shop's **products, stock and promotions**. SellerSideKick is an AI chatbot and back office for online shops selling on LINE, Facebook, Instagram, Shopee, Lazada and TikTok.\n\nThe usual job: the shop owner gives you a spreadsheet (prices, stock counts, new products), and you apply it here.\n\n## Access\n- The shop owner creates a key under **Settings → API keys** in the SellerSideKick dashboard. Keys work on the monthly plans only.\n- Send it on every request as `Authorization: Bearer ssk_...`.\n- The key decides the shop: there is no shop id to pass. Never put the key in a URL, a file you share, or a reply to the owner.\n- A key can be limited to some permissions and can expire. `GET /me` shows both. The permissions are `catalog:read` (always: shop, categories, products, promotions), `catalog:write` (create and update products; an opening `stock` on a new product also needs `stock:write`), `stock:write` and `promotions:write`. A call the key doesn't allow answers 403 `Y35` naming the permission; tell the owner.\n- In an AI assistant app that supports connectors (MCP), the owner can connect the shop instead, with no key: the SellerSideKick connector at `https://mcp.sellersidekick.ai/mcp` (sign in, pick the shop and permissions).\n\n## How to work\n1. Call `GET /me` to confirm which shop the key is for, and tell the owner the shop name.\n2. Read before writing:\n   - `GET /categories` for the allowed category names;\n   - `GET /products?sku=...` for the current prices and stock.\n3. Send every write with `dry_run: true` first. Show the owner the summary and every row with status `error`, and wait for a clear yes.\n4. Send the same request with `dry_run: false`. Report the summary and every `error` row.\n\n## Rules\n- At most **500 rows per request**: split bigger sheets. Each row succeeds or fails on its own, so read `results`.\n- Products are matched by **SKU**. Stock changes only through `POST /stock`, never through `POST /products/upsert`.\n- For stock, give each new change its own `idempotency_key`, and reuse it only to retry that same request.\n- **Nothing can be deleted.** To end a promotion, set its status to `archive`.\n\n## What reaches the marketplaces\n- **Stock changes** reach the shop's linked Shopee, Lazada and TikTok listings, usually within about 10 minutes.\n- **Prices, names, descriptions and promotions** change the SellerSideKick store and the shop's chat bot only, not the marketplace listings.\n- Only products on the SellerSideKick store can be changed. A SKU that exists only on a marketplace is an error row.\n\n## Limits and errors\n- 120 requests a minute per shop. On `429`, wait the `Retry-After` seconds and retry.\n- Errors are `{ code, message, rcode }`.\n- 400 means fix the field the message names. 401 and 403 mean the key can't be used; tell the owner, because retrying won't help.\n\nThai text is welcome everywhere (names, descriptions, promotion names). Prices are in THB.","contact":{"name":"SellerSideKick","url":"https://sellersidekick.ai"}},"servers":[{"url":"https://api.sellersidekick.ai/v1/api","description":"SellerSideKick"}],"security":[{"shopApiKey":[]}],"tags":[{"name":"Shop","description":"Which shop the key belongs to, and its categories."},{"name":"Products","description":"Read, create and update products by SKU. Prices and content stay on the SellerSideKick store."},{"name":"Stock","description":"Count or adjust on-hand units. Reaches linked marketplace listings."},{"name":"Promotions","description":"The shop's own coupons, discounts, bundles and buy-X-get-Y offers. Never deleted, only archived."}],"components":{"securitySchemes":{"shopApiKey":{"type":"http","scheme":"bearer","description":"The shop's API key, `ssk_` + 40 characters, from Settings → API keys. One key acts on one shop."}},"schemas":{"Error":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"Product":{"type":"object","properties":{"sku":{"type":"string","description":"The shop's own product code. Products are matched by it."},"name":{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}}},"description":{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}}},"usage_information":{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}}},"price":{"type":"number","description":"Selling price on the SellerSideKick store, in THB."},"currency":{"type":"string","examples":["THB"]},"category":{"type":["string","null"],"description":"One of GET /categories."},"brand":{"type":"string"},"parent_sku":{"type":["string","null"],"description":"Set on a variant (a size or colour of another product): the main product's SKU."},"stock":{"type":"object","properties":{"on_hand":{"type":["integer","null"],"description":"Units on the shelf; null when never counted."},"available":{"type":["integer","null"],"description":"On hand minus what open orders hold: what can still be sold."}}},"archived":{"type":"boolean"},"updated_at":{"type":["string","null"],"format":"date-time"}}},"Promotion":{"type":"object","description":"A promotion as the API shows it: the input fields, plus these.","properties":{"id":{"type":"string","description":"Use with the type in update and status calls."},"type":{"type":"string","enum":["coupon","discount","bundle","bogo"]},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"times_used":{"type":"integer"},"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":["string","null"],"enum":["percentage","fixed",null],"description":"null on a discount with product_discounts."},"value":{"type":["number","null"],"description":"Percent (at most 100) or THB, by discount_type; null on a discount with product_discounts."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","description":"Discount only: a rate per SKU. When present, discount_type and value are null and an update sending them alone is refused. Sending product_discounts replaces the rates; applies_to (with discount_type and value) replaces them with one rate.","items":{"type":"object","properties":{"sku":{"type":["string","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent, or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}},"PromotionInput":{"type":"object","description":"The fields depend on the type in the path.\n- **coupon / discount:** `discount_type`, `value`, `applies_to`; optional `min_order_amount`, `max_discount_amount` (coupon also `can_stack`).\n- **discount with a rate per product:** `product_discounts` (each `sku` its own `discount_type` and `value`) instead of `discount_type`, `value`, `max_discount_amount` and `applies_to`.\n- **bundle:** `products` (2 or more, `sku` + `quantity`), `bundle_discount_type` with `bundle_discount` (fixed THB) or `bundle_discount_percentage`.\n- **bogo:** `products` with `role` `buy` or `get` (at least one of each).\n\nOn create, `code`, `name`, `starts_at`, `ends_at` and the type's main fields are required. An update sends only the fields that change and is checked as the promotion will be afterwards.","properties":{"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent (at most 100) or THB, by discount_type."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","maxItems":500,"description":"Discount only: a rate per SKU, off every unit of it, instead of one rate for the listed products. Leave out discount_type, value, max_discount_amount and applies_to. On an update the list replaces the saved one.","items":{"type":"object","required":["sku","discount_type","value"],"properties":{"sku":{"type":"string"},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent (at most 100), or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}}}},"paths":{"/me":{"get":{"operationId":"getShop","tags":["Shop"],"summary":"Which shop this key belongs to.","description":"**Use first,** before any other call. It confirms the key works and names the shop so the owner can check it is the right one.\n\n**Changes nothing.** Safe to call any time.","responses":{"200":{"description":"The shop and the key's name.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"shop":{"type":"object","properties":{"id":{"type":"string"},"uuid":{"type":"string"},"name":{"type":"string"},"currency":{"type":"string"}}},"key":{"type":"object","properties":{"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["catalog:read","catalog:write","stock:write","promotions:write"]},"description":"What this key may do."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the key stops working; null if never."}}}}}}},"examples":{"ok":{"value":{"code":200,"message":"OK","data":{"shop":{"id":"6650a1b2c3d4e5f607182930","uuid":"3833993e-2bcd-41e6-af1b-b8d115aafae0","name":"ร้านของเล่นนิชิ","currency":"THB"},"key":{"name":"Catalogue update from sheet","prefix":"ssk_AbCdEfGh","scopes":["catalog:read","catalog:write","stock:write","promotions:write"],"expires_at":"2027-01-23T03:00:00.000Z"}}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/categories":{"get":{"operationId":"listCategories","tags":["Shop"],"summary":"The category names a product may use.","description":"**Use before creating products** or changing a category. Map the sheet's categories onto these names; matching ignores case. A name that isn't here is an error row.\n\n**Changes nothing.**","responses":{"200":{"description":"Category names.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"categories":{"type":"array","items":{"type":"string"}}}}}},"examples":{"ok":{"value":{"code":200,"message":"OK","data":{"categories":["Toys","Books","Clothing"]}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/products":{"get":{"operationId":"listProducts","tags":["Products"],"summary":"The shop's products with price and stock, by SKU or page by page.","description":"**Use to read** current prices, names and stock before you change them. Read before a stock count you plan to guard with `expected_from`, and to check which SKUs already exist.\n\n- Lists only products on the shop's SellerSideKick store.\n- Pass `sku` (comma-separated) for specific products, or omit it to page through everything. Results are paged either way: set `limit` (up to 500) when asking for many SKUs.\n\n**Changes nothing.**","parameters":[{"name":"sku","in":"query","schema":{"type":"string"},"description":"Comma-separated SKUs, e.g. `TM-001,TM-002`.","example":"TM-001,TM-002"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}}],"responses":{"200":{"description":"A page of products.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"products":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string","description":"The shop's own product code. Products are matched by it."},"name":{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}}},"description":{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}}},"usage_information":{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}}},"price":{"type":"number","description":"Selling price on the SellerSideKick store, in THB."},"currency":{"type":"string","examples":["THB"]},"category":{"type":["string","null"],"description":"One of GET /categories."},"brand":{"type":"string"},"parent_sku":{"type":["string","null"],"description":"Set on a variant (a size or colour of another product): the main product's SKU."},"stock":{"type":"object","properties":{"on_hand":{"type":["integer","null"],"description":"Units on the shelf; null when never counted."},"available":{"type":["integer","null"],"description":"On hand minus what open orders hold: what can still be sold."}}},"archived":{"type":"boolean"},"updated_at":{"type":["string","null"],"format":"date-time"}}}},"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"total_pages":{"type":"integer"}}}}},"examples":{"ok":{"value":{"code":200,"message":"OK","data":{"products":[{"sku":"TM-001","name":{"th":"รถเหล็กโทมิก้า รุ่นรถตำรวจ","en":"Tomica police car"},"description":{"th":"รถเหล็กสเกล 1:64 ประตูเปิดได้","en":"1:64 die-cast car, opening doors"},"usage_information":{"th":"เหมาะสำหรับเด็กอายุ 3 ปีขึ้นไป","en":"Ages 3 and up"},"price":129,"currency":"THB","category":"Toys","brand":"Tomica","parent_sku":null,"stock":{"on_hand":40,"available":37},"archived":false,"updated_at":"2026-10-24T09:15:00.000Z"}],"page":1,"limit":100,"total":1,"total_pages":1}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/products/upsert":{"post":{"operationId":"upsertProducts","tags":["Products"],"summary":"Create new products and update existing ones by SKU (up to 500 rows).","description":"**Use when** the owner wants prices, names, descriptions, categories or brands changed, or new products added.\n\n**Don't use for** stock: an existing product's stock changes only through `POST /stock`. Sending `stock` for an existing SKU is an error row.\n\n**Before calling:**\n- Get category names from `GET /categories`.\n- Send with `dry_run: true`, show the owner the summary and the error rows, and only send it for real after they agree.\n\n**How rows are handled:**\n- A SKU the shop has is updated with only the fields you send; the rest stay.\n- A new SKU is created. It needs `name`, `price`, `category` and `brand`, and may set `stock` (the opening count) and `parent_sku` (to make it a variant of a main product, which may be created in the same request).\n- Each row answers `created`, `updated` (with `changed`), `unchanged` or `error`.\n\n**Effect:** changes the SellerSideKick store and the shop's chat bot. Marketplace listings keep their own price and content.\n\n**Safe to retry:** yes. Sending the same rows again leaves them `unchanged`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["products"],"additionalProperties":false,"properties":{"dry_run":{"type":"boolean","default":false,"description":"true: check every row and report what would happen, changing nothing."},"products":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"object","required":["sku"],"additionalProperties":false,"properties":{"sku":{"type":"string"},"name":{"description":"A string is the Thai text (the saved English text is kept), or send { th, en } to set either or both.","oneOf":[{"type":"string"},{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}},"additionalProperties":false}]},"description":{"description":"A string is the Thai text (the saved English text is kept), or send { th, en } to set either or both.","oneOf":[{"type":"string"},{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}},"additionalProperties":false}]},"usage_information":{"description":"How to use or care for the product. A string is the Thai text (the saved English text is kept), or send { th, en } to set either or both.","oneOf":[{"type":"string"},{"type":"object","properties":{"th":{"type":"string"},"en":{"type":"string"}},"additionalProperties":false}]},"price":{"type":"number","minimum":0,"description":"THB."},"category":{"type":"string","description":"A name from GET /categories."},"brand":{"type":"string","description":"A new brand is added to the shop's brand list."},"stock":{"type":"integer","minimum":0,"description":"New products only: the opening count on the shelf."},"parent_sku":{"type":"string","description":"New products only: the main product this one is a variant of. A new variant that gives no category or brand takes the main product's."}}}}}},"examples":{"price_update":{"summary":"Update two prices (dry run first)","value":{"dry_run":true,"products":[{"sku":"TM-001","price":139},{"sku":"TM-002","price":159}]}},"new_product":{"summary":"Add a new product with Thai and English names","value":{"dry_run":true,"products":[{"sku":"TM-010","name":{"th":"รถดับเพลิงโทมิก้า","en":"Tomica fire engine"},"price":189,"category":"Toys","brand":"Tomica","stock":12}]}},"variants":{"summary":"A main product and two variants in one request","value":{"products":[{"sku":"SHIRT-01","name":"เสื้อยืดลายแมว","price":290,"category":"Clothing","brand":"Nichi"},{"sku":"SHIRT-01-M","name":"เสื้อยืดลายแมว ไซซ์ M","price":290,"category":"Clothing","brand":"Nichi","parent_sku":"SHIRT-01","stock":20},{"sku":"SHIRT-01-L","name":"เสื้อยืดลายแมว ไซซ์ L","price":290,"category":"Clothing","brand":"Nichi","parent_sku":"SHIRT-01","stock":15}]}}}}}},"responses":{"200":{"description":"Each row's result. Report the summary and every `error` row to the owner.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"dry_run":{"type":"boolean","description":"true: nothing was changed; the statuses say what would happen."},"results":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"status":{"type":"string","enum":["created","updated","unchanged","error"]},"error":{"type":"string","description":"Only on status \"error\": why this row was not applied, in plain words. Report it to the owner."},"changed":{"type":"array","items":{"type":"string"},"description":"On `updated`: the fields that changed."}}},"description":"One result per row, in the order sent."},"summary":{"type":"object","additionalProperties":{"type":"integer"},"description":"How many rows got each status, plus `total`."}}}}},"examples":{"ok":{"value":{"code":200,"message":"Dry run: nothing was changed","data":{"dry_run":true,"results":[{"sku":"TM-001","status":"updated","changed":["price"]},{"sku":"TM-002","status":"unchanged"},{"sku":"TM-099","status":"error","error":"Unknown category \"Gadgets\". Use one of the shop's top-level categories (GET /v1/api/categories)."}],"summary":{"total":3,"updated":1,"unchanged":1,"error":1}}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/stock":{"post":{"operationId":"updateStock","tags":["Stock"],"summary":"Set counted stock or adjust it by SKU (up to 500 rows).","description":"**Use when** the owner has counted stock (send `value`, the units now on the shelf) or wants to add or remove units (send `delta`, e.g. `-3` for damaged goods). Never both.\n\n**Before calling:**\n- To be safe against changes made since you read, send `expected_from` (the on-hand you read) with `value`. A row whose on-hand moved meanwhile is refused instead of overwritten.\n- Send `dry_run: true` first and confirm with the owner.\n- A dry run doesn't check holds by open orders, a delta on a never-counted SKU, or a replay, so the real call can still answer some rows differently.\n\n**Effect:**\n- Recorded in the shop's stock history.\n- Sent to the SKU's linked Shopee, Lazada and TikTok listings, usually within about 10 minutes.\n- Only SKUs on the SellerSideKick store; a marketplace-only SKU is an error row.\n- A `delta` can't take on-hand below what open orders hold. A counted `value` is recorded as counted, even below that.\n\n**Safe to retry:** yes, with the same `idempotency_key`. A row already applied answers `already_applied`. Use a new key for each new change: a row with different numbers under a reused key is applied.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"additionalProperties":false,"properties":{"dry_run":{"type":"boolean","default":false},"idempotency_key":{"type":"string","minLength":8,"maxLength":100,"pattern":"^[A-Za-z0-9._:-]+$","description":"Your id for this change, e.g. `count-2026-10-25-sheet1`. Resending with it never applies a row twice."},"items":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"object","required":["sku"],"additionalProperties":false,"description":"Send `value` or `delta`, not both.","properties":{"sku":{"type":"string"},"value":{"type":"integer","minimum":0,"description":"The units now on the shelf (a count)."},"delta":{"type":"integer","description":"Units to add (positive) or remove (negative). Not 0."},"expected_from":{"type":"integer","minimum":0,"description":"With `value` only: the on-hand you read. If it has changed, the row is refused."},"reason":{"type":"string","maxLength":200,"description":"Shown in the stock history, e.g. 'monthly count'."}}}}}},"examples":{"count":{"summary":"A stock count, guarded against changes since it was read","value":{"idempotency_key":"count-2026-10-25-sheet1","dry_run":true,"items":[{"sku":"TM-001","value":40,"expected_from":37,"reason":"monthly count"},{"sku":"TM-002","value":0,"reason":"monthly count"}]}},"adjust":{"summary":"Remove 3 damaged units","value":{"idempotency_key":"damaged-2026-10-25","items":[{"sku":"TM-001","delta":-3,"reason":"damaged in transit"}]}}}}}},"responses":{"200":{"description":"Each row's result. Report the summary and every `error` row to the owner.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"dry_run":{"type":"boolean","description":"true: nothing was changed; the statuses say what would happen."},"results":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"status":{"type":"string","enum":["updated","would_update","already_applied","error"]},"error":{"type":"string","description":"Only on status \"error\": why this row was not applied, in plain words. Report it to the owner."},"from":{"type":"integer","description":"On hand before."},"to":{"type":"integer","description":"On hand after."},"available":{"type":"integer","description":"What can still be sold after the change."}}},"description":"One result per row, in the order sent."},"summary":{"type":"object","additionalProperties":{"type":"integer"},"description":"How many rows got each status, plus `total`."}}}}},"examples":{"ok":{"value":{"code":200,"message":"Stock processed","data":{"dry_run":false,"results":[{"sku":"TM-001","status":"updated","from":37,"to":40,"available":38},{"sku":"TM-002","status":"already_applied"},{"sku":"ZZ-9","status":"error","error":"This SKU is not a product on your SellerSideKick store (it may be only on a marketplace channel)."}],"summary":{"total":3,"updated":1,"already_applied":1,"error":1}}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/promotions":{"get":{"operationId":"listPromotions","tags":["Promotions"],"summary":"The shop's own promotions, optionally by type and status.","description":"**Use to find a promotion's id** before updating or archiving it, and to check a code is free before creating one.\n\nMarketplace promotions (Shopee, Lazada, TikTok) are not listed: they are managed in each Seller Center.\n\n**Changes nothing.**","parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["coupon","discount","bundle","bogo"]},"description":"Only this type."},{"name":"status","in":"query","schema":{"type":"string","enum":["active","inactive","archive"]},"description":"Only this status."}],"responses":{"200":{"description":"Promotions, newest first within each type.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"promotions":{"type":"array","items":{"type":"object","description":"A promotion as the API shows it: the input fields, plus these.","properties":{"id":{"type":"string","description":"Use with the type in update and status calls."},"type":{"type":"string","enum":["coupon","discount","bundle","bogo"]},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"times_used":{"type":"integer"},"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":["string","null"],"enum":["percentage","fixed",null],"description":"null on a discount with product_discounts."},"value":{"type":["number","null"],"description":"Percent (at most 100) or THB, by discount_type; null on a discount with product_discounts."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","description":"Discount only: a rate per SKU. When present, discount_type and value are null and an update sending them alone is refused. Sending product_discounts replaces the rates; applies_to (with discount_type and value) replaces them with one rate.","items":{"type":"object","properties":{"sku":{"type":["string","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent, or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}}}}}}},"examples":{"ok":{"value":{"code":200,"message":"OK","data":{"promotions":[{"id":"6650f0c2a1b2c3d4e5f60718","type":"coupon","status":"active","times_used":0,"code":"WELCOME10","name":"ส่วนลด 10% สำหรับลูกค้าใหม่","discount_type":"percentage","value":10,"max_discount_amount":100,"applies_to":{"all_products":true},"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-30T23:59:59+07:00","max_uses_per_customer":1}]}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/promotions/{type}":{"post":{"operationId":"createPromotion","tags":["Promotions"],"summary":"Create a coupon, discount, bundle or BOGO promotion.","description":"**Use when** the owner asks for a new promotion.\n\n**Before calling:**\n- Confirm the code, dates, amount and the products it covers with the owner.\n- SKUs must be on the shop's SellerSideKick store; an unknown SKU is a 400.\n- A code the shop already uses is refused (400).\n\n**Effect:** live in the shop's SellerSideKick store and chat bot from `starts_at` (status `active` unless you send `inactive`). Not pushed to marketplaces.\n\n**Safe to retry:** no. Check with `GET /promotions` whether it was created before sending again.","parameters":[{"name":"type","in":"path","required":true,"description":"The promotion's type.","schema":{"type":"string","enum":["coupon","discount","bundle","bogo"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"The fields depend on the type in the path.\n- **coupon / discount:** `discount_type`, `value`, `applies_to`; optional `min_order_amount`, `max_discount_amount` (coupon also `can_stack`).\n- **discount with a rate per product:** `product_discounts` (each `sku` its own `discount_type` and `value`) instead of `discount_type`, `value`, `max_discount_amount` and `applies_to`.\n- **bundle:** `products` (2 or more, `sku` + `quantity`), `bundle_discount_type` with `bundle_discount` (fixed THB) or `bundle_discount_percentage`.\n- **bogo:** `products` with `role` `buy` or `get` (at least one of each).\n\nOn create, `code`, `name`, `starts_at`, `ends_at` and the type's main fields are required. An update sends only the fields that change and is checked as the promotion will be afterwards.","properties":{"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent (at most 100) or THB, by discount_type."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","maxItems":500,"description":"Discount only: a rate per SKU, off every unit of it, instead of one rate for the listed products. Leave out discount_type, value, max_discount_amount and applies_to. On an update the list replaces the saved one.","items":{"type":"object","required":["sku","discount_type","value"],"properties":{"sku":{"type":"string"},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent (at most 100), or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}},"examples":{"coupon":{"summary":"Coupon: 10% off the whole shop, at most 100 THB","value":{"code":"WELCOME10","name":"ส่วนลด 10% สำหรับลูกค้าใหม่","discount_type":"percentage","value":10,"max_discount_amount":100,"applies_to":{"all_products":true},"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-30T23:59:59+07:00","max_uses_per_customer":1}},"discount_per_sku":{"summary":"Discount: a rate per SKU","value":{"code":"WHSALE","name":"Warehouse sale","product_discounts":[{"sku":"TM-001","discount_type":"fixed","value":120},{"sku":"TM-002","discount_type":"percentage","value":25}],"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-15T23:59:59+07:00"}},"discount":{"summary":"Discount: 50 THB off two SKUs","value":{"code":"TOMICA50","name":"Tomica ลด 50 บาท","discount_type":"fixed","value":50,"applies_to":{"skus":["TM-001","TM-002"]},"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-15T23:59:59+07:00"}},"bundle":{"summary":"Bundle: two cars for 20% off","value":{"code":"PAIR20","name":"ซื้อคู่ลด 20%","products":[{"sku":"TM-001","quantity":1},{"sku":"TM-002","quantity":1}],"bundle_discount_type":"percentage","bundle_discount_percentage":20,"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-12-31T23:59:59+07:00"}},"bogo":{"summary":"BOGO: buy 2 cars, get a track free","value":{"code":"B2G1TRACK","name":"ซื้อ 2 แถมราง 1","products":[{"sku":"TM-001","quantity":2,"role":"buy"},{"sku":"TR-100","quantity":1,"role":"get"}],"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-30T23:59:59+07:00"}}}}}},"responses":{"201":{"description":"The new promotion.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"promotion":{"type":"object","description":"A promotion as the API shows it: the input fields, plus these.","properties":{"id":{"type":"string","description":"Use with the type in update and status calls."},"type":{"type":"string","enum":["coupon","discount","bundle","bogo"]},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"times_used":{"type":"integer"},"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":["string","null"],"enum":["percentage","fixed",null],"description":"null on a discount with product_discounts."},"value":{"type":["number","null"],"description":"Percent (at most 100) or THB, by discount_type; null on a discount with product_discounts."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","description":"Discount only: a rate per SKU. When present, discount_type and value are null and an update sending them alone is refused. Sending product_discounts replaces the rates; applies_to (with discount_type and value) replaces them with one rate.","items":{"type":"object","properties":{"sku":{"type":["string","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent, or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}}}}}},"examples":{"ok":{"value":{"code":200,"message":"Promotion created","data":{"promotion":{"id":"6650f0c2a1b2c3d4e5f60718","type":"coupon","status":"active","times_used":0,"code":"WELCOME10","name":"ส่วนลด 10% สำหรับลูกค้าใหม่","discount_type":"percentage","value":10,"max_discount_amount":100,"applies_to":{"all_products":true},"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-30T23:59:59+07:00","max_uses_per_customer":1}}}}}}}},"400":{"description":"The request is invalid (the message names the field), a SKU isn't on the shop's store, or the code is already used. Fix it and send again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}},"unknown_sku":{"value":{"code":400,"message":"Unknown SKU(s): TM-404"}},"duplicate_code":{"value":{"code":400,"message":"Promo code already exists"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/promotions/{type}/{id}":{"patch":{"operationId":"updatePromotion","tags":["Promotions"],"summary":"Change a promotion: send only the fields that change.","description":"**Use when** the owner wants a promotion's dates, amount, products, limits, name or code changed.\n\n**Before calling:** get the id from `GET /promotions` and confirm the change with the owner. The type in the path must match the promotion's own.\n\n**Checked as the promotion will be afterwards.** For example, `ends_at` must stay after the saved `starts_at`, a percentage stays at 100 or below, and a BOGO keeps a buy and a get product.\n\n**Don't use to** switch it on or off or end it: use `POST /promotions/{type}/{id}/status`. Marketplace promotions can't be changed here.\n\n**Safe to retry:** yes. Sending the same fields again gives the same result.","parameters":[{"name":"type","in":"path","required":true,"description":"The promotion's type.","schema":{"type":"string","enum":["coupon","discount","bundle","bogo"]}},{"name":"id","in":"path","required":true,"description":"The promotion's id from GET /promotions.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"The fields depend on the type in the path.\n- **coupon / discount:** `discount_type`, `value`, `applies_to`; optional `min_order_amount`, `max_discount_amount` (coupon also `can_stack`).\n- **discount with a rate per product:** `product_discounts` (each `sku` its own `discount_type` and `value`) instead of `discount_type`, `value`, `max_discount_amount` and `applies_to`.\n- **bundle:** `products` (2 or more, `sku` + `quantity`), `bundle_discount_type` with `bundle_discount` (fixed THB) or `bundle_discount_percentage`.\n- **bogo:** `products` with `role` `buy` or `get` (at least one of each).\n\nOn create, `code`, `name`, `starts_at`, `ends_at` and the type's main fields are required. An update sends only the fields that change and is checked as the promotion will be afterwards.","properties":{"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent (at most 100) or THB, by discount_type."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","maxItems":500,"description":"Discount only: a rate per SKU, off every unit of it, instead of one rate for the listed products. Leave out discount_type, value, max_discount_amount and applies_to. On an update the list replaces the saved one.","items":{"type":"object","required":["sku","discount_type","value"],"properties":{"sku":{"type":"string"},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent (at most 100), or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}},"examples":{"extend":{"summary":"Extend the end date","value":{"ends_at":"2026-12-15T23:59:59+07:00"}},"raise":{"summary":"Raise a discount to 15%","value":{"value":15}}}}}},"responses":{"200":{"description":"The promotion as saved.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"promotion":{"type":"object","description":"A promotion as the API shows it: the input fields, plus these.","properties":{"id":{"type":"string","description":"Use with the type in update and status calls."},"type":{"type":"string","enum":["coupon","discount","bundle","bogo"]},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"times_used":{"type":"integer"},"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":["string","null"],"enum":["percentage","fixed",null],"description":"null on a discount with product_discounts."},"value":{"type":["number","null"],"description":"Percent (at most 100) or THB, by discount_type; null on a discount with product_discounts."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","description":"Discount only: a rate per SKU. When present, discount_type and value are null and an update sending them alone is refused. Sending product_discounts replaces the rates; applies_to (with discount_type and value) replaces them with one rate.","items":{"type":"object","properties":{"sku":{"type":["string","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent, or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}}}}}},"examples":{"ok":{"value":{"code":200,"message":"Promotion updated","data":{"promotion":{"id":"6650f0c2a1b2c3d4e5f60718","type":"coupon","status":"active","times_used":0,"code":"WELCOME10","name":"ส่วนลด 10% สำหรับลูกค้าใหม่","discount_type":"percentage","value":10,"max_discount_amount":100,"applies_to":{"all_products":true},"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-30T23:59:59+07:00","max_uses_per_customer":1}}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"404":{"description":"No promotion with this id and type in this shop. List promotions to get a valid id.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"not_found":{"value":{"code":404,"message":"No coupon promotion with id 6650f0c2a1b2c3d4e5f60718 in this shop."}}}}}},"409":{"description":"The new code is already used by another promotion of this type in the shop. Pick another code.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"duplicate":{"value":{"code":409,"message":"This shop already has a promotion with the code PAIR20."}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}},"/promotions/{type}/{id}/status":{"post":{"operationId":"setPromotionStatus","tags":["Promotions"],"summary":"Switch a promotion on or off, or end it (archive).","description":"**Use when** the owner wants to pause a promotion (`inactive`), resume it (`active`) or end it for good (`archive`). Archive is how a promotion is removed: nothing is deleted, and it stays in the history.\n\n**Before calling:** confirm with the owner, especially before `archive`.\n\n**Safe to retry:** yes.","parameters":[{"name":"type","in":"path","required":true,"description":"The promotion's type.","schema":{"type":"string","enum":["coupon","discount","bundle","bogo"]}},{"name":"id","in":"path","required":true,"description":"The promotion's id from GET /promotions.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"additionalProperties":false,"properties":{"status":{"type":"string","enum":["active","inactive","archive"]}}},"examples":{"pause":{"value":{"status":"inactive"}},"end":{"value":{"status":"archive"}}}}}},"responses":{"200":{"description":"The promotion with its new status.","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"integer"},"message":{"type":"string"},"data":{"type":"object","properties":{"promotion":{"type":"object","description":"A promotion as the API shows it: the input fields, plus these.","properties":{"id":{"type":"string","description":"Use with the type in update and status calls."},"type":{"type":"string","enum":["coupon","discount","bundle","bogo"]},"status":{"type":"string","enum":["active","inactive"],"description":"Create only. Default active."},"times_used":{"type":"integer"},"code":{"type":"string","description":"Upper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop."},"name":{"type":"string"},"description":{"type":"string"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time","description":"Must be after starts_at."},"max_uses":{"type":["integer","null"],"description":"Total uses allowed; null for no limit."},"max_uses_per_customer":{"type":["integer","null"]},"discount_type":{"type":["string","null"],"enum":["percentage","fixed",null],"description":"null on a discount with product_discounts."},"value":{"type":["number","null"],"description":"Percent (at most 100) or THB, by discount_type; null on a discount with product_discounts."},"min_order_amount":{"type":"number","description":"THB."},"max_discount_amount":{"type":["number","null"],"description":"THB cap on a percentage discount."},"can_stack":{"type":"boolean","description":"Coupon only: usable together with other promotions."},"applies_to":{"type":"object","description":"Which products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).","properties":{"all_products":{"type":"boolean"},"categories":{"type":"array","items":{"type":"string"},"description":"Category names from GET /categories."},"brands":{"type":"array","items":{"type":"string"}},"skus":{"type":"array","items":{"type":"string"},"description":"Must be SKUs on the shop's SellerSideKick store."}}},"product_discounts":{"type":"array","description":"Discount only: a rate per SKU. When present, discount_type and value are null and an update sending them alone is refused. Sending product_discounts replaces the rates; applies_to (with discount_type and value) replaces them with one rate.","items":{"type":"object","properties":{"sku":{"type":["string","null"]},"discount_type":{"type":"string","enum":["percentage","fixed"]},"value":{"type":"number","description":"Percent, or THB off every unit."}}}},"products":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"role":{"type":"string","enum":["buy","get"],"description":"BOGO only."}}}},"bundle_discount_type":{"type":"string","enum":["fixed","percentage"]},"bundle_discount":{"type":"number","description":"THB off the bundle, when bundle_discount_type is fixed."},"bundle_discount_percentage":{"type":"number","description":"Percent off, when bundle_discount_type is percentage."}}}}}}},"examples":{"ok":{"value":{"code":200,"message":"Promotion archive","data":{"promotion":{"id":"6650f0c2a1b2c3d4e5f60718","type":"coupon","status":"active","times_used":0,"code":"WELCOME10","name":"ส่วนลด 10% สำหรับลูกค้าใหม่","discount_type":"percentage","value":10,"max_discount_amount":100,"applies_to":{"all_products":true},"starts_at":"2026-11-01T00:00:00+07:00","ends_at":"2026-11-30T23:59:59+07:00","max_uses_per_customer":1}}}}}}}},"400":{"description":"The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"unknown_field":{"value":{"code":400,"message":"\"products[0].prize\" is not allowed"}},"missing_field":{"value":{"code":400,"message":"\"products[0].sku\" is required"}}}}}},"401":{"description":"No key, or the key is unknown, revoked or expired. Ask the shop owner for a valid key (Settings → API keys). Do not retry with the same key.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"missing":{"value":{"code":401,"message":"Send the shop's API key as the header 'Authorization: Bearer ssk_...'.","rcode":"Y20"}},"invalid":{"value":{"code":401,"message":"This API key is not valid or has been revoked.","rcode":"Y21"}},"expired":{"value":{"code":401,"message":"This API key has expired. Ask an owner or admin for a new key.","rcode":"Y22"}}}}}},"403":{"description":"The key is real but can't be used now. Tell the owner why; retrying won't help.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"plan":{"value":{"code":403,"message":"API keys are part of the monthly plans: subscribe to a monthly plan to use them.","rcode":"Y31"}},"inactive":{"value":{"code":403,"message":"This shop is not active.","rcode":"Y32"}},"creator_gone":{"value":{"code":403,"message":"The member who created this key no longer manages this shop's API keys. Ask an owner or admin for a new key.","rcode":"Y34"}},"scope":{"value":{"code":403,"message":"This API key doesn't allow this: it needs the \"stock:write\" permission. An owner or admin can grant it in Settings → API keys.","rcode":"Y35"}}}}}},"404":{"description":"No promotion with this id and type in this shop. List promotions to get a valid id.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"not_found":{"value":{"code":404,"message":"No coupon promotion with id 6650f0c2a1b2c3d4e5f60718 in this shop."}}}}}},"429":{"description":"Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.","content":{"application/json":{"schema":{"type":"object","description":"Every error has this shape. `rcode` is a stable code to branch on; `message` is for people.","properties":{"code":{"type":"integer","description":"The HTTP status again."},"message":{"type":"string"},"rcode":{"type":["string","null"],"description":"Y20 no key · Y21 invalid key · Y22 key expired · Y31 not on a monthly plan · Y32 shop not active · Y34 the key's creator no longer manages keys · Y35 the key lacks the permission this call needs · R01 rate limited."}}},"examples":{"too_many":{"value":{"code":429,"message":"Rate limit exceeded for shopApi endpoints. Please try again later.","rcode":"R01"}}}}}}}}}}}