API Marketplace Merchant API
Phase 1: products and inbound

Merchant API: connect SKUs, purchase forecasts and Shenzhen stock to your system

For companies selling on Shopee, Coupang and momo. Send the product barcodes created when you list items, forecast tracking numbers and contents when you purchase, and when goods reach the Shenzhen warehouse they are labeled and scanned into stock piece by piece — with inventory and discrepancies reported back in real time.

Companies

Who can apply (8-digit tax ID required)

200

SKUs per batch request

WO-SK

Labeling work order on arrival

60 / min

Default rate limit per key

1 Overview

Phase 1 covers products and inbound: from creating a SKU to goods being shelved as stock in the Shenzhen warehouse.

What you can do today

  • Bulk-create or update SKUs keyed by your own seller_sku, with a separate product barcode for each color and size, linked to your Shopee / Coupang / momo listing variants.
  • Automatically check import requirements from the 11-digit Taiwan tariff code and flag products that need inspection, permits or cannot be imported.
  • Forecast tracking numbers and box contents when you purchase; on arrival in Shenzhen a WO-SK labeling work order is created and the warehouse labels and scans every piece.
  • Query per-warehouse stock and the stock ledger, and receive webhooks for arrival, completed receiving, quantity discrepancies and stock changes.

Inbound flow

  1. Create SKUs: send barcodes, variants, declared names and tariff codes.
  2. Forecast inbound: send tracking numbers and the expected quantity of each SKU.
  3. Arrival in Shenzhen: scanning the tracking number creates and prints a WO-SK work order.
  4. Label and shelve: the warehouse prints product barcodes, labels each piece and scans it in.
  5. Receiving complete: received units go into Shenzhen stock, damaged units are counted separately and discrepancies are sent to your system.
About import requirements: the system checks import regulation codes from Taiwan Customs tariff data for operational reference only. Whether a product actually needs inspection, a permit or a formal customs declaration is determined by Customs and our partner customs broker. Stock bought for resale is commercial and does not qualify for personal-use exemptions.

Planned features

Bulk outbound orders, shipment batches grouped automatically under declaration rules, and forward stock, sorting and marketplace shipping labels at the Taoyuan warehouse. They will be announced on this page when released.

2 Access and API keys

The Merchant API is available to registered companies only. Every key is bound to an approved merchant account.

How to get access

  1. In the member center, add a “company” declarant with its 8-digit Taiwan tax ID.
  2. Contact HowBridge support to enable the Merchant API. A key is issued after approval.
  3. The key is shown only once. Store it in a server-side environment variable and never ship it in a browser or mobile app.
  4. For webhooks, first set up and verify your URL on the member API page, then subscribe to merchant events.
Send Authorization: Bearer <key> with every request. Tokens created by signing in to the member app or website cannot call the Merchant API.
Authorization: Bearer 12|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

3 Conventions

These rules apply to every endpoint.

Base URL

https://howbridge.cn/api/v1/merchant

Format and language

Requests and responses are JSON. Error messages default to Traditional Chinese; send Accept-Language: en for English. Always branch on the fixed English error.code.

Response envelope

Success returns {"success": true, "data": …, "meta": …}; failure returns {"success": false, "error": {"code", "message", "errors", "context"}}, where errors lists per-field details. The only exception is a missing or invalid key: HTTP 401 with body {"message": "Unauthenticated."}.

{
  "success": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "…",
    "errors": [
      { "code": "TRACKING_IN_USE", "field": "packages.0.tracking_no", "message": "…" }
    ]
  }
}

Rate limits

60 requests per minute and 5,000 per day per key (300 per minute and 50,000 per day on the high-volume plan). A batch request counts as one. For real-time data, subscribe to webhooks instead of polling.

Response headerDescription
X-RateLimit-LimitPer-minute limit
X-RateLimit-RemainingRequests left this minute
X-RateLimit-Limit-Day
X-RateLimit-Remaining-Day
Daily limit and remaining requests
Retry-AfterSeconds to wait after hitting the limit

Idempotency (Idempotency-Key)

Write requests accept an Idempotency-Key header (8–128 characters: letters, digits and _-:.). Retrying with the same key and the same body after a timeout returns the original response with Idempotent-Replayed: true; reusing a key with a different body returns 422. Keys are kept for 24 hours.

Batch responses

Batch endpoints report each item: 200 when all succeed, 207 when some fail and 422 when all fail. Every item has an index, a status (created / updated / failed) and the failure reasons.

Identifier characters

seller_sku may contain letters, digits and ._- (1–64 characters); product barcodes may also contain / and + (3–64 characters). Matching is case-insensitive.

4 Product SKUs

A SKU is identified by your seller_sku: sending the same seller_sku again updates it, changing only the fields you include.

Bulk create or update SKUs

POST/api/v1/merchant/skus/batch

