Inventory Manager API · assets
Attach a file to a SKU
POST /items/{sku}/assets
The body is the raw file — not multipart, not JSON — streamed into the private bucket, with filename and the upload's reference in the query string and the media type in Content-Type. At most 100 MiB: a larger Content-Length is refused before a byte is read, and a body that grows past the cap is cut off with 413 AssetTooLarge and nothing stored. Appends inventory.asset-uploaded and answers the asset with a signed URL (201). Idempotent on reference: the asset id is derived from it, so a retry re-mints the same ast_ id, is acknowledged from the journal and appends nothing. 404 ItemNotFound if the owner does not exist; 404 AssetNotFound if the retried upload's asset has since been removed.
Access
Requires products:write or stock:write.
Any one of these scopes is enough.
How scopes come from roles: Scopes and roles. Getting a token: Authentication and app tokens.
Retries
Idempotent on reference: the asset id is derived from it, so a retry re-mints the same ast_ id, is acknowledged from the journal and appends nothing.
A retry with the same key answers 201, with what the first call recorded.
Why every write carries a key: Idempotency and retries.
Facts appended
inventory.asset-uploaded
What a fact is: Facts and the journal.
AI agent tool
No agent tool. The file's raw bytes are the request body, which a JSON tool call cannot carry; photos and spec sheets are uploaded in the admin.
Connecting an agent: AI agents.
Example
From Attach a photo to a SKU, step 2: Upload the photo.
curl -X POST 'https://inventory.getmanifold.io/items/WIDGET-PHOTO/assets?filename=front.png&reference=upload%3AWIDGET-PHOTO%3Afront' \
-H "Authorization: Bearer $MANIFOLD_TOKEN" \
-H 'Content-Type: image/png' \
--data-binary @front.pngimport { openAsBlob } from "node:fs";
const response = await fetch("https://inventory.getmanifold.io/items/WIDGET-PHOTO/assets?filename=front.png&reference=upload%3AWIDGET-PHOTO%3Afront", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.MANIFOLD_TOKEN}`, "Content-Type": "image/png" },
body: await openAsBlob("front.png"),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const answer = await response.json();(the file front.png, sent as image/png){
"id": "ast_01a15861-f150-7e8f-9a0b-1c2d3e4f5a6b",
"owner": {
"_tag": "sku",
"sku": "WIDGET-PHOTO"
},
"filename": "front.png",
"contentType": "image/png",
"size": 27,
"uploadedAt": "2026-08-17T09:00:00.000Z",
"url": "https://files.example/org_…/ast_3c1f9a2e-…?X-Amz-Expires=900&X-Amz-Signature=…",
"urlExpiresAt": "2026-08-17T09:00:00.000Z"
}The panel writes this call in other languages. It does not send it: Manifold's APIs do not take calls from a web
page on another site, so run the call from your own code or terminal. The whole API as OpenAPI: inventory-manager.openapi.json. Base URL https://inventory.getmanifold.io.
Errors
Every refusal is a status and a JSON body whose _tag names it. A refused call changes nothing. Some can answer a retry of a call that already worked: they are marked.
400
| Refusal | What it means | What to do |
|---|---|---|
AssetUnreadable reason | The file is empty, or its Content-Type is not one a file can be stored as. | Send the file's bytes with its own Content-Type. |
401
| Refusal | What it means | What to do |
|---|---|---|
Unauthorized reason | The token is missing, expired or not one Manifold issued. | Get a fresh token and send the call again. |
TokenAlreadyUsed tokenId | A single-use app token was already spent. | Mint a new token for this call. |
402
| Refusal | What it means | What to do |
|---|---|---|
SubscriptionRequired status | The organization's subscription has lapsed, so writes are refused. | An owner settles billing. Reads still work meanwhile. |
403
| Refusal | What it means | What to do |
|---|---|---|
TenantRequired userId | The token names no organization. | Choose the organization, then get a new token. |
ScopeRequired required | The token's roles do not grant the scope this call needs. | Ask an owner or admin for a role that grants it. |
404
| Refusal | What it means | What to do |
|---|---|---|
ItemNotFound sku | The owner does not exist. | Check the id in the path or the body. |
AssetNotFound assetId Can answer a retry | No such record in this organization. | If this answers a retry, your first call probably landed. Read the record before doing anything else. |
409
| Refusal | What it means | What to do |
|---|---|---|
MirroredFromSource source | This area is fed by your source system until the organization goes live. | Make the change in the source system, or go live first. |
413
| Refusal | What it means | What to do |
|---|---|---|
AssetTooLarge limit, size | The file is over the size limit. limit gives it in bytes. | Send a smaller file. |
503
| Refusal | What it means | What to do |
|---|---|---|
AssetStoreUnavailable reason | File storage did not answer, so nothing was stored. | Send the same request again, with the same key. |
RequestLogUnavailable tokenId, reason | The service could not record the request, so it did nothing. | Send the same request again, with the same key. |
How refusals work everywhere: Errors.
Used in
- Attach a photo to a SKU, step 2: Upload the photo
- Attach a photo to a SKU, step 3: Retry it