Pod-Sourcing
Log In Register Chat Cart Orders
Home Cart Orders My Goods My Link My Logos
How can we help?
WhatsApp WeChat Email Chat with us
Dropship Program

Dropship API documentation

View online, or download one Markdown file to feed to an AI assistant.

Partner API v1 — Technical Documentation

Base URL: https://www.pod-sourcing.com/api/v1
Version: 1.0 · 2026-11-03
Audience: approved dropshipping partners (independent sites)


1. What this API does

  • Sync our product links (with print templates and personalization regions) to your site.
  • Upload customer images.
  • Create orders with custom text/images.
  • Read order status and tracking.
  • (Sandbox) simulate production/shipping transitions without charges or production.
You build your own product pages and editor. We return structured JSON; there is no iframe/embed.

1.1 Fulfillment modes

Your partner account has one of two modes:

ModeWe ship toShipping costTypical partner
consolidationYour domestic consolidation warehouse (Shenzhen or another city)CNY 6 first kg + CNY 2.50/kgIndependent site / 集运
directThe end customer's receipt address (international)Live Yanwen quote; channel and DDP/DDU configured by salesEtsy / direct-to-consumer
The mode is set by our sales team when your account is approved. In the application form, **filling a consolidation warehouse address selects consolidation; leaving it empty selects direct**.

2. Access and authentication

2.1 Apply and tokens

Ask your account manager to open a partner account. Approval does not issue a token automatically.

After approval, open account/dropship → API tokens:

  • Create test token — pd_test_... (sandbox; no production, no charges)
  • Create live token — pd_live_... (production; use after the sandbox pilot passes)
Each token can be **bound to a shop**. Orders created with that token default to that shop; sending a different `shop_id` is rejected. A token created for **All shops** (legacy) applies to every shop. This lets you keep one live token for an existing shop and a separate test token for a new shop.

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.

2.2 Send the token

Authorization: Bearer pd_test_xxxxxxxxxxxxxxxx

Every request must include it. Tokens must not be placed in query strings or request bodies.

2.3 Scopes

ScopeWhat it allows
catalog:readList/read links
catalog:selectAdd/remove links from your catalogue
assets:writeUpload/read customer images
orders:writeCreate orders
orders:readRead order status
balance:readRead balance
sandbox:simulateAdvance sandbox order status
A missing scope returns `403 forbidden_scope`.

2.4 Rate limits

120 requests/minute per token by default. Over the limit returns 429 with Retry-After: 60.


3. First call

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
}

4. Catalogue

4.1 List links

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 dropshipping
  • scope=selected — links you added to your catalogue (default)
  • since — only rows updated after this ISO-8601 time
  • ids — optional comma-separated filter
  • page / limit (max 50)
Response:
{
  "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"
}

4.2 Read one link

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:

  • Link level: price_from_usd / price_to_usd — our wholesale price range (your cost).
  • Item level: price_usd / price_max_usd and per-variant price_usd.
  • Each item also carries 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.
**Important:** prices are indicative. The authoritative amount is recalculated when the order is created.

4.3 Select / unselect

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.


5. Images

5.1 Upload

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..."
}
  • Accepted: JPEG, PNG, WebP
  • Max upload: 12 MB; stored near 1 MB
  • Minimum long edge: 300 px
  • Order validation is stricter: each image region checks imgMinX / imgMinY.

5.2 Read

GET /assets/{asset_id}
Authorization: Bearer ...

Returns the binary image. Assets are scoped to the same partner and mode (test/live).


6. Orders

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.

6.1 Create

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).
  • 1–50 lines; each quantity 1–999; total ≤ 5000.
  • Prefer 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.
  • Per-piece content: use pieces: [{seq, values}]; count must equal quantity.
  • Recipient rules depend on your fulfillment mode (see §1.1):
- `consolidation`: `country_code: CN`, `postcode` and `consolidation_code` required. - `direct`: the end customer's international address (`country_code`, `name`, `phone`, `address`); no `consolidation_code`.
  • The mode is configured by our sales team on your partner account.
  • Optional 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.
  • HS code is not an input. We derive it from the product's underlying blank record (the same one your link's SKUs point to) for customs/PI. Partners do not send HS codes.
  • Stock is managed internally; the API does not accept inventory changes. Treat in_stock from the catalogue as authoritative.
Response:
{
  "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.

6.2 Read

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
      }
    ]
  }
}

