SellerSideKick Shop API reference

Every shop API operation with its parameters, request body, examples and errors, generated from the OpenAPI spec.

Try it in the browser

Send real requests with your API key. They go straight from your browser to the API and change your shop unless you set dry_run: true.

Base URL: https://api.sellersidekick.ai/v1/api. Send the shop's key on every request as Authorization: Bearer ssk_….

How to use this API (from the spec)

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.

The usual job: the shop owner gives you a spreadsheet (prices, stock counts, new products), and you apply it here.

Access#

  • The shop owner creates a key under Settings → API keys in the SellerSideKick dashboard. Keys work on the monthly plans only.
  • Send it on every request as Authorization: Bearer ssk_....
  • 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.
  • 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.
  • 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).

How to work#

  1. Call GET /me to confirm which shop the key is for, and tell the owner the shop name.
  2. Read before writing:
    • GET /categories for the allowed category names;
    • GET /products?sku=... for the current prices and stock.
  3. 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.
  4. Send the same request with dry_run: false. Report the summary and every error row.

Rules#

  • At most 500 rows per request: split bigger sheets. Each row succeeds or fails on its own, so read results.
  • Products are matched by SKU. Stock changes only through POST /stock, never through POST /products/upsert.
  • For stock, give each new change its own idempotency_key, and reuse it only to retry that same request.
  • Nothing can be deleted. To end a promotion, set its status to archive.

What reaches the marketplaces#

  • Stock changes reach the shop's linked Shopee, Lazada and TikTok listings, usually within about 10 minutes.
  • Prices, names, descriptions and promotions change the SellerSideKick store and the shop's chat bot only, not the marketplace listings.
  • Only products on the SellerSideKick store can be changed. A SKU that exists only on a marketplace is an error row.

Limits and errors#

  • 120 requests a minute per shop. On 429, wait the Retry-After seconds and retry.
  • Errors are { code, message, rcode }.
  • 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.

Thai text is welcome everywhere (names, descriptions, promotion names). Prices are in THB.

Shop

Which shop the key belongs to, and its categories.

GET/me#

Which shop this key belongs to.

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.

Changes nothing. Safe to call any time.

Response 200

The shop and the key's name.

ok

{
  "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"
    }
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

GET/categories#

The category names a product may use.

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.

Changes nothing.

Response 200

Category names.

ok

{
  "code": 200,
  "message": "OK",
  "data": {
    "categories": [
      "Toys",
      "Books",
      "Clothing"
    ]
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

Products

Read, create and update products by SKU. Prices and content stay on the SellerSideKick store.

GET/products#

The shop's products with price and stock, by SKU or page by page.

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.

  • Lists only products on the shop's SellerSideKick store.
  • 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.

Changes nothing.

Parameters

FieldTypeRequiredDescription
sku (query)stringnoComma-separated SKUs, e.g. `TM-001,TM-002`.
page (query)integerno(min 1) (default 1)
limit (query)integerno(min 1) (max 500) (default 100)

Response 200

A page of products.

ok

{
  "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
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

POST/products/upsert#

Create new products and update existing ones by SKU (up to 500 rows).

Use when the owner wants prices, names, descriptions, categories or brands changed, or new products added.

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.

Before calling:

  • Get category names from GET /categories.
  • Send with dry_run: true, show the owner the summary and the error rows, and only send it for real after they agree.

How rows are handled:

  • A SKU the shop has is updated with only the fields you send; the rest stay.
  • 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).
  • Each row answers created, updated (with changed), unchanged or error.

Effect: changes the SellerSideKick store and the shop's chat bot. Marketplace listings keep their own price and content.

Safe to retry: yes. Sending the same rows again leaves them unchanged.

Request body (required)

FieldTypeRequiredDescription
dry_runbooleannotrue: check every row and report what would happen, changing nothing. (default false)
productsarray of object {sku, name, description, usage_information, price, category, brand, stock, parent_sku}yes(at least 1 items) (at most 500 items)
products[].skustringyes
products[].namestring or object {th, en}noA string is the Thai text (the saved English text is kept), or send { th, en } to set either or both.
products[].descriptionstring or object {th, en}noA string is the Thai text (the saved English text is kept), or send { th, en } to set either or both.
products[].usage_informationstring or object {th, en}noHow 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.
products[].pricenumbernoTHB. (min 0)
products[].categorystringnoA name from GET /categories.
products[].brandstringnoA new brand is added to the shop's brand list.
products[].stockintegernoNew products only: the opening count on the shelf. (min 0)
products[].parent_skustringnoNew 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.

Request examples

Update two prices (dry run first) price_update

{
  "dry_run": true,
  "products": [
    {
      "sku": "TM-001",
      "price": 139
    },
    {
      "sku": "TM-002",
      "price": 159
    }
  ]
}

Add a new product with Thai and English names new_product

{
  "dry_run": true,
  "products": [
    {
      "sku": "TM-010",
      "name": {
        "th": "รถดับเพลิงโทมิก้า",
        "en": "Tomica fire engine"
      },
      "price": 189,
      "category": "Toys",
      "brand": "Tomica",
      "stock": 12
    }
  ]
}

A main product and two variants in one request variants

{
  "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
    }
  ]
}

