An order is created in new state by /storefront/checkout-cart. From there, every state transition is a separate POST endpoint. The endpoints are deliberately verb-named (process, ship, deliver) rather than a single PATCH — the server runs side effects (inventory, notifications, automation triggers) per transition.
Lifecycle
new → processing → shipped → delivered → completed
↘ on-hold ↗
↘ cancelled (terminal)
↘ refunded (terminal)
Each transition runs a hook chain:
| State | What happens |
|---|---|
new | Created by checkout. Customer email queued. Inventory reserved. |
processing | Picked / packed. Inventory still reserved. |
shipped | Tracking number stored. Inventory decremented. Customer notified. |
delivered | Carrier confirmed. Triggers review-request automation. |
completed | Manual close (post-return-window). Locks the order. |
on-hold | Operations pause; reason recorded. Reversible. |
cancelled | Inventory released. Refund the payment separately if captured. |
refunded | Money returned. Order frozen. |
Move an order through the lifecycle
All staff-side transitions require a JWT with the User principal and a role that has order-write permissions.
Process
/storefront/order/process/:orderNumberJWTcurl -X POST https://appengine.appmint.io/storefront/order/process/ORD-2026-0042 \
-H "orgid: my-org" -H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{ "note": "Picked, ready to pack" }'
Ship
/storefront/order/ship/:orderNumberJWT| Field | Type | Description |
|---|---|---|
| carrier* | string |
|
| tracking* | string | The tracking number from the carrier label. |
| service | string |
|
| cost | number | Actual shipping cost (for reconciliation). |
| items | Array<{sku, quantity}> | For partial shipments. Omit to ship the whole order. |
| note | string |
await fetch(`/api/storefront/order/ship/${orderNumber}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
body: JSON.stringify({
carrier: 'ups',
tracking: '1Z999AA10123456784',
service: 'ground',
}),
});
Partial shipments are first-class: ship some line items now, ship the rest later. The order stays in processing until everything is out the door, then flips to shipped.
Deliver
/storefront/order/deliver/:orderNumberJWTMarks the order delivered. Carrier webhooks (when a logistics integration is connected) fire this automatically; you can also POST it manually for offline channels.
Complete
/storefront/order/complete/:orderNumberJWTCloses the order after the return window. After complete, only refund can change the state.
Hold and release
/storefront/order/hold/:orderNumberJWT/storefront/order/release/:orderNumberJWTHold accepts { reason: string } and is required. Release moves the order back to its prior state. Both transitions log to the activity feed.
Cancel and refund
/storefront/order/cancel/:orderNumberJWT/storefront/order/refund/:orderNumberJWTCancel before the order ships; refund after. Refund body: { amount?, items?, reason }. amount defaults to the order total; pass items + amount for partial refunds. Refund posts back to the original payment gateway via the Connect module.
Update order info
/storefront/order/update/:orderNumberJWTFor non-state changes — addresses, notes, tags. Useful when the customer emails to fix a typo in the shipping address before the order ships.
{
"shippingAddress": { "street1": "456 New St", "city": "Brooklyn", "state": "NY", "zip": "11201", "country": "US" },
"internalNotes": "Customer called to update address",
"tags": ["address-corrected"]
}
Send order welcome email
/storefront/order/send-welcome/:orderNumberJWTRe-fires the order-confirmation email. Handy after fixing a delivery issue.
Customer-side tracking
The customer doesn't have staff JWT — these endpoints are designed to work from a public order-lookup page or a customer-account portal.
/storefront/order/email/:email/:orderNumberNo auth/storefront/order/get/:author/:orderNumberNo auth/storefront/orders/get/:authorNo authThe first variant matches by email + order number — the receipt has both, and the combination is unguessable in practice. The second uses the customer's SK (:author); call it from an authenticated account page where the SK is known. The third lists all orders for a customer SK — guard it with auth on the front end.
// base-app/src/app/account/orders/page.tsx (server component)
const orders = await storefrontAPI.getOrders(customerSk);
Shipment tracking
/shipping/track/:trackingNumberNo auth/shipping/order/:orderNumber/statusNo authThe tracking endpoint pulls live status from the carrier integration. The order/status variant aggregates: which line items shipped, which are pending, the latest scan event for each tracking number.
{
"orderNumber": "ORD-2026-0042",
"overallStatus": "in_transit",
"shipped": [
{ "sku": "SNK-AIR-1-9-BLK", "quantity": 1, "trackingNumber": "1Z999AA10123456784", "carrier": "ups", "status": "in_transit", "shipDate": "2026-04-22T15:00:00Z" }
],
"pending": [],
"shipments": [
{ "id": "shp-001", "tracking": "1Z999AA10123456784", "carrier": "ups", "status": "in_transit", "shipDate": "2026-04-22T15:00:00Z" }
]
}
Subscriptions
/storefront/subscriptions/get/:author/:subscriptionid?No auth/storefront/update-subscriptionJWTSubscriptions live alongside orders. Each successful billing cycle creates a child order linked to the subscription. The update-subscription endpoint accepts { subscriptionId, action: 'pause' | 'resume' | 'cancel' | 'change-plan', priceId?, quantity? }.
Order remove
/storefront/order/remove/:author/:orderNumber?JWTHard-deletes a test order. Don't expose to staff UI — used for cleanup scripts only. Roles(User) enforced.
Workflows
/storefront/workflows/order-managementJWT/storefront/workflows/order-management/:name?JWTOrder workflows are reusable Automation flows tied to order events (order.shipped, order.delivered). Use them to wire up review-request emails, NPS surveys, or replenishment-reminder schedules without writing code. The Automation module documentation covers the trigger payload shape.