6.3 Automatic order pull & de-duplication

POD also pulls orders automatically every ~5 minutes:

  • Etsy shops: POD reads paid, not-yet-shipped receipts via getShopReceipts.
  • Independent sites with an Orders pull URL: POD calls it every run.
  • The manual Pull orders button performs the same pull immediately.
Whatever path creates the order (automatic pull or `POST /orders`), POD de-duplicates on **`partner_order_id`**:
  • partner_orders(partner_id, partner_order_id) is UNIQUE.
  • If the order already exists, POST /orders returns the original order (HTTP 200,
replay) instead of creating a second one — even with a different `Idempotency-Key`.
  • Successful imports are skipped on later pulls; **failed imports are retried on the
next pull** (the sync cursor is not advanced while a pull has errors).

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.

6.4 Simulate status (sandbox only)

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:

FromTo
paidin_production, cancelled
in_productionshipped, cancelled
shippeddelivered, refunded
deliveredrefunded
Invalid transitions return `409 invalid_transition`.

7. Balance

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.


7.5 Order webhooks

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.paid
  • order.in_production
  • order.shipped (with tracking_no)
  • order.delivered
  • order.cancelled
  • order.refunded
  • ping (test button)
Headers:
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.


8. Pricing

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 linkTier used
0–41-piece tier
5–495-piece tier
50–9950-piece tier
100–999100-piece tier
1,000+1,000-piece tier
The effective tier is `max(that link's historical tier, this order's quantity tier for the link)`. The order total is always recalculated by us; the catalogue price is for display only.

Sandbox orders do not update the 12-month quantity.


9. Shipping

9.1 Consolidation mode

We ship to your domestic consolidation warehouse (Shenzhen or any other city).

ItemRate
First 1 kgCNY 6.00
Additional weightCNY 2.50 per kg (rounded up)
  • Charged per order
  • Converted to USD at the current rate
  • Included in charges.shipping_usd
  • Recipient must include postcode and consolidation_code

9.2 Direct mode

We ship directly to the end customer's receipt address (for example an Etsy order address).

  • Shipping is quoted live from our carrier network (Yanwen), using the channel and tax mode configured by our sales team on your partner account.
  • 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.
  • If no channel can quote the destination, the order is rejected with shipping_unavailable.

10. Errors

{
  "ok": false,
  "error": {
    "code": "missing_required_region",
    "message": "Region \"r21\" is required (line)."
  }
}
HTTPMeaning
400/422Validation error
401Missing/invalid/revoked/expired token
403Missing scope; not selected
404Link/SKU/order/asset not found
409Idempotency in progress / invalid transition
429Rate limited
500/503Server or upstream unavailable
Common codes: `invalid_token`, `token_revoked`, `forbidden_scope`, `link_not_found`, `sku_not_found`, `sku_not_selected`, `ambiguous_sku`, `not_customizable`, `missing_required_region`, `text_too_long`, `asset_too_small`, `invalid_asset`, `invalid_recipient`, `unsupported_destination`, `insufficient_balance`, `idempotency_key_reuse`, `idempotency_in_progress`, `invalid_transition`.

11. Sandbox testing flow

  1. Receive a pd_test_... token.
  2. GET /ping and GET /whoami.
  3. GET /catalog/links?scope=available.
  4. POST /catalog/links/select with the sample link.
  5. GET /catalog/links/{id} and build your product page/editor.
  6. POST /assets with a customer image.
  7. POST /orders with Idempotency-Key.
  8. POST /sandbox/orders/{order_no}/advance to simulate production/shipping.
  9. GET /orders/{order_no} to poll.
**Sandbox never charges, produces, ships, or creates a real order.**

12. Preview parity — matching preview to print

12.1 Coordinate systems

ObjectSpaceOrigin
Region left/top/width/height1000 × 1000top-left
Template cliptemplogo pixelstop-left
Template clip_normalized1000 × 1000top-left
Placement clip_x/clip_y1000 × 1000centre
To convert a placement to top-left:
left = clip_x - clip_width / 2
top  = clip_y - clip_height / 2

12.2 Crop and mask

  • 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.
  • Image regions use the uploaded asset as-is; the server does not re-crop at order time.
  • Text regions: the contract guarantees the box, rotation, label, colour and length limits, but not font family, size or glyph rendering.

