Fulfillments API
Last Updated: 2026-07-02
List Fulfillments
/open/v1/fulfillmentsScope: fulfillment:readReturns a paginated list of fulfillment records for the seller bound to your access token. A fulfillment represents one shipment of one or more items from an order; an order can have multiple fulfillments when it ships in several packages. You can filter by status or parent order, choose a sort direction, and walk large result sets efficiently with cursor pagination.
Only fulfillments belonging to your seller are ever returned — the seller scope is enforced server-side from your access token and cannot be overridden by any query parameter.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | No | Filter by fulfillment status. One of pending, shipped, delivered, cancelled. Any other value returns VALIDATION_ERROR (HTTP 400). |
| order_id | string | No | Restrict the list to fulfillments of a single order ID. |
| page | integer | No | 1-based page number for offset pagination. Ignored when cursor is supplied. Default 1. |
| limit | integer | No | Page size. Default 20. |
| sort | string | No | Sort direction by created timestamp: created_at_desc (newest first, default) or created_at_asc. Any other value falls back to the default. |
| cursor | string | No | Opaque keyset cursor returned as meta.nextCursor. When present, switches to cursor pagination: page and sort are ignored, and the response meta omits page/total. A malformed cursor returns VALIDATION_ERROR (HTTP 400). |
Example Response — offset pagination (default)
{
"success": true,
"data": [
{
"id": "ful_01J9WX4K2Q",
"status": "shipped",
"orderId": "order_01J9MV7H8S",
"trackingNumber": "PHJT20260416000123",
"trackingCompany": "J&T Express",
"created_at": "2026-04-16T08:00:00Z",
"updated_at": "2026-04-16T08:05:00Z",
"items": [
{ "itemId": "item_01J9MV7H8S", "quantity": 2 }
]
}
],
"meta": { "page": 1, "limit": 20, "total": 1 }
}Example Response — cursor pagination
{
"success": true,
"data": [ /* …same Fulfillment objects… */ ],
"meta": {
"limit": 50,
"nextCursor": "eyJ0cyI6IjIwMjYtMDQtMTZU..."
}
}Pagination modes: omit cursor for classic page/limit/total offset paging. To walk a very large set without counting rows, pass the previous meta.nextCursor back as ?cursor=; stop when nextCursor is absent.
Get Fulfillment
/open/v1/fulfillments/:idScope: fulfillment:read Returns a single fulfillment by ID. Use the id returned from the list endpoint or from a fulfillment.* webhook payload.
The returned object is the partner-safe projection described in Fulfillment Object — carrier-internal fields (e.g. J&T mailno, bill_code, sortingcode) are stripped at the API boundary.
Example Response
{
"success": true,
"data": {
"id": "ful_01J9WX4K2Q",
"status": "shipped",
"orderId": "order_01J9MV7H8S",
"trackingNumber": "PHJT20260416000123",
"trackingCompany": "J&T Express",
"created_at": "2026-04-16T08:00:00Z",
"updated_at": "2026-04-16T08:05:00Z",
"items": [
{ "itemId": "item_01J9MV7H8S", "quantity": 2 }
]
}
}Not Found
Returns NOT_FOUND (HTTP 404) when the fulfillment does not exist or belongs to a different seller. A 404 (not 403) is returned for cross-seller IDs so that the existence of another seller's fulfillment cannot be inferred.
{
"success": false,
"error": { "code": "NOT_FOUND", "message": "Fulfillment not found" }
}Create Fulfillment
/open/v1/fulfillmentsScope: fulfillment:writeCreate a new fulfillment for an order you own. Specify which items are being fulfilled and optionally include tracking information. Supports partial fulfillment — you can fulfill different items in separate shipments.
The request body is validated strictly; unknown keys return VALIDATION_ERROR (HTTP 400). If the parent order does not exist or belongs to another seller, the endpoint returns NOT_FOUND (HTTP 404) for the order — it does not leak the order's existence.
This endpoint is idempotent: send an Idempotency-Key header to safely retry after a network failure without creating duplicate fulfillments.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| orderId | string | Yes | The order to create the fulfillment for. Must belong to your seller. Max 64 chars. |
| items | array | Yes | Array of { itemId, quantity } objects (1–100 items, each quantity 1–1,000,000). |
| trackingNumber | string | No | Carrier tracking number. Max 120 chars. |
| trackingCompany | string | No | Carrier / shipping company name. Max 80 chars. |
Example Request
curl -X POST "https://api.mallplus.com/open/v1/fulfillments" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f3c9e2a-1b4d-4c8a-9e2f-1234567890ab" \
-d '{
"orderId": "order_01J9MV7H8S",
"items": [{ "itemId": "item_01J9MV7H8S", "quantity": 1 }],
"trackingNumber": "PHJT20260416000123",
"trackingCompany": "J&T Express"
}'Example Response (HTTP 201)
{
"success": true,
"data": {
"id": "ful_01J9WX4K2Q",
"status": "pending",
"orderId": "order_01J9MV7H8S",
"trackingNumber": "PHJT20260416000123",
"trackingCompany": "J&T Express",
"created_at": "2026-04-16T08:00:00Z",
"items": [{ "itemId": "item_01J9MV7H8S", "quantity": 1 }]
}
}Webhook: a successful create fires fulfillment.created.
Update Fulfillment
/open/v1/fulfillments/:idScope: fulfillment:write Update the status and/or tracking information of an existing fulfillment. Use this to mark a fulfillment as shipped, delivered, or cancelled, and to add or update tracking details. All body fields are optional; send only the ones you want to change.
As with the other endpoints, cross-seller writes return NOT_FOUND (HTTP 404), and the body is validated strictly.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | No | One of pending, shipped, delivered, cancelled. |
| trackingNumber | string | No | Updated carrier tracking number. Max 120 chars. |
| trackingCompany | string | No | Updated carrier / shipping company name. Max 80 chars. |
Example Request
curl -X PUT "https://api.mallplus.com/open/v1/fulfillments/ful_01J9WX4K2Q" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"status": "shipped",
"trackingNumber": "PHJT20260416000123",
"trackingCompany": "J&T Express"
}'Webhooks: every successful update fires fulfillment.updated. When the status field on the updated fulfillment is shipped, a separate fulfillment.shipped event is also fired so partners can listen for the shipping-specific signal (mirrors the shipped-event pattern used by Shopee/Lazada/TikTok). Note this is level-triggered on the resulting status, not edge-triggered on a state transition: the event fires on every update whose resulting status is shipped, including a repeat PUT sent on an already-shipped fulfillment (for example, correcting only the tracking company). Make your handler idempotent on fulfillment.shipped.
Fulfillment Object
The partner-safe projection of a fulfillment returned by every endpoint on this page. Field names are camelCase. Carrier-internal metadata (e.g. J&T mailno, bill_code, sortingcode, jt_*) is dropped at the API boundary; only tracking_number and tracking_company are forwarded when present inside the internal metadata blob.
| Field | Type | Description |
|---|---|---|
| id | string | Fulfillment ID. |
| status | string | One of pending, shipped, delivered, cancelled. |
| orderId | string | Parent order ID. Omitted when not known. |
| trackingNumber | string | Carrier tracking number. Omitted when not set. |
| trackingCompany | string | Carrier / shipping company name. Omitted when not set. |
| created_at | string (ISO 8601) | Creation timestamp. Omitted when not known. |
| updated_at | string (ISO 8601) | Last-update timestamp. Omitted when not known. |
| items | array | Array of { itemId, quantity } objects. Omitted when empty. |
| metadata | object | Allowlisted passthrough. Only tracking_number and tracking_company keys ever appear here. Omitted when empty. |
Carrier-agnostic contract: the Open API never exposes carrier-specific tracking events or internal identifiers. J&T pickup, AWB/label retrieval, pickup-slot booking, and shipment cancellation are handled internally by MallPlus and are not yet exposed as partner-facing endpoints. When you need to ship an order, create a fulfillment (above) with your own tracking number — MallPlus records it as-is.