status: "COMPLETED" while one of its line items is still waiting for delivery.
Use asynchronous fulfillment when your system needs more than 15 seconds to generate content. Use manual fulfillment when a merchant needs to prepare an account, link, code, or file after payment. PHYSICAL products have their own lane: the order is marked shipped and then delivered. See Physical products.
Fulfillment is tracked per line item. The invoice payment status does not change when a line item moves between delivery states.
Delivery states
A line item enters
AWAITING_FULFILLMENT in any of these cases:
- its dynamic delivery callback returns a clear pending acknowledgement:
{"status":"pending"} - it belongs to a
DYNAMICproduct that has nodynamic_webhookURL configured - it belongs to a
PHYSICALproduct
AWAITING_FULFILLMENT after the order is marked shipped, and moves to DELIVERED when the order is marked delivered. The shipment itself is reported by the ship endpoint and the order:shipped event.
Read fulfillment state
Order read responses expose these fields on every item. The example below shows the relevant response fields:id is the line item ID used as {item_id} in the fulfillment endpoints. The order path accepts the same order identifier used by the existing order endpoints.
Fulfill a line item
status: "COMPLETED", and the line item must not already be delivered. Provide at least one of message, codes, links, or files.
Dynamic delivery callbacks must include delivery_id with the deliveryId from the original webhook request. Shoppex rejects a missing or stale generation, or a recipient that no longer matches that request, with 422. Manual fulfillment without a dynamic delivery event does not require this field.
cURL
Each
files[].attachment_id must refer to an existing attachment owned by the same shop. Upload attachments in the dashboard before calling this endpoint. If any attachment is missing or belongs to another shop, the entire request fails and no partial delivery is stored.Customer email
notify_customer controls the buyer’s “product ready” email:
- Omit it, or set it to
true, to request the customer email after the content is stored. - Set it to
falseto store the content without sending that email.
order:item.delivered event after a successful fulfillment.
Error responses
Both endpoints reject invalid requests with the standard error envelope. Themessage distinguishes the cause:
Idempotency
Both fulfillment endpoints support the standardIdempotency-Key header. Reuse the same key when retrying the same request after a network interruption. Shoppex scopes the key to the authenticated API key and route and replays the stored response during the 24-hour idempotency window.
For example, every retry of the delivery above must keep Idempotency-Key: fulfill-order_123-018f6f2e-7c3a-7b21-9d4e-5a1b2c3d4e5f-v1.
Retry failed dynamic delivery
FAILED to PENDING.
It does not take a request body.
cURL
FAILED dynamic line item with a failed delivery event can be retried. If no retryable event exists, fulfill the line item manually instead.
Physical products
APHYSICAL line item is fulfilled by shipping it, not by sending content. The flow:
- The buyer enters a shipping address in hosted checkout. Shoppex adds the flat price of the matching shipping zone, once per order.
- After payment, every physical line is
AWAITING_FULFILLMENT. Digital lines in the same cart deliver as usual. - The merchant or your system marks the order shipped, with an optional tracking code and link. Shoppex sends
order:shippedand, by default, a shipping email to the buyer. - The merchant or your system marks the order delivered. Every open physical line moves to
DELIVERED, and Shoppex sendsorder:item.deliveredfor each of them.
Mark an order shipped
orders.write scope. {id} accepts the order id or uniqid. All body fields are optional.
cURL
/ship again on a shipped order corrects the tracking. shipped_at keeps the time of the first shipment. A field you leave out keeps its stored value; send null to clear it.
The buyer gets one shipping email per distinct tracking. Repeating the call with the same tracking sends no second email; a changed tracking sends an update. The email also follows the shop’s buyer-email settings and the buyer’s notification preferences. Set notify_customer: false to skip it.
Mark an order delivered
orders.write scope. There is no request body.
cURL
DELIVERED and gets this delivered-items snapshot:
tracking_codes and trackingCodes are present only when the shipment has a tracking code or link. fulfilled_by is API for Developer API calls and MERCHANT for the dashboard.
You can deliver an order that was never marked shipped. shipped_at is then set to the delivery time. Delivering sends no email to the buyer.
Which orders can ship
Both endpoints need an order that:- has at least one
PHYSICALline item, - is paid:
COMPLETED, orPARTIAL_REFUNDwhere every open physical line is still owed, - has a shipping address.
PARTIAL_REFUND order where one of those lines was refunded cannot ship at all. The request is refused rather than settling a line the order no longer owes.
Error responses
A409 CONFLICT carries error.subcode with the specific reason, so you can branch on it instead of the message.
Idempotency
Both endpoints support the standardIdempotency-Key header, scoped to your API key, the route, and the order. A retry with the same key within 24 hours replays the stored response, including a stored error.
Without a key, a repeated /ship corrects the tracking (see above) and a repeated /deliver returns 409 CONFLICT with subcode: "ALREADY_DELIVERED". No retry can deliver a line twice.
Orders without an address
A physical order is not payable until the buyer has entered an address in a country one of the shop’s zones covers. Paths that would complete such an order without payment refuse it too:POST /dev/v1/orders/{id}/complete and POST /dev/v1/orders/{id}/fulfill answer 422 VALIDATION_ERROR “This order has physical items but no shipping address yet.”
Physical products can be bought through hosted checkout only. Creating an order that contains one through
POST /dev/v1/orders, a dashboard invoice, or POS fails with 400 PHYSICAL_PRODUCT_REQUIRES_CHECKOUT. The reseller program refuses them with RESELLER_PHYSICAL_PRODUCT_UNSUPPORTED: physical products cannot be enrolled in the reseller catalog or ordered wholesale.Buyer-facing shipping state
The public invoice that hosted checkout reads carries ashipping object on physical orders. Because anyone with the checkout link can read it, it holds no name, street, or phone, only address_summary with city and country:
address_summary is null until the buyer saves an address. countries lists every country the shop ships to. The full address is shown only in the dashboard and, to the signed-in buyer, in the customer portal.
The buyer-facing errors while saving the address or paying are:
Shipping zones
A shipping zone is a flat shipping price for a list of countries. Manage zones with theproducts.read and products.write scopes.
POST and PUT take the same body:
cURL
countries comes back upper-case and sorted. created_at and updated_at are Unix timestamps. The list endpoint returns the same objects in data, oldest first, with pagination.has_more: false.
A country can belong to only one zone per shop. Write errors:
Every refusal but the 404 carries error.subcode.
All write endpoints support
Idempotency-Key.
Changing or deleting a zone does not change orders that are already paid. An unpaid order is quoted again with the current zone the next time the buyer changes the address, coupon, tip, or payment method; if no zone covers its country any more, that step fails with SHIPPING_COUNTRY_NOT_SERVED.
Fulfillment webhook events
These are normal merchant webhook events. Subscribe to them in Settings → Webhooks or through the Developer API. They use the normal event-webhook signature, not the product’s dynamic delivery signing secret.order:item.delivered
Sent after late content is stored through manual or API fulfillment, and once per physical line when an order is marked delivered.
deliveredItems is the committed delivery snapshot returned by the fulfillment
request. licenseKeys is included as a convenience when the snapshot contains
codes, so consumers do not need to fetch the order after receiving the event.
Both fields can contain sensitive buyer delivery content and should not be logged.
For a physical line, deliveredItems is the physical snapshot
with delivery_type: "PHYSICAL" and the tracking, and there is no licenseKeys field.
order:shipped
Sent when an order’s physical items are marked shipped, from the dashboard or through
POST /dev/v1/orders/{id}/ship. A tracking correction on an already shipped order sends it
again with firstShipment: false.
The payload does not contain the shipping address.
order:item.delivery_failed
Sent when the single dynamic delivery attempt ends in a terminal failure. Shoppex also marks the line item FAILED and notifies the merchant.
These events report fulfillment state. They do not change or replace payment events such as
order:paid.
They go to shop-level webhook subscriptions only; a webhook configured on a single product does not receive them.Dynamic product delivery
Implement the synchronous and asynchronous callback contract.
Webhook events
Verify normal merchant webhook signatures and review event payloads.