12.3 Text and Hebrew

  • If you render the final text into an image on your side and upload that image, preview and print use the same pixels.
  • If you send raw text, our production team prepares the print file; the final typography may differ slightly from your preview.
  • For the first orders, we can send a print proof for approval before production.

12.4 Asset rules

  • JPEG/PNG/WebP
  • At least imgMinX × imgMinY for the target image region
  • sRGB
  • No external URLs; only asset_ids from the same partner and mode

13. Current limitations

  • Live orders are enabled — they debit the prepaid balance and create a real order. Our admin still books the waybill and pushes to production; the API does not do that automatically.
  • Webhooks are enabled — each site has its own order webhook URL in Dropship → Shops. Polling (GET /orders/{order_no}) remains available as a fallback.
  • Orders are pulled automatically every ~5 minutes — Etsy via OAuth 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.
  • Insufficient balance — no order is created (no unpaid record); the API returns 402 insufficient_balance, and the partner gets an in-site message + email (≤ once per 12 hours). Topping up makes the next pull import it.
  • Fulfillment status — POD pushes order.in_production / shipped / delivered / cancelled / refunded immediately on status change; Etsy gets tracking back automatically when the order is marked shipped.
  • Stock is boolean and managed by us. in_stock is provided; stock stays null. We do not expose real inventory levels — assume default availability unless in_stock is false.
  • HS code is derived from our blank/product record, never supplied by the partner. It is used on the PI and customs documents.
  • Consolidation addresses must be CN. Direct orders accept international end-customer addresses.

14. Support

For integration questions, contact your account manager. Include the X-Partner-Request-Id response header when reporting an error.


*POD Sourcing · JIANGKAO TRADING CO., LIMITED*

Partner Link Push Contract (v1)

Audience: partners who want us to push selected links to their own site.
Direction: POD Sourcing → your site.


1. What this is

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


2. Configure the site

In Dropship → Shops, add:

  • Name — e.g. My site
  • Push endpoint — a public HTTPS URL, e.g. https://your-site.com/api/pod/links/import
  • Secret — a shared secret used for HMAC signing
We reject non-HTTPS URLs, private IPs and non-443 ports.

3. Request

POST 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>

3.1 Signature

signed = timestamp + "." + raw_request_body
expected = "sha256=" + hex(HMAC-SHA256(secret, signed))

Use the raw request body bytes, not a re-encoded JSON object.

3.2 Ping

{
  "action": "ping",
  "partner_id": 12,
  "shop_id": 3,
  "time": "2026-11-03T10:00:00+00:00"
}

Respond 2xx with {"ok":true}.

3.3 Upsert (create or update a product)

{
  "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.

3.4 Delete

{
  "action": "delete",
  "version": "ddc3fa23380e3ca5",
  "partner_id": 12,
  "shop_id": 3,
  "link_id": 80
}

Remove or unpublish the product from your site. Return 2xx.


4. Response

{
  "ok": true,
  "external_id": "your-product-123"
}
  • Return 2xx for success.
  • external_id is optional but recommended; we store it so support can trace the link.
  • Return a non-2xx status to trigger retry.
  • Respond quickly (under ~5 seconds). Queue heavy work on your side.

5. Retry

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


6. Security requirements

  • HTTPS only, public host, port 443.
  • Reject requests with |now - X-POD-Timestamp| > 300 seconds.
  • Verify X-POD-Signature with a constant-time compare.
  • Use the raw body for the HMAC.
  • You may deduplicate on X-POD-Delivery.

PHP verification example

$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]);

7. Receiver responsibilities

  1. Verify the signature and timestamp.
  2. ping → return {ok:true}.
  3. upsert → create or update the product using link_id + version.
  4. delete → remove/unpublish by link_id.
  5. Return 2xx quickly; store external_id if you have one.
  6. Be idempotent: the same version may arrive twice after a retry.

8. Orders (optional, two-way)

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.

8.1 Pull paid orders (your site → POD)

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.

8.2 Status webhook (POD → your site)

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.


9. Testing

  1. Add the site in Dropship → Shops.
  2. Click Ping; it must return {ok:true}.
  3. Tick a link and click Publish selected.
  4. Check your receiver logs for action: upsert and verify the signature.
  5. Open Sync queue in the Dropship page; the link job should be success.
  6. Click Unpublish to send action: delete.
If a job is `failed`, the last error is shown in the job table.
*POD Sourcing · JIANGKAO TRADING CO., LIMITED*