STUFFOWL / DEVELOPERS

API reference

All paths below are relative to /api/v1. Download OpenAPI 3.1 JSON →

Account

GET/meCurrent user, inventory, membership, and quota

inventory:read

Success: 200 · application/json

Inventory

GET/homeHome dashboard and hierarchy

inventory:read

Success: 200 · application/json

GET/kindsAvailable node kinds

inventory:read

Success: 200 · application/json

GET/nodesList and filter inventory

inventory:read

Stable ascending UUID order. Follow nextCursor until null. Pages are not a point-in-time snapshot; records created during a scan may require another pass.

Query parameters

{
  "limit": {
    "type": "integer",
    "minimum": 1,
    "maximum": 100,
    "default": 50
  },
  "cursor": {
    "type": "string",
    "format": "uuid"
  },
  "kind": {
    "enum": [
      "location",
      "box",
      "bag",
      "item"
    ]
  },
  "parentId": {
    "type": "string",
    "description": "Parent UUID or root for top-level records."
  },
  "unnamed": {
    "type": "boolean"
  },
  "hasPhoto": {
    "type": "boolean"
  },
  "archived": {
    "type": "boolean"
  }
}

Success: 200 · application/json

POST/nodesCreate a record

inventory:write

Items and places need a name. Boxes and bags may be unnamed. For unnamed photo capture, use the capture endpoint.

JSON request schema

{
  "type": "object",
  "properties": {
    "kind": {
      "enum": [
        "location",
        "box",
        "bag",
        "item"
      ]
    },
    "name": {
      "type": "string",
      "maxLength": 500
    },
    "parentId": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid"
    },
    "quantity": {
      "type": [
        "number",
        "string",
        "null"
      ]
    }
  },
  "required": [
    "kind"
  ]
}

Success: 201 · application/json

GET/nodes/{slug}Record detail and current revision

inventory:read

Use a record UUID as slug. Legacy printed slugs remain resolvable. Includes parent, breadcrumb, attachments, tags, label status, readOnly, and moveCandidates. intent=preview-subtree-delete returns a deletion preview instead.

Query parameters

{
  "intent": {
    "enum": [
      "preview-subtree-delete"
    ]
  }
}

Success: 200 · application/json

PATCH/nodes/{slug}Update record fields

inventory:write

Omitted fields stay unchanged; nullable fields can be cleared with null. PAT and OAuth integrations must send If-Match with the last node revision to reject concurrent edits with 409. Dates use YYYY-MM-DD; amounts use integer cents.

JSON request schema

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1
    },
    "description": {
      "type": [
        "string",
        "null"
      ]
    },
    "quantity": {
      "type": [
        "number",
        "string",
        "null"
      ],
      "description": "Non-negative quantity; returned as decimal text."
    },
    "unit": {
      "type": [
        "string",
        "null"
      ]
    },
    "status": {
      "type": [
        "string",
        "null"
      ]
    },
    "brand": {
      "type": [
        "string",
        "null"
      ]
    },
    "model": {
      "type": [
        "string",
        "null"
      ]
    },
    "serialNumber": {
      "type": [
        "string",
        "null"
      ]
    },
    "condition": {
      "type": [
        "string",
        "null"
      ]
    },
    "currency": {
      "type": [
        "string",
        "null"
      ]
    },
    "purchasePriceCents": {
      "type": [
        "integer",
        "null"
      ],
      "minimum": 0
    },
    "replacementCostCents": {
      "type": [
        "integer",
        "null"
      ],
      "minimum": 0
    },
    "purchasedAt": {
      "type": [
        "string",
        "null"
      ],
      "format": "date"
    },
    "warrantyExpiresAt": {
      "type": [
        "string",
        "null"
      ],
      "format": "date"
    }
  }
}

Success: 200 · application/json

DELETE/nodes/{slug}Delete one record

inventory:delete

Irreversible: removes the record and its attachments; direct children move to its parent. This does not delete the whole subtree.

Success: 200 · application/json

POST/nodes/{slug}Confirm subtree deletion or use legacy move

inventory:delete

Preview deletion with GET ?intent=preview-subtree-delete and submit its fingerprint and short code. Changed subtrees return 409. Integrations should use /scan/move for ordinary moves with inventory:write.

JSON request schema

{
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "intent": {
          "const": "delete-subtree"
        },
        "confirmation": {
          "type": "object",
          "properties": {
            "fingerprint": {
              "type": "string"
            },
            "shortCode": {
              "type": "string"
            }
          },
          "required": [
            "fingerprint",
            "shortCode"
          ]
        }
      },
      "required": [
        "intent",
        "confirmation"
      ]
    },
    {
      "type": "object",
      "properties": {
        "intent": {
          "const": "move"
        },
        "parentId": {
          "type": [
            "string",
            "null"
          ]
        }
      },
      "required": [
        "intent",
        "parentId"
      ]
    }
  ]
}

