Guide · Developers

A fulfillment API is judged by its failure modes

A production fulfillment integration needs five properties: idempotent writes with stored-response replay, HMAC-signed webhooks carrying immutable event IDs, estimates and packed quotes modelled as separate objects, inventory read from an immutable movement ledger, and tracking normalized across the China leg and the international carrier. Miss one and the failure mode is a physical parcel, not a database row.

Integrating China-leg fulfillment into a storefront, OMS or agent runtime is not a CRUD exercise: a retried request can ship a duplicate parcel, an unverified webhook can corrupt order state, and an estimate mistaken for a bill breaks trust with real money. Merchant API V1 (45 operations, 12 webhook events, 22 MCP tools) is the worked example throughout.

Last updated: 2026-08-01 · Reviewed by Alice Zhou

The five properties that matter

Idempotent writes with stored-response replay

Order pre-alerts carry an external order ID; retrying the same creation returns the original result. This is what makes network failures boring instead of expensive.

Signed events you can trust and deduplicate

Webhooks are HMAC-SHA256-signed over the raw body, carry immutable event IDs, and are delivered at-least-once. Verify, dedupe, then process — in that order.

Estimates and final quotes as separate objects

The API never lets a pre-measurement estimate masquerade as a bill. The packed quote is produced after measurement and is what settlement reconciles against, line by line.

Ledger-computed inventory

Stock is a fold over immutable movements, not a mutable number. Your reconciliation job can replay the ledger and must land on the same statement the platform produces.

Normalized tracking with re-registration

China-leg events and international carrier events merge into one timeline; when a carrier reissues a tracking number mid-transit, it is re-registered instead of going dark.

Eight failure modes, and what the contract does about each

Diff your own implementation against the contract behaviour for each failure. Every one of them happens in production rather than in theory, and each has exactly one correct response.

ScenarioWhat a naive integration doesWhat the contract does instead
Order creation times out and you retryCreates a second order. A duplicate physical shipment goes to a real customer, and you find out from the settlement.The retry carries the same Idempotency-Key and external order id, and replays the stored response. A conflicting body with a used key is refused outright rather than silently accepted.
Two workers retry the same request concurrentlyBoth proceed. Whichever wins, the loser’s result is lost or duplicated.A matching idempotent request that still owns the processing lease is refused with a distinct code, so the caller retries after a short delay with the same key and body.
A webhook is delivered twiceOrder state is written twice. Anything non-idempotent downstream — a customer email, a ledger entry — fires twice.Delivery is at-least-once by design and every event carries an immutable event ID. Verify the signature, deduplicate on the ID, then process — in that order.
A webhook body is tampered with in transitParsed and trusted, because the JSON looked fine.HMAC-SHA256 is computed over the raw request body and compared before any parsing. Mismatches are rejected, not logged and processed.
An estimate is stored as the priceThe customer is charged a number produced before the goods were measured, and the settlement disagrees with it.Estimate and packed quote are distinct objects. The packed quote is produced after measurement and is what the merchant approves; settlement reconciles against it line by line.
Inventory is cached as a counterThe counter and reality drift, and nothing in the system can tell you when they diverged.Stock is a fold over immutable movements. Your reconciliation job can replay the ledger and must land on the same statement the platform produces.
A carrier reissues a tracking number mid-transitThe shipment goes dark and support gets a ticket.The new number is re-registered against the same order, and China-leg plus carrier events stay in one merged timeline.
The wallet balance is short when work is createdThe request fails and your queue treats it as a permanent error.A distinct payment-required response names an insufficient available balance. The order is held pending top-up rather than rejected, and shipments already in flight are never held.

The 12 webhook events, and what your receiver should do

Delivery is at-least-once and every event carries an immutable event ID. Verify the HMAC over the raw body, deduplicate on the event ID, then act. The receiver behaviour set out for each event assumes the first two steps have already happened.

