# Inventory API

URL: https://smartcut.dev/docs/ecommerce-inventory

> Manage your store's materials and stock over HTTPS — create, update and delete product lines and the sheets beneath them, adjust quantities atomically, and sync from an ERP with your existing SmartCut API key.

If you run a [hosted SmartCut store](/docs/ecommerce), you can manage its inventory from your own systems — an ERP, a warehouse system, a stock-control spreadsheet, a nightly script. Everything the admin Inventory page does to materials and stock is available over HTTPS, through the **same validated path**, so nothing the API does can put your store into a state the admin couldn't.

The full endpoint reference, with request/response schemas and a try-it console, is at **[/api-docs/ecommerce](/api-docs/ecommerce)**. This page explains the model and the things worth knowing before you write against it.

## The model

Inventory has two levels:

- A **material** is a product line — "18mm Oak MFC". It carries a `code` that is unique within your store, plus the defaults shared by everything beneath it: price, colour, grain, category, edge banding.
- A **stock** item is one cuttable sheet or length of that material, with its own dimensions and quantity.

One material has many stock items — typically one per thickness, or per sheet size.

```text
Material  OAK-18  "18mm Oak MFC"   cost 42.50
   ├── Stock  2440 × 1220 × 18   q 40
   └── Stock  3050 × 1220 × 18   q 12
```

Stock is always addressed **under its material**, so the relationship comes from the URL and is never something you have to send:

```bash
POST /ecommerce/api/materials/{materialId}/stock
```

## Credentials

You authenticate with your **existing SmartCut API key** — the same one the optimisation API uses.

1. Sign in at [smartcut.dev/account](https://smartcut.dev/account). You need an active **e-commerce subscription**.
2. Copy your **API key** from the account page.
3. Send it as an `Authorization` header. The value is the **raw key — no `Bearer ` prefix**.

```bash
curl https://api.smartcut.dev/ecommerce/api/materials \
  -H 'Authorization: YOUR_API_KEY'
```

:::caution
Your API key can change and delete live catalogue data. Keep it in an environment variable, never commit it, and don't ship it to a browser.
:::

If your account belongs to more than one store, add `x-smartcut-org: <slug-or-id>` to say which. Without it, ambiguous requests return `409` and list your options rather than guessing.

## Creating a product line

A material needs a `code` and a `name`. Everything else is optional and inherited by its stock.

```bash
curl -X POST https://api.smartcut.dev/ecommerce/api/materials \
  -H 'Authorization: YOUR_API_KEY' -H 'Content-Type: application/json' \
  -d '{ "code": "OAK-18", "name": "18mm Oak MFC", "t": [18], "cost": 42.50 }'
```

Then add stock beneath it. Set `ecommerce: true` for anything you sell — those rows are validated more strictly and require a positive `cost`.

```bash
curl -X POST https://api.smartcut.dev/ecommerce/api/materials/{id}/stock \
  -H 'Authorization: YOUR_API_KEY' -H 'Content-Type: application/json' \
  -d '{ "l": 2440, "w": 1220, "t": 18, "q": 40, "cost": 42.50, "ecommerce": true }'
```

Send an array to create a batch — `{ "materials": [ … ] }` or `{ "stock": [ … ] }`. Batches are **all or nothing**: if any row fails, nothing is written and the response tells you which row index was at fault.

## Inheritance

Stock inherits `cost`, `grain`, `category`, `finish`, edge banding and the other shared fields from its material. **Set only what differs.** Reads always return the values actually in effect, so you never have to resolve the hierarchy yourself.

Two rules worth knowing:

- **Setting an inherited field on a stock item pins it.** That sheet keeps your value, and later edits to the material no longer move it. This is automatic — you don't manage a flag.
- **Setting it to `null` un-pins it**, and the item resumes following its material.

```bash
# this 3050mm sheet costs more than the material default
curl -X PATCH .../stock/{id} -d '{ "cost": 49.99 }'

# changed our minds — follow the material again
curl -X PATCH .../stock/{id} -d '{ "cost": null }'
```

## Changing quantities

Use **adjust**, not `PATCH`, for stock movements:

```bash
curl -X POST https://api.smartcut.dev/ecommerce/api/stock/{id}/adjust \
  -H 'Authorization: YOUR_API_KEY' -H 'Content-Type: application/json' \
  -d '{ "delta": -5, "reason": "goods out" }'
```

`delta` is relative — negative removes, positive adds. It's a single atomic operation, so two systems adjusting the same item at the same moment can't oversell it. A `PATCH` of `q` reads and writes separately and can lose a concurrent update.

Two behaviours to rely on:

- **The row is never deleted at zero.** It stays at `0`, so your next sync still finds it.
- **Going below zero is refused** with `409` and reports how many are actually available, rather than silently clamping. If you see this, your stock model and SmartCut's have diverged.

Items marked `unlimitedQuantity` return `200` with `adjusted: false` — their quantity isn't tracked.

## Syncing from an ERP

Use `PUT /ecommerce/api/materials/by-code/{code}` — it creates the material if it's absent and updates it if it's present, keyed on **your own product code**. The same request can be replayed safely, so a nightly job needs no state and never has to check existence first.

```bash
curl -X PUT https://api.smartcut.dev/ecommerce/api/materials/by-code/OAK-18 \
  -H 'Authorization: YOUR_API_KEY' -H 'Content-Type: application/json' \
  -d '{ "name": "18mm Oak MFC", "t": [18], "cost": 44.95 }'
```

It responds `201` when it created and `200` when it updated; the `created` flag tells you which. URL-encode codes containing spaces or slashes.

To find stock without tracking SmartCut ids, filter the flat list by material code: `GET /ecommerce/api/stock?code=OAK-18`.

## When something is wrong

Errors carry a machine-readable `code` and name the field. **Unrecognised fields are rejected, not ignored** — a misspelled key fails loudly instead of returning success without changing anything.

```jsonc
{
  "error": "Validation failed",
  "code": "VALIDATION_FAILED",
  "errors": [
    { "field": "quantiy", "code": "UNKNOWN_FIELD",
      "message": "Unknown field \"quantiy\" for stock. Accepted fields: cost, discount, …" }
  ]
}
```

| `code` | Meaning |
|---|---|
| `UNKNOWN_FIELD` | Not a field on this resource. The message lists the accepted set. |
| `READONLY_FIELD` | Real, but managed by SmartCut — `id`, `area`, and a stock item's `material` name. |
| `FORBIDDEN_FIELD` | Internal. Most often `db_materialId` — create stock under its material instead. |
| `REQUIRED_FIELD` / `INVALID_VALUE` | Missing or out of range. The message names the value. |
| `DUPLICATE_CODE` | Another material already uses this code. In a batch, reported against the row index. |
| `INSUFFICIENT_QUANTITY` | An adjust would go below zero. Carries `available`. |

Branch on `code`; the message text is for people and may be reworded.

A few things live on the material rather than on stock, and the error will say so: product `code`, the thickness **list**, and the form (`sheet` / `linear` / `roll`). A bonded board is one material thickness written `"18,36"`; creating it produces a stock item for each half.

## Rate limits

Inventory calls get their own budget, well above general read traffic, so a catalogue sync of a few hundred rows is fine. Exceeding it returns `429` with a `Retry-After` header — wait that many seconds and continue.

## See also

- [Endpoint reference](/api-docs/ecommerce) — every route, schema and response, with a try-it console.
- [Store config MCP](/docs/ecommerce-mcp) — the same operations from an LLM client, if you'd rather ask than integrate.
- [Hosted store](/docs/ecommerce) — the storefront, analytics, webhooks and order APIs.
