Skip to content

ConfirmShipments reports packages that have shipped, draining the shipped quantities from ListNeedToShip.

You call thissales:orders:fulfillment:self

POST
/v2/shipping/shipments
curl --request POST \
--url https://api.zentail.com/v2/shipping/shipments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "shipments": [ { "fulfillmentOrderId": "example", "externalShipmentId": "example", "carrier": "example", "trackingNumber": "example", "trackingUrl": "example", "serviceLevel": "example", "shippedTs": "2026-04-15T12:00:00Z", "lines": [ { "lineItemId": "example", "quantity": 1 } ], "shippingCost": { "amount": "example", "currencyCode": "example" }, "handlingCost": { "amount": "example", "currencyCode": "example" } } ] }'

Idempotent on external_shipment_id: replaying a shipment is a no-op, so a retry after a timeout can never double-ship.

Media typeapplication/json
object
shipments

One entry per package. Required and server-capped: an empty list, or one over the cap, fails the whole request rather than answering per entry.

Array<object>
object
fulfillmentOrderId

Which fulfillment order this package is against. One package cannot span two fulfillment orders, so a consignment covering both halves of a split order is two Shipments.

string
externalShipmentId

Your identifier for this package. Idempotency key: a repeat is a no-op.

string
carrier

Who is carrying it, as you name them — free text, not a Zentail enum. Shown to the buyer and to support alongside the tracking number.

string
trackingNumber

Required. A package sent without one is refused rather than recorded, because Zentail drops an untracked package silently: accepting it would report success for a shipment that was never stored, leaving the units owed with nothing saying why.

string
trackingUrl

Not currently stored. Zentail tracks by carrier and tracking number and has nowhere to put a per-package URL, so anything sent here is accepted and discarded. Measured against sales-orders on 2026-09-21.

string
serviceLevel

Required, and not defaulted. The shipping speed you actually used, in your own vocabulary — Zentail will not substitute a value, because one guess for every fulfiller hides which of them reported nothing. Send a placeholder of your own if you have no real level to report.

string
shippedTs

When the package was handed to the carrier. Omit it and Zentail stamps the time the request arrived, which is the right behaviour for a confirmation sent immediately; send it when you are catching up on a backlog.

string format: date-time
lines

What is in the package. Omit to ship everything the fulfillment order still owes — the common case. Naming lines ships those quantities only; a line_item_id not on the order, a quantity below one, or quantities summing past what is owed fails this package and leaves the units owed.

Array<object>
object
lineItemId

Which line of the fulfillment order, echoed from FulfillmentOrderLine. A SKU is not accepted here: one SKU can appear on two lines.

string
quantity

How many units of that line are in this package. At least one, and never more than the line still owes — across the whole request, so naming one line twice is summed before it is checked.

integer format: int32
shippingCost

Money is an amount and the currency it is in.

The amount is a decimal string, not a float or units+nanos. Money in this contract crosses into Shopify’s MoneyBagInput, which is itself a decimal string, so a string maps across untouched — no scaling and no rounding at the edge, which is where money bugs come from. Follows the Money already in api-proto’s listing contract rather than google.type.Money, for that reason.

currency_code travels with every amount rather than sitting once on the order. Multi-currency is real (etp has purchase_orders.non_standard_currencies), and a silently wrong currency is worse than a wrong amount: the number still looks plausible.

object
amount

Decimal string, e.g. “13.00”. Negative for a credit.

string
currencyCode

ISO 4217, e.g. “USD”.

string
handlingCost

Money is an amount and the currency it is in.

The amount is a decimal string, not a float or units+nanos. Money in this contract crosses into Shopify’s MoneyBagInput, which is itself a decimal string, so a string maps across untouched — no scaling and no rounding at the edge, which is where money bugs come from. Follows the Money already in api-proto’s listing contract rather than google.type.Money, for that reason.

currency_code travels with every amount rather than sitting once on the order. Multi-currency is real (etp has purchase_orders.non_standard_currencies), and a silently wrong currency is worse than a wrong amount: the number still looks plausible.

object
amount

Decimal string, e.g. “13.00”. Negative for a credit.

string
currencyCode

ISO 4217, e.g. “USD”.

string
Examplegenerated
{
"shipments": [
{
"fulfillmentOrderId": "example",
"externalShipmentId": "example",
"carrier": "example",
"trackingNumber": "example",
"trackingUrl": "example",
"serviceLevel": "example",
"shippedTs": "2026-04-15T12:00:00Z",
"lines": [
{
"lineItemId": "example",
"quantity": 1
}
],
"shippingCost": {
"amount": "example",
"currencyCode": "example"
},
"handlingCost": {
"amount": "example",
"currencyCode": "example"
}
}
]
}

A successful response.

Media typeapplication/json
object
results

One result per shipment sent. Match on external_shipment_id rather than on position, and read every entry — one package failing does not fail the others, and the units it covered are still owed.

Array<object>
object
fulfillmentOrderId

The fulfillment order this package was against, echoed back.

string
externalShipmentId

Your own id for the package, echoed back. This is what to match a result on: one call may carry several packages for the same fulfillment order.

string
success

True when the package is recorded, including when it already was — a replay reports success with already_recorded set. False is always a real failure, and the units it covered are still owed.

boolean
errorMessage

Why it failed, in prose, for logs and support. Empty on success. No error code accompanies it, so log the string rather than branching on it.

string
alreadyRecorded

True when this shipment was already recorded. Treat as success.

boolean
Examplegenerated
{
"results": [
{
"fulfillmentOrderId": "example",
"externalShipmentId": "example",
"success": true,
"errorMessage": "example",
"alreadyRecorded": true
}
]
}

An unexpected error response.

Media typeapplication/json
object
code
integer format: int32
message
string
details
Array<object>
object
@type
string
key
additional properties
Examplegenerated
{
"code": 1,
"message": "example",
"details": [
{
"@type": "example"
}
]
}

Sourceshipping-api-proto@24ac5150 via [email protected]

Built0c509dd3

This page describes the source document as deployed: the version sales-orders pinned in its release r2026.09.24. A change merged upstream shows up here once it has deployed and the pins are next refreshed.

API changelog