---
title: "Create one order"
method: POST
path: "/orders"
tags: ["orders"]
---

# Create one order

`POST /orders`

## Request

- Base URL: `https://0.0.0.0:3000/api/v1`
- URL: `https://0.0.0.0:3000/api/v1/orders`
- Auth: API key in header `x-api-key`

Name the SKU and where it goes. The SKU decides the product, the variant and the price — sending a product id alongside would let a client name its own price, so the server reads all three from the SKU and ignores anything else you send about them.

Two ways to name what you want: `product_variant_id` from the catalogue, or `product_id` plus an `options` map of catalog-v2 axis codes (the server resolves those to a physical SKU, creating it on first use).

An `Idempotency-Key` header is REQUIRED. Without one, a dropped response leaves you unable to retry safely — which is exactly how duplicate orders get made. Replaying a key returns the original order with status 200 instead of 201.

## Headers

- `Idempotency-Key` string, required

## Request body

- object
  - `external_id` string, required: Your own order id. Not unique here — one marketplace order can split into several rows — but it is how support finds anything.
  - `product_variant_id` integer: The SKU, from `GET /catalog`. Send this OR `product_id` + `options`.
  - `product_id` integer: Catalog v2: the product, paired with `options`.
  - `options` object: Catalog v2: one active value code per option axis, e.g. `{ "method": "P", "color": "KL" }`. Keys are axis codes and are NOT converted — send them exactly as the catalogue lists them.
  - `quantity` integer, required: 1–10000.
  - `shipping` object, required: `name`, `company`, `email`, `phone`, `line1`, `line2`, `city`, `state`, `zip`, `country`. `name`, `line1`, `city`, `state`, `zip` and `country` are REQUIRED and must be the customer's real details: a placeholder such as `1`, `x`, `n/a` or a masked `73** ****` is refused with `invalid_request`, and `error.fields` names each field. `state` is the two-letter code, e.g. `IL` or `WA` — a full name such as `Texas`, or anything longer than two letters, is refused the same way. `company`, `email`, `phone` and `line2` are optional.
  - `shipping_type` string, required: REQUIRED. Who ships the parcel — exactly `Ship by OP` or `Ship by Platform`. `Ship by OP`: we ship it, we buy the label, and postage is added to what you pay. `Ship by Platform`: you or the marketplace ships it and already holds the label, so no postage is charged and the customer-ship price applies. There is no default — an order with none would be priced as though you shipped it, and no label could be bought for it. A carrier service (`Standard`, `express`) is a different thing and is refused here. Letter case does not matter; `ship by op` is accepted and stored as `Ship by OP`.
  - `marketplace` string: Which shop it came from, e.g. `Etsy UK`.
  - `placed_at` string: When the marketplace took the order. Defaults to now.
  - `deadline` string: When it must ship by.
  - `image_url` string, required: REQUIRED. The design to print. An order cannot be produced without one, so it is refused rather than accepted and held.
  - `positions` unknown[]: Optional. Where each design goes when one item carries several, or a face needs two files. One entry per decorated face: `position` (`FRONT`, `BACK`, `BOTTOM`, `INSIDE`, `WRAP`, `CENTER`, `LEFT`, `RIGHT`, `BASE`), `method` (`ENGRAVE`, `PHOTO_ENGRAVE`, `UV_PRINT`, `HEAT_TRANSFER`) and `files`, each `{ type: PRINT | CUT | ENGRAVE, url }`. Stored exactly as sent and returned on `GET /orders/{id}`; URLs are never changed. It does not change the SKU or the price, and the faces are not checked against the SKU's options.
    - unknown
  - `mockup_url` string, required: REQUIRED (or `mockup_id`). The mockup picture — what the listing shows. Stored once per picture per account, so re-sending the same link reuses the same mockup rather than making another.
  - `mockup_id` integer: A mockup already in your library, from a previous order. An alternative to `mockup_url`; either one satisfies the mockup requirement.
  - `add_on` boolean: `true` adds the product's add-on — a gift box, say — to every unit of this order, at the per-unit price `GET /catalog` shows as `add_on`. It is billed inside `base_cost`. Refused on a product whose `add_on` is `null`.
  - `note` string: Anything the floor should read.

## Response `201`

Created. Status 200 instead when the Idempotency-Key was a replay.

## Other responses

- `401`: No key, or a key that is unknown, revoked or expired. One message for all four — telling you which would confirm to a guesser that a key exists.
- `403`: The key is valid but its owner may not do this.
- `422`: The body or query is malformed. Field-level detail arrives in `error.fields`.
- `429`: Over the per-key limit. `Retry-After` says how many seconds to wait.
- `500`: Our fault. `error.request_id` matches the X-Request-Id header — quote it to support.

---

[API](https://skmtc.dev/opcreative/apis/opcreative-api.md) · [All operations](https://skmtc.dev/opcreative/apis/opcreative-api/llms.txt) · [OpenAPI document](https://skmtc.dev/opcreative/apis/opcreative-api/revisions/105bb30aa87e?raw)
