Developer notes
Technical reference for developers who customize Shopee Marketplace Connector.
Add-on identity
Section titled “Add-on identity”| Item | Value |
|---|---|
| Add-on id | kv_shopee |
| Function prefix | fn_kv_shopee_ |
| Language variables | kv_shopee.* |
| CSS scope | .kv-shopee |
| PHP namespace | Tygh\Addons\KvShopee |
| Preset | marketplace (Marketplace Hub, generated by kv new --kind integration --class marketplace) |
Structure
Section titled “Structure”| Class | Role |
|---|---|
Client\BaseClient |
HTTP (cURL, TLS verification, timeouts), JSON, error mapping, masked logging |
Client\Api |
Shopee Open Platform v2: request signing, authorization link, code exchange, per-shop token refresh under a database lock, error mapping, one thin method per endpoint |
Drivers\Shopee\Driver, Mapper |
Hub driver (SKU lookup with search_item, add_item with image upload, stock and price, orders, fulfilment, cancel, returns) and the pure translation, unit tested with the official response samples in tests/samples/ |
Hub\* |
Channel independent marketplace layer (category and attribute mapping, price rules, stock buffer, listing builder, order import, status map, shipments, returns, errors per item) |
Accounts, Secret |
Credentials (test and live) and the stored authorization, sealed with AES-256-GCM; one account per store owner or per vendor |
Config, Access |
Settings, owner mode, who may see and manage what |
Queue, Jobs, JobException |
Job queue: back-off 5 min / 15 min / 1 h / 3 h / 12 h, per-job isolation, overlap lock; handlers in Jobs::handle(), recurring work in Jobs::schedule() |
Log |
Event log with masking (partner key, sign, tokens), filters, CSV, retention |
Webhook |
Push signature check and event ids; the controller kv_shopee.webhook?c=<account> stores the event as a job |
API conformance
Section titled “API conformance”Every call below was read from the official Shopee Open Platform v2 documentation on 2026-11-07 (Developer Guide pages open.shopee.com/developer-guide/<id> and the API reference open.shopee.com/documents/v2/<api name>?module=<n>&type=1). The reference pages are rendered by a JavaScript application; the text was read from the same JSON service the pages use (open.shopee.com/opservice/api/v1/doc/api/?api_name=<api>&version=2). The samples used by the unit tests are the response samples of those pages; several are not valid JSON as published and were repaired structurally only (see tests/samples/README.md).
Requirement of the platform (Developer Guide, “Developer account registration” and “App management”): the seller of the connected shop, or you as the developer of the app, needs a free Shopee Open Platform developer account with an app (partner ID and partner key). The owner of this add-on does not provide one; each store owner registers their own.
Common to all calls (Developer Guide: “API calls”, id 16)
Section titled “Common to all calls (Developer Guide: “API calls”, id 16)”| Topic | Official behaviour | Add-on |
|---|---|---|
| Production hosts | https://partner.shopeemobile.com (developers near Singapore), https://openplatform.shopee.cn (near mainland China), https://openplatform.shopee.com.br (near the US) |
Setting “API host”: global, China mainland, Brazil |
| Sandbox hosts | https://openplatform.sandbox.test-stable.shopee.sg (all developers), https://openplatform.sandbox.test-stable.shopee.cn (mainland China developers). The API reference lists https://partner.test-stable.shopeemobile.com as test_url of every API |
Test mode uses the sandbox host of the Guide; the setting “Sandbox host” selects the host of the API reference instead. For Brazil the Guide names no separate sandbox API host (only the auth page open.sandbox.test-stable.shopee.com.br): the global sandbox host is used (flagged, see below) |
| Methods, protocol | GET and POST; HTTP/JSON, FORM for file upload | GET with query parameters, POST with a JSON body, multipart for upload_image |
| Common parameters | partner_id, timestamp (valid 5 minutes), sign; shop APIs add access_token (valid 4 hours) and shop_id; all in the query |
As documented |
| Signature | Base string = partner_id + api path (no host) + timestamp [+ access_token + shop_id for Shop APIs]; HMAC-SHA256 with the partner key; lower-case hex |
Client\Api::sign(); checked in tests/unit.php against vectors computed independently (Node crypto) from the base strings printed in the Guide |
| Response | request_id, error (empty on success), message, warning, response |
Api::checkBusiness(); answers with HTTP 200 and an error are errors |
| Error names | error_auth (invalid partner_id, access_token, permission), error_param, error_sign (Wrong sign), error_rate_limit, error_limit (daily call limit), error_server, error_inner, error_network, source_ip_undeclared, error_partner_key_expired, shop_banned, invalid_acceess_token (sic) |
401 (token: one refresh and repeat), 429 and 503 (the job is retried), 403 (credentials, permission, IP: a person acts), 422 (this request is wrong) |
| IP allow list | The Console has an “IP Address Whitelist” per app (source_ip_undeclared) |
The error text tells the owner to declare the server IP |
Authorization and tokens (Developer Guide: “Authorization and Authentication”, id 20; API reference module Public)
Section titled “Authorization and tokens (Developer Guide: “Authorization and Authentication”, id 20; API reference module Public)”| Step | Official | Add-on |
|---|---|---|
| Authorization link | Fixed URL https://open.shopee.com/auth (open.shopee.cn, open.shopee.com.br; sandbox open.sandbox.test-stable.shopee.*) with partner_id, auth_type=seller, redirect_uri, response_type=code and an optional state against CSRF (Developer Guide “Authorization and Authentication”). Older apps: legacy GET {host}/api/v2/shop/auth_partner?partner_id×tamp&sign&redirect |
The open.shopee.com/auth link with redirect_uri = index.php?dispatch=kv_shopee.authorize and state = <account>.<time>.<signature> (valid one hour). Account option Authorization link: legacy builds the signed auth_partner link with the state inside redirect |
| Redirect | Shopee redirects to the redirect URL with code (valid once, 10 minutes) and shop_id (shop account) or main_account_id (main account) |
kv_shopee.authorize reads code, shop_id, main_account_id |
| Get token | POST /api/v2/auth/token/get (API v2.public.get_access_token, Public API); query partner_id, timestamp, sign (over partner_id + path + timestamp); body code, partner_id, shop_id or main_account_id; answer access_token, refresh_token, expire_in, shop_id_list (main account), merchant_id_list |
Stored per shop id; a main account yields one token pair per authorized shop |
| Refresh token | POST /api/v2/auth/access_token/get (v2.public.refresh_access_token); body refresh_token, partner_id, shop_id; answer new access_token, new refresh_token, expire_in; access token 4 hours, refresh token 30 days, single use and rotating; “valid for each shop_id” |
Refreshed when it expires within 5 minutes, under a database lock (GET_LOCK), the new pair is stored before the call continues; one forced refresh on error_auth for the token |
| Validity | Authorization lasts at most 365 days; the seller must authorize again afterwards | The Connection page shows the refresh deadline |
| Shop info | GET /api/v2/shop/get_shop_info: shop_name, region, status, expire_time |
Connection test text, shop region for currency |
Products (API reference module Product, 89, and MediaSpace, 91; Developer Guide “Creating product”, id 211)
Section titled “Products (API reference module Product, 89, and MediaSpace, 91; Developer Guide “Creating product”, id 211)”| API | Method and path | Fields used |
|---|---|---|
v2.product.get_category |
GET /api/v2/product/get_category |
request language; response category_list[]: category_id, parent_category_id, original_category_name, display_category_name, has_children |
v2.product.get_attribute_tree |
GET /api/v2/product/get_attribute_tree |
request category_id_list (max 20), language; response list[].attribute_tree[]: attribute_id, mandatory, name, attribute_value_list[] (value_id, name), attribute_info.input_type (1 SINGLE_DROP_DOWN, 2 SINGLE_COMBO_BOX, 3 FREE_TEXT_FILED, 4 MULTI_DROP_DOWN, 5 MULTI_COMBO_BOX) |
v2.product.get_brand_list |
GET /api/v2/product/get_brand_list |
request offset, page_size (max 100), category_id, status (1 normal), language; response brand_list[], has_next_page, next_offset, is_mandatory |
v2.product.get_item_limit |
GET /api/v2/product/get_item_limit |
request category_id; response price_limit.min_limit, item_name_length_limit.max_limit, item_description_length_limit.min_limit/max_limit, item_image_count_limit.max_limit |
v2.logistics.get_channel_list |
GET /api/v2/logistics/get_channel_list |
response logistics_channel_list[]: logistics_channel_id, logistics_channel_name, enabled, fee_type (SIZE_SELECTION, SIZE_INPUT, FIXED_DEFAULT_PRICE, CUSTOM_PRICE), size_list[].size_id, mask_channel_id |
v2.media_space.upload_image |
POST /api/v2/media_space/upload_image (listed as a Public API: signed without token and shop) |
multipart image (JPG/JPEG/PNG, 10 MB), scene = normal for item images; response response.image_info.image_id. The Guide says URLs are not supported, so the store’s images are downloaded and uploaded |
v2.product.add_item |
POST /api/v2/product/add_item |
item_name, description, original_price, weight (kg), dimension (package_length, package_width, package_height), category_id, image.image_id_list, logistic_info[] (logistic_id, enabled, is_free, size_id), attribute_list[] (attribute_id, attribute_value_list[] with value_id and original_value_name; custom values use value_id 0), seller_stock[] (stock), item_sku, item_status, condition, brand (brand_id, original_brand_name); response response.item_id |
v2.product.search_item |
GET /api/v2/product/search_item |
request page_size, item_sku, item_status (repeated); response item_id_list[] |
v2.product.get_item_base_info |
GET /api/v2/product/get_item_base_info |
request item_id_list (max 50); response item_list[]: item_id, item_sku, item_status (NORMAL, BANNED, UNLIST, REVIEWING, SELLER_DELETE, SHOPEE_DELETE), has_model |
v2.product.get_model_list |
GET /api/v2/product/get_model_list |
request item_id; response model[]: model_id, model_sku |
v2.product.update_stock |
POST /api/v2/product/update_stock |
item_id, stock_list[] (model_id 0 for an item without models, seller_stock[].stock); response failure_list[] (model_id, failed_reason), success_list[] |
v2.product.update_price |
POST /api/v2/product/update_price |
item_id, price_list[] (model_id, original_price); response failure_list[], success_list[] |
Price decimals: two decimals are accepted in SG, MY, BR, MX, PL (Creating product guide) and ES, AR (get_model_list description); other regions round to an integer, so the add-on rounds before sending.
Orders and logistics (module Order, 94, and Logistics, 95; Developer Guide “Order Management”, id 229)
Section titled “Orders and logistics (module Order, 94, and Logistics, 95; Developer Guide “Order Management”, id 229)”| API | Method and path | Fields used |
|---|---|---|
v2.order.get_order_list |
GET /api/v2/order/get_order_list |
request time_range_field (update_time), time_from, time_to (at most 15 days apart), page_size (1 to 100), cursor, request_order_status_pending; response more, next_cursor, order_list[].order_sn |
v2.order.get_order_detail |
GET /api/v2/order/get_order_detail |
request order_sn_list (comma separated, 1 to 50), response_optional_fields, request_order_status_pending; response order_list[]: order_sn, order_status, currency, cod, create_time, update_time, total_amount, payment_method, estimated_shipping_fee, actual_shipping_fee, message_to_seller, recipient_address (name, phone, full_address, town, district, city, state, region, zipcode), item_list[] (item_id, item_name, item_sku, model_id, model_name, model_sku, model_quantity_purchased, model_discounted_price, order_item_id), package_list[] (package_number, shipping_carrier), invoice_data.number. Fields that are not “returned by default” must be listed in response_optional_fields |
v2.logistics.get_shipping_parameter |
GET /api/v2/logistics/get_shipping_parameter |
request order_sn, package_number; response info_needed.pickup / dropoff / non_integrated, pickup.address_list[] (address_id, address_flag, time_slot_list[] with pickup_time_id, flags), dropoff.branch_list[].branch_id, dropoff.slug_list[].slug |
v2.logistics.ship_order |
POST /api/v2/logistics/ship_order |
order_sn, package_number, exactly one of pickup (address_id, pickup_time_id, tracking_number), dropoff (branch_id, sender_real_name, tracking_number, slug), non_integrated (tracking_number) |
v2.logistics.get_tracking_number |
GET /api/v2/logistics/get_tracking_number |
request order_sn, package_number; response response.tracking_number |
v2.order.cancel_order |
POST /api/v2/order/cancel_order |
order_sn, cancel_reason (OUT_OF_STOCK, CUSTOMER_REQUEST, UNDELIVERABLE_AREA (TW, MY), COD_NOT_SUPPORTED), item_list[] (item_id, model_id; required for OUT_OF_STOCK), partial_cancel_item_list[] (item_id, model_id, model_quantity) |
Order status values (Developer Guide “V2.0 Data Definition”, id 31): UNPAID, PENDING, READY_TO_SHIP, PROCESSED, RETRY_SHIP, SHIPPED, TO_CONFIRM_RECEIVE, IN_CANCEL, CANCELLED, TO_RETURN, COMPLETED. Imported: everything except UNPAID and PENDING.
Returns (module Returns, 102; Developer Guide “Return Refund Management”, id 227)
Section titled “Returns (module Returns, 102; Developer Guide “Return Refund Management”, id 227)”| API | Method and path | Fields used |
|---|---|---|
v2.returns.get_return_list |
GET /api/v2/returns/get_return_list |
request page_no, page_size, update_time_from, update_time_to (at most 15 days apart); response more, return[]: return_sn, order_sn, status (REQUESTED, ACCEPTED, CANCELLED, JUDGING, CLOSED, PROCESSING, SELLER_DISPUTE), reason, text_reason, return_solution (0 return and refund, 1 refund only), create_time, update_time, item[] (item_id, model_id, name, item_sku, variation_sku) |
v2.returns.confirm |
POST /api/v2/returns/confirm |
return_sn. The Guide: “agree to the buyer’s return application, only for the Full Refund type, the buyer does not need to return the product”. Therefore only requests with return_solution 1 are offered for approval |
Push (webhook) (Developer Guide “Push Mechanism notifications”, id 18, and the push reference)
Section titled “Push (webhook) (Developer Guide “Push Mechanism notifications”, id 18, and the push reference)”| Topic | Official | Add-on |
|---|---|---|
| Authorization | Request header Authorization = hex HMAC-SHA256 of the callback URL, a vertical bar character and the raw body, with the partner key; the signature check is “technically optional” but recommended |
Verified with the callback URL as the store knows it (https URL of the Connection page, plus https and http forms of the request host and URI); a push without a valid signature is answered with 400 |
| Answer | A push counts as delivered only for a 2xx answer with an empty body within the timeout (3 seconds); retries after 300, 1800 and 10800 seconds; success rate below 70% gives warnings, below 30% disables the subscription | 200 with an empty body; the event is stored as a job first |
| Events used | code 3 (order_status_push), 4 (order_trackingno_push): data ordersn, status, tracking_no; code 1, 2, 12 (authorization pushes) |
Codes 3 and 4 start an import (the push only says that something changed); codes 1, 2, 12 are logged |
| Registration | In the Console (Push Mechanism page, callback URL + Verify) or with v2.push.set_push_config |
The documentation page of v2.push.set_push_config needs a login: not readable, so not implemented. Set the callback URL in the Console. Without a push the add-on polls on the cron schedule |
Flagged: not verifiable from the public documentation, or an interpretation
Section titled “Flagged: not verifiable from the public documentation, or an interpretation”| Item | What is uncertain | What the add-on does |
|---|---|---|
v2.push.set_push_config |
The documentation page is login gated | Not used; Console registration |
get_attribute_tree parameter name |
The parameter table names category_id_list; the Java request sample in the same page uses category_ids=1234,5678 |
category_id_list, comma separated |
| Array parameters in GET calls | order_sn_list is documented as comma separated; the samples for item_id_list and category_id_list use commas too; item_status is documented as repeated |
As documented per parameter |
get_return_list page_no |
“Specifies the starting entry … Default is 0” while the sample uses 1 | Starts at 0, stops when more is false, results are de-duplicated by return_sn |
search_item by item_sku |
The documentation does not say whether item_sku also matches model SKUs |
Candidates are read with get_item_base_info; for items with models get_model_list is read and model_sku compared |
| Brazil sandbox API host | The Guide names a sandbox auth page for Brazil but no sandbox API host | Global sandbox host |
| Redirect URL query | The Guide shows redirects to a plain URL with ?code=&shop_id= appended; it does not say how an existing query string is treated |
state is part of the redirect URL’s query; if Shopee drops it, use the manual code field on the Connection page (code and shop ID from the address bar) |
package_number on ship_order |
The error texts say to send it for split orders and not for unsplit orders | Sent only when the order has more than one package |
| Partial cancellation | item_list is “required when cancel_reason is OUT_OF_STOCK”; the page does not say whether it must be combined with partial_cancel_item_list |
Both are sent for a partial OUT_OF_STOCK cancellation |
| Region to currency table | No table in the documentation (orders carry currency) |
A fixed table in Mapper::REGIONS used for price conversion; orders use the currency Shopee sends |
| Rate limits | error_rate_limit and error_limit exist; no numbers are published |
100 ms between product calls, retries with back-off |
Not part of 1.0
Section titled “Not part of 1.0”Variations (tier variations and models: items are created without variations, existing models are matched by model_sku for stock and price), bundles and kits, videos, size charts, pre-orders, certifications, global products and Merchant APIs (cross-border merchants), SIP, FBS and Instant/Mart shops, shipping documents (air waybills: print them in Shopee Seller Centre), buyer cancellation requests (handle_buyer_cancellation), return and refund offers and disputes, escrow and payout reports, Shopee Ads, vouchers, flash sales and chat.
Database tables
Section titled “Database tables”| Table | Purpose |
|---|---|
?:kv_shopee_accounts |
Partner ID and key per company (key sealed), last connection check, sealed authorization (tokens per shop and shop names) |
?:kv_shopee_log |
Log |
?:kv_shopee_jobs |
Queue |
?:kv_shopee_product_map, ?:kv_shopee_order_map |
Product mapping and imported orders |
?:kv_shopee_category_map, ?:kv_shopee_attr_map, ?:kv_shopee_options, ?:kv_shopee_cache, ?:kv_shopee_batches, ?:kv_shopee_returns |
Hub: mappings, rules, remembered ids, batches and returns |
Hooks used
Section titled “Hooks used”PHP hooks: update_product_post, delete_product_post (mark mapped products dirty) and init_templater_post (license UI plugin). Template hooks: addons:manage / tabs_content (license card) and index:styles (admin CSS).
Test the HTTP layer without Shopee
Section titled “Test the HTTP layer without Shopee”The base URL can be overridden for tests in config.local.php (development only):
$config['kv_shopee_base_url'] = 'http://127.0.0.1:12880';node tests/mock-server/server.js 12880 starts a local stand-in that follows the documented signatures, parameters and answers; node tests/e2e/e2e.mjs runs the HTTP end-to-end test against it; php tests/unit.php runs the unit tests.
Template overrides
Section titled “Template overrides”Admin templates live in design/backend/templates/addons/kv_shopee/. Do not edit them in a store; they are replaced on update.
Trademark
Section titled “Trademark”Shopee is a trademark of Sea Limited and its affiliates. This add-on is made by Kavio Labs Software Limited and is not affiliated with, endorsed by or sponsored by Shopee.