Pengine Store API
Endpoints

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

POST /store-api/2026-08/stores/:storeId/orders
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

201 Created
{
  "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

ResponseCause
400 with a list of messagesThe 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 SKUA 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 IDYou 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:

StatusMeaning
pendingReceived, not yet moving.
production_requestedQueued for production, waiting to be sent.
sent_to_productionHanded to the production system.
in_productionBeing printed and packed.
fulfilledShipped. trackingInfo and fulfilledAt are set.

status usually matches fulfillmentStatus but also represents interruptions:

StatusMeaning
on_holdPaused in the app. fulfillmentStatus still holds the stage it was at.
cancellation_requestedA cancellation is waiting on a decision.
canceledCanceled. canceledAt is set and processing stops.

paymentStatus tracks what your store owes Pengine. It does not represent your customer's payment:

StatusMeaning
dueInvoiced, not yet paid.
paidSettled.
failedA payment attempt failed.
refundedRefunded to you.
canceledThe 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:

  1. Production starts according to your store's settings: automatically after a delay, or when you approve it in the app.
  2. The order climbs the ladder to in_production, then fulfilled when the carrier takes it.
  3. trackingInfo appears 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

GET /store-api/2026-08/stores/:storeId/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, limitStandard 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

GET /store-api/2026-08/stores/:storeId/orders/:orderId
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:

404 Not Found
{ "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.

On this page