Response 200

Each row's result. Report the summary and every `error` row to the owner.

ok

{
  "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
    }
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

Stock

Count or adjust on-hand units. Reaches linked marketplace listings.

POST/stock#

Set counted stock or adjust it by SKU (up to 500 rows).

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.

Before calling:

  • 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.
  • Send dry_run: true first and confirm with the owner.
  • 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.

Effect:

  • Recorded in the shop's stock history.
  • Sent to the SKU's linked Shopee, Lazada and TikTok listings, usually within about 10 minutes.
  • Only SKUs on the SellerSideKick store; a marketplace-only SKU is an error row.
  • A delta can't take on-hand below what open orders hold. A counted value is recorded as counted, even below that.

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.

Request body (required)

FieldTypeRequiredDescription
dry_runbooleanno(default false)
idempotency_keystringnoYour id for this change, e.g. `count-2026-10-25-sheet1`. Resending with it never applies a row twice. (min length 8) (max length 100) (pattern `^[A-Za-z0-9._:-]+$`)
itemsarray of object {sku, value, delta, expected_from, reason}yes(at least 1 items) (at most 500 items)
items[].skustringyes
items[].valueintegernoThe units now on the shelf (a count). (min 0)
items[].deltaintegernoUnits to add (positive) or remove (negative). Not 0.
items[].expected_fromintegernoWith `value` only: the on-hand you read. If it has changed, the row is refused. (min 0)
items[].reasonstringnoShown in the stock history, e.g. 'monthly count'. (max length 200)

Request examples

A stock count, guarded against changes since it was read count

{
  "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"
    }
  ]
}

Remove 3 damaged units adjust

{
  "idempotency_key": "damaged-2026-10-25",
  "items": [
    {
      "sku": "TM-001",
      "delta": -3,
      "reason": "damaged in transit"
    }
  ]
}

Response 200

Each row's result. Report the summary and every `error` row to the owner.

ok

{
  "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
    }
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

Promotions

The shop's own coupons, discounts, bundles and buy-X-get-Y offers. Never deleted, only archived.

GET/promotions#

The shop's own promotions, optionally by type and status.

Use to find a promotion's id before updating or archiving it, and to check a code is free before creating one.

Marketplace promotions (Shopee, Lazada, TikTok) are not listed: they are managed in each Seller Center.

Changes nothing.

Parameters

FieldTypeRequiredDescription
type (query)"coupon" | "discount" | "bundle" | "bogo"noOnly this type.
status (query)"active" | "inactive" | "archive"noOnly this status.

Response 200

Promotions, newest first within each type.

ok

{
  "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
      }
    ]
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

POST/promotions/{type}#

Create a coupon, discount, bundle or BOGO promotion.

Use when the owner asks for a new promotion.

Before calling:

  • Confirm the code, dates, amount and the products it covers with the owner.
  • SKUs must be on the shop's SellerSideKick store; an unknown SKU is a 400.
  • A code the shop already uses is refused (400).

Effect: live in the shop's SellerSideKick store and chat bot from starts_at (status active unless you send inactive). Not pushed to marketplaces.

Safe to retry: no. Check with GET /promotions whether it was created before sending again.

Parameters

FieldTypeRequiredDescription
type (path)"coupon" | "discount" | "bundle" | "bogo"yesThe promotion's type.

Request body (required)

FieldTypeRequiredDescription
codestringnoUpper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop.
namestringno
descriptionstringno
starts_atstringno(format date-time)
ends_atstringnoMust be after starts_at. (format date-time)
status"active" | "inactive"noCreate only. Default active.
max_usesinteger | nullnoTotal uses allowed; null for no limit.
max_uses_per_customerinteger | nullno
discount_type"percentage" | "fixed"no
valuenumbernoPercent (at most 100) or THB, by discount_type.
min_order_amountnumbernoTHB.
max_discount_amountnumber | nullnoTHB cap on a percentage discount.
can_stackbooleannoCoupon only: usable together with other promotions.
applies_toobject {all_products, categories, brands, skus}noWhich products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).
applies_to.all_productsbooleanno
applies_to.categoriesarray of stringnoCategory names from GET /categories.
applies_to.brandsarray of stringno
applies_to.skusarray of stringnoMust be SKUs on the shop's SellerSideKick store.
product_discountsarray of object {sku, discount_type, value}noDiscount 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. (at most 500 items)
product_discounts[].skustringyes
product_discounts[].discount_type"percentage" | "fixed"yes
product_discounts[].valuenumberyesPercent (at most 100), or THB off every unit.
productsarray of object {sku, quantity, role}no
products[].skustringyes
products[].quantityintegeryes(min 1)
products[].role"buy" | "get"noBOGO only.
bundle_discount_type"fixed" | "percentage"no
bundle_discountnumbernoTHB off the bundle, when bundle_discount_type is fixed.
bundle_discount_percentagenumbernoPercent off, when bundle_discount_type is percentage.

