# Storefront · Orders

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /storefront/order/email/{email}/{orderNumber}

**Look up an order by email**

`operationId: StorefrontController_orderByEmail`

Guest order lookup: resolves an order from the email address used at checkout plus the order number. This is the pairing behind "track my order" forms, where the customer has no account.

#### Signature

```http
GET /storefront/order/email/{email}/{orderNumber} (email: string, orderNumber: string) -> The matching order
```

#### Access

Public — no credentials required.

#### Notes

- Knowing an email and an order number is sufficient to read the order. Rate-limit any public form built on this.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /storefront/order/get/{author}/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `email` | path | string | yes | Email address used at checkout. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The matching order |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/order/get/{author}/{orderNumber}

**Get a customer's order**

`operationId: StorefrontController_order`

Fetches one order, scoped to the customer who placed it. Both the author and the order number must match.

#### Signature

```http
GET /storefront/order/get/{author}/{orderNumber} (author: string, orderNumber: string) -> The matching order
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /storefront/order/email/{email}/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `author` | path | string | yes | Customer email or username. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The matching order |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/orders/get/{author}

**List a customer's orders**

`operationId: StorefrontController_orders`

Returns every order placed by the given customer, identified by the email or username recorded as the order author.

#### Signature

```http
GET /storefront/orders/get/{author} (author: string) -> The customer's orders
```

#### Access

Public — no credentials required.

#### Notes

- This is a customer-scoped read on a public route — treat the author value as sensitive and do not expose it in client-side URLs you did not construct.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /storefront/order/get/{author}/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `author` | path | string | yes | Customer email or username. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The customer's orders |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/order/remove/{author}/{orderNumber}

**Remove an order**

`operationId: StorefrontController_orderRemove`

Deletes an order belonging to the given customer.

Note that this destructive operation is mounted on `GET`, so it can be triggered by anything that follows a link — a crawler, a prefetch, a preview. Never place this URL where it can be fetched automatically.

#### Signature

```http
GET /storefront/order/remove/{author}/{orderNumber} (author: string, orderNumber: string) -> Removal result
```

#### Access

Public — no credentials required.

#### Notes

- Prefer `POST /storefront/order/cancel/{orderNumber}`, which preserves the record and its audit trail.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/cancel/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `author` | path | string | yes | Customer email or username. |
| `orderNumber` | path | string | yes | The public order number. Optional segment. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Removal result |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/order/send-welcome/{orderNumber}

**Send the order confirmation email**

`operationId: StorefrontController_orderSendWelcome`

Re-sends the order confirmation to the address on the order, through the org's notification pipeline (template chain: org → shared-org → factory default). Useful when a customer did not receive the original.

#### Signature

```http
GET /storefront/order/send-welcome/{orderNumber} (orderNumber: string) -> Dispatch result
```

#### Access

Public — no credentials required.

#### Notes

- Sends every time it is called — there is no once-only guard, so a retry mails the customer again.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |

Plus the standard platform errors: `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Dispatch result |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/refund/{orderNumber}

**Refund an order**

`operationId: StorefrontController_orderRefund`

Refunds a paid order, in full or in part, without cancelling it. Use this for goodwill refunds and returns where the order itself stands.

For refunding one specific tender on a POS tab, use `POST /storefront/pos/tab/{id}/refund`, which targets an individual payment line.

#### Signature

