Skip to content

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/
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. Header CJ-Access-Token on 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 1600001 or 1600002.
  • The openId returned 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 with strtotime() (offset honoured) and falls back to 180 days from now. Tokens are stored whole (docs: up to 1024 characters, never truncate).

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).

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.

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.

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 shippingProvince and shippingCity. 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>).
  1. The order map row (state P, the deterministic CJ number, the lines) is written before the request.
  2. GET_LOCK per order and warehouse group surrounds “look up, create, store”.
  3. Before every create the order is looked up by its number; an existing order is adopted.
  4. 1603003 (“Order exist”) is adopted the same way.
  5. 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).
  6. 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.
  7. The order is matched by our own number, never by the CJ id alone: createOrderV2 warns that orderId can change when a VID is not a CJ SKU.
  • Register: POST /webhook/set with 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 (openId 123, body {"messageId":"123111","messageType":"INSERT","params":"123","type":"PRODUCT"} gives AHxoGFMoS/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) and STOCK (params keyed by vid). 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.

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.

  • 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; cron and run_now are 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.

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.