# Orders

> Create an order with your reference, items, your customer's address and white-label instructions; pricing, payment, acceptance, holds and cancelling.

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

An order is one delivery to one of your customers: one or more items — each a `ready` file in a material at a quantity — plus your own reference, the delivery address, white-label instructions and any metadata you want back. You create it with one call; we price it, review it, accept it, produce it, ship it and tell you about every step.

## Create an order

```bash
curl -X POST "https://engine.layercrew.com/v1/orders" \
  -H "Authorization: Bearer $LAYERCREW_API_KEY" \
  -H "Idempotency-Key: 2f5c1c2e-0f6a-4d2b-9a1e-7b3c4d5e6f70" \
  -H "Content-Type: application/json" \
  -d '{
  "reference": "SHOP-10231",
  "items": [
    {
      "file_id": "4f1b2c3d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "quantity": 2,
      "material": "PLA",
      "colour": "black",
      "nozzle": "0.4",
      "notes": "Visible face is the flat side."
    }
  ],
  "delivery": {
    "name": "Claire Martin",
    "company": "Atelier Martin",
    "phone": "+33 6 12 34 56 78",
    "email": "claire@example.com",
    "line1": "12 rue de la Paix",
    "line2": "Bâtiment B",
    "city": "Lyon",
    "postal_code": "69001",
    "country_code": "FR"
  },
  "white_label": {
    "sender_name": "Studio NS",
    "card_text": "Merci Claire — Studio NS",
    "packaging_preset": "neutral",
    "no_invoice_in_box": true,
    "language": "fr"
  },
  "notes": "Fit check before the batch, please.",
  "metadata": {
    "shop_order_id": "10231",
    "customer_id": "c_8812"
  }
}'
```

```node
import crypto from "node:crypto";

const response = await fetch("https://engine.layercrew.com/v1/orders", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LAYERCREW_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(), // store it with your shop order so a retry reuses it
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    reference: "SHOP-10231",
    items: [{ file_id: "4f1b2c3d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", quantity: 2, material: "PLA", colour: "black" }],
    delivery: {
      name: "Claire Martin", phone: "+33 6 12 34 56 78", email: "claire@example.com",
      line1: "12 rue de la Paix", city: "Lyon", postal_code: "69001", country_code: "FR",
    },
    white_label: { sender_name: "Studio NS", card_text: "Merci Claire — Studio NS", language: "fr" },
    metadata: { shop_order_id: "10231" },
  }),
});
const payload = await response.json();
if (response.status === 409 && payload.error.code === "REFERENCE_TAKEN") {
  console.log("already created:", payload.error.details.order_id);
} else if (!response.ok) {
  throw new Error(`${payload.error.code}: ${payload.error.message}`);
}
console.log(payload.data.number, payload.data.status); // "PA-2026-0007" "received"
```

```python
import os, uuid, requests

response = requests.post(
    "https://engine.layercrew.com/v1/orders",
    headers={
        "Authorization": f"Bearer {os.environ['LAYERCREW_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),  # store it with your shop order so a retry reuses it
    },
    json={
        "reference": "SHOP-10231",
        "items": [{"file_id": "4f1b2c3d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "quantity": 2, "material": "PLA", "colour": "black"}],
        "delivery": {
            "name": "Claire Martin", "phone": "+33 6 12 34 56 78", "email": "claire@example.com",
            "line1": "12 rue de la Paix", "city": "Lyon", "postal_code": "69001", "country_code": "FR",
        },
        "white_label": {"sender_name": "Studio NS", "card_text": "Merci Claire — Studio NS", "language": "fr"},
        "metadata": {"shop_order_id": "10231"},
    },
    timeout=30,
)
payload = response.json()
if response.status_code == 409 and payload["error"]["code"] == "REFERENCE_TAKEN":
    print("already created:", payload["error"]["details"]["order_id"])
elif not response.ok:
    raise RuntimeError(f"{payload['error']['code']}: {payload['error']['message']}")
print(payload["data"]["number"], payload["data"]["status"])  # PA-2026-0007 received
```