Success: 200 · application/json

GET/nodes/{slug}/childrenDirect contents of a container

inventory:read

Success: 200 · application/json

GET/searchSearch inventory

inventory:read

Fuzzy name, code, tag, and breadcrumb search. Returns q, results, and exactMatch.

Query parameters

{
  "q": {
    "type": "string",
    "minLength": 2
  }
}

Success: 200 · application/json

GET/cleanupRecords needing attention

inventory:read

Success: 200 · application/json

History & export

GET/historyPaginated audit history

inventory:read

Newest first. Events survive record deletion. Integration events identify the account and token or OAuth client.

Query parameters

{
  "limit": {
    "type": "integer",
    "minimum": 1,
    "maximum": 100,
    "default": 50
  },
  "cursor": {
    "type": "string",
    "format": "uuid"
  },
  "nodeId": {
    "type": "string",
    "format": "uuid"
  }
}

Success: 200 · application/json

GET/nodes/{slug}/exportExport a record and its subtree

inventory:read

Includes fields, breadcrumbs, tags, and attachment metadata. JSON is the default; format=csv downloads CSV. Images require separate authenticated downloads. Export remains available when storage is read-only.

Query parameters

{
  "format": {
    "enum": [
      "json",
      "csv"
    ],
    "default": "json"
  }
}

Success: 200 · application/json

Photos & receipts

GET/nodes/{slug}/attachmentsList attachment metadata

inventory:read

Success: 200 · application/json

POST/nodes/{slug}/attachmentsUpload a photo or receipt

photos:write

Multipart upload, maximum 15 MB per file. Photos accept JPEG, PNG, WebP, HEIC, or HEIF. Receipts also accept PDF. Images are normalized and compressed by the server. Storage exhaustion returns 402.

Multipart request schema

{
  "type": "object",
  "properties": {
    "kind": {
      "enum": [
        "photo",
        "receipt"
      ]
    },
    "file": {
      "type": "string",
      "format": "binary"
    },
    "caption": {
      "type": "string"
    }
  },
  "required": [
    "kind",
    "file"
  ]
}

Success: 201 · application/json

GET/attachments/{id}Attachment metadata and download path

inventory:read

Success: 200 · application/json

GET/attachments/{id}/contentDownload a private image or PDF

photos:read

Returns original stored bytes with Content-Type image/jpeg or application/pdf. Foreign and unknown IDs both return 404. URLs are private; send the bearer header on every download.

Success: 200 · image/jpeg

DELETE/attachments/{id}Delete an attachment

photos:delete

Irreversible. Repoints or clears the cover photo when necessary.

Success: 200 · application/json

Tags & classification

GET/tagsList inventory tags

inventory:read

Success: 200 · application/json

GET/nodes/{slug}/tagsList a record’s tags

inventory:read

Success: 200 · application/json

POST/nodes/{slug}/tagsAdd a category or tag

inventory:write

Creates the tag if needed; attaching an existing tag is idempotent. Other tags remain unchanged.

JSON request schema

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": [
    "name"
  ]
}

Success: 201 · application/json

DELETE/nodes/{slug}/tags/{tagId}Remove a tag from a record

inventory:write

Success: 200 · application/json

Capture & scan

POST/scan/resolveResolve a printed code or URL

inventory:read

Unrecognized scans return HTTP 200 with ok:false and reason invalid or not-found.

JSON request schema

{
  "type": "object",
  "properties": {
    "code": {
      "type": "string"
    },
    "manual": {
      "type": "boolean"
    }
  },
  "required": [
    "code"
  ]
}

Success: 200 · application/json

POST/scan/moveMove a record

inventory:write

destId is a container UUID, or null for the top level. Invalid containment and cycles return 409.

JSON request schema

{
  "type": "object",
  "properties": {
    "itemId": {
      "type": "string",
      "format": "uuid"
    },
    "destId": {
      "type": [
        "string",
        "null"
      ],
      "format": "uuid"
    }
  },
  "required": [
    "itemId",
    "destId"
  ]
}

Success: 200 · application/json

GET/capture/contextPlaces, recent destinations, and capture context

inventory:read

Query parameters

{
  "bin": {
    "type": "string"
  }
}

Success: 200 · application/json

POST/captureNative capture workflow

inventory:write + photos:write

Existing mobile multipart workflow. Integrations use PATCH with If-Match instead of rename-item; finish with print=on additionally requires labels:print. Fields depend on intent; see the mobile capture contract in the repository. For scripts, prefer creating a record and uploading its attachment separately.

