Developer notes
Technical notes for integrators and reviewers. Every endpoint and field below is quoted from the official CJ Dropshipping API 2.0
documentation (https://developers.cjdropshipping.com/en/api/api2/, read 2026-11-07). Anything the documentation does not state is
marked UNVERIFIED and explained.
| Part | Where |
|---|---|
| CJ API client (envelope, token, throttle) | Client/Api.php (extends Client/BaseClient.php) |
| Pure mappings (CJ answers and requests) | Mapper.php, tested with tests/fixtures/*.json |
| Price rules | PriceRules.php, Options.php |
| Text safety (emoji, invalid UTF-8, HTML) | Text.php |
| Image download (CJ hosts, SSRF guard) | ImageFetcher.php |
| Import and update of products | Catalog.php, Sync.php |
| Freight quotes and checkout shipping method | Freight.php, RateCalculator.php, Tygh/Shippings/Services/KvCjDropshipping.php, Installer.php |
| Order push, tracking, cancel | Fulfilment.php |
| Queue, log, licence, upgrade synchronizer | Queue.php, Log.php, License/, Upgrade/ |
Authentication (docs: Authentication)
Section titled “Authentication (docs: Authentication)”| Call | Endpoint | Notes |
|---|---|---|
| Get access token | POST /authentication/getAccessToken, body {"apiKey": "..."} |
Returns openId, accessToken, accessTokenExpiryDate, refreshToken, refreshTokenExpiryDate, createDate. Limit: 1 call per second. |
| Refresh | POST /authentication/refreshAccessToken, body {"refreshToken": "..."} |
Same fields without openId. Error 1600003. |
| Logout | POST /authentication/logout |
Not used. |
- Base URL
https://developers.cjdropshipping.com/api2.0/v1. HeaderCJ-Access-Tokenon every other call. - Lifetimes: access token 180 days, refresh token 180 days (defaults). Repeated logins within 24 hours return the same tokens.
- The add-on keeps the token set sealed (AES-256-GCM) in the account row, uses the refresh token before logging in again, and logs in again once when a call answers
1600001or1600002. - The
openIdreturned by the login is the signing key of webhooks. It is stored sealed with the account. - Expiry dates are ISO 8601 with the UTC+8 offset (docs example
2027-04-22T22:33:41+08:00); the add-on parses them withstrtotime()(offset honoured) and falls back to 180 days from now. Tokens are stored whole (docs: up to 1024 characters, never truncate).
Envelope and errors (docs: Appendix 1)
Section titled “Envelope and errors (docs: Appendix 1)”Every answer is {code, result, message, data, requestId, success}. Business errors arrive with HTTP 200 and code != 200, so the client checks code == 200 and result == true. requestId is stored on the exception for support requests.
| Code | Meaning (docs) | Handling |
|---|---|---|
1600001 / 1600002 |
invalid API key or token / empty token | one new login, repeat once |
1600003 |
invalid refresh token | fresh login |
1600005 |
API key is wrong | connection test shows the message |
1600200 (HTTP 429) |
too many requests | retryable, job back-off |
1600000, 1600100, 1600301, 1613001, 1613002, 1612001 |
busy / interface offline / read timeout / another change in progress / result unknown | retryable; for orders the next attempt queries the order first (docs: “query the order before retrying”) |
1600300 |
parameter invalid (Appendix 1); the order query page lists it as “order not found” | not retried; order query maps it to “not found” (a wrong “not found” is caught by 1603003 on the create) |
1603003 |
order exists, do not duplicate | the order is looked up and adopted |
1603100 |
order not found | “not found” on lookup |
1604000 |
balance insufficient | order marked failed with the message, job dead; top up, then “Send to CJ” |
1602001 |
product not found | product marked “gone from CJ” |
Appendix 1 (Global Error Codes, checked 2026-11-07) is authoritative: 1600100 = interface offline (retried), 1600300 = parameter invalid.
Rate limits (docs: Interface Call Restrictions)
Section titled “Rate limits (docs: Interface Call Restrictions)”QPS per account level: Free 1, Plus 2, Prime 4, Advanced 6; 10 requests per second per IP; at most 3 users per IP. There is also a points-based daily quota (rules in “Points Resource Rules”, not read). The setting “CJ requests per second” spaces calls accordingly (Api::throttle()); a free account therefore checks about 12 products per run (two calls each).
Product endpoints
Section titled “Product endpoints”| Use | Endpoint | Fields used |
|---|---|---|
| Category tree (connection test) | GET /product/getCategory |
categoryFirstName, categoryFirstList[], categorySecondName, categorySecondList[], categoryId, categoryName |
| Search | GET /product/listV2 (page 1-1000, size 1-100) |
request keyWord, page, size, categoryId, countryCode, startSellPrice, endSellPrice, orderBy, sort; answer content[].productList[]: id, nameEn, sku, bigImage, sellPrice, nowPrice, listedNum, categoryId, threeCategoryName, oneCategoryName, addMarkStatus, warehouseInventoryNum; paging pageNumber, totalRecords, totalPages |
| Details with variants | GET /product/query?pid= |
pid, productNameEn, productSku, bigImage, productImageSet, productWeight (g), categoryId, categoryName, description (HTML), sellPrice, status, productKeyEn; variants[]: vid, variantNameEn, variantSku, variantImage, variantKey, variantWeight (g), variantSellPrice (USD), barcode |
| Inventory by product | GET /product/stock/getInventoryByPid?pid= |
variantInventories[] with vid and inventory[]: countryCode, totalInventory, cjInventory, factoryInventory, verifiedWarehouse |
Notes: sellPrice can be a range text in some answers (docs 1.9 shows "1.20-3.50"), so the first number is read. productImageSet is a string array in the docs; a JSON array in a string is accepted too. UNVERIFIED: the exact meaning of status on product details; the add-on treats 3 as “on sale” (the webhook documents productStatus 2 = off sale, 3 = on sale) and ignores other shapes. Stock is counted for the configured warehouse country; “total” uses totalInventory (CJ plus factory), “cj” uses cjInventory.
Freight and shipping method
Section titled “Freight and shipping method”POST /logistic/freightCalculate with startCountryCode, endCountryCode, products[{vid, quantity}] (+ zip). Answer rows: logisticName, logisticPrice (USD), logisticAging (“7-12” days). Rows without a name or price are dropped. The cheapest or fastest row is chosen: the CJ service the customer chose at checkout (cheapest / fastest) wins, otherwise the account setting. The logisticName of the chosen row goes into the order. The checkout service (Tygh\Shippings\Services\KvCjDropshipping) quotes only the CJ products of the package, converts USD to the store currency and adds the shipping markup.
Orders
Section titled “Orders”| Use | Endpoint | Notes |
|---|---|---|
| Create | POST /shopping/order/createOrderV2 |
Required by the docs: orderNumber (<= 50), shippingCountryCode, shippingCountry, shippingProvince, shippingCity, shippingCustomerName, shippingAddress, logisticName, fromCountryCode, products[] with vid or sku and quantity. Used optionally: shippingAddress2, shippingZip, shippingPhone, email, remark, payType (2 balance, 3 create only), isSandbox, storeLineItemId. Answer: orderId, orderStatus, productAmount, postageAmount, orderAmount, logisticsMiss, interceptOrderReasons[{code, message}]. |
| Query | GET /shopping/order/getOrderDetail?orderId= |
orderId is “custom or CJ id”: our own order number is used for the lookup before every create. Fields: orderId, orderNum, cjOrderId, orderStatus, logisticName, trackNumber, trackingProvider, trackingUrl, productAmount, postageAmount, orderAmount. |
| Delete | DELETE /shopping/order/deleteOrder?orderId= |
Only CREATED and IN_CART orders. |
| Tracking | GET /logistic/trackInfo?trackNumber= (repeatable) |
Client method exists; tracking comes from the order query and webhooks. |
Order statuses (docs): CREATED, IN_CART, UNPAID, PENDING, PROCESSING, UNSHIPPED, SHIPPED, DELIVERED, CANCELLED, OTHER.
- Province. CJ requires
shippingProvinceandshippingCity. When the shopper gave no state, the city is sent as the province. UNVERIFIED whether CJ accepts that for every country. - Sandbox. CJ has no sandbox host: test mode sends
isSandbox = 1(docs “Sandbox”). The sandbox helper endpoints (simulatePay,updateStatus,updateTrackNumber) are not called by the add-on. - Splitting. The lines of one CS-Cart order are grouped by CJ warehouse country; each group is one CJ order (
<prefix>-<order id>or<prefix>-<order id>-<country>).
Exactly one CJ order per CS-Cart order
Section titled “Exactly one CJ order per CS-Cart order”- The order map row (
state P, the deterministic CJ number, the lines) is written before the request. GET_LOCKper order and warehouse group surrounds “look up, create, store”.- Before every create the order is looked up by its number; an existing order is adopted.
1603003(“Order exist”) is adopted the same way.- A timeout or network error leaves the row pending and retries later; the next attempt starts with the lookup (tested by dropping the answer of a create that succeeded).
- Canceled, declined, failed, incomplete and temporary CS-Cart orders, and orders in one of the account’s own cancel statuses, are never sent, also by hand.
- The order is matched by our own number, never by the CJ id alone: createOrderV2 warns that
orderIdcan change when a VID is not a CJ SKU.
Webhooks (docs: Webhook)
Section titled “Webhooks (docs: Webhook)”- Register:
POST /webhook/setwith one object per topic{type: ENABLE|CANCEL, callbackUrls: [one https url]}. Required topics:product,stock,order,logistics. The add-on registers those four. - Delivery: HTTPS POST, JSON, answer HTTP 200 within 3 seconds, no retries by CJ, messages can repeat (deduplicate by
messageId), the topic closes after low success rates. - Signature: header
sign= Base64(HMAC-SHA256(secret = openId string, message = raw body)) (docs “Signature verification”). The unit test uses a vector computed with that formula (openId123, body{"messageId":"123111","messageType":"INSERT","params":"123","type":"PRODUCT"}givesAHxoGFMoS/4mZfJ5vFes5//Pz2QibFQhh3GlrTtnWpk=); the docs page gives no vector of its own. - Payloads handled:
ORDER(params.orderNumber,cjOrderId,orderStatus,logisticName,trackNumber),LOGISTIC(params.orderId,storeOrderNumbers,logisticName,trackingNumber,trackingProvider,trackingStatus),PRODUCT/VARIANT(params.pid/vid) andSTOCK(paramskeyed byvid). Product, variant and stock messages queue a re-read of that product; order and logistic messages are applied only to orders of the receiving account. - The handler stores the message as a job and answers at once; polling (
track_orders, every sync interval) is the safety net.
Images
Section titled “Images”CJ names no image hosts in the API documentation. UNVERIFIED: the default allow list is cjdropshipping.com and cjdropshiping.com (suffix match on a dot boundary); extra hosts can be added in the settings. Downloads are https only on port 443, the host name is resolved once and the connection is pinned to a public address (private, loopback, link-local and reserved ranges are refused), redirects are followed by hand (max 2) and every hop is checked again, the size limit is 8 MB, the content must decode as JPEG, PNG, GIF or WebP with at least 50 px per side and at most 40 megapixels. A failing image is logged and skipped; the product is still imported.
Product names and descriptions go through Text: invalid UTF-8 is repaired, control characters and 4-byte characters (emoji) are removed because CS-Cart tables are utf8 (3 bytes), text is cut by characters (mb_substr), and descriptions keep only a short list of HTML tags without attributes (images keep an https src and alt). Scripts, styles, frames, forms and event handlers are removed.
Multi-Vendor
Section titled “Multi-Vendor”- Owner mode (default): one CJ account (company 0) serves all vendors; only administrators manage CJ.
- Vendor mode: each vendor connects an own account; products, orders, jobs and rules are scoped by
company_id; every POST compares the row’s company with the vendor’s company;cronandrun_noware administrator-only (vendor_multivendor.post.php). - The account of an order is
Config::accountCompany(order.company_id); the account that receives a webhook must be the account that serves the order.
php81 tests/unit.php and php83 tests/unit.php (pure logic, mappings, signature vector, image guard, API envelope), node tests/mock-server/server.js 11830 and node tests/e2e/e2e.mjs (HTTP e2e with a real signed-in customer checkout), kv qa lic, kv qa fpc.
Hooks used
Section titled “Hooks used”PHP: change_order_status_post, delete_product_post, init_templater_post (upgrade synchronizer only while pending). Template: orders:tabs_extra (post). No core file is modified.