Orders
How to create orders and track their statuses and fulfillment.
Create an order after your customer pays. The order contains line items and one shipping address. Pengine prints and ships the items, then bills your store.
The Store API can create and read orders. Edit, cancel, or hold an order in the Pengine app.
Create an order
curl -X POST https://api.pengine.io/store-api/2026-08/stores/$STORE_ID/orders \
-H "X-API-Key: $PENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalOrderId": "web-10482",
"orderedAt": "2026-08-19T12:31:00.000Z",
"lineItems": [
{ "productVariantId": "ak88bwfjjqn5cjt5jaczmn1n", "quantity": 2 },
{ "sku": "PGN15884420374096221552", "quantity": 1 }
],
"shippingAddress": {
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"phone": "+12025550176",
"address": {
"streetAddress": "1600 Pennsylvania Avenue NW",
"apartmentOrSuite": "Suite 4",
"cityOrTown": "Washington",
"stateOrProvince": "DC",
"zipOrPostalCode": "20500",
"country": "US"
}
}
}'Prop
Type
Line item
A line item is either a product you built in advance, or a custom item assembled at order time.
Prop
Type
For a standard line item, send productVariantId or sku. If you send both, they must identify the
same variant. A variant can appear in only one line item, so combine duplicate entries by increasing
the quantity.
For a custom line item, set isCustom to true and send catalogProductVariantId and decorations.
This creates the item at order time without saving a product. Custom and standard line items can
appear on the same order.
See Custom items for the decoration fields, the placement rules and the failures they cause.
Shipping address
Prop
Type
Prop
Type
{
"id": "p948dnb4qbo2d86i2mx67svp",
"name": "#127505",
"externalOrderId": "web-10482",
"status": "pending",
"fulfillmentStatus": "pending",
"paymentStatus": "due",
"isTest": false,
"totalQuantity": 2,
"subtotalCost": 261.4,
"shippingCost": 17.85,
"totalCost": 279.25,
"lineItems": [
{
"id": "l0x557ng2f08ilidmdvl4cnq",
"productId": "saryo4m9p6ki3u1558m6h56j",
"productVariantId": "ak88bwfjjqn5cjt5jaczmn1n",
"sku": "PGN70872789915240044115",
"name": "Midweight Hoodie - Black",
"color": "Black",
"size": "S",
"quantity": 2,
"isCustom": false,
"unitCost": 130.7,
"totalCost": 261.4,
"images": [
{
"id": "zkk2lkpxredez88sw0ljgwia",
"color": "Black",
"image": {
"id": "kl1v18gmjnd420uziof6ecv0",
"url": "https://cdn.pengine.io/files/kl1v18gmjnd420uziof6ecv0/Midweight-Hoodie-Front-Black.png"
},
"position": 1
}
],
"position": 1
}
],
"shippingAddress": {
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada@example.com",
"phone": "+12025550176",
"address": {
"streetAddress": "1600 Pennsylvania Avenue NW",
"apartmentOrSuite": "Suite 4",
"cityOrTown": "Washington",
"stateOrProvince": "DC",
"zipOrPostalCode": "20500",
"country": "US"
}
},
"orderedAt": "2026-08-19T12:31:00.000Z",
"createdAt": "2026-08-19T12:37:20.897Z",
"updatedAt": "2026-08-19T12:37:20.932Z"
}Why a create fails
| Response | Cause |
|---|---|
400 with a list of messages | The body failed validation: a missing field, a quantity below 1, a country that is not ISO 3166-1 alpha-2, a US state that does not exist. |
400 Each line item must have a product variant ID or a SKU | A normal line item named neither. |
400 No product found in the store for a line item: ... | The variant ID or SKU is not in this store. |
400 Product variant and SKU do not match for a line item: ... | Both were sent and they point at different variants. |
400 The same product variant appears in more than one line item: ... | Merge them into one line item. |
400 Products that are not active or unlisted cannot be ordered: ... | A draft or archived product. |
400 Discontinued or out of stock product variants cannot be ordered: ... | The variant is no longer available. |
409 An order already exists with this external order ID | You already created this one. Fetch it instead. |
Custom line items add a dozen more 400s of their own, covering the blank, the artwork and the
placement geometry. They are listed in Custom items.
The order object
Prop
Type
Order line item
Prop
Type
Each entry in images carries a color, a position, and a nested image object holding id and
url.
Tracking info
Prop
Type
Costs are what Pengine charges you
unitCost, subtotalCost, shippingCost and totalCost are what Pengine charges you, in US
dollars. What your customer paid is not in this API. Round to cents in your own code before
displaying or storing a total.
Statuses
fulfillmentStatus tracks production progress and only moves forward:
| Status | Meaning |
|---|---|
pending | Received, not yet moving. |
production_requested | Queued for production, waiting to be sent. |
sent_to_production | Handed to the production system. |
in_production | Being printed and packed. |
fulfilled | Shipped. trackingInfo and fulfilledAt are set. |
status usually matches fulfillmentStatus but also represents interruptions:
| Status | Meaning |
|---|---|
on_hold | Paused in the app. fulfillmentStatus still holds the stage it was at. |
cancellation_requested | A cancellation is waiting on a decision. |
canceled | Canceled. canceledAt is set and processing stops. |
paymentStatus tracks what your store owes Pengine. It does not represent your customer's payment:
| Status | Meaning |
|---|---|
due | Invoiced, not yet paid. |
paid | Settled. |
failed | A payment attempt failed. |
refunded | Refunded to you. |
canceled | The invoice was canceled, usually with the order. |
Treat unknown values as 'something else'
New statuses can appear without a new API version. Handle known values and use a neutral fallback for unknown values. See Versioning.
What happens after you create one
An order starts as pending with payment due. Pengine then processes it according to your store
settings:
- Production starts according to your store's settings: automatically after a delay, or when you approve it in the app.
- The order climbs the ladder to
in_production, thenfulfilledwhen the carrier takes it. trackingInfoappears on the order response, with a URL you can send to your customer.
The API does not control these transitions. Use the orders_update webhook to react to changes. See
Events.
List orders
| Parameter | |
|---|---|
filter[status] | One or more order statuses. |
filter[fulfillmentStatus] | One or more fulfillment statuses. |
filter[paymentStatus] | One or more payment statuses. |
filter[externalOrderId] | Exact match on your own order number. |
sort[updatedAt] | asc or desc. |
sort[createdAt] | asc or desc. Defaults to desc, and is always applied last as a tiebreak. |
page, limit | Standard paging. See Lists. |
# Everything still in production
curl "https://api.pengine.io/store-api/2026-08/stores/$STORE_ID/orders?filter[fulfillmentStatus]=sent_to_production,in_production&limit=100" \
-H "X-API-Key: $PENGINE_API_KEY"
# Find the order you created as web-10482
curl "https://api.pengine.io/store-api/2026-08/stores/$STORE_ID/orders?filter[externalOrderId]=web-10482" \
-H "X-API-Key: $PENGINE_API_KEY"
# What changed since you last looked
curl "https://api.pengine.io/store-api/2026-08/stores/$STORE_ID/orders?sort[updatedAt]=desc&limit=50" \
-H "X-API-Key: $PENGINE_API_KEY"Items are full order objects, the same shape POST and GET /orders/:orderId return.
Get one order
curl https://api.pengine.io/store-api/2026-08/stores/$STORE_ID/orders/p948dnb4qbo2d86i2mx67svp \
-H "X-API-Key: $PENGINE_API_KEY"An ID from another store and an ID that does not exist return the same response:
{ "message": "Order not found", "error": "Not Found", "statusCode": 404 }Creating orders safely
Always send externalOrderId. It prevents retries from creating duplicate orders. After a timeout,
retry with the same value: 201 means the first request did not succeed, and 409 means it did.
Check availability before checkout. A draft or archived product, or a discontinued variant,
fails at order creation. Validate availability while the customer is browsing.
Validate placement before checkout. For custom items, the print area dimensions are what the order
is checked against, so checking them yourself while the customer is still designing avoids a 400 at
the end. See Custom items.
Store the ID. Save the Pengine id with your own order. Later API responses and webhook payloads
refer to it.