Flow · 5 steps

Attach a photo to a SKU

Files — photos, spec sheets, drawings — hang off SKUs and products. The bytes go to a private bucket and the journal records the attachment as a fact; every read answers a signed URL that expires, so a browser can show the picture without the bucket ever being public. The upload is the raw file as the request body, capped at 100 MiB, and carries its own idempotency key: a retry is the same file, not a second one.

Services involved

A filled dot is a read; a ring is a call that changes something.

  • Inventory Manager: https://inventory.getmanifold.io

Every call carries Authorization: Bearer $MANIFOLD_TOKEN unless its step says otherwise (getting a token). Run a flow in your sandbox, not your live organization: see Before you start.

1 Create the item

A file needs an owner; an unknown SKU is refused with 404 before a byte is stored.

POST /items Inventory Manager

Request · application/json
{
  "sku": "WIDGET-PHOTO",
  "name": "Widget, photographed",
  "quantity": 0
}

Answers 201. What the answer holds: the operation's example.

Records inventory.item-created.

2 Upload the photo

The raw bytes as the body, the media type in Content-Type, the filename and the upload's reference in the query string. The answer is the asset with a signed URL good for fifteen minutes.

POST /items/WIDGET-PHOTO/assets?filename=front.png&reference=upload%3AWIDGET-PHOTO%3Afront Inventory Manager

Request · image/png
‰PNG

(not really a png)
Response · 201
{
  "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"
}

From your answer, later steps send id (here ast_01a15861-f150-7e8f-9a0b-1c2d3e4f5a6b) as {assetId}.

Records inventory.asset-uploaded.

3 Retry it

The same reference again — a client that never saw the first answer. The service acknowledges it from the journal: same asset id, nothing read, nothing stored, nothing appended.

POST /items/WIDGET-PHOTO/assets?filename=front.png&reference=upload%3AWIDGET-PHOTO%3Afront Inventory Manager

Request · image/png
‰PNG

(not really a png)

Answers 201. What the answer holds: the operation's example.

Records nothing.

4 List the SKU's files

Newest first, each with a fresh signed URL — the one to put in an <img src>.

GET /items/WIDGET-PHOTO/assets Inventory Manager

What the answer holds: the operation's example.

5 Remove it

The fact commits first; the object is deleted after. A retry meets 404.

DELETE /assets/ast_01a15861-f150-7e8f-9a0b-1c2d3e4f5a6b Inventory Manager

Response
{
  "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"
}

Records inventory.asset-removed.

What a recorded fact is: Facts and the journal. Why a repeated step changes nothing: Idempotency and retries.