# Lifecycle and events

> Every state a partner order passes through, every transition and who makes it, the event envelope with its sequence number, and every event type.

Updated 2026-10-24 · HTML: https://layercrew.com/developers/lifecycle · Markdown: https://layercrew.com/developers/lifecycle.md · OpenAPI: https://layercrew.com/openapi.json

A partner order moves through a fixed set of states. Every move is an event with a timestamp, an actor and a per-partner sequence number — listable on the order, on the account feed and delivered to your webhooks.

```text
received ──order.priced──► pending_review ──order.accepted──► accepted ──► in_production ──► ready_to_ship ──► shipped ──► delivered
   │                            │                                 │                                                 (final)
   │                            ├──order.rejected──► rejected     │
   │                            │                    (final)      │
   └────────── order.cancelled (you, while received / pending_review / accepted) ──► cancelled (final)

any state before shipped ──order.on_hold──► on_hold ──order.released──► back to the previous state
```

## States

| State | Moved there by | What it means |
| --- | --- | --- |
| `received` | you | Your `POST /v1/orders` was accepted and every item is being priced. You may cancel. |
| `pending_review` | pricing | Every item has a price (`pricing_status: priced`), or one needs a hand to price (`failed`); an engineer is looking at the order. You may cancel. |
| `accepted` | engineer | In the production queue. The price is frozen (`pricing.final: true`), payment instructions are on the order, and production starts after payment unless your plan says otherwise. You may still cancel. |
| `in_production` | production | Printing. From here on, cancellation goes through us: write from the order page. |
| `ready_to_ship` | production | Passed quality check and packed. |
| `shipped` | courier | Handed to the courier; `shipments[]` carries the tracking number and URL. Tracking updates follow as `order.tracking_updated`. |
| `delivered` | courier | The parcel arrived. Final. |
| `on_hold` | engineer | We need something from you before going on — `hold.reason` says what. The order returns to its previous state when released. |
| `rejected` | engineer | Declined with a reason you can act on: `rejection.code`, `rejection.message` and the engineer's `rejection.note`. Final. |
| `cancelled` | you | Cancelled by you (`POST /v1/orders/{id}/cancel`) or by us at your request. Final. |

## Transitions

| From | To | Event | Actor |
| --- | --- | --- | --- |
| `received` | `pending_review` | `order.priced` | pricing |
| `received` | `pending_review` | `order.pricing_failed` | pricing |
| `pending_review` | `accepted` | `order.accepted` | engineer |
| `pending_review` | `rejected` | `order.rejected` | engineer |
| `accepted` | `in_production` | `order.in_production` | production |
| `in_production` | `ready_to_ship` | `order.ready_to_ship` | production |
| `ready_to_ship` | `shipped` | `order.shipped` | courier |
| `shipped` | `delivered` | `order.delivered` | courier |
| `received` | `cancelled` | `order.cancelled` | you |
| `pending_review` | `cancelled` | `order.cancelled` | you |
| `accepted` | `cancelled` | `order.cancelled` | you |
| `in_production` | `cancelled` | `order.cancelled` | engineer |
| `ready_to_ship` | `cancelled` | `order.cancelled` | engineer |
| `received` | `on_hold` | `order.on_hold` | engineer |
| `pending_review` | `on_hold` | `order.on_hold` | engineer |
| `accepted` | `on_hold` | `order.on_hold` | engineer |
| `in_production` | `on_hold` | `order.on_hold` | engineer |
| `ready_to_ship` | `on_hold` | `order.on_hold` | engineer |
| `on_hold` | (previous state) | `order.released` | engineer |

## Who acts

- **You** — create the order, cancel it while it is `received`, `pending_review` or `accepted`, erase the recipient once it is final; in test, advance it.
- **Pricing** — slices and prices every item within minutes: `order.priced`, or `order.pricing_failed` when an item needs a hand.
- **An engineer** — accepts, rejects, holds and releases; prices by hand the items the slicer left unpriced.
- **Production** — `in_production` when printing starts, `ready_to_ship` after quality check and packing.
- **The courier** — `shipped` with tracking, `tracking_updated`, `delivered`, and `returned` when a parcel comes back.

## The event envelope

The envelope is the same on the account feed, on the order's events and in a webhook body:

```json
{
  "id": "9c8b7a6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
  "type": "order.shipped",
  "api_version": "2026-10-24",
  "created_at": "2026-10-27T14:05:12Z",
  "seq": 1234,
  "environment": "live",
  "reason_code": null,
  "data": {
    "order": {
      "id": "7a3e9c10-2b4d-4f61-9e8a-5c6d7e8f9a0b",
      "number": "PA-2026-0007",
      "reference": "SHOP-10231",
      "status": "shipped",
      "shipments": [
        {
          "carrier": "FAN Courier",
          "tracking_number": "2057100012345",
          "tracking_url": "https://www.fancourier.ro/awb-tracking/?awb=2057100012345",
          "status": "shipped",
          "shipped_at": "2026-10-27T14:05:00Z",
          "delivered_at": null
        }
      ],
      "metadata": {
        "shop_order_id": "10231"
      },
      "…": "the rest of the order, exactly as GET /v1/orders/{id} returned it at that moment"
    }
  }
}
```