Multipart request schema

{
  "type": "object",
  "properties": {
    "intent": {
      "enum": [
        "mint-and-capture",
        "capture-item",
        "resolve-destination",
        "mint-place",
        "move-captured",
        "rename-item",
        "finish"
      ]
    }
  },
  "required": [
    "intent"
  ]
}

Success: 200 · application/json

GET/products/lookupLook up a retail barcode

inventory:read

Returns found and normalized gtin, with optional name, brand, model, and source. Provider unavailable returns 503.

Query parameters

{
  "code": {
    "type": "string"
  }
}

Success: 200 · application/json

Contents checks

GET/nodes/{slug}/checksActive and recent box checks

inventory:read

Success: 200 · application/json

POST/nodes/{slug}/checksStart or update a contents check

inventory:write

start needs no checkId. Other operations require checkId and current revision. mark/remove need entryId; mark also needs status; unexpected needs name; scan needs code. Stale revisions return 409.

JSON request schema

{
  "type": "object",
  "properties": {
    "intent": {
      "enum": [
        "start",
        "mark",
        "remove",
        "unexpected",
        "scan",
        "finish",
        "cancel"
      ]
    },
    "checkId": {
      "type": "string",
      "format": "uuid"
    },
    "revision": {
      "type": "integer",
      "minimum": 0
    },
    "entryId": {
      "type": "string",
      "format": "uuid"
    },
    "status": {
      "enum": [
        "present",
        "missing",
        "unchecked"
      ]
    },
    "name": {
      "type": "string",
      "maxLength": 200
    },
    "code": {
      "type": "string",
      "maxLength": 2048
    }
  },
  "required": [
    "intent"
  ]
}

Success: 200 · application/json

Labels & printing

GET/nodes/{slug}/label.payloadDownload M110 binary print data

labels:read

Returns printer bytes with X-Payload-Sha256. The mover template is not supported for BLE and returns 422.

Query parameters

{
  "template": {
    "enum": [
      "standard",
      "code"
    ],
    "default": "standard"
  }
}

Success: 200 · application/octet-stream

POST/nodes/{slug}/print-jobsQueue physical labels

labels:print

Queues physical output on the configured bridge. Obtain user intent before printing. room and flags apply to mover labels.

JSON request schema

{
  "type": "object",
  "properties": {
    "template": {
      "enum": [
        "standard",
        "mover",
        "code"
      ]
    },
    "copies": {
      "type": "integer",
      "minimum": 1,
      "maximum": 50
    },
    "room": {
      "type": "string"
    },
    "flags": {
      "type": "array",
      "items": {
        "enum": [
          "Fragile",
          "Open first"
        ]
      }
    }
  }
}

Success: 200 · application/json

POST/nodes/{slug}/printsRecord a successful direct print

labels:print

Call only after a direct print succeeds. This records completion and does not queue another print.

JSON request schema

{
  "type": "object",
  "properties": {
    "template": {
      "enum": [
        "standard",
        "code"
      ]
    }
  },
  "required": [
    "template"
  ]
}

Success: 200 · application/json

AI enrichment

GET/enrichment/batchesRecent enrichment batches

inventory:read

The most recent 100 batches, without individual changes.

Success: 200 · application/json

POST/enrichment/batchesPreview a batch of suggestions

inventory:write

Saves before/after proposals for 1–50 records. Does not change inventory. Supported fields: name, description, brand, model, condition, status. Use tags for categories.

JSON request schema

{
  "$ref": "#/components/schemas/EnrichmentInput"
}

Success: 201 · application/json

GET/enrichment/batches/{id}Read a saved batch

inventory:read

Success: 200 · application/json

POST/enrichment/batches/{id}/applyApply a reviewed batch

inventory:write

Atomic and single-use. If any record changed or disappeared after preview, returns 409 and changes nothing.

Success: 200 · application/json

POST/enrichment/batches/{id}/undoUndo an applied batch

inventory:write

Restores the fields changed by this batch. If any record was edited after application, returns 409 and changes nothing.

Success: 200 · application/json

Errors

StatusWhat to do
400Fix the input; read the error code and message.
401Sign in again, refresh OAuth access, or replace the expired/revoked token.
402The inventory needs storage credits for this operation.
403Use a token with the required scopes and a membership that permits them.
404The record is missing or outside the authorized inventory.
405Use the documented HTTP method.
409Read current state and make a new preview or use the new revision.
413Reduce the upload size (maximum 15 MB).
422Choose a supported label template.
428Read the record and send its revision in If-Match.
429Wait for Retry-After seconds; reduce request frequency.
500 / 503Retry later. Check whether a write completed before repeating it.