```http
POST /storefront/order/refund/{orderNumber} (orderNumber: string, body) -> The refund result
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | NO_PAYMENT | Order has no payment to refund | The order has never been paid. | Cancel the order instead — there is nothing to return. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/pos/tab/{id}/refund`
- `POST /storefront/order/cancel/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

Refund details.

```json
{
  "reason": "Damaged in transit"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The refund result |
| `400` | Order has no payment to refund — The order has never been paid. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/cancel/{orderNumber}

**Cancel an order**

`operationId: StorefrontController_orderCancel`

Cancels an order and, when it has been paid, triggers a refund as part of the same operation.

Cancellation is blocked once goods are in the customer's hands or on their way — a shipped, in-transit or delivered order must be returned rather than cancelled.

#### Signature

```http
POST /storefront/order/cancel/{orderNumber} (orderNumber: string, body) -> The cancelled order, with refund details when one was issued
```

#### Access

Public — no credentials required.

#### Notes

- Cancelling a paid order refunds it automatically — do not also call the refund endpoint, or you will attempt to refund twice.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | NOT_CANCELLABLE | Cannot cancel order with status "<status>". Orders that are shipped, in transit, or delivered cannot be cancelled. | The order status is one of `shipped`, `in_transit`, `delivered`, `cancelled` or `refunded`. | Process a return or a refund instead. `POST /storefront/order/refund/{orderNumber}` refunds without cancelling. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/refund/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

Optional cancellation details.

```json
{
  "reason": "Customer changed their mind"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The cancelled order, with refund details when one was issued |
| `400` | Cannot cancel order with status "<status>". Orders that are shipped, in transit, or delivered cannot be cancelled. — The order status is one of `shipped`, `in_transit`, `delivered`, `cancelled` or `refunded`. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/process/{orderNumber}

**Mark an order as being fulfilled**

`operationId: StorefrontController_orderProcess`

Moves the order to `processing` and stamps `processedAt`. This is the first step of fulfilment — the point where a warehouse picks up the order.

Unlike `set-status`, this transition fires the automation and events attached to processing.

#### Signature

```http
POST /storefront/order/process/{orderNumber} (orderNumber: string, body) -> The updated order and a confirmation message
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | INVALID_TRANSITION | Cannot process order with status "<status>" | The order's current status is one of `cancelled`, `refunded`, `shipped`, `delivered`. | An order that has already shipped or been closed cannot re-enter fulfilment. Use `set-status` if you must override. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/ship/{orderNumber}`
- `POST /storefront/order/set-status/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

Optional note recorded in the status history.

```json
{
  "note": "Picked and packed"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated order and a confirmation message |
| `400` | Cannot process order with status "<status>" — The order's current status is one of `cancelled`, `refunded`, `shipped`, `delivered`. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/ship/{orderNumber}

**Ship an order**

`operationId: StorefrontController_orderShip`

Records a shipment and moves the order to `shipped`, stamping `shippedAt`.

Each call appends an entry to `shippingInfo[]` rather than replacing it, so an order fulfilled in several parcels accumulates one entry per shipment. Pass `items` to record which lines went in which parcel.

#### Signature

```http
POST /storefront/order/ship/{orderNumber} (orderNumber: string, body) -> The updated order and a confirmation message
```

#### Access

Public — no credentials required.

#### Notes

- An already-`shipped` order can be shipped again — that is how multi-parcel fulfilment is recorded.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | INVALID_TRANSITION | Cannot ship order with status "<status>" | The order's current status is one of `cancelled`, `refunded`, `delivered`. | A cancelled, refunded or already-delivered order cannot be shipped. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/deliver/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

Shipment details. `carrier` and `tracking` are both required.

```json
{
  "carrier": "ups",
  "tracking": "1Z999AA10123456784",
  "service": "ground",
  "cost": 12.5
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated order and a confirmation message |
| `400` | Cannot ship order with status "<status>" — The order's current status is one of `cancelled`, `refunded`, `delivered`. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/deliver/{orderNumber}

**Mark an order as delivered**

`operationId: StorefrontController_orderDeliver`

Moves the order to `delivered` and stamps `deliveredAt`.

This transition is gated the opposite way to the others: rather than blocking a set of statuses it **requires** the order to already be in transit — `shipped`, `in_transit` or `out_for_delivery`. An order that was never marked shipped cannot be marked delivered.

#### Signature

```http
POST /storefront/order/deliver/{orderNumber} (orderNumber: string, body) -> The updated order and a confirmation message
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | NOT_IN_TRANSIT | Cannot mark as delivered - order status is "<status>" | The order is not in `shipped`, `in_transit` or `out_for_delivery`. | Ship the order first, or use `set-status` for a flow that skips shipping — local pickup, for instance. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/ship/{orderNumber}`
- `POST /storefront/order/complete/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

Optional note recorded in the status history.

```json
{
  "note": "Signed for by Ada"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated order and a confirmation message |
| `400` | Cannot mark as delivered - order status is "<status>" — The order is not in `shipped`, `in_transit` or `out_for_delivery`. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/hold/{orderNumber}

**Put an order on hold**

`operationId: StorefrontController_orderHold`

Pauses an order: sets the status to `on_hold`, records `holdReason` and `heldAt`, and saves the current status in `previousStatus` so the order can be restored exactly where it left off.

A `reason` is required — a held order with no explanation is not useful to whoever picks it up next.

#### Signature

```http
POST /storefront/order/hold/{orderNumber} (orderNumber: string, body) -> The held order and a confirmation message
```

#### Access

Public — no credentials required.

#### Notes

- Holding an already-held order overwrites `previousStatus` with `on_hold`, so release would restore it to `on_hold`. Check the status before holding.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | INVALID_TRANSITION | Cannot hold order with status "<status>" | The order's current status is one of `cancelled`, `refunded`, `delivered`, `completed`. | A closed order cannot be held. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/release/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

Why the order is being held.

```json
{
  "reason": "Awaiting stock"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The held order and a confirmation message |
| `400` | Cannot hold order with status "<status>" — The order's current status is one of `cancelled`, `refunded`, `delivered`, `completed`. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/release/{orderNumber}

**Release an order from hold**

`operationId: StorefrontController_orderRelease`

Restores a held order to the status it had before the hold, clearing `previousStatus` and `holdReason`. When no previous status was recorded the order falls back to `processing`.

#### Signature

```http
POST /storefront/order/release/{orderNumber} (orderNumber: string) -> The released order and a confirmation message
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | NOT_ON_HOLD | Order is not on hold | The order status is anything other than `on_hold`. | Only a held order can be released. Read the order first if you are unsure of its state. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/hold/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The released order and a confirmation message |
| `400` | Order is not on hold — The order status is anything other than `on_hold`. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/complete/{orderNumber}

**Complete an order**

`operationId: StorefrontController_orderComplete`

Closes the order, moving it to `completed` and stamping `completedAt`. This is the terminal step of a successful order.

The order **must** be `delivered` first — completion is not a shortcut past the rest of the lifecycle.

#### Signature

```http
POST /storefront/order/complete/{orderNumber} (orderNumber: string) -> The completed order and a confirmation message
```

#### Access

Public — no credentials required.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | NOT_DELIVERED | Cannot complete order - must be delivered first | The order status is anything other than `delivered`. | Mark the order delivered first, or use `set-status` to close an order that never followed the ship → deliver path. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/deliver/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The completed order and a confirmation message |
| `400` | Cannot complete order - must be delivered first — The order status is anything other than `delivered`. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/set-status/{orderNumber}

**Set an order status directly**

`operationId: StorefrontController_orderSetStatus`

Operator override. Sets `data.status`, stamps the matching timestamp field and appends to the status history — and does nothing else.

**It deliberately skips both the preconditions and the automation** of the dedicated transitions: no delivery emails, no workflow advance, no step validation. Reach for it to correct a mistaken state, or to drive an order that never follows ship → deliver (local pickup, digital goods). Use the dedicated endpoints whenever you want the side effects.

The status must be one of the recognised values; anything else is rejected rather than stored.

#### Signature

```http
POST /storefront/order/set-status/{orderNumber} (orderNumber: string, body) -> The updated order and a confirmation message
```

#### Access

Public — no credentials required.

#### Notes

- No events fire. If a customer should be notified of this change, send the notification yourself.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |
| `400` | INVALID_STATUS | Invalid status "<status>" | `status` is missing, or is not one of the recognised values. | Use one of: `new`, `awaiting-payment`, `paid-partial`, `paid`, `confirmed`, `processing`, `shipped`, `in_transit`, `out_for_delivery`, `delivered`, `completed`, `on_hold`, `failed`, `returned`, `refunded`, `cancelled`. |

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /storefront/order/process/{orderNumber}`
- `POST /storefront/order/deliver/{orderNumber}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

The status to set.

```json
{
  "status": "completed",
  "note": "Collected in store"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated order and a confirmation message |
| `400` | Invalid status "<status>" — `status` is missing, or is not one of the recognised values. |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/order/update/{orderNumber}

**Update order details**

`operationId: StorefrontController_orderUpdateInfo`

Edits the non-lifecycle fields of an order — addresses, notes and tags. It does not change the status; use the lifecycle endpoints for that.

Only the fields present in the body are written, so a partial update leaves everything else intact.

#### Signature

```http
POST /storefront/order/update/{orderNumber} (orderNumber: string, body) -> The updated order
```

#### Access

Public — no credentials required.

#### Notes

- `tags` replaces the whole array rather than merging — send the full set you want to keep.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | ORDER_NOT_FOUND | Order not found | No order in the org has that public order number. | Confirm the number. These endpoints take `data.number`, not the record `sk`. |

Plus the standard platform errors: `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `orderNumber` | path | string | yes | The public order number (`data.number`), not the record `sk`. |

### Request body

Fields to update. All optional; omitted fields are left unchanged.

```json
{
  "shippingAddress": {
    "line1": "12 Ada Way",
    "city": "London",
    "postcode": "E1 6AN",
    "country": "GB"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated order |
| `404` | Order not found — No order in the org has that public order number. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