| Field | Meaning |
| --- | --- |
| `id` | The event id. A webhook delivery of this event carries it as `event_id`; the delivery's own id is `webhook-id`. |
| `type` | One of the types below. |
| `api_version` | The API version the payload follows — `2026-10-24`. |
| `seq` | Per partner and per environment, gap-free, strictly increasing. The authoritative order of everything that happened. |
| `reason_code` | Set when a transition had a reason you can act on — a hold, a rejection, a reconciliation. |
| `data` | The object the event is about: `order` for `order.*`, `quote` for `quote.*`, `file` for `file.*`, `endpoint` for `webhook_endpoint.*`, `key` for `key.*` (never the secret), `usage` for `usage.warning`, `{}` for `test.ping`. Rendered once at event time and stored; `data.order` is exactly what `GET /v1/orders/{id}` returned then, and a redelivery sends the same bytes. |

## Event types

| Type | `data` key | Produced by | When |
| --- | --- | --- | --- |
| `order.received` | `order` | you | Your `POST /v1/orders` was accepted for pricing. |
| `order.priced` | `order` | pricing | Every item has a price; the order is `pending_review`. |
| `order.pricing_failed` | `order` | pricing | An item needs a hand to price; an engineer prices it by hand or declines the order with a reason. |
| `order.accepted` | `order` | LayerCrew | An engineer accepted the order; the price is frozen and payment instructions are on it. |
| `order.rejected` | `order` | LayerCrew | The order was declined; `data.order.rejection` carries the code, the message and the engineer's note. |
| `order.on_hold` | `order` | LayerCrew | We need something before going on; `data.order.hold.reason` says what. |
| `order.released` | `order` | LayerCrew | The hold is over; the order is back in the state it had before. |
| `order.in_production` | `order` | LayerCrew | Printing started. |
| `order.ready_to_ship` | `order` | LayerCrew | Passed quality check and packed. |
| `order.shipped` | `order` | courier | Handed to the courier; `data.order.shipments` carries carrier, tracking number and URL. |
| `order.tracking_updated` | `order` | courier | The courier reported a new tracking status. |
| `order.delivered` | `order` | courier | The parcel arrived. Final. |
| `order.cancelled` | `order` | you | Cancelled by you, or by us at your request. Final. |
| `order.paid` | `order` | LayerCrew | Your transfer arrived; `data.order.payment.status` is `paid`. |
| `order.invoiced` | `order` | LayerCrew | An invoice was issued; fetch it with `GET /v1/orders/{id}/invoices`. |
| `order.returned` | `order` | courier | The parcel came back to us; we write to you. |
| `order.recipient_redacted` | `order` | system | Your customer's personal data on this order was erased, on your request or at the end of the retention window. |
| `quote.priced` | `quote` | pricing | The quote has a price. |
| `quote.failed` | `quote` | pricing | Pricing stopped; `data.quote.failure_code` says why and what to change. |
| `file.ready` | `file` | system | `complete` verified the upload; the file can be quoted and ordered. |
| `file.rejected` | `file` | system | `complete` refused the upload; `data.file.rejection_code` says why. |
| `file.purged` | `file` | system | The file's bytes were deleted at the end of its retention. |
| `agreement.accepted` | `agreement` | you | Your account owner accepted the integration and data processing agreements. |
| `agreement.countersigned` | `agreement` | LayerCrew | LayerCrew countersigned; live keys can be issued. |
| `key.issued` | `key` | you | An API key was created (never carries the secret). |
| `key.revoked` | `key` | you | An API key was revoked. |
| `webhook_endpoint.disabled` | `endpoint` | system | An endpoint was disabled after sustained failures or a 410; an email goes out too. |
| `webhook_endpoint.secret_rotated` | `endpoint` | you | The signing secret was rotated; both secrets are valid for 24 hours. |
| `partner.suspended` | `partner` | LayerCrew | The account was suspended; keys answer `PARTNER_INACTIVE` until it is active again. |
| `partner.activated` | `partner` | LayerCrew | The account is active. |
| `usage.warning` | `usage` | system | A monthly cap on your plan reached 80 percent; once per cap per month. |
| `test.ping` | `{}` | you | You asked for a test delivery with `POST /v1/webhook-endpoints/{id}/ping`. |

## Polling the feed

```bash
curl "https://engine.layercrew.com/v1/events?after=1200&limit=100&types=order.shipped,order.delivered" \
  -H "Authorization: Bearer $LAYERCREW_API_KEY"
```

```node
let after = Number(await store.get("layercrew:last_seq")) || 0;
for (;;) {
  const res = await fetch(`https://engine.layercrew.com/v1/events?after=${after}&limit=200`, {
    headers: { Authorization: `Bearer ${process.env.LAYERCREW_API_KEY}` },
  });
  const { data, meta } = await res.json();
  for (const event of data) {
    await handle(event);          // idempotent on event.id
    after = event.seq;
    await store.set("layercrew:last_seq", String(after));
  }
  if (!meta.has_more) break;
}
```

Store the last `seq` you processed and ask for what came after it; `meta.next_after` and `meta.has_more` drive the loop. `GET /v1/orders/{id}/events` does the same for one order. Webhooks carry the same events sooner; the feed is the truth when a webhook is late or missed — reconcile against it on a timer and on every restart. See [Webhooks](https://layercrew.com/developers/webhooks).
