Base URL: https://www.pod-sourcing.com/api/v1
Version: 1.0 · 2026-11-03
Audience: approved dropshipping partners (independent sites)
Your partner account has one of two modes:
| Mode | We ship to | Shipping cost | Typical partner |
|---|---|---|---|
consolidation | Your domestic consolidation warehouse (Shenzhen or another city) | CNY 6 first kg + CNY 2.50/kg | Independent site / 集运 |
direct | The end customer's receipt address (international) | Live Yanwen quote; channel and DDP/DDU configured by sales | Etsy / direct-to-consumer |
Ask your account manager to open a partner account. Approval does not issue a token automatically.
After approval, open account/dropship → API tokens:
pd_test_... (sandbox; no production, no charges)pd_live_... (production; use after the sandbox pilot passes)Tokens are shown once. Store them in your secret manager. We store only a SHA-256 hash. You can revoke any token from the same page; our admin can also revoke it.
Authorization: Bearer pd_test_xxxxxxxxxxxxxxxx
Every request must include it. Tokens must not be placed in query strings or request bodies.
| Scope | What it allows |
|---|---|
catalog:read | List/read links |
catalog:select | Add/remove links from your catalogue |
assets:write | Upload/read customer images |
orders:write | Create orders |
orders:read | Read order status |
balance:read | Read balance |
sandbox:simulate | Advance sandbox order status |
120 requests/minute per token by default. Over the limit returns 429 with Retry-After: 60.
curl -s https://www.pod-sourcing.com/api/v1/ping \
-H "Authorization: Bearer pd_test_..."
{
"ok": true,
"time": "2026-11-03T01:50:02+00:00",
"mode": "test",
"scopes": ["catalog:read", "catalog:select", "assets:write", "orders:write", "orders:read", "balance:read", "sandbox:simulate"]
}
whoami:
curl -s https://www.pod-sourcing.com/api/v1/whoami \
-H "Authorization: Bearer pd_test_..."
{
"ok": true,
"partner": { "id": 2, "name": "99Plus", "slug": "99plus" },
"mode": "test",
"scopes": ["catalog:read", "..."],
"sandbox_balance_usd": 1000.0
}
GET /catalog/links?scope=available|selected&since=2026-11-01T00:00:00Z&ids=123,124&page=1&limit=50
scope=available — links we have opened for dropshippingscope=selected — links you added to your catalogue (default)since — only rows updated after this ISO-8601 timeids — optional comma-separated filterpage / limit (max 50){
"ok": true,
"data": [
{
"link_id": 123,
"title": "Custom Family Mugs",
"cover": "https://cdn.../cover.jpg",
"item_count": 1,
"customizable": true,
"version": "ddc3fa23380e3ca5",
"updated_at": "2026-11-03T01:47:35+00:00",
"selected": true
}
],
"page": 1,
"limit": 50,
"total": 1,
"next_page": null,
"scope": "selected"
}
GET /catalog/links/{link_id}
Returns the complete template JSON:
{
"ok": true,
"data": {
"link_id": 80,
"title": "customize personalize name baby romper",
"cover": "https://.../composite_80.jpg",
"currency": "USD",
"customizable": true,
"version": "ddc3fa23380e3ca5",
"updated_at": "2026-11-03 01:47:35",
"selected": true,
"items": [
{
"product_id": 80,
"title": "customize personalize name baby romper",
"print_no": "TR1305805",
"cover": "https://.../composite_80.jpg",
"gallery": ["https://.../composite_80.jpg"],
"customizable": true,
"sizes": [
{ "size_id": 1020, "name": "6 month" },
{ "size_id": 1021, "name": "12 month" }
],
"template": {
"id": 836,
"name": "TR-15cm x 15cm 烫画",
"craft": "TH",
"format": "png",
"density": 99,
"size_cm": { "w": 15, "h": 15 },
"source_px": { "w": 585, "h": 585 },
"clip": { "x": 0, "y": 0, "width": 573, "height": 573, "unit": "templogo_px_top_left" },
"clip_normalized": { "x": 0, "y": 0, "width": 979.49, "height": 979.49, "unit": "canvas1000_top_left" },
"full_bleed": false,
"clip_ratio": 1
},
"colors": [
{
"color_id": 830,
"name": "Blue",
"label": "Blue",
"mockup": "https://.../composite_80.jpg",
"blank_mockup": "https://.../placement.jpg",
"full_bleed": false,
"clip_ratio": 1,
"placement": {
"template_id": 836,
"clip_x": 455,
"clip_y": 440,
"clip_width": 358,
"clip_height": 291,
"clip_adj_y": 360,
"clip_angle": 0,
"unit": "canvas1000_center",
"mask_url": "https://.../mask.png"
},
"regions": [
{
"id": "r21",
"type": "text",
"left": 381, "top": 465, "width": 148, "height": 108,
"angle": 0,
"fontLabel": "name",
"hint": "mike",
"required": true,
"fontMinLen": 0,
"fontMaxLen": 20,
"fontColor": "#1a1a1a"
},
{
"id": "r22",
"type": "image",
"left": 363, "top": 292, "width": 196, "height": 167,
"angle": 0,
"fontLabel": "photo",
"hint": "",
"required": true,
"imgMinX": 1000,
"imgMinY": 1000
}
],
"sizes": [
{
"size_id": 1020,
"sku_id": 2601,
"name": "6 month",
"variant_id": "80:2601",
"sku": "@TR1305805-A007BU-66/j",
"raw_sku": "A007BU-66",
"price_usd": 3.3,
"in_stock": true,
"stock": null,
"weight_g": 52,
"price_tiers": [
{ "qty": 1, "percent": 200, "price_usd": 3.3 },
{ "qty": 5, "percent": 180, "price_usd": 3.27 },
{ "qty": 50, "percent": 130, "price_usd": 3.2 },
{ "qty": 100, "percent": 120, "price_usd": 3.18 },
{ "qty": 1000, "percent": 110, "price_usd": 3.17 }
]
}
]
}
]
}
]
}
}
Price and template data:
price_from_usd / price_to_usd — our wholesale price range (your cost).price_usd / price_max_usd and per-variant price_usd.template and tempdzinfo — the complete template/placement/regions JSON (tempdzinfo.colors[colorId] = placement + regions + full_bleed + clip_ratio).tempdzinfo is the data your editor needs to reproduce the print area exactly.curl -s -X POST https://www.pod-sourcing.com/api/v1/catalog/links/select \
-H "Authorization: Bearer pd_test_..." \
-H "Content-Type: application/json" \
-d '{"link_ids":[80]}'
{ "ok": true, "selected": [80], "ignored": [] }
curl -s -X POST https://www.pod-sourcing.com/api/v1/catalog/links/unselect \
-H "Authorization: Bearer pd_test_..." \
-H "Content-Type: application/json" \
-d '{"link_ids":[80]}'
{ "ok": true, "removed": 1 }
Only links in our dropship catalogue can be selected. Unknown ids are returned in ignored.
POST /assets
Content-Type: multipart/form-data
file=<binary>
curl -s -X POST https://www.pod-sourcing.com/api/v1/assets \
-H "Authorization: Bearer pd_test_..." \
-F "[email protected]"
{
"ok": true,
"asset_id": "pa_e118bd131f4e071a7d1469d1",
"mime": "image/png",
"bytes": 8381,
"width": 1200,
"height": 1200,
"sha256": "bebb2231..."
}
imgMinX / imgMinY.GET /assets/{asset_id}
Authorization: Bearer ...
Returns the binary image. Assets are scoped to the same partner and mode (test/live).
pd_test_ creates a sandbox order (no production, no charge). pd_live_ creates a real order, debits your prepaid balance, and enters our production queue. Admin still books the waybill and pushes to production.
POST /orders
Authorization: Bearer pd_test_...
Idempotency-Key: 99P-TEST-0001
Content-Type: application/json
{
"partner_order_id": "99P-TEST-0001",
"lines": [
{
"variant_id": "80:2601",
"quantity": 1,
"values": {
"r21": "LUCA",
"r22": "pa_e118bd131f4e071a7d1469d1"
}
}
],
"recipient": {
"name": "Shenzhen Consolidation Warehouse / Member 12345",
"phone": "+86 13800000000",
"address": "深圳市南山区 ...",
"postcode": "518000",
"consolidation_code": "99P-12345"
},
"note": "optional"
}
Rules:
Idempotency-Key required (8–120 chars). Repeating the same key returns the original result with Idempotent-Replay: 1.partner_order_id required (max 120).variant_id; sku is accepted as a fallback.values is keyed by region id. Image regions take an asset_id; text regions take a string.pieces: [{seq, values}]; count must equal quantity.shop_id: which site the order belongs to. With one shop it is inferred; with multiple, pass it so order webhooks go to the right shop.in_stock from the catalogue as authoritative.{
"ok": true,
"sandbox": true,
"order_no": "PDT261103015002772",
"partner_order_id": "99P-TEST-0001",
"status": "paid",
"tier": { "qty_12m": 0, "effective_qty": 1 },
"charges": {
"goods_usd": 3.3,
"shipping_usd": 0.9,
"total_usd": 4.2,
"shipping_weight_g": 52,
"shipping_kg": 1,
"shipping_cny": 6
},
"balance_usd": 1000,
"created_at": "2026-11-03T01:50:02+00:00"
}
Sandbox orders do not charge the balance, do not enter production, and do not create a real order. They are stored in separate partner_sandbox_* tables — they never appear in your order list or in the POD buyer order list. You can delete all sandbox orders and test data at any time from Dropship → API tokens → Clear sandbox data. Live orders (pd_live_) do create a real order and debit the prepaid balance.
GET /orders/{order_no}
{
"ok": true,
"data": {
"ok": true,
"sandbox": true,
"order_no": "PDT261103015002772",
"partner_order_id": "99P-TEST-0001",
"status": "shipped",
"currency": "USD",
"charges": { "goods_usd": 3.3, "shipping_usd": 0.9, "total_usd": 4.2 },
"tracking_no": "SF123456789",
"carrier": "SF Express",
"created_at": "2026-11-03 01:50:02",
"items": [
{
"line_no": 1,
"sku": "@TR1305805-A007BU-66/j",
"name": "customize personalize name baby romper",
"color": "Blue",
"size": "6 month",
"quantity": 1,
"unit_price": 3.3,
"line_total": 3.3
}
]
}
}
POD also pulls orders automatically every ~5 minutes:
getShopReceipts.partner_orders(partner_id, partner_order_id) is UNIQUE.POST /orders returns the original order (HTTP 200,If the prepaid balance is insufficient, the order is not created at all (no unpaid
order): the API returns 402 insufficient_balance, and the partner receives an
in-site message + email (at most once per 12 hours). After topping up, the next
automatic pull imports the pending order.
POST /sandbox/orders/{order_no}/advance
Request:
{ "status": "in_production" }
Response (same shape as GET /orders/{order_no}):
{
"ok": true,
"data": {
"ok": true,
"sandbox": true,
"order_no": "PDT261103015002772",
"partner_order_id": "99P-TEST-0001",
"status": "shipped",
"currency": "USD",
"charges": { "goods_usd": 3.3, "shipping_usd": 0.9, "total_usd": 4.2 },
"tracking_no": "SF123456789",
"carrier": "SF Express",
"created_at": "2026-11-03 01:50:02",
"items": []
}
}
Allowed transitions:
| From | To |
|---|---|
paid | in_production, cancelled |
in_production | shipped, cancelled |
shipped | delivered, refunded |
delivered | refunded |
GET /balance
Test token:
{
"ok": true,
"sandbox": true,
"currency": "USD",
"balance_usd": 1000,
"pending_balance_usd": 0,
"transactions": []
}
Live token returns the real balance and the last 20 wallet transactions.
In account/dropship → Shops, each site has its own Order webhook URL and signing secret (separate from its Link push URL). Orders can pass shop_id; with one shop it is inferred automatically. A partner-level fallback webhook can still be configured by our team.
Events:
order.paidorder.in_productionorder.shipped (with tracking_no)order.deliveredorder.cancelledorder.refundedping (test button)X-Partner-Event: order.shipped
X-Partner-Delivery: evt_...
X-Partner-Timestamp: 1793672738
X-Partner-Signature: sha256=<hex>
Signature:
expected = "sha256=" + hex(HMAC-SHA256(secret, timestamp + "." + rawBody))
Respond 2xx quickly. Failed deliveries retry at 1m / 5m / 30m / 2h / 6h / 24h (max 6 attempts), then stop.
order.paid is queued when a live order is created. Later statuses are enqueued immediately when the order status changes (shipped / received / cancelled / refunded); the partner:sync-events cron (every 5 minutes) remains a safety net. All events are delivered by partner:webhooks (every minute); sandbox status events are queued by advance. You can watch the queue from Dropship → Sync queue.
Your unit price follows the same volume tiers as our website (1 / 5 / 50 / 100 / 1,000 pieces).
Tiers are tracked per link, exactly like our buyer checkout: all colours and sizes of one link combine, but different links do not.
Your tier for a link is selected by that link's rolling 12-month order quantity:
| 12-month units for that link | Tier used |
|---|---|
| 0–4 | 1-piece tier |
| 5–49 | 5-piece tier |
| 50–99 | 50-piece tier |
| 100–999 | 100-piece tier |
| 1,000+ | 1,000-piece tier |
Sandbox orders do not update the 12-month quantity.
We ship to your domestic consolidation warehouse (Shenzhen or any other city).
| Item | Rate |
|---|---|
| First 1 kg | CNY 6.00 |
| Additional weight | CNY 2.50 per kg (rounded up) |
charges.shipping_usdpostcode and consolidation_codeWe ship directly to the end customer's receipt address (for example an Etsy order address).
recipient is the end customer's international address; no consolidation code.charges.shipping_usd includes the international freight; charges.tax_mode is ddu or ddp.shipping_unavailable.{
"ok": false,
"error": {
"code": "missing_required_region",
"message": "Region \"r21\" is required (line)."
}
}
| HTTP | Meaning |
|---|---|
| 400/422 | Validation error |
| 401 | Missing/invalid/revoked/expired token |
| 403 | Missing scope; not selected |
| 404 | Link/SKU/order/asset not found |
| 409 | Idempotency in progress / invalid transition |
| 429 | Rate limited |
| 500/503 | Server or upstream unavailable |
pd_test_... token.GET /ping and GET /whoami.GET /catalog/links?scope=available.POST /catalog/links/select with the sample link.GET /catalog/links/{id} and build your product page/editor.POST /assets with a customer image.POST /orders with Idempotency-Key.POST /sandbox/orders/{order_no}/advance to simulate production/shipping.GET /orders/{order_no} to poll.| Object | Space | Origin |
|---|---|---|
Region left/top/width/height | 1000 × 1000 | top-left |
Template clip | templogo pixels | top-left |
Template clip_normalized | 1000 × 1000 | top-left |
Placement clip_x/clip_y | 1000 × 1000 | centre |
left = clip_x - clip_width / 2
top = clip_y - clip_height / 2
full_bleed = true (template xy_ratio != 0): centre-crop the asset to clip_ratio, then fill the clip box. No letterboxing.full_bleed = false: fit the whole asset inside the box preserving aspect ratio (contain), no crop.mask_url is the printable mask. Outside the mask is not printed.imgMinX × imgMinY for the target image regionasset_ids from the same partner and modeDropship → Shops. Polling (GET /orders/{order_no}) remains available as a fallback.getShopReceipts; independent sites via Orders pull URL (X-POD-Secret). The Pull orders button does the same on demand. Orders are de-duplicated by partner_order_id; failed imports are retried on the next pull.402 insufficient_balance, and the partner gets an in-site message + email (≤ once per 12 hours). Topping up makes the next pull import it.order.in_production / shipped / delivered / cancelled / refunded immediately on status change; Etsy gets tracking back automatically when the order is marked shipped.in_stock is provided; stock stays null. We do not expose real inventory levels — assume default availability unless in_stock is false.CN. Direct orders accept international end-customer addresses.For integration questions, contact your account manager. Include the X-Partner-Request-Id response header when reporting an error.
Audience: partners who want us to push selected links to their own site.
Direction: POD Sourcing → your site.
In your buyer account → Dropship, you select links and click Publish selected.
We send the full link JSON to the HTTPS endpoint you configure. Your site creates or updates the product.
Orders still flow the other way: your site calls our order API (POST /api/v1/orders).
In Dropship → Shops, add:
My sitehttps://your-site.com/api/pod/links/importPOST https://your-site.com/api/pod/links/import
Content-Type: application/json
X-POD-Signature: sha256=<hex>
X-POD-Timestamp: <unix seconds>
X-POD-Delivery: <random id>
signed = timestamp + "." + raw_request_body
expected = "sha256=" + hex(HMAC-SHA256(secret, signed))
Use the raw request body bytes, not a re-encoded JSON object.
{
"action": "ping",
"partner_id": 12,
"shop_id": 3,
"time": "2026-11-03T10:00:00+00:00"
}
Respond 2xx with {"ok":true}.
{
"action": "upsert",
"version": "ddc3fa23380e3ca5",
"partner_id": 12,
"shop_id": 3,
"link": {
"link_id": 80,
"title": "customize personalize name baby romper",
"cover": "https://.../composite_80.jpg",
"currency": "USD",
"customizable": true,
"items": [
{
"product_id": 80,
"title": "customize personalize name baby romper",
"cover": "https://.../composite_80.jpg",
"gallery": ["https://.../composite_80.jpg"],
"template": { "id": 836, "craft": "TH", "size_cm": { "w": 15, "h": 15 }, "..." : "..." },
"colors": [
{
"color_id": 830,
"name": "Blue",
"mockup": "https://.../composite_80.jpg",
"regions": [
{ "id": "r21", "type": "text", "left": 381, "top": 465, "width": 148, "height": 108, "fontMaxLen": 20, "required": true },
{ "id": "r22", "type": "image", "left": 363, "top": 292, "width": 196, "height": 167, "imgMinX": 1000, "imgMinY": 1000, "required": true }
],
"sizes": [
{
"size_id": 1020,
"sku_id": 2601,
"name": "6 month",
"variant_id": "80:2601",
"sku": "@TR1305805-A007BU-66/j",
"raw_sku": "A007BU-66",
"price_usd": 3.30,
"in_stock": true,
"weight_g": 52
}
],
"placement": { "clip_x": 455, "clip_y": 440, "clip_width": 358, "clip_height": 291, "unit": "canvas1000_center", "mask_url": "https://.../mask.png" }
}
]
}
]
}
}
Rules:
version changes when any field changes. If your stored version is the same, you may skip the update.action: upsert is idempotent: use link_id + version as your key.link_id is our product group ID; product_id is the item inside it.{
"action": "delete",
"version": "ddc3fa23380e3ca5",
"partner_id": 12,
"shop_id": 3,
"link_id": 80
}
Remove or unpublish the product from your site. Return 2xx.
{
"ok": true,
"external_id": "your-product-123"
}
2xx for success.external_id is optional but recommended; we store it so support can trace the link.Link pushes are not retried automatically. A non-2xx response marks the job
failed immediately, and the partner receives an in-site message with the
error. Fix the endpoint / secret, then press Retry on the failed job in
Dropship → Sync queue.
Order webhooks are different: they retry at 1 min → 5 min → 30 min → 2 h →
6 h → 24 h (max 6 attempts).
|now - X-POD-Timestamp| > 300 seconds.X-POD-Signature with a constant-time compare.X-POD-Delivery.$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_POD_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_POD_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (abs(time() - (int) $ts) > 300 || ! hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$payload = json_decode($raw, true);
// handle action: ping / upsert / delete
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'external_id' => $externalId]);
ping → return {ok:true}.upsert → create or update the product using link_id + version.delete → remove/unpublish by link_id.2xx quickly; store external_id if you have one.version may arrive twice after a retry.A site can also sync orders in both directions. Both are triggered from the Pull orders / Push status buttons on each site, and the status side also runs on a cron.
If you set Orders pull URL on the site, POD calls it automatically every ~5
minutes (and immediately when the partner clicks Pull orders). Failed imports are
retried on the next pull; already-imported orders are de-duplicated by
partner_order_id:
GET <pull_url>?since=2026-11-03T00:00:00Z&shop_id=12&limit=50
X-POD-Secret: <the same push secret>
Accept: application/json
Return 200 with:
{
"orders": [
{
"partner_order_id": "99P-0001",
"lines": [{ "sku": "TEE-WHT-M", "quantity": 1, "values": { "r21": "LUCA" } }],
"recipient": { "name": "...", "phone": "...", "address": "...", "country_code": "US", "city": "...", "state": "...", "postcode": "..." },
"note": "optional"
}
]
}
orders[] uses the same body as POST /api/v1/orders. Include only orders that are paid and not yet fulfilled. POD deduplicates by partner_order_id, so returning the same order twice is safe. Invalid orders are skipped and shown in the site's Last sync error.
Etsy sites do not need pull_url: POD reads paid receipts from the Etsy API using the OAuth connection.
Set an Order webhook URL on the site. POD pushes order.paid,
order.in_production, order.shipped, order.delivered, order.cancelled
and order.refunded.
Headers (⚠️ these are **X-Partner-***, not the link-push X-POD-* headers):
X-Partner-Event: order.shipped
X-Partner-Delivery: evt_...
X-Partner-Timestamp: 1793672738
X-Partner-Signature: sha256=<hex(HMAC-SHA256(secret, timestamp + "." + rawBody))>
The secret is the site's Order webhook secret (separate from the link-push
secret). Body:
{
"id": "evt_...",
"event": "order.shipped",
"created_at": "2026-11-03T10:00:00+00:00",
"data": { "order_no": "...", "status": "shipped", "tracking_no": "..." }
}
Respond 2xx quickly. Status changes are enqueued immediately (shipped,
received, cancelled, refunded, and waybill booking → in_production);
partner:sync-events (every 5 minutes) remains a safety net. partner:webhooks
delivers within about a minute.
Dropship → Shops.{ok:true}.action: upsert and verify the signature.success.action: delete.Leave a name and at least one way to reach you — email or WhatsApp. Without one we have no way to reply if nobody is online right now.