The answer is `202` with the order in `received` and `pricing_status: pending`. Within minutes every item is priced and `order.priced` follows with the order in `pending_review`. The `Idempotency-Key` header is required: see [Idempotency](https://layercrew.com/developers/idempotency).

## The body

| Field | Required | Rules |
| --- | --- | --- |
| `reference` | yes | Your own identifier, up to 120 characters, unique among your orders in this environment. |
| `items[]` | yes | One to your plan's `max_items_per_order` (`max_test_items_per_order` in test). |
| `items[].file_id` | yes | A `ready` file of yours in the same environment. |
| `items[].quantity` | yes | 1 to your plan's `max_quantity_per_item`. |
| `items[].material` | yes | A `material` from `GET /v1/materials`. |
| `items[].colour` | no | A colour `id` of that material. Left out, the engineer picks at production. |
| `items[].nozzle` | no | From the material's `nozzles`; default `0.4`. |
| `items[].notes` | no | Up to 500 characters, read by the engineer. |
| `delivery` | yes | Your customer's address — below. |
| `white_label` | no | Sender name, card text, packaging — below. White label is on even when the block is left out. |
| `notes` | no | Up to 2000 characters, read by the engineer. |
| `metadata` | no | Up to 20 keys with string values up to 200 characters; returned untouched everywhere. |

### Reference

`reference` is your identifier for the order — a shop order number, a job id. It must be unique among your orders in the environment: a duplicate answers `409 REFERENCE_TAKEN` and `error.details.order_id` names the existing order, so even a retry without its `Idempotency-Key` finds its order. Look orders up by it with `GET /v1/orders?reference=SHOP-10231`.

### Items

- `file_id` must be a `ready` file of yours in the same environment — `409 FILE_NOT_READY` otherwise.
- `material`, and the optional `colour` id, come from `GET /v1/materials` and your plan — `422 MATERIAL_UNAVAILABLE` otherwise.
- A 3MF with several objects is priced as the plate it holds; `estimate.unit_yield` says how many parts one plate makes.
- Every string passes a cleaning step that drops control characters and markup before it is stored.

### Delivery

Your customer's address as the courier needs it: `name`, `phone` (the courier requires it), `line1`, `city`, `postal_code` and `country_code` (ISO 3166-1 alpha-2) are required; `company`, `email`, `line2` and `region` are optional outside Romania.

> **Romania.** A Romanian address needs `region` — the county (județ) in its Romanian spelling, for example `Cluj` or `Ilfov`. In Bucharest, `region` is `București` and `city` names the sector, for example `Sector 3`: the invoice and the courier label both need it.

Delivery fields are personal data of your customer. We process them under the data processing agreement, show them to production and shipping staff only, redact them after your plan's `recipient_retention_days` (90 days on the default plan) and at once on request: `DELETE /v1/orders/{id}/recipient` on a finished order keeps city, postal code and country for fiscal records and replaces the rest with `[redacted]`, on the order and on its stored events. `order.recipient_redacted` confirms it.

### White label

White label is on by default. Every order ships in neutral packaging with no LayerCrew marks on the parts, the packaging or the inserts, and no invoice in the box.

| Field | Default | Meaning |
| --- | --- | --- |
| `enabled` | `true` | Set `false` only if you want our standard packaging. |
| `sender_name` | your company name | The contact line on the courier label, up to 80 characters. |
| `card_text` | none | Printed on a card in the parcel, up to 500 characters; can differ on every order. |
| `packaging_preset` | `neutral` | `neutral`, or `partner_supplied` when you send us your packaging. |
| `no_invoice_in_box` | `true` | The invoice goes to you, never into the parcel. |
| `language` | your account's locale | The language of any printed insert, as a two-letter code. |

> **The courier label.** The courier label names LayerCrew as the shipping account (a courier requirement) and shows your sender name as the contact line; nothing in or on the parcel carries our name.

### Metadata and notes

`metadata` is yours: up to 20 keys with string values up to 200 characters, returned untouched on the order and in every webhook — a shop order id, a customer id, a campaign. `notes` (up to 2000 characters) and `items[].notes` are read by the engineer who reviews the order.

## Pricing

`pricing` carries `currency` (EUR), `subtotal`, `discount_total`, `shipping_total`, `tax_total`, `tax_treatment` and `grand_total`, each a string with two decimals. Per item: `list_unit_price`, `discount_pct`, `unit_price`, `line_total` and the slicer's `estimate`. The block is provisional at `order.priced` (`final: false`) and frozen at acceptance (`final: true`); once frozen it is the price you pay.

| `tax_treatment` | When |
| --- | --- |
| `standard` | Romanian VAT applies — a Romanian company, or a company without a valid EU VAT number. |
| `reverse_charge` | An EU company with a valid VAT number: no VAT on the invoice, you account for it. |
| `export` | Delivery outside the EU. |
| `exempt` | An exemption agreed in writing. |
| `test` | A test order; nothing is invoiced. |

_pricing and payment on an accepted order_

```json
{
  "pricing": {
    "currency": "EUR",
    "subtotal": "24.80",
    "discount_total": "0.00",
    "shipping_total": "14.99",
    "tax_total": "0.00",
    "tax_treatment": "reverse_charge",
    "grand_total": "39.79",
    "final": true
  },
  "payment": {
    "method": "bank_transfer",
    "status": "unpaid",
    "ship_before_payment": false,
    "amount_due": "39.79",
    "instructions": {
      "beneficiary": "LAYER CREW SRL",
      "iban": "RO49AAAA1B31007593840000",
      "bank": "Example Bank",
      "reference": "PA-2026-0007"
    },
    "paid_at": null
  }
}
```

## Payment

Payment is by bank transfer against the order. `payment.instructions` appear at acceptance — `beneficiary`, `iban`, `bank` and the `reference` to put on the transfer, which is the order number. On the default plan production starts once the transfer arrives and `order.paid` is emitted; `ship_before_payment: true` on a plan or an agreed override starts production at acceptance instead. The invoice follows as `order.invoiced`: `GET /v1/orders/{id}/invoices` lists it and `GET /v1/orders/{id}/invoices/{invoice_id}/download-url` returns a 15-minute link to the PDF. Card payment is a later addition; a plan with payment terms is agreed in writing.

## Acceptance and rejection

Every order is read by an engineer before it is accepted, usually within one business day — the same design-for-manufacturing look every file gets. An accepted order carries `accepted_at`, a frozen `pricing` block and `payment.instructions`. A declined order is `rejected` with `rejection.code`, `rejection.message` (the fact and the next step) and `rejection.note` (what the engineer wrote for you).

| `rejection.code` | What it means | Next step |
| --- | --- | --- |
| `needs_split` | The part is laid out larger than our print bed. | Split it into sub-assemblies, or ask us to take it by hand. |
| `design_change_needed` | A feature will not print as drawn; the note says which. | Change the design and order again. |
| `material_unavailable` | The material is out of stock for this run. | Pick another material, or wait for stock. |
| `out_of_capacity` | The queue is full for the date you need. | Order again with a later date in mind. |
| `address_unserviceable` | The courier has no route to the address as given. | Correct the address and order again. |
| `price_review` | The order needs a price by hand. | We write to you with the number. |
| `other` | See the note. | The note says what to do. |

## Holds

When we need something from you — a question about a feature, a colour that ran out — the order goes `on_hold` with `hold.reason` and `hold.since` and `order.on_hold` is emitted. `order.released` brings it back to the state it had before. Holds are possible from `received` through `ready_to_ship`.

## Cancelling

`POST /v1/orders/{id}/cancel` with an optional `reason` is allowed while the order is `received`, `pending_review` or `accepted` — before production starts. Once printing has started the call answers `409 CANNOT_CANCEL`; write to us from the order page in your dashboard and we tell you what is still possible. A cancelled order is final and is never invoiced.

## Reading orders

- `GET /v1/orders/{id}` takes the id or the number (`PA-2026-0007`).
- `GET /v1/orders?status=&reference=&created_after=&page=&per_page=` lists newest first, 25 a page and up to 100.
- `GET /v1/orders/{id}/events` returns the order's events in `seq` order; `GET /v1/events` the whole account's. See [Lifecycle and events](https://layercrew.com/developers/lifecycle).
- `links.self` and `links.events` on every order point at both.

Partner orders are yours alone: they are invisible on the customer-facing routes and on the customer dashboard, and visible under Integrations → Orders in yours. Every endpoint is in the [reference](https://layercrew.com/developers/reference#tag-orders).
