> ## Documentation Index
> Fetch the complete documentation index at: https://developer.kalixo.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Order lifecycle

> How async orders move from processing to completed - including large orders.

Every v2 order is **asynchronous**. You place an order, receive an `orderId` immediately,
then poll until the codes are ready.

## Statuses

<ResponseField name="processing" type="status">
  The order was accepted and is being fulfilled. Poll again after `estimatedReadyAt`.
</ResponseField>

<ResponseField name="completed" type="status">
  All codes were delivered. The response includes `products[].codes`.
</ResponseField>

<ResponseField name="partially_completed" type="status">
  Some - but not all - codes were delivered. The response includes `delivered`, `total`, and
  `products[].codes` for **every code that was delivered** (fewer than `quantity` on each line).
  See [Partial deliveries](#partial-deliveries) below.
</ResponseField>

<ResponseField name="failed" type="status">
  No codes were delivered. The response includes a `reason`.
</ResponseField>

<Note>
  **Codes are returned when `status` is `completed` or `partially_completed`.** While
  `processing`, you receive
  `{"status": "processing", "message": "Order is still processing"}` with no codes.
</Note>

Every order response (place and poll) also includes a **`wallet`** snapshot with your current
balances and low-balance flag.

## The flow

<Steps>
  <Step title="Place the order">
    `POST /orders` returns `202 Accepted` with `orderId`, `status: "processing"`,
    `estimatedReadyAt`, and `wallet`.
  </Step>

  <Step title="Wait for the estimate">
    `estimatedReadyAt` tells you roughly when codes will be ready. It scales with the order
    size - small orders are near-instant; large orders take longer.
  </Step>

  <Step title="Poll the order">
    `GET /orders/{reference}` by `orderId` or `externalOrderCode`. Repeat until you get a
    terminal status (`completed`, `partially_completed`, or `failed`).
  </Step>
</Steps>

## Order pricing

Each line in `orderProducts` carries a **unit price** in minor units (validated against the catalog).

| Field                   | Meaning                                                                                                                                                      |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `orderProducts[].price` | **Unit price** per line. Required for open-denomination products; for fixed products it must match the catalog price.                                        |
| Top-level `price`       | **Optional order total**. If you send it, it must equal `sum(unitPrice × quantity)` across all lines. If omitted, Kalixo computes the total from your lines. |

### Multi-line example

Two fixed-price products totalling **15000** minor units (£100 + £50):

```json theme={"dark"}
{
  "externalOrderCode": "O-67890",
  "currency": "GBP",
  "price": 15000,
  "orderProducts": [
    { "productCode": "10020000213575", "price": 10000, "quantity": 1 },
    { "productCode": "10001000000421", "price": 5000, "quantity": 1 }
  ]
}
```

## Redemption codes

When an order completes - or partially completes - each line includes a **`codes`** array with
one object per **delivered** unit:

```json theme={"dark"}
{
  "productCode": "10020000213575",
  "price": 10000,
  "quantity": 2,
  "codes": [
    { "code": "ABCDE-FGHIJ-KLMNO-PQRST-UVWXY" },
    { "code": "ZYXWV-UTSRQ-PONML-KJIHG-FEDCB" }
  ]
}
```

Quantity `2` with two successful deliveries yields two `{ code }` entries. On a
`partially_completed` order, `codes.length` may be less than `quantity` for a line.

## Large orders

Use the same `POST /orders` endpoint for any quantity. The response includes an
`estimatedReadyAt` timestamp that scales with the order size - larger orders may take longer
to complete.

Poll `GET /orders/{reference}` until you reach a terminal status. While the order is still
`processing`, you'll see `"message": "Order is still processing"`. When `completed` or
`partially_completed`, the response includes delivered codes in `products[].codes`.

<CardGroup cols={2}>
  <Card title="Polling cadence" icon="clock">
    Poll at or after `estimatedReadyAt`, then every minute or so. Avoid tight polling loops.
  </Card>

  <Card title="Idempotency" icon="fingerprint" href="/concepts/idempotency">
    Re-submitting the same `externalOrderCode` returns the existing order - never a duplicate.
  </Card>
</CardGroup>

## Partial deliveries

If you receive `partially_completed`:

1. **Save the codes** in `products[].codes` - these are the units that were delivered.
2. Compare `delivered` and `total`, and each line's `codes.length` vs `quantity`, to see what is still outstanding.
3. **Do not** place a new order for the missing quantity - billing and idempotency risks apply.
4. **Contact support** at [integrations@kalixo.io](mailto:integrations@kalixo.io) with your
   `orderId` and `externalOrderCode` for the remaining units.

Example - 3 of 5 codes delivered:

```json theme={"dark"}
{
  "orderId": "b9f6f9a2-4d0a-4f5c-9a1e-7c2d8f3b6e10",
  "externalOrderCode": "O-12345",
  "status": "partially_completed",
  "delivered": 3,
  "total": 5,
  "products": [
    {
      "productCode": "10020000213575",
      "price": 10000,
      "quantity": 5,
      "codes": [
        { "code": "AAAAA-BBBBB-CCCCC-DDDDD-EEEEE" },
        { "code": "FFFFF-GGGGG-HHHHH-IIIII-JJJJJ" },
        { "code": "KKKKK-LLLLL-MMMMM-NNNNN-OOOOO" }
      ]
    }
  ],
  "wallet": { "balances": { "GBP": 196.01 }, "lowBalance": false }
}
```

## Polling example

```javascript Node theme={"dark"}
const POLL_MS = 60_000;

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

function waitUntil(iso) {
  const ms = Math.max(0, new Date(iso).getTime() - Date.now());
  return sleep(ms);
}

async function fetchOrder(ref) {
  const res = await fetch(`https://api.kalixo.io/v2/orders/${ref}`, {
    headers: { "x-api-key": process.env.KALIXO_API_KEY },
  });
  if (res.status === 429) {
    const retryAfter = Number(res.headers.get("Retry-After") ?? "60");
    await sleep(retryAfter * 1000);
    return fetchOrder(ref);
  }
  if (!res.ok) throw new Error(`Order fetch failed: ${res.status}`);
  return res.json();
}

async function waitForOrder(ref, estimatedReadyAt) {
  if (estimatedReadyAt) await waitUntil(estimatedReadyAt);
  for (;;) {
    const order = await fetchOrder(ref);
    if (order.status !== "processing") return order;
    await sleep(POLL_MS);
  }
}

// After POST /orders:
// const { orderId, estimatedReadyAt } = await placeRes.json();
// const order = await waitForOrder(orderId, estimatedReadyAt);
```