Up to 200 items per request. Each item is processed independently, so one failure does not affect the others.

FieldTypeRequiredDescription
seller_skustringRequiredYour SKU code, e.g. the variant SKU in Shopee.
product_namestringRequired on createProduct name. SKUs with the same name are grouped under one product.
barcodestringOptionalProduct barcode printed on labels and scanned at shelving. Each color and size must have a different barcode. If you have none, leave it empty and the system generates one. Barcodes longer than 16 characters are printed as the system barcode instead.
gtinstringOptionalGTIN (8 / 12 / 13 / 14 digits, check digit verified). For Shopee products without a GTIN, send 00.
specobjectOptionalVariant attributes, e.g. {"color": "black", "size": "M"}, up to 5.
declared_name_zh
declared_name_en
stringOptionalDeclared name in Chinese / English. Describe material and use; do not write “sample” or “gift”.
declared_unit_valuenumberOptionalDeclared unit value: your purchase price from the supplier, not the marketplace selling price.
declared_currencystringOptionalCNY, TWD or USD; required when a unit value is given.
hs_codestringOptional11-digit Taiwan tariff code used to check import requirements.
origin_countrystringOptionalISO country of origin, default CN.
weight_gintegerOptionalUnit weight in grams.
dimensions_cmarrayOptionalLength, width and height in cm, e.g. [20, 15, 3].
battery_typestringOptionalnone, contained (built in), packed (packed with the item) or standalone (battery only).
channelsarrayOptionalMarketplace listing links, up to 20: platform (shopee / coupang / momo / other), shop_id, item_id, model_id, platform_sku.

Request example (POST JSON)

curl -X POST https://howbridge.cn/api/v1/merchant/skus/batch \
  -H "Authorization: Bearer $LUCKY_MERCHANT_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sku-sync-20260917-001" \
  -d '{
  "items": [
    {
      "seller_sku": "TEE-RED-S",
      "barcode": "4710000000011",
      "product_name": "Cotton T-shirt",
      "spec": { "color": "red", "size": "S" },
      "declared_name_en": "Cotton knitted T-shirt",
      "declared_unit_value": 18.5,
      "declared_currency": "CNY",
      "hs_code": "61091000006",
      "weight_g": 160,
      "channels": [ { "platform": "shopee", "shop_id": "123456", "item_id": "2233", "model_id": "445566" } ]
    }
  ]
}'

Response 200

{
  "success": true,
  "data": [
    {
      "index": 0,
      "seller_sku": "TEE-RED-S",
      "status": "created",
      "sku": {
        "seller_sku": "TEE-RED-S",
        "barcode": "4710000000011",
        "system_barcode": null,
        "pick_code": "131999",
        "hs_code": "61091000006",
        "regulation": { "status": "ok", "import_codes": [], "reasons": [] },
        "channels": [ { "platform": "shopee", "shop_id": "123456", "item_id": "2233", "model_id": "445566", "platform_sku": null } ]
      }
    }
  ],
  "meta": { "summary": { "created": 1, "updated": 0, "failed": 0 } }
}

Import requirement check (regulation.status)

okNo import regulation codes.
review_requiredThe requirement covers only some products under this code (e.g. C02 “some products under this item require inspection”); confirm based on the actual product.
formal_requiredImport requirements apply (inspection, quarantine, permits, etc.); simplified declaration cannot be used.
blockedMainland China products that may not be imported (MW0, origin China); cannot be forecast for inbound.
unclassifiedNo tariff code provided yet, so the check cannot run.

List SKUs

GET/api/v1/merchant/skus

Filter by seller_sku, barcode, regulation_status or updated_since (ISO 8601); paginate with page / per_page (max 100).

Get one SKU

GET/api/v1/merchant/skus/{seller_sku}

Update marketplace links

PUT/api/v1/merchant/skus/{seller_sku}/channels

Replaces all existing links with the list you send. The same marketplace variant cannot be linked to two SKUs.

Download barcode labels

GET/api/v1/merchant/skus/{seller_sku}/label.pdf?qty=10

Returns a 40×30 mm label PDF; qty is the number of labels (1–1000). The Shenzhen warehouse prints the same label when labeling; use this if you want to label goods yourself before shipping.

5 Inbound forecasts

After your supplier ships, forecast the tracking numbers and contents. One inbound order can have several tracking numbers, each listing SKUs and quantities.

Create an inbound order

POST/api/v1/merchant/inbound-orders

Validated as a whole: nothing is created if any error occurs. A tracking number can belong to only one open inbound order; duplicate SKUs within a package are merged.

