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
- Create SKUs: send barcodes, variants, declared names and tariff codes.
- Forecast inbound: send tracking numbers and the expected quantity of each SKU.
- Arrival in Shenzhen: scanning the tracking number creates and prints a WO-SK work order.
- Label and shelve: the warehouse prints product barcodes, labels each piece and scans it in.
- Receiving complete: received units go into Shenzhen stock, damaged units are counted separately and discrepancies are sent to your system.
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
- In the member center, add a “company” declarant with its 8-digit Taiwan tax ID.
- Contact HowBridge support to enable the Merchant API. A key is issued after approval.
- The key is shown only once. Store it in a server-side environment variable and never ship it in a browser or mobile app.
- For webhooks, first set up and verify your URL on the member API page, then subscribe to merchant events.
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
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 header | Description |
|---|---|
X-RateLimit-Limit | Per-minute limit |
X-RateLimit-Remaining | Requests left this minute |
X-RateLimit-Limit-DayX-RateLimit-Remaining-Day | Daily limit and remaining requests |
Retry-After | Seconds 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.
| Field | Type | Required | Description |
|---|---|---|---|
seller_sku | string | Required | Your SKU code, e.g. the variant SKU in Shopee. |
product_name | string | Required on create | Product name. SKUs with the same name are grouped under one product. |
barcode | string | Optional | Product 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. |
gtin | string | Optional | GTIN (8 / 12 / 13 / 14 digits, check digit verified). For Shopee products without a GTIN, send 00. |
spec | object | Optional | Variant attributes, e.g. {"color": "black", "size": "M"}, up to 5. |
declared_name_zhdeclared_name_en | string | Optional | Declared name in Chinese / English. Describe material and use; do not write “sample” or “gift”. |
declared_unit_value | number | Optional | Declared unit value: your purchase price from the supplier, not the marketplace selling price. |
declared_currency | string | Optional | CNY, TWD or USD; required when a unit value is given. |
hs_code | string | Optional | 11-digit Taiwan tariff code used to check import requirements. |
origin_country | string | Optional | ISO country of origin, default CN. |
weight_g | integer | Optional | Unit weight in grams. |
dimensions_cm | array | Optional | Length, width and height in cm, e.g. [20, 15, 3]. |
battery_type | string | Optional | none, contained (built in), packed (packed with the item) or standalone (battery only). |
channels | array | Optional | Marketplace 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://0523.tw/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)
| ok | No import regulation codes. |
| review_required | The 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_required | Import requirements apply (inspection, quarantine, permits, etc.); simplified declaration cannot be used. |
| blocked | Mainland China products that may not be imported (MW0, origin China); cannot be forecast for inbound. |
| unclassified | No 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.
| Field | Type | Required | Description |
|---|---|---|---|
reference_no | string | Required | Your purchase or inbound order number, unique per merchant (letters, digits and ._-/). |
supplier_order_no | string | Optional | Supplier order number, e.g. a 1688 order ID. |
warehouse_code | string | Optional | Receiving warehouse; currently only SZ (Shenzhen). |
notes | string | Optional | Notes (up to 1,000 characters). |
packages | array | Required | Parcels, up to 50. |
packages[].tracking_no | string | Required | Chinese courier tracking number. |
packages[].carrier | string | Optional | Courier code, e.g. yto or sf. |
packages[].items | array | Required | Parcel contents: seller_sku and qty, up to 500 lines per inbound order. |
Request example (POST JSON)
curl -X POST https://0523.tw/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 } ] } ] }'
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
| created | Forecast; no parcel has arrived yet. |
| partially_arrived | Some parcels have arrived in Shenzhen. |
| arrived | All parcels have arrived; labeling and shelving in progress. |
| completed | All received, quantities match the forecast. |
| completed_with_discrepancy | All received, with shortages, overages or damage. |
| cancelled | Cancelled. |
Parcel status
| awaiting_arrival | Waiting to arrive. |
| labeling | Arrived; work order created, labeling and shelving in progress. |
| received | Shelving complete. |
| mismatch | The parcel was checked in under a different account than your forecast; support is reviewing it. |
| cancelled | Cancelled 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_hand | Units in stock. |
reserved | Units reserved for orders. |
available | Available = on hand − reserved. |
damaged | Damaged 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
- 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"}. - Call
PUT /api/v1/merchant/webhook/eventsto subscribe to merchant events. Changing the URL later in the member center keeps your merchant event subscriptions.
curl -X PUT https://0523.tw/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_flagged | A SKU’s import check changed to review required, formal declaration required or not importable. |
inbound.arrived | A parcel arrived in Shenzhen and a work order was created. |
inbound.mismatch | A parcel was checked in under a different account than the forecast; support is reviewing it. |
inbound.received | A parcel finished shelving, with expected, received and damaged quantities per SKU. |
inbound.discrepancy | Shelved quantities differ from the forecast or include damage (sent together with inbound.received). |
inventory.changed | Stock 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:
| HTTP | error.code | Description |
|---|---|---|
| 401 | — | Missing, invalid or expired key (body is {"message": "Unauthenticated."}). |
| 403 | MERCHANT_TOKEN_REQUIRED | This token has no Merchant API access (e.g. a token from a member sign-in). |
| 403 | MERCHANT_NOT_APPROVEDMERCHANT_SUSPENDED | The account is not an approved merchant, or the merchant is suspended. |
| 403 | APP_SUSPENDEDAPP_REVOKEDAPP_NOT_FOUND | The API application for this key is suspended or revoked. |
| 422 | VALIDATION_FAILEDPRODUCT_NAME_REQUIRED | Invalid data; see the per-field details in error.errors. |
| 422 | BATCH_TOO_LARGE | Too many items in a batch. |
| 207 / 409 | SKU_BARCODE_DUPLICATESKU_BARCODE_CONFLICT | The barcode is already used by another SKU, or equals another SKU’s system barcode or pick code. |
| 207 / 422 | GTIN_INVALIDHS_CODE_NOT_FOUND | Invalid GTIN check digit, or the tariff code does not exist. |
| 404 / 422 | SKU_NOT_FOUNDSKU_IMPORT_BLOCKED | SKU not found, or the SKU is not importable and cannot be forecast. |
| 409 | INBOUND_REFERENCE_EXISTS | The inbound reference number already exists. |
| 422 | TRACKING_IN_USETRACKING_FORECASTED_BY_OTHERTRACKING_ALREADY_WAREHOUSEDTRACKING_NOT_AVAILABLE | The tracking number is on an open inbound order, forecast by another member, or already checked in under another member. |
| 409 | INBOUND_NOT_CANCELLABLE | A parcel has already arrived, so the inbound order cannot be cancelled. |
| 400 / 409 / 422 | IDEMPOTENCY_KEY_INVALIDIDEMPOTENCY_IN_PROGRESSIDEMPOTENCY_KEY_REUSED | Invalid Idempotency-Key, reused with a different body, or the previous request is still in progress. |
| 409 | CONFLICT_RETRY | The same record is being changed by another request; retry shortly. |
| 429 | RATE_LIMIT_EXCEEDED | Rate 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://0523.tw/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');
// Node.js 18+ (server side only) import { randomUUID } from 'node:crypto'; const base = 'https://0523.tw/api/v1/merchant'; const headers = { Authorization: `Bearer ${process.env.LUCKY_MERCHANT_KEY}`, 'Content-Type': 'application/json', Accept: 'application/json', }; const res = await fetch(`${base}/skus/batch`, { method: 'POST', headers: { ...headers, 'Idempotency-Key': randomUUID() }, body: JSON.stringify({ items }), }); const { data, meta } = await res.json(); console.log(meta.summary); // { created, updated, failed } const stock = await fetch(`${base}/inventory?warehouse_code=SZ`, { headers }).then(r => r.json());
import os, uuid, requests base = "https://0523.tw/api/v1/merchant" session = requests.Session() session.headers.update({ "Authorization": f"Bearer {os.environ['LUCKY_MERCHANT_KEY']}", "Accept": "application/json", }) r = session.post(f"{base}/inbound-orders", headers={"Idempotency-Key": "po-20260917-0001"}, json={ "reference_no": "PO-20260917-0001", "packages": [{"tracking_no": "YT1234567890123", "items": [{"seller_sku": "TEE-RED-S", "qty": 40}]}], }, timeout=20) if r.status_code == 409: existing_id = r.json()["error"]["context"]["inbound_order_id"] else: r.raise_for_status()
10 Support
If you run into problems, send us the request time, endpoint and error.code so we can investigate.
LINE official account @0523tw
Technical email howyunqisoftware@gmail.com
Version v1 Phase 1 (2026-09-17): product SKUs, inbound forecasts, Shenzhen labeling and shelving, inventory, webhooks.