Request examples

Coupon: 10% off the whole shop, at most 100 THB coupon

{
  "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: a rate per SKU discount_per_sku

{
  "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: 50 THB off two SKUs discount

{
  "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: two cars for 20% off bundle

{
  "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: buy 2 cars, get a track free bogo

{
  "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"
}

Response 201

The new promotion.

ok

{
  "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
    }
  }
}

Errors

  • 400: 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.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

PATCH/promotions/{type}/{id}#

Change a promotion: send only the fields that change.

Use when the owner wants a promotion's dates, amount, products, limits, name or code changed.

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.

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.

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.

Safe to retry: yes. Sending the same fields again gives the same result.

Parameters

FieldTypeRequiredDescription
type (path)"coupon" | "discount" | "bundle" | "bogo"yesThe promotion's type.
id (path)stringyesThe promotion's id from GET /promotions.

Request body (required)

FieldTypeRequiredDescription
codestringnoUpper-case letters, digits, - and _; lower case is upper-cased. Unique in the shop.
namestringno
descriptionstringno
starts_atstringno(format date-time)
ends_atstringnoMust be after starts_at. (format date-time)
status"active" | "inactive"noCreate only. Default active.
max_usesinteger | nullnoTotal uses allowed; null for no limit.
max_uses_per_customerinteger | nullno
discount_type"percentage" | "fixed"no
valuenumbernoPercent (at most 100) or THB, by discount_type.
min_order_amountnumbernoTHB.
max_discount_amountnumber | nullnoTHB cap on a percentage discount.
can_stackbooleannoCoupon only: usable together with other promotions.
applies_toobject {all_products, categories, brands, skus}noWhich products the discount covers: `all_products: true`, or any mix of categories, brands and SKUs (not both).
applies_to.all_productsbooleanno
applies_to.categoriesarray of stringnoCategory names from GET /categories.
applies_to.brandsarray of stringno
applies_to.skusarray of stringnoMust be SKUs on the shop's SellerSideKick store.
product_discountsarray of object {sku, discount_type, value}noDiscount 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. (at most 500 items)
product_discounts[].skustringyes
product_discounts[].discount_type"percentage" | "fixed"yes
product_discounts[].valuenumberyesPercent (at most 100), or THB off every unit.
productsarray of object {sku, quantity, role}no
products[].skustringyes
products[].quantityintegeryes(min 1)
products[].role"buy" | "get"noBOGO only.
bundle_discount_type"fixed" | "percentage"no
bundle_discountnumbernoTHB off the bundle, when bundle_discount_type is fixed.
bundle_discount_percentagenumbernoPercent off, when bundle_discount_type is percentage.

Request examples

Extend the end date extend

{
  "ends_at": "2026-12-15T23:59:59+07:00"
}

Raise a discount to 15% raise

{
  "value": 15
}

Response 200

The promotion as saved.

ok

{
  "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
    }
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 404: No promotion with this id and type in this shop. List promotions to get a valid id.
  • 409: The new code is already used by another promotion of this type in the shop. Pick another code.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.

POST/promotions/{type}/{id}/status#

Switch a promotion on or off, or end it (archive).

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.

Before calling: confirm with the owner, especially before archive.

Safe to retry: yes.

Parameters

FieldTypeRequiredDescription
type (path)"coupon" | "discount" | "bundle" | "bogo"yesThe promotion's type.
id (path)stringyesThe promotion's id from GET /promotions.

Request body (required)

FieldTypeRequiredDescription
status"active" | "inactive" | "archive"yes

Request examples

pause

{
  "status": "inactive"
}

end

{
  "status": "archive"
}

Response 200

The promotion with its new status.

ok

{
  "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
    }
  }
}

Errors

  • 400: The request is invalid. The message names the field; fix it and send again. Unknown fields are refused by name.
  • 401 (Y20, Y21, Y22): 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.
  • 403 (Y31, Y32, Y34, Y35): The key is real but can't be used now. Tell the owner why; retrying won't help.
  • 404: No promotion with this id and type in this shop. List promotions to get a valid id.
  • 429 (R01): Too many requests: 120 a minute per shop. Wait the number of seconds in the Retry-After header, then send the same request again.