EventWhat it meansWhat your receiver should do
parcel.receivedA domestic supplier parcel was scanned and recorded at the warehouse.Mark the supplier parcel as arrived and surface the intake evidence to the merchant. Safe to receive more than once per parcel.
inspection.completedAn inspection you requested finished.Attach the inspection outcome to the order. Do not treat it as permission to dispatch.
quote.readyPacking and measurement produced the final packed quote; the order moved to payment.The binding number now exists. Stop using any stored estimate for this order and route the quote to whoever approves it.
payment.completedThe shipment settlement was paid; the order moved on to dispatch.Settlement is paid. Reconcile the line items against the packed quote before writing the order to your ledger.
shipment.dispatchedCarrier events confirm the shipment entered the provider network.Push tracking to your storefront. Expect the tracking number to be re-registered if the carrier reissues it.
tracking.updatedA normalized WooliiPorter parcel tracking fact changed. The payload contains public shipment and parcel identifiers only.Reserved for future granular pushes — poll the tracking endpoint today rather than depending on this event.
shipment.exceptionAn exception was recorded against the shipment.Raise it to a human. This is the event most worth alerting on rather than logging.
shipment.deliveredEvery outbound package on the order has a delivered carrier fact.Every outbound package on the order has a delivered carrier fact. Safe to close the order.
inbound.exception.openedWooliiPorter opened an inbound receiving review. The payload contains the IB number and customer-safe exception projection only.Verify, deduplicate on the event ID, then apply idempotently.
inbound.exception.updatedThe public state, customer-action requirement or resolution of an inbound receiving review changed.Verify, deduplicate on the event ID, then apply idempotently.
cancellation.updatedThe interception state of a cancellation request changed — warehouse stop, stock return, review, final cancellation or rejection. The payload mirrors the cancellation object returned by GET fulfillment intent; once dispatched, orders can no longer be cancelled and the request resolves through review instead.Verify, deduplicate on the event ID, then apply idempotently.
account.balance_lowThe merchant wallet is below the operating threshold. This account-level event has no order identity.Verify, deduplicate on the event ID, then apply idempotently.

An endpoint is created with the event types it wants and returns a signing secret once — it is never returned again. Disabling an endpoint keeps its delivery ledger, because the ledger is evidence. Accounts hold up to five active endpoints. Full parameter and signature detail lives in the Merchant API reference.

A minimal integration checklist

  • Store your external order ID with every creation call and treat retries as replays, never as new orders
  • Verify webhook HMAC over the raw body before JSON parsing; reject on mismatch; dedupe on event ID
  • Model estimate and packed quote as different types in your system, with approval gating dispatch
  • Reconcile settlements line-by-line against the packed quote and alert on unexplained lines instead of averaging them away
  • For stocked SKUs, sync from the movement ledger, not from point-in-time counters
  • Start against a sandbox key after approval; use production only after workflow validation and applicable account terms

Full reference — every operation, parameter, error shape and webhook event — lives in the Merchant API documentation. Store-side WooCommerce integration is covered separately on the plugin page.

Common questions

Why is idempotency non-negotiable for fulfillment APIs?

Because the failure mode is a duplicate physical shipment, not a duplicate database row. A timeout on order creation must be safely retryable: with stored-response replay, the retry returns the original response instead of creating a second order that ships real goods to a real customer.

How should webhook authenticity be verified?

Compute an HMAC-SHA256 over the raw request body with your endpoint secret and compare it to the signature header — before parsing the JSON. Combine that with immutable event IDs and at-least-once delivery semantics: your receiver deduplicates on the event ID, so replays and retries are harmless.

What is the difference between an estimate and the packed quote?

An estimate is computed before goods are measured; the packed quote is generated after packing from real dimensions and weight against the live rate table, and is the number the merchant approves before dispatch. A correct integration never presents an estimate as a final price — the API keeps them as distinct objects.

How do I keep my system’s inventory truthful?

Read inventory that is computed from an immutable stock-movement ledger rather than a mutable counter — the same ledger that produces the monthly statement. Every receive, reserve, dispatch, withdrawal and adjustment is a movement; your integration can re-derive state at any point in time.

How do I get credentials?

Account-first and approval-gated: create a merchant login and submit the integration application in-account. Approval creates and activates the merchant account, then opens sandbox and production key management. There is no public unauthenticated API, and no agent or third party can issue keys on your behalf.

What should my error handling distinguish between?

Three classes, because they need three different behaviours. Retry-with-backoff: rate limiting and the in-progress idempotency lease. Retry-after-action: an insufficient wallet balance, which needs a top-up rather than a retry loop. Do-not-retry: validation failures, scope and auth problems, and state conflicts such as paying before the packed quote exists. Treating all non-2xx responses identically is how a queue melts down on a 402.

What happens if the merchant’s wallet balance is short?

Creating new warehouse work returns a distinct payment-required response naming the shortfall, and the order is held pending top-up rather than rejected. Shipments already in flight are never held for a balance question. Your integration should surface this to the merchant as an action rather than retrying it as a transient failure.

Is there an MCP option instead of raw HTTP?

Yes — a standalone MCP server exposes the supported subset as typed tools for AI agents (estimates, idempotent orders, value-added services, quotes, tracking, settlement, stocking, fulfillment). Same authentication model, same human-approval gates on packed quotes and mixed orders.

Build against the sandbox this week.

Account-first: signup, in-account application, approval, then API-key management.