FieldTypeRequiredDescription
reference_nostringRequiredYour purchase or inbound order number, unique per merchant (letters, digits and ._-/).
supplier_order_nostringOptionalSupplier order number, e.g. a 1688 order ID.
warehouse_codestringOptionalReceiving warehouse; currently only SZ (Shenzhen).
notesstringOptionalNotes (up to 1,000 characters).
packagesarrayRequiredParcels, up to 50.
packages[].tracking_nostringRequiredChinese courier tracking number.
packages[].carrierstringOptionalCourier code, e.g. yto or sf.
packages[].itemsarrayRequiredParcel contents: seller_sku and qty, up to 500 lines per inbound order.

Request example (POST JSON)

curl -X POST https://howbridge.cn/api/v1/merchant/inbound-orders \
  -H "Authorization: Bearer $LUCKY_MERCHANT_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: po-20260917-0001" \
  -d '{
  "reference_no": "PO-20260917-0001",
  "supplier_order_no": "1688-3312456789",
  "packages": [
    {
      "tracking_no": "YT1234567890123",
      "carrier": "yto",
      "items": [
        { "seller_sku": "TEE-RED-S", "qty": 40 },
        { "seller_sku": "TEE-RED-M", "qty": 60 }
      ]
    }
  ]
}'
Duplicate protection: creating the same reference_no again returns 409 INBOUND_REFERENCE_EXISTS with the original ID in error.context.inbound_order_id. To retry safely, send an Idempotency-Key.

Inbound order status

createdForecast; no parcel has arrived yet.
partially_arrivedSome parcels have arrived in Shenzhen.
arrivedAll parcels have arrived; labeling and shelving in progress.
completedAll received, quantities match the forecast.
completed_with_discrepancyAll received, with shortages, overages or damage.
cancelledCancelled.

Parcel status

awaiting_arrivalWaiting to arrive.
labelingArrived; work order created, labeling and shelving in progress.
receivedShelving complete.
mismatchThe parcel was checked in under a different account than your forecast; support is reviewing it.
cancelledCancelled or voided; the tracking number has been released.

List inbound orders

GET/api/v1/merchant/inbound-orders

GET/api/v1/merchant/inbound-orders/{id}

Filter by status, reference_no or updated_since. Each parcel returns expected, received and damaged quantities and the difference.

Cancel an inbound order

POST/api/v1/merchant/inbound-orders/{id}/cancel

Allowed only while no parcel has arrived; the tracking numbers can then be forecast again.

6 Inventory

Stock is kept per warehouse: SZ Shenzhen and TY Taoyuan (Taoyuan stock opens together with outbound features).

Get inventory

GET/api/v1/merchant/inventory?warehouse_code=SZ

Filter by warehouse_code and seller_sku[] (up to 200).

on_handUnits in stock.
reservedUnits reserved for orders.
availableAvailable = on hand − reserved.
damagedDamaged units; cannot be shipped.

Response 200

{
  "success": true,
  "data": [
    { "seller_sku": "TEE-RED-S", "barcode": "4710000000011", "warehouse_code": "SZ",
      "on_hand": 38, "reserved": 0, "available": 38, "damaged": 2, "updated_at": "2026-09-17T15:20:11+08:00" }
  ],
  "meta": { "total": 1, "page": 1, "per_page": 100, "last_page": 1 }
}

Stock ledger

GET/api/v1/merchant/inventory/movements?after_id=0&limit=200

Append-only movement records. Page forward with the after_id cursor (up to 500 per request) and keep meta.next_after_id to continue reconciliation.

7 Webhook events

Events are pushed to your URL as they happen, so you do not need to poll.

Setup

  1. Set your webhook URL (https only) on the member API page and complete ownership verification: your endpoint must respond HTTP 200 with body {"challenge": "the value you received"}.
  2. Call PUT /api/v1/merchant/webhook/events to subscribe to merchant events. Changing the URL later in the member center keeps your merchant event subscriptions.
curl -X PUT https://howbridge.cn/api/v1/merchant/webhook/events \
  -H "Authorization: Bearer $LUCKY_MERCHANT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events": ["inbound.arrived", "inbound.received", "inbound.discrepancy", "inventory.changed"]}'

Events

sku.regulation_flaggedA SKU’s import check changed to review required, formal declaration required or not importable.
inbound.arrivedA parcel arrived in Shenzhen and a work order was created.
inbound.mismatchA parcel was checked in under a different account than the forecast; support is reviewing it.
inbound.receivedA parcel finished shelving, with expected, received and damaged quantities per SKU.
inbound.discrepancyShelved quantities differ from the forecast or include damage (sent together with inbound.received).
inventory.changedStock changed, with the latest inventory of the affected SKUs.

inbound.received

