# Webhook Mechanism
# Introduction
Product details including addition, deletion & modification of variants.
# Setup Procedure
- 1)Set up message monitoring and reference.webhook
- 2)Set up client monitoring interface, example as below(Java).
- 3)Test monitoring.
# Webhook Configuration Requirements
# 1. Protocol Requirements
- Supported Protocols: HTTPS
- Encryption: TLS 1.2 or TLS 1.3 recommended for secure transmission
- Request Method: POST
- Content Type:
Content-Type: application/json
# 2. Signature Authentication
To make sure the Webhook request really comes from CJ, please verify the signature of every incoming request.
1. Signing algorithm
- Algorithm: HMAC-SHA256, encoded as standard Base64 (padding kept).
- Secret: string form of your
openId(e.g.12312->"12312"). - Message: the raw JSON string of the HTTP request body.
- Output: the Base64-encoded HMAC-SHA256 bytes. CJ sends this value through the
signHTTP header.
2. About openId
- How to get it: call the Get Access Token API; the response returns your
openId. - How to use it: save the
openIdin your system. After registering the Webhook, use thisopenIdas the HMAC secret to compute the signature for every incoming push and compare with thesignheader.
3. Verification steps
- Read the
signvalue from the request header (signature issued by CJ). - Read the raw bytes of the request body. Use the original body you received — do not deserialize and re-serialize, otherwise field order may differ and the signature will not match.
- Use
openIdas the secret and the body string as the message, compute HMAC-SHA256 and Base64-encode it. - Compare the computed signature against the
signheader as plain strings. Accept the request when they match; reject otherwise.
Security notice
The openId delivered in Webhook pushes is the same value returned by the Get Access Token API; it serves as both your account identifier and the signing secret. Never share your Webhook receiver URL, the raw push payload, or any logs/screenshots that contain the openId with any third party (integration providers, ERP plugins, third-party debugging tools, etc.). Leaking the openId allows others to forge valid signatures, impersonate CJ pushes against your endpoint, or correlate your account identity. Mask the openId before forwarding traffic for support or debugging.
4. Sample code – generating a signature (Java)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class SignUtil {
/**
* Compute the signature.
*
* @param openId developer openId returned by the Get Access Token API, saved in your system
* @param bodyJson raw JSON string of the request body
* @return Base64-encoded HMAC-SHA256 signature
*/
public static String encrypt(Long openId, String bodyJson) throws Exception {
String secret = openId.toString();
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
byte[] bytes = mac.doFinal(bodyJson.getBytes());
return Base64.getEncoder().encodeToString(bytes);
}
}
Invocation example and expected output:
long openId = 123L;
// Request body — exactly the same JSON string (and field order) pushed by CJ.
// Note: the CJ server uses fastjson, which serializes keys in alphabetical order.
String body = "{\"messageId\":\"123111\",\"messageType\":\"INSERT\",\"params\":\"123\",\"type\":\"PRODUCT\"}";
String sign = SignUtil.encrypt(openId, body);
// Expected signature: AHxoGFMoS/4mZfJ5vFes5//Pz2QibFQhh3GlrTtnWpk=
5. Sample code – verifying a signature (Java + Spring Boot)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WebhookController {
/** openId returned by the Get Access Token API and saved in your system */
private static final Long OPEN_ID = 12312L;
@PostMapping("/webhook/cj")
public Object receive(@RequestHeader("sign") String sign,
@RequestBody String body) throws Exception {
String expected = SignUtil.encrypt(OPEN_ID, body);
if (!expected.equals(sign)) {
throw new RuntimeException("sign verify failed");
}
// signature verified — continue with business logic
return "{\"code\":200,\"result\":\"success\",\"message\":\"ok\"}";
}
}
6. Sample code – generating and verifying a signature (Python)
import base64
import hashlib
import hmac
def encrypt(open_id: int, body: bytes) -> str:
"""
Compute the signature.
:param open_id: developer openId returned by the Get Access Token API
:param body: raw bytes of the HTTP request body (do not deserialize and re-serialize)
:return: Base64-encoded HMAC-SHA256 signature
"""
secret = str(open_id).encode("utf-8")
digest = hmac.new(secret, body, hashlib.sha256).digest()
return base64.b64encode(digest).decode("utf-8")
# Generation example
open_id = 123
body = b'{"messageId":"123111","messageType":"INSERT","params":"123","type":"PRODUCT"}'
print(encrypt(open_id, body))
# Expected signature: AHxoGFMoS/4mZfJ5vFes5//Pz2QibFQhh3GlrTtnWpk=
Flask-based verification example (the same idea applies to FastAPI and other frameworks — the key is to read the raw body bytes before any middleware mutates them):
from flask import Flask, request, abort, jsonify
import hmac
OPEN_ID = 12312 # saved after calling the Get Access Token API
app = Flask(__name__)
@app.post("/webhook/cj")
def receive():
sign = request.headers.get("sign", "")
raw_body = request.get_data() # bytes — must be the raw body
expected = encrypt(OPEN_ID, raw_body)
# Use hmac.compare_digest for constant-time comparison to avoid timing attacks
if not hmac.compare_digest(expected, sign):
abort(401)
return jsonify({"code": 200, "result": "success", "message": "ok"})
Notes:
- Always sign and verify against the raw body string that you received, not against any re-serialized object.
- The signing algorithm does not use API Key. The secret is the
openIdstring itself.openIdis not carried in the request header — use theopenIdyou saved on your side to compute the signature.
# 3. Response Specifications
- Success Status Code:
200 OK - Timeout: Response must be returned within 3 seconds
(Avoid long-running or complex business logic to ensure prompt response)
# List of topics
# Product Message: PRODUCT
# Occurs when a product is created or updated.
- Sample Payload
{
"messageId": "ca72a4834cd14b9588e88ce206f614a0",
"type": "PRODUCT",
"messageType": "UPDATE",
"params": {
"categoryId": null,
"categoryName": null,
"pid": "1424608189734850560",
"productDescription": "xxxxxx",
"productImage": null,
"productName": null,
"productNameEn": null,
"productProperty1": null,
"productProperty2": null,
"productProperty3": null,
"productSellPrice": null,
"productSku": null,
"productStatus": null,
"fields" : [
"productDescription"
]
}
}
| Parameter | Definition | Type | Required | Length | Note |
|---|---|---|---|---|---|
| messageId | Message Id | string | Y | 200 | Message Id |
| type | Data Type | string | Y | 20 | PRODUCT |
| messageType | Message type | string | Y | 15 | INSERT、UPDATE、DELETE |
| params | object | Y | 5 | ||
| - categoryId | category Id | string | Y | 200 | |
| - categoryName | category Name | string | Y | 200 | |
| - pid | product id | string | Y | 200 | |
| - productDescription | product description | string | Y | 2000 | |
| - productImage | product image | string | Y | 200 | |
| - productName | product name | string | Y | 200 | |
| - productNameEn | product name(english) | string | Y | 200 | |
| - productProperty1 | product property | string | Y | 200 | |
| - productProperty2 | product property | string | Y | 200 | |
| - productProperty3 | product property | string | Y | 200 | |
| - productSellPrice | product sell price | double | Y | 20 | |
| - productSku | product sku | string | Y | 200 | |
| - productStatus | product status | int | Y | 5 | status:2-Off sale, 3-On Sale |
| - fields | fields list | list | Y | 5 |
Product Status
| ProductStatus | Description |
|---|---|
| 2 | Off sale |
| 3 | On Sale |
# Inbound message for Variant
{
"messageId": "7cceede817dc47ed9748328b64353c5c",
"type": "VARIANT",
"messageType": "UPDATE",
"params": {
"vid": "1424608152007086080",
"variantName": null,
"variantWeight": null,
"variantLength": null,
"variantWidth": null,
"variantHeight": null,
"variantImage": null,
"variantSku": null,
"variantKey": null,
"variantSellPrice": null,
"variantStatus": null,
"variantValue1": null,
"variantValue2": null,
"variantValue3": null,
"fields" : [
"variantLength"
],
}
}
| Parameter | Definition | Type | Required | Length | Note |
|---|---|---|---|---|---|
| messageId | Message id | string | Y | 50 | Message Id |
| type | Data Type | string | Y | 20 | VARIANT |
| messageType | Message Type | string | Y | 15 | INSERT、UPDATE、DELETE |
| params | object | Y | |||
| - vid | variant Id | string | Y | 50 | |
| - variantName | variant name | string | Y | 200 | |
| - variantWeight | variant weight, unit:g | int | Y | ||
| - variantLength | variant length, unit:mm | int | Y | ||
| - variantWidth | variant width, unit:mm | int | Y | ||
| - variantHeight | variant height, unit:mm | int | Y | ||
| - variantImage | variant image | string | Y | 200 | |
| - variantSku | variant sku | string | Y | 200 | |
| - variantKey | variant key | string | Y | 200 | |
| - variantSellPrice | variant sell price, USD | double | Y | ||
| - variantStatus | variant status | int | Y | 5 | |
| - variantValue1 | variant value1 | string | Y | 100 | |
| - variantValue2 | variant value2 | string | Y | 100 | |
| - variantValue3 | variant value3 | string | Y | 100 | |
| - fields | fields list | list | Y | 5 |
Variant Status
| variantStatus | Description |
|---|---|
| 0 | Off sale |
| 1 | On sale |
# Stock Message
{
"messageId": "ca72a4834cd14b9588e88ce206f614a0",
"type": "STOCK",
"messageType": "UPDATE",
"params": {
"1424608152007086080": [
{
"vid": "1424608152007086080",
"areaId": "2",
"areaEn": "US Warehouse",
"countryCode": "US",
"storageNum": 12
}
],
"AE7DB9BC-4290-4C85-B8A6-F8957F3DB053": [
{
"vid": "AE7DB9BC-4290-4C85-B8A6-F8957F3DB053",
"areaId": "2",
"areaEn": "US Warehouse",
"countryCode": "US",
"storageNum": 1
}
]
}
}
# Order message
Private inventory outbound orders reuse this Order message (ORDER) topic: once the order webhook is enabled you will receive them. Unlike regular orders, a private inventory outbound order is pushed at creation time as a
messageType=INSERTmessage, distinguishable byparams.privateOutboundOrder=true.
{
"messageId": "7cceede817dc47ed9748328b64353c5c",
"type": "ORDER",
"messageType": "UPDATE",
"params": {
"orderNumber": "api_52f268d40b8d460e82c0683955e63cc9",
"cjOrderId": "210823100016290555",
"orderStatus": "CREATED",
"logisticName": "CJPacket Ordinary",
"trackNumber": null,
"createDate": "2021-08-23 11:31:45",
"updateDate": "2021-08-23 11:31:45",
"payDate": null,
"deliveryDate": null,
"completeDate": null,
"privateOutboundOrder": false,
"orderItems": [
"vid": "1392053744945991680",
"quantity": 1,
"sellPrice": 0.57,
"lineItemId": "2505170958390976500",
"storeLineItemId": "16045188153625",
"productionOrderStatus": 1,
"abnormalType": [
6,
9
]
]
}
}
| Parameter | Definition | Type | Required | Length | Note |
|---|---|---|---|---|---|
| messageId | Message id | string | Y | 50 | Message Id |
| type | Data Type | string | Y | 20 | ORDER |
| messageType | Message Type | string | Y | 15 | INSERT、UPDATE、DELETE、ORDER_CONNNECTED: This type requires special attention:The product has been re-associated in the CJ system, and the order status has been updated from incomplete to complete. At this point, The actual CJ order id is returned in this message. |
| params | object | Y | |||
| - cjOrderId | CJ order id | string | Y | 200 | |
| - orderNum | Customer order number | string | Y | 200 | Will be deprecated, please use orderNumber instead |
| - orderNumber | Customer order number | string | Y | 200 | |
| - orderStatus | CJ order status | string | Y | 200 | |
| - logisticName | logistic name | string | Y | 200 | |
| - trackNumber | track number | string | Y | 200 | |
| - trackingUrl | tracking URL | string | N | 200 | |
| - updateDate | update date | string | Y | 200 | |
| - createDate | create date | string | Y | 200 | |
| - payDate | pay date | string | Y | 200 | |
| - deliveryDate | delivery date | string | Y | 200 | |
| - completeDate | complete date | string | Y | 200 | |
| - privateOutboundOrder | Whether a private inventory outbound order | boolean | true=private inventory outbound order (pushed at creation as messageType=INSERT), false=regular order | ||
| - orderItems | order item list | List | |||
| -- vid | Variant Id | string | 200 | ||
| -- quantity | quantity | int | 20 | ||
| -- sellPrice | Sell Price | BigDecimal | (18,2) | unit:$(USA) | |
| -- storeLineItemId | The lineItemId of your store order | string | 125 | ||
| -- lineItemId | Unique ID of the order item in CJ | string | 50 | ||
| -- productionOrderStatus | Production Status | Number | 1 | 1=Pending Order, 2=Pending Production, 3=In Production, 4=Production Completed, 5=Production Abnormality | |
| -- abnormalType | Abnormal Reason | int[] | 6= Image link error, 9= Production drawings don't match the renderings, 10= Missing hanging ring, 11= Mismatch between die-cutting diagram and printing diagram, 12= uneven edges, 13= letters not connected, 14= Missing order images |
# Order splitting message
{
"messageId": "7cceede817dc47ed9748328b64353c5c",
"type": "ORDERSPLIT",
"messageType": "UPDATE",
"params": {
"originalOrderId": "original order id",
"splitOrderList": [
{
"createAt":1673490845706,
"orderCode":"SD1613355441583259648-2",
"orderStatus":300,
"productList":[
{
"sku":"CJNSSYLY01043-Claret-S",
"vid":"2547992D-CEE1-4BFD-99AC-9E30354F771F",
"quantity":1,
"productCode":"1613355657229205504"
},
{
"sku":"CJJSAQXF00016-Orange",
"vid":"A9C95BCB-D824-4AA1-A389-E86F3CCB10EF",
"quantity":1,
"productCode":"1613355657229205506"
},
{
"sku":"CJNSSYCS03214-Photo Color-XXL",
"vid":"E5FED43E-F9DE-483F-ADCE-8C95D3380315",
"quantity":1,
"productCode":"1613355657229205507"
}
]
},
{
"createAt":1673490845706,
"orderCode":"SD1613355441583259648-1",
"orderStatus":300,
"productList":[
{
"sku":"CJNSSYLY01043-White-M",
"vid":"0550DFC6-7FF7-4662-AE7D-B4DF0E4EB24A",
"quantity":1,
"productCode":"1613355657229205505"
}
]
}
],
"orderSplitTime": "拆单时间"
}
}
| Parameter | Definition | Type | Required | Length | Note |
|---|---|---|---|---|---|
| messageId | Message id | string | Y | 50 | Message Id |
| type | Data Type | string | Y | 20 | ORDERSPLIT |
| messageType | Message Type | string | Y | 15 | INSERT、UPDATE、DELETE |
| params | Object | Y | |||
| - originalOrderId | Original CJ order id | string | N | 200 | |
| - orderSplitTime | Order Split Date | string | N | 200 | |
| - splitOrderList | Order List | Order[] | N | ||
| - - orderCode | CJ order id | string | N | 200 | |
| - - createAt | Create date | string | N | 200 | |
| - - orderStatus | Order status | int | N | 11 | |
| - - productList | Product Information List | Product[] | N | 200 | |
| - - - productCode | product code | string | N | 200 | |
| - - - vid | Variant id | string | N | 200 | |
| - - - quantity | Quantity | int | N | 10 | |
| - - - sku | Sku | string | N | 50 |
# Source product creation result
{
"messageId": "7cceede817dc47ed9748328b64353c5c",
"type": "SOURCINGCREATE",
"messageType": "UPDATE",
"params": {
"cjProductId":"0550DFC6-7FF7-4662-AE7D-B4DF0E4EB24A",
"cjVariantId":"0550DFC6-7FF7-4662-AE7D-B4DF0E4EB24A",
"cjVariantSku":"CJ123582565212",
"cjSourcingId":"125522",
"status": "completed",
"failReason":"",
"createDate": "2023-02-07 00:00:00"
}
}
| 返回字段 | 字段意思 | 字段类型 | Required | 长度 | 备注 |
|---|---|---|---|---|---|
| messageId | Message id | string | Y | 50 | Message Id |
| type | Data Type | string | Y | 20 | ORDERSPLIT |
| messageType | Message Type | string | Y | 15 | INSERT、UPDATE、DELETE |
| params | Object | Y | |||
| - cjProductId | CJ product id | string | N | 100 | |
| - cjVariantId | CJ variant id | string | N | 100 | |
| - cjVariantSku | CJ variant sku | string | N | 50 | |
| - cjSourcingId | CJ sourcing Id | string | N | 50 | |
| - status | status | string | N | 20 | |
| - failReason | fail reason | string | N | 20 | |
| - createDate | create date | String | N | 50 |
# Logistics message
{
"messageId": "7cceede817dc47ed9748328b64353c5c",
"type": "LOGISTIC",
"messageType": "UPDATE",
"openId": 12312,
"params": {
"orderId": "210823100016290555",
"storeOrderNumbers": ["orderNum1"],
"logisticName": "CJPacket Ordinary",
"trackingNumber": "number12345678",
"trackingStatus": 12,
"logisticsTrackEvents": "[{\"status\":12,\"activity\":\" Delivered, PO Box\",\"location\":\" NENANA,AK 99760\",\"eventTime\":\"2024-01-18 07:59:22\",\"statusDesc\":\"Delivered\",\"thirdActivity\":\"Delivered, PO Box\",\"thirdLocation\":\"NENANA,AK 99760\",\"thirdEventTime\":\"2024-01-18 07:59:22\"}]"
}
}
| Parameter | Definition | Type | Required | Length | Note |
|---|---|---|---|---|---|
| messageId | Message Id | string | Y | 200 | Message Id |
| type | Data Type | string | Y | 200 | LOGISTIC |
| messageType | Message Type | string | Y | 15 | INSERT、UPDATE、DELETE |
| params | object | Y | |||
| - orderId | CJ order id | string | Y | 200 | 210823100016290555 |
| - storeOrderNumbers | Store order ids | string[] | Y | 200 | ["orderNum1"] |
| - logisticName | logistic name | string | Y | 200 | CJPacket Ordinary |
| - trackingNumber | tracking number | string | Y | 200 | number12345678 |
| - trackingUrl | tracking URL | string | N | 200 | |
| - trackingStatus | tracking status | int | Y | 20 | 0=No tracking information available at the moment, 1=Warehouse Shipped Out, 2=Forwarder Warehouse Received, 3=Forwarder Return Initiated, 4=Forwarder Shipment Dispatched, 5=First-Leg International Transit, 6=Arrived at Destination Country, 7=Customs Clearance Initiated, 8=Customs Clearance Completed, 9=Last-Mile Pickup, 10=Out For Delivery, 11=Ready For Pickup, 12=Delivered, 13=Delivery Failed / Exception, 14=Return |
| - logisticsTrackEvents | logistics track events | string | Y | 200 | [{"status":12,"activity":" Delivered, PO Box","location":" NENANA,AK 99760","eventTime":"2024-01-18 07:59:22","statusDesc":"Delivered","thirdActivity":"Delivered, PO Box","thirdLocation":"NENANA,AK 99760","thirdEventTime":"2024-01-18 07:59:22"}] |
# Makeup Bill Message: MAKEUP
# Occurs when a makeup bill is created, canceled or paid.
Pushed to the registered makeup callback URL (see webhook setting for registration).
| messageType | Trigger | params.status |
|---|---|---|
INSERT | Makeup bill created | CREATED |
CANCEL | Makeup bill canceled | CANCELED |
PAID | Makeup bill paid (completed) | PAID |
- Sample Payload
{
"messageId": "f3c2a1d09e8b4c5da6b7c8d9e0f1a2b3",
"type": "MAKEUP",
"messageType": "PAID",
"openId": 12312,
"params": {
"orderId": "BK260526000001",
"relationOrderId": "ZF260526000001",
"payOrderId": "2605260000000001",
"amount": 12.35,
"reason": "Postage difference",
"type": 1,
"diffUseType": 0,
"status": "PAID",
"createDate": "2026-06-04 10:00:00",
"paymentDate": "2026-06-04 12:00:00"
}
}
| Parameter | Definition | Type | Required | Length | Note |
|---|---|---|---|---|---|
| messageId | Message Id | string | Y | 200 | Unchanged on retry, usable for idempotent dedup |
| type | Data Type | string | Y | 20 | MAKEUP |
| messageType | Message Type | string | Y | 15 | INSERT-created, CANCEL-canceled, PAID-paid |
| openId | Open Id | number | Y | ||
| params | object | Y | |||
| - orderId | Makeup bill number | string | Y | 200 | Same as orderCode in the makeup list |
| - relationOrderId | Related CJ order number | string | Y | 200 | |
| - payOrderId | Makeup payment order number | string | N | 200 | Returned after payment |
| - amount | Makeup amount | double | Y | (18,2) | Unit: $ (USD) |
| - reason | Makeup reason | string | N | 500 | English |
| - type | Bill type | int | Y | 5 | 1=makeup |
| - diffUseType | Makeup purpose | int | Y | 5 | 0=order makeup, 1=Balance Top-up, 2=Repayment, 3=Transfer Shipping Fee |
| - status | Makeup bill status | string | Y | 20 | CREATED / CANCELED / PAID |
| - createDate | Create time | string | Y | 50 | yyyy-MM-dd HH:mm:ss |
| - paymentDate | Payment time | string | N | 50 | Returned when PAID |
Status
| Status | Description |
|---|---|
| CREATED | Makeup bill created |
| CANCELED | Makeup bill canceled |
| PAID | Makeup bill paid |
# Listening example
# Example
package com.cj.cn.controller;
import com.alibaba.fastjson.JSON;
import com.cj.cn.constant.callback.domain.CallbackParams;
import com.cj.cn.util.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* CJ Webhook Listening Example
*
* @author : kay
*/
@RestController
@RequestMapping("/webhookListener")
@Slf4j
public class TestController {
@PostMapping("/productMessage")
public Result productMessage(@RequestBody @Validated CallbackParams query) {
log.info("product message:{}", JSON.toJSONString(query));
return Result.success(Boolean.TRUE);
}
}
package com.cj.cn.constant.callback.domain;
import lombok.Data;
/**
* @author : kay
*/
@Data
public class CallbackParams {
private String messageId;
private String type;
private Object params;
}
package com.cj.cn.constant.callback.domain;
import lombok.Getter;
import org.springframework.util.StringUtils;
/**
* @author : kay
*/
@Getter
public enum CallbackBusinessTypeEnum {
PRODUCT,
VARIANT,
STOCK;
public static CallbackBusinessTypeEnum create(String name) {
if (!StringUtils.isEmpty(name)) {
for (CallbackBusinessTypeEnum typeEnum: CallbackBusinessTypeEnum.values()) {
if (typeEnum.name().equals(name.toUpperCase())) {
return typeEnum;
}
}
}
return null;
}
}