{
  "event": "inbound.received",
  "delivery_id": "5b0f1c2e-8f3a-4d7e-9c21-0a6b2d4e8f10",
  "created_at": "2026-09-17T15:20:11+08:00",
  "data": {
    "inbound_order_id": 128,
    "reference_no": "PO-20260917-0001",
    "order_status": "completed_with_discrepancy",
    "tracking_no": "YT1234567890123",
    "work_order": "WO-SK-315",
    "warehouse_code": "SZ",
    "items": [
      { "seller_sku": "TEE-RED-S", "expected_qty": 40, "received_qty": 38, "damaged_qty": 2, "difference": 0 },
      { "seller_sku": "TEE-RED-M", "expected_qty": 60, "received_qty": 59, "damaged_qty": 0, "difference": -1 }
    ],
    "received_at": "2026-09-17T15:20:10+08:00"
  }
}

Verifying signatures

Every delivery includes X-Lucky-Event, X-Lucky-Delivery-Id and X-Lucky-Signature: t=timestamp,v1=signature. Compute HMAC-SHA256 with your webhook secret over “timestamp + . + raw body”, compare it with v1, and reject timestamps older than 5 minutes. Retries of the same event keep the same delivery_id, which you can use for de-duplication.

Non-2xx responses are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. After 20 consecutive failures the webhook is disabled and must be set up again in the member center.

// PHP
$body   = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_LUCKY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig);   // ['t' => '1789…', 'v1' => 'ab12…']

$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, getenv('LUCKY_WEBHOOK_SECRET'));

if (! hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
    http_response_code(400);
    exit;
}
$event = json_decode($body, true);   // de-duplicate by $event['delivery_id']
http_response_code(200);

8 Error codes

error.code is always English. Common errors:

HTTPerror.codeDescription
401—Missing, invalid or expired key (body is {"message": "Unauthenticated."}).
403MERCHANT_TOKEN_REQUIREDThis token has no Merchant API access (e.g. a token from a member sign-in).
403MERCHANT_NOT_APPROVED
MERCHANT_SUSPENDED
The account is not an approved merchant, or the merchant is suspended.
403APP_SUSPENDED
APP_REVOKED
APP_NOT_FOUND
The API application for this key is suspended or revoked.
422VALIDATION_FAILED
PRODUCT_NAME_REQUIRED
Invalid data; see the per-field details in error.errors.
422BATCH_TOO_LARGEToo many items in a batch.
207 / 409SKU_BARCODE_DUPLICATE
SKU_BARCODE_CONFLICT
The barcode is already used by another SKU, or equals another SKU’s system barcode or pick code.
207 / 422GTIN_INVALID
HS_CODE_NOT_FOUND
Invalid GTIN check digit, or the tariff code does not exist.
404 / 422SKU_NOT_FOUND
SKU_IMPORT_BLOCKED
SKU not found, or the SKU is not importable and cannot be forecast.
409INBOUND_REFERENCE_EXISTSThe inbound reference number already exists.
422TRACKING_IN_USE
TRACKING_FORECASTED_BY_OTHER
TRACKING_ALREADY_WAREHOUSED
TRACKING_NOT_AVAILABLE
The tracking number is on an open inbound order, forecast by another member, or already checked in under another member.
409INBOUND_NOT_CANCELLABLEA parcel has already arrived, so the inbound order cannot be cancelled.
400 / 409 / 422IDEMPOTENCY_KEY_INVALID
IDEMPOTENCY_IN_PROGRESS
IDEMPOTENCY_KEY_REUSED
Invalid Idempotency-Key, reused with a different body, or the previous request is still in progress.
409CONFLICT_RETRYThe same record is being changed by another request; retry shortly.
429RATE_LIMIT_EXCEEDEDRate limit exceeded; wait for Retry-After and retry.

9 Code samples

Run these samples on your server. Never put your key in front-end code.

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

$api = Http::withToken(config('services.lucky.merchant_key'))
    ->acceptJson()
    ->baseUrl('https://howbridge.cn/api/v1/merchant')
    ->timeout(20);

// 1) Sync SKUs (up to 200 per request)
$res = $api->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
    ->post('/skus/batch', ['items' => $items]);

foreach ($res->json('data') as $row) {
    if ($row['status'] === 'failed') {
        logger()->warning('SKU failed', $row['errors']);
    }
}

// 2) Forecast an inbound order
$order = $api->withHeaders(['Idempotency-Key' => 'po-' . $po->id])
    ->post('/inbound-orders', [
        'reference_no' => $po->number,
        'packages'     => [[
            'tracking_no' => $po->tracking_no,
            'items'       => [['seller_sku' => 'TEE-RED-S', 'qty' => 40]],
        ]],
    ])->throw()->json('data');

// 3) Read Shenzhen stock
$stock = $api->get('/inventory', ['warehouse_code' => 'SZ'])->json('data');

10 Support

If you run into problems, send us the request time, endpoint and error.code so we can investigate.

WeCom official account

Technical email howyunqisoftware@gmail.com

Version v1 Phase 1 (2026-09-17): product SKUs, inbound forecasts, Shenzhen labeling and shelving, inventory, webhooks.