{"openapi":"3.1.1","info":{"title":"Yanni API","version":"1.0.0","summary":"Inventory and sales for vintage resellers.","description":"Read and change your inventory from a script or an AI agent, without a browser session.\n\nMint a token in **Settings → API tokens**. Owners and admins only. A token is bound to one business, carries scopes, and inherits the permissions of the person who created it.\n\n| Scope | Grants |\n|---|---|\n| `items:read` | Read inventory, measurements and photos |\n| `items:write` | Create items, and change ones that exist — list, unlist, re-price |\n| `sales:read` | Read recorded sales and their line items |\n| `sales:write` | Record sales. This books revenue, so items:write alone cannot |\n| `sales:sync` | Record sales a marketplace reported for listings Yanni posted there — nothing else |\n| `listings:sync` | Keep Yanni's record of the listings it posted in step with the marketplace, and flag what left it for review |\n\n`GET /business` is readable by any valid token regardless of scope, so a client can discover what it is allowed to do before trying.\n\n**Use the `www` host.** `yanni.app` 307s to `www.yanni.app`, and clients drop the `Authorization` header across a cross-host redirect — a call to the apex arrives unauthenticated and is refused with a 401, however valid the token.\n\n**Money is integer cents. Fields are snake_case. Timestamps are ISO 8601 UTC.** Query parameters on `GET /items` are the exception: they are camelCase, because they are the same parameters the web app's own list uses.\n\n**Rate limit: 300 requests per 60 seconds, per token.** The window is fixed, not sliding, so a caller timed exactly wrong can burst to twice that across a boundary. A 429 carries `Retry-After` and the `RateLimit-*` trio.\n\n**There is no DELETE**, and that is a decision rather than an omission: agents get prompt-injected, and destructive operations should be bounded by the API's shape rather than by a model's judgement.\n\nIdempotency, the redaction rules and the pagination loop are described on the operations and schemas themselves — this document is the whole contract, not a summary of one kept elsewhere.","contact":{"name":"Yanni support","url":"https://www.yanni.app/help","email":"support@yanni.app"}},"servers":[{"url":"https://www.yanni.app/api/v1","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Items","description":"Inventory."},{"name":"Sales","description":"Recorded sales and the money behind them."},{"name":"Account","description":"What this token may do, and where things live."}],"paths":{"/items":{"get":{"operationId":"listItems","tags":["Items"],"summary":"List inventory","description":"Newest first. Cursor-paginated. Requires `items:read`.","x-required-scope":"items:read","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"search","in":"query","description":"Free text across title, brand and SKU.","schema":{"type":"string"}},{"name":"status","in":"query","description":"One status, or `stale` for anything unsold that has sat 60 days, or `all`. **Omit it and sold items are excluded** — no status means the working inventory, which is what the web grid shows. Pass `all` to sync everything.","schema":{"type":"string","enum":["holding","listed","sold","stale","all"]}},{"name":"category","in":"query","description":"Exact category.","schema":{"type":"string"}},{"name":"genre","in":"query","description":"Items carrying this genre.","schema":{"type":"string"}},{"name":"condition","in":"query","description":"Exact condition.","schema":{"type":"string"}},{"name":"listingType","in":"query","description":"Fixed price or auction.","schema":{"type":"string"}},{"name":"listedOn","in":"query","description":"A platform name; `none` for items listed nowhere; `not:<platform>` for everything off that one.","schema":{"type":"string"}},{"name":"intent","in":"query","description":"Omitted, both are returned — the grid is your whole inventory.","schema":{"type":"string","enum":["resell","personal"]}},{"name":"storageLocationId","in":"query","description":"Everything in one location, or `none` for stock that is not on a shelf — where an item lands when it comes back from a sale.","schema":{"type":"string"}},{"name":"storagePlaceId","in":"query","description":"Everything on one shelf.","schema":{"type":"string"}},{"name":"eventId","in":"query","description":"Everything packed for one event.","schema":{"type":"string"}},{"name":"bundleId","in":"query","description":"Everything bought in one lot.","schema":{"type":"string"}},{"name":"cost","in":"query","description":"`missing` — no acquisition cost recorded.","schema":{"type":"string","enum":["missing"]}},{"name":"priced","in":"query","description":"`no` — no asking price set.","schema":{"type":"string","enum":["no"]}},{"name":"owner","in":"query","description":"A member's user id, or `me` for the token's creator.","schema":{"type":"string"}},{"name":"skus","in":"query","description":"Comma-separated SKUs. Naming rows explicitly overrides the hide-sold default — you get the rows you asked for.","schema":{"type":"string"}},{"name":"ids","in":"query","description":"Comma-separated item ids. Overrides the hide-sold default, like `skus`.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemPage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createItem","tags":["Items"],"summary":"Create an item","description":"Creates one item, photoless. Requires `items:write`, and a role that can see inventory — a seller-role token is refused. Every field is optional.","x-required-scope":"items:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemCreate"}}}},"responses":{"201":{"description":"The item, in the shape a GET returns.","headers":{"Idempotent-Replay":{"$ref":"#/components/headers/IdempotentReplay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Item"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/items/mark-sold":{"post":{"operationId":"markItemsSold","tags":["Sales"],"summary":"Record that items sold","description":"Writes the sale behind the status change: line items, cost of goods and the seller splits, exactly as marking them sold in the app does. Requires `sales:write` and a paid plan — `items:write` alone can list, re-price and unlist, and cannot book a cent of revenue. Every named item is sold out, all remaining quantity.","x-required-scope":"sales:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkSold"}}}},"responses":{"201":{"description":"The sale's id and how many items it moved. Read the money back from GET /sales, where costs and profit are redacted for your role.","headers":{"Idempotent-Replay":{"$ref":"#/components/headers/IdempotentReplay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkSoldResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/integrations/depop/sales":{"post":{"operationId":"syncDepopSales","tags":["Sales"],"summary":"Record what sold on Depop","description":"What the Yanni browser extension calls with the receipts it reads from the seller's Depop Sold page. Each receipt's products are matched to the Depop listings Yanni posted, by Depop's own listing id — a product Yanni never listed comes back in `unmatched` and is not recorded. One receipt is one sale at the price the buyer paid, dated when Depop says it sold, and recording it twice is impossible. Requires `sales:sync` — a scope that can record these and nothing else — and a paid plan.","x-required-scope":"sales:sync","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepopReceipts"}}}},"responses":{"200":{"description":"What was recorded, and what was not.","headers":{"Idempotent-Replay":{"$ref":"#/components/headers/IdempotentReplay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepopSyncResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/integrations/depop/listings/check":{"post":{"operationId":"checkDepopListings","tags":["Items"],"summary":"Find what left Depop without a sale","description":"What the Yanni browser extension calls with every listing in the seller's Depop shop, read to its last page. A Depop listing Yanni posted that the shop no longer has on sale — deleted, or marked sold by hand — is marked ended and Depop comes off `listed_on`; the piece is flagged for the seller to review and is never marked sold, its stock untouched. A listing Depop shows as purchased is left to POST /integrations/depop/sales. Listings Yanni did not post are ignored. A shop with nothing on sale while Yanni has live Depop listings ends nothing until `confirm_empty` is sent. Requires `listings:sync`.","x-required-scope":"listings:sync","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepopShop"}}}},"responses":{"200":{"description":"What was ended, or how many live listings are waiting on `confirm_empty`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepopListingCheckResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/integrations/depop/listings/to-end":{"get":{"operationId":"getDepopToEnd","tags":["Items"],"summary":"Depop listings to take down","description":"What the Yanni browser extension takes down on Depop: every Depop listing Yanni posted that is still up where the piece has sold, Depop is no longer ticked on it, or it is waiting to be deleted. Yanni's server cannot reach Depop, so a listing stays on this list until POST /integrations/depop/listings/ended says it is down. Requires `listings:sync`.","x-required-scope":"listings:sync","responses":{"200":{"description":"The listings, oldest first.","content":{"application/json":{"schema":{"type":"object","required":["listings"],"properties":{"listings":{"type":"array","items":{"type":"object","required":["id","sku","reason"],"properties":{"id":{"type":"string","description":"Depop's listing id."},"sku":{"type":"string"},"reason":{"type":"string","enum":["sold","unlisted","deleting"]}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/integrations/depop/listings/ended":{"post":{"operationId":"recordDepopEnded","tags":["Items"],"summary":"Report Depop listings taken down","description":"The extension took these listings down on Depop. Each one on the to-end list is marked ended; any other id is ignored. A piece that was waiting to be deleted is deleted now, unless it went onto a sale while it waited. Requires `listings:sync`.","x-required-scope":"listings:sync","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","pattern":"^\\d{1,20}$"}}}}}}},"responses":{"200":{"description":"SKUs whose listing was marked ended, and SKUs deleted.","content":{"application/json":{"schema":{"type":"object","required":["ended","deleted"],"properties":{"ended":{"type":"array","items":{"type":"string"}},"deleted":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/items/{id}":{"parameters":[{"$ref":"#/components/parameters/ItemId"}],"get":{"operationId":"getItem","tags":["Items"],"summary":"Read one item","description":"By id or by SKU. Requires `items:read`.","x-required-scope":"items:read","responses":{"200":{"description":"The item.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Item"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateItem","tags":["Items"],"summary":"Change one item","description":"Changes the fields you name and leaves the rest alone: a field sent as `null` is cleared, a field you do not mention is untouched. Listing, re-pricing and unlisting live here and need only `items:write` — no money moves. Selling does not: `status: \"sold\"` is refused with a 422 pointing at POST /items/mark-sold, and an item that is already sold refuses status changes with a 409. Read-only fields are accepted and ignored so a read-modify-write round trip works; a field this API does not recognise is a 422 naming it.","x-required-scope":"items:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemUpdate"}}}},"responses":{"200":{"description":"The whole item, after the change.","headers":{"Idempotent-Replay":{"$ref":"#/components/headers/IdempotentReplay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Item"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/items/{id}/listings":{"parameters":[{"$ref":"#/components/parameters/ItemId"}],"get":{"operationId":"getItemListings","tags":["Items"],"summary":"Where one item is listed","description":"Every listing the item has had, live and ended, oldest first, with its link and the platform's id where known. `listed_on` on the item names the platforms; this names the listings. Requires `items:read`.","x-required-scope":"items:read","responses":{"200":{"description":"The item's listings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListingList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"recordItemListing","tags":["Items"],"summary":"Record a listing, or end one","description":"Records that the item is listed on `platform`, with its link and id if you have them: the platform joins `listed_on`, and a `holding` item becomes `listed`. On a listing that is still live, a field you leave out keeps its value. With `ended: true` the platform leaves `listed_on` and the listing is marked ended. **An eBay listing Yanni pushed itself is ended on eBay too**, and if eBay refuses, nothing changes and the answer is a 502. Such a listing cannot be recorded over — it is managed from the app. A sold item cannot be listed. Requires `items:write`.","x-required-scope":"items:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordListing"}}}},"responses":{"200":{"description":"The item's listings, after the change.","headers":{"Idempotent-Replay":{"$ref":"#/components/headers/IdempotentReplay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListingList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/MarketplaceError"}}}},"/sales":{"get":{"operationId":"listSales","tags":["Sales"],"summary":"List sales","description":"Newest first. Requires `sales:read` and a paid plan. **`since` is how you answer \"what sold\"** — there are no webhooks, so poll this on the order of minutes.","x-required-scope":"sales:read","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"event_id","in":"query","description":"Only sales attributed to one event.","schema":{"type":"string"}},{"name":"since","in":"query","description":"Only sales recorded strictly after this instant. ISO 8601.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"A page of sales.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SalePage"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanLimit"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Validation"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/storage":{"get":{"operationId":"listStorage","tags":["Account"],"summary":"List storage locations and their places","description":"The permanent two-level hierarchy. Unpaginated on purpose — this is a handful of rows describing where a seller keeps things, read once to populate a picker. Requires `items:read`.","x-required-scope":"items:read","responses":{"200":{"description":"Every location, oldest first, each with its places.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorageList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/business":{"get":{"operationId":"getBusiness","tags":["Account"],"summary":"Plan, limits, usage and this token's scopes","description":"Read this first and respect `limits` rather than discovering them through a 402. Open to any valid token regardless of scope: a credential that cannot discover its own permissions without already holding them is no use to a client.","responses":{"200":{"description":"The business, the plan, and what this credential may do.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Business"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"`Authorization: Bearer ysk_live_…`. The secret is shown once, at creation, and is not recoverable — only a SHA-256 fingerprint is stored. Each operation names the scope it needs in `x-required-scope`."}},"parameters":{"Limit":{"name":"limit","in":"query","description":"Rows per page. Anything unparseable or below 1 reads as the default; anything above the maximum is clamped to it.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},"Cursor":{"name":"cursor","in":"query","description":"The previous page's `next_cursor`. Absent for the first page.","schema":{"type":"string"}},"ItemId":{"name":"id","in":"path","required":true,"description":"The item's id, or its SKU (`YNI-0042`) — SKUs are unique within a business.","schema":{"type":"string"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Send one on every write. Same key and same body replays the first response byte for byte with `Idempotent-Replay: true`; same key and a different body is a 422; same key while the first is still running is a 409. A failed request releases its key. Keys are remembered 24 hours and are private to your business.","schema":{"type":"string","maxLength":255}}},"headers":{"IdempotentReplay":{"description":"`true` when this response is a replay of an earlier request with the same `Idempotency-Key`. Absent otherwise.","schema":{"type":"string","enum":["true"]}}},"responses":{"Unauthorized":{"description":"401 `unauthorized` — missing, unknown, revoked or expired token, or its creator is no longer a member. Which of the four is deliberately not said.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"403 `forbidden` — valid token, wrong scope or wrong role. Carries `required_scope` when a scope is what was missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"404 `not_found` — no such row **in your business**. Never 403 — a 403 would confirm the id names something real.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"409 `conflict` — the state moved under you: already sold, or an idempotency key still in flight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Validation":{"description":"422 `validation` — bad parameter or body. Carries `field`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PlanLimit":{"description":"402 `plan_limit_exceeded` — your plan does not include this, or you are at its item limit. Carries `limit` and `usage`, or `required_plan`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"MarketplaceError":{"description":"502 `marketplace_error` — a marketplace Yanni acts on for you (eBay) refused or could not be reached. Nothing changed in Yanni, so the listing is still live there; the message says what the marketplace said.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"429 `rate_limited` — over 300 requests in the window. A refused request is not processed, so a 429 on a write means the write did not happen: retry it under the same `Idempotency-Key`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds until the window rolls, never below 1.","schema":{"type":"integer","minimum":1}},"RateLimit-Limit":{"description":"The ceiling.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"What is left in the window, floored at 0.","schema":{"type":"integer","minimum":0}},"RateLimit-Reset":{"description":"Seconds until the window rolls.","schema":{"type":"integer","minimum":1}}}}},"schemas":{"Error":{"type":"object","description":"Every failure has this shape. Branch on `code`, never on `message` — the message is for a human reading a log and may be reworded.","required":["error"],"additionalProperties":false,"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["unauthorized","forbidden","not_found","conflict","validation","plan_limit_exceeded","rate_limited","marketplace_error"]},"message":{"type":"string"},"field":{"type":"string","description":"Which field was wrong. On a `validation`."},"required_scope":{"type":"string","enum":["items:read","items:write","sales:read","sales:write","sales:sync","listings:sync"],"description":"The scope the token would have needed. On a `forbidden`."},"required_plan":{"type":"string","description":"The lowest plan that includes this. On a `plan_limit_exceeded`."},"limit":{"type":["integer","null"],"description":"Your plan's ceiling — null means unlimited. On a `plan_limit_exceeded`."},"usage":{"type":"integer","description":"What you are at. On a `plan_limit_exceeded`."},"retry_after_seconds":{"type":"integer","minimum":1,"description":"On a `rate_limited`. The same number as `Retry-After`."}}}}},"Item":{"type":"object","description":"A piece of inventory.","required":["asking_price_cents","brand","bundle_id","category","cogs_cents","color","condition","created_at","cut","department","description","era","event_place_id","features","floor_price_cents","genre","id","intent","listed_on","listing_type","material","measurements","owner_user_id","photos","price_firmness","provenance","quantity","show_in_showroom","size","sku","sold_channel","sold_date","sold_price_cents","source_id","status","storage_location_id","storage_place_id","title","true_price_cents","updated_at","vintage"],"properties":{"id":{"type":"string","description":"Opaque id.","readOnly":true},"sku":{"type":"string","description":"Allocated by Yanni — `YNI-0042`. Unique within a business, and usable in place of the id in a path.","readOnly":true},"photos":{"type":"array","items":{"type":"string","format":"uri"},"description":"Signed URLs, valid 24 hours. Fetch what you need and re-read the item for fresh links rather than storing them. **Cannot be written**: items created through this API arrive with `photos: []` and are photographed in the Yanni app.","readOnly":true},"owner_user_id":{"type":["string","null"],"description":"Which member of a shared business the piece belongs to.","readOnly":true},"event_place_id":{"type":["string","null"],"description":"The rack it is packed on for an event.","readOnly":true},"sold_price_cents":{"type":["integer","null"],"minimum":0,"description":"What it actually sold for. Integer cents, never a float.","readOnly":true},"sold_date":{"type":["string","null"],"format":"date-time","readOnly":true},"sold_channel":{"type":["string","null"],"description":"Where it sold — free text.","readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"title":{"type":["string","null"],"description":"What the piece is called."},"brand":{"type":["string","null"]},"category":{"type":["string","null"],"description":"What kind of garment this is. Normalized onto T-Shirt, Hoodie, Jacket, Pants, Shorts, Hat, Sweater, Button-Up, Jersey, Dress, Skirt, Blouse, Other; a value outside that list is kept verbatim rather than refused."},"genre":{"type":"array","items":{"type":"string"},"description":"Aesthetic tags. Normalized onto Avant Garde, Biker, Boho, Casual, Coquette, Cosplay, Costume, Cottage, Emo, Fairy, Futuristic, Glam, Gorpcore, Goth, Grunge, Indie, Kidcore, Loungewear, Minimalist, Preppy, Punk, Rave, Regency, Retro, Skater, Sportswear, Streetwear, Twee, Utility, Western, Whimsygoth, Y2K plus whatever custom genres this business has added; anything else is kept verbatim. Duplicates are dropped."},"color":{"type":["string","null"]},"era":{"type":["string","null"],"description":"The decade the piece is from. Normalized onto Antique, 50s, 60s, 70s, 80s, 90s, 2000s, 2010s, 2020s; a value outside that list is kept verbatim rather than refused."},"condition":{"type":["string","null"],"description":"Best to worst. Normalized onto Deadstock, Clean, Excellent, Good, Worn In, Worn, Distressed, Fair, Thrashed; a value outside that list is kept verbatim rather than refused."},"size":{"type":["string","null"],"description":"The size on the tag. Normalized onto Depop's own size vocabulary — the letter sizes and waists it accepts — and a size outside it (`36x30`) is kept verbatim rather than refused."},"vintage":{"type":["boolean","null"],"description":"Reconciled against `era` on write: a piece from an era the app counts as vintage is marked vintage whatever you send."},"provenance":{"type":"array","items":{"type":"string"},"description":"How the piece was come by: Custom, Deadstock, Designer, Handmade, Preloved, Repaired, Reworked / Upcycled, Vintage. Unlike the other vocabularies this one drops what it cannot match rather than keeping it, and only the first 2 survive — so a third value, or a word outside the list, is accepted and then gone."},"department":{"type":["string","null"],"enum":["Men","Women","Teens","Unisex Adults","Boys","Girls","Unisex Kids",null],"description":"Who the garment is cut for. Refused with a 422 if outside the list, because every eBay clothing category requires it."},"material":{"type":["string","null"]},"cut":{"type":["string","null"]},"features":{"type":"array","items":{"type":"string"},"description":"Free-form construction and flaw notes. The app's own picker offers Single Stitch, Faded, Pin Holes, Rips, Stained; anything else is stored as sent."},"description":{"type":["string","null"]},"measurements":{"type":"object","description":"Inches. Null when unset.","properties":{"pit_to_pit":{"type":["number","null"],"exclusiveMinimum":0},"length":{"type":["number","null"],"exclusiveMinimum":0}},"additionalProperties":false},"quantity":{"type":"integer","minimum":1,"default":1},"status":{"type":"string","enum":["holding","listed","sold"],"description":"`sold` is refused with a 422 pointing at POST /items/mark-sold, which writes the sale that explains the money. Not settable on create — a new item is `holding`."},"intent":{"type":"string","enum":["resell","personal"],"default":"resell","description":"Why the piece is tracked. Orthogonal to `status` — a keeper still sits in `holding`. The two intents hold separate plan budgets. Anything outside the list is read as `resell`."},"listing_type":{"type":["string","null"],"description":"Fixed price or auction. Normalized onto Buy it Now, Auction; a value outside that list is kept verbatim rather than refused."},"listed_on":{"type":["string","null"],"description":"Where the piece is listed, as free text — `eBay`, `Depop`. Setting it is bookkeeping: delisting from the marketplace itself is the caller's job. Not settable on create."},"show_in_showroom":{"type":"boolean","description":"Whether the piece appears in this business's public showroom. Not settable on create."},"price_firmness":{"type":"string","enum":["flexible","firm","bundle"],"description":"How negotiable the asking price is: flexible (open to offers), firm (price is firm), bundle (deal on bundles). Seller-facing. Not settable on create."},"cogs_cents":{"type":["integer","null"],"minimum":0,"description":"What the piece cost to acquire. Integer cents, never a float. Reads 0 when the token's role may not see acquisition cost — redaction is indistinguishable from a genuinely free item, by design."},"asking_price_cents":{"type":["integer","null"],"minimum":0,"description":"What it is listed at. Integer cents, never a float."},"true_price_cents":{"type":["integer","null"],"minimum":0,"description":"The offer that is accepted without asking. Integer cents, never a float. Defaults to the asking price on create."},"floor_price_cents":{"type":["integer","null"],"minimum":0,"description":"The lowest price worth taking. Integer cents, never a float. Seller-facing; never shown to a buyer."},"bundle_id":{"type":["string","null"],"description":"The lot this piece was bought in."},"source_id":{"type":["string","null"],"description":"Who it was bought from."},"storage_location_id":{"type":["string","null"],"description":"Where it lives. Omitted on create, the item lands in the oldest location on its first place. A business with no location at all gets a 422."},"storage_place_id":{"type":["string","null"],"description":"The shelf, rack or bin inside that location."}}},"ItemCreate":{"type":"object","description":"Everything is optional — a photo is the only thing the app itself requires, and this door cannot send one. `status`, `listed_on`, `show_in_showroom` and `price_firmness` belong to an item that already exists: create it, then PATCH it.","properties":{"id":{"type":"string","description":"Opaque id.","readOnly":true},"sku":{"type":"string","description":"Allocated by Yanni — `YNI-0042`. Unique within a business, and usable in place of the id in a path.","readOnly":true},"photos":{"type":"array","items":{"type":"string","format":"uri"},"description":"Signed URLs, valid 24 hours. Fetch what you need and re-read the item for fresh links rather than storing them. **Cannot be written**: items created through this API arrive with `photos: []` and are photographed in the Yanni app.","readOnly":true},"owner_user_id":{"type":["string","null"],"description":"Which member of a shared business the piece belongs to.","readOnly":true},"event_place_id":{"type":["string","null"],"description":"The rack it is packed on for an event.","readOnly":true},"sold_price_cents":{"type":["integer","null"],"minimum":0,"description":"What it actually sold for. Integer cents, never a float.","readOnly":true},"sold_date":{"type":["string","null"],"format":"date-time","readOnly":true},"sold_channel":{"type":["string","null"],"description":"Where it sold — free text.","readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"title":{"type":["string","null"],"description":"What the piece is called."},"brand":{"type":["string","null"]},"category":{"type":["string","null"],"description":"What kind of garment this is. Normalized onto T-Shirt, Hoodie, Jacket, Pants, Shorts, Hat, Sweater, Button-Up, Jersey, Dress, Skirt, Blouse, Other; a value outside that list is kept verbatim rather than refused."},"genre":{"type":"array","items":{"type":"string"},"description":"Aesthetic tags. Normalized onto Avant Garde, Biker, Boho, Casual, Coquette, Cosplay, Costume, Cottage, Emo, Fairy, Futuristic, Glam, Gorpcore, Goth, Grunge, Indie, Kidcore, Loungewear, Minimalist, Preppy, Punk, Rave, Regency, Retro, Skater, Sportswear, Streetwear, Twee, Utility, Western, Whimsygoth, Y2K plus whatever custom genres this business has added; anything else is kept verbatim. Duplicates are dropped."},"color":{"type":["string","null"]},"era":{"type":["string","null"],"description":"The decade the piece is from. Normalized onto Antique, 50s, 60s, 70s, 80s, 90s, 2000s, 2010s, 2020s; a value outside that list is kept verbatim rather than refused."},"condition":{"type":["string","null"],"description":"Best to worst. Normalized onto Deadstock, Clean, Excellent, Good, Worn In, Worn, Distressed, Fair, Thrashed; a value outside that list is kept verbatim rather than refused."},"size":{"type":["string","null"],"description":"The size on the tag. Normalized onto Depop's own size vocabulary — the letter sizes and waists it accepts — and a size outside it (`36x30`) is kept verbatim rather than refused."},"vintage":{"type":["boolean","null"],"description":"Reconciled against `era` on write: a piece from an era the app counts as vintage is marked vintage whatever you send."},"provenance":{"type":"array","items":{"type":"string"},"description":"How the piece was come by: Custom, Deadstock, Designer, Handmade, Preloved, Repaired, Reworked / Upcycled, Vintage. Unlike the other vocabularies this one drops what it cannot match rather than keeping it, and only the first 2 survive — so a third value, or a word outside the list, is accepted and then gone."},"department":{"type":["string","null"],"enum":["Men","Women","Teens","Unisex Adults","Boys","Girls","Unisex Kids",null],"description":"Who the garment is cut for. Refused with a 422 if outside the list, because every eBay clothing category requires it."},"material":{"type":["string","null"]},"cut":{"type":["string","null"]},"features":{"type":"array","items":{"type":"string"},"description":"Free-form construction and flaw notes. The app's own picker offers Single Stitch, Faded, Pin Holes, Rips, Stained; anything else is stored as sent."},"description":{"type":["string","null"]},"measurements":{"type":"object","description":"Inches. Null when unset.","properties":{"pit_to_pit":{"type":["number","null"],"exclusiveMinimum":0},"length":{"type":["number","null"],"exclusiveMinimum":0}},"additionalProperties":false},"quantity":{"type":"integer","minimum":1,"default":1},"intent":{"type":"string","enum":["resell","personal"],"default":"resell","description":"Why the piece is tracked. Orthogonal to `status` — a keeper still sits in `holding`. The two intents hold separate plan budgets. Anything outside the list is read as `resell`."},"listing_type":{"type":["string","null"],"description":"Fixed price or auction. Normalized onto Buy it Now, Auction; a value outside that list is kept verbatim rather than refused."},"cogs_cents":{"type":["integer","null"],"minimum":0,"description":"What the piece cost to acquire. Integer cents, never a float. Reads 0 when the token's role may not see acquisition cost — redaction is indistinguishable from a genuinely free item, by design."},"asking_price_cents":{"type":["integer","null"],"minimum":0,"description":"What it is listed at. Integer cents, never a float."},"true_price_cents":{"type":["integer","null"],"minimum":0,"description":"The offer that is accepted without asking. Integer cents, never a float. Defaults to the asking price on create."},"floor_price_cents":{"type":["integer","null"],"minimum":0,"description":"The lowest price worth taking. Integer cents, never a float. Seller-facing; never shown to a buyer."},"bundle_id":{"type":["string","null"],"description":"The lot this piece was bought in."},"source_id":{"type":["string","null"],"description":"Who it was bought from."},"storage_location_id":{"type":["string","null"],"description":"Where it lives. Omitted on create, the item lands in the oldest location on its first place. A business with no location at all gets a 422."},"storage_place_id":{"type":["string","null"],"description":"The shelf, rack or bin inside that location."}},"additionalProperties":false},"ItemUpdate":{"type":"object","description":"Name only what changes. `null` clears a field; an unnamed field is untouched. The `readOnly` fields are accepted and ignored, so an object read from a GET can be posted straight back; anything this API does not recognise is a 422 naming it.","properties":{"id":{"type":"string","description":"Opaque id.","readOnly":true},"sku":{"type":"string","description":"Allocated by Yanni — `YNI-0042`. Unique within a business, and usable in place of the id in a path.","readOnly":true},"photos":{"type":"array","items":{"type":"string","format":"uri"},"description":"Signed URLs, valid 24 hours. Fetch what you need and re-read the item for fresh links rather than storing them. **Cannot be written**: items created through this API arrive with `photos: []` and are photographed in the Yanni app.","readOnly":true},"owner_user_id":{"type":["string","null"],"description":"Which member of a shared business the piece belongs to.","readOnly":true},"event_place_id":{"type":["string","null"],"description":"The rack it is packed on for an event.","readOnly":true},"sold_price_cents":{"type":["integer","null"],"minimum":0,"description":"What it actually sold for. Integer cents, never a float.","readOnly":true},"sold_date":{"type":["string","null"],"format":"date-time","readOnly":true},"sold_channel":{"type":["string","null"],"description":"Where it sold — free text.","readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true},"title":{"type":["string","null"],"description":"What the piece is called."},"brand":{"type":["string","null"]},"category":{"type":["string","null"],"description":"What kind of garment this is. Normalized onto T-Shirt, Hoodie, Jacket, Pants, Shorts, Hat, Sweater, Button-Up, Jersey, Dress, Skirt, Blouse, Other; a value outside that list is kept verbatim rather than refused."},"genre":{"type":"array","items":{"type":"string"},"description":"Aesthetic tags. Normalized onto Avant Garde, Biker, Boho, Casual, Coquette, Cosplay, Costume, Cottage, Emo, Fairy, Futuristic, Glam, Gorpcore, Goth, Grunge, Indie, Kidcore, Loungewear, Minimalist, Preppy, Punk, Rave, Regency, Retro, Skater, Sportswear, Streetwear, Twee, Utility, Western, Whimsygoth, Y2K plus whatever custom genres this business has added; anything else is kept verbatim. Duplicates are dropped."},"color":{"type":["string","null"]},"era":{"type":["string","null"],"description":"The decade the piece is from. Normalized onto Antique, 50s, 60s, 70s, 80s, 90s, 2000s, 2010s, 2020s; a value outside that list is kept verbatim rather than refused."},"condition":{"type":["string","null"],"description":"Best to worst. Normalized onto Deadstock, Clean, Excellent, Good, Worn In, Worn, Distressed, Fair, Thrashed; a value outside that list is kept verbatim rather than refused."},"size":{"type":["string","null"],"description":"The size on the tag. Normalized onto Depop's own size vocabulary — the letter sizes and waists it accepts — and a size outside it (`36x30`) is kept verbatim rather than refused."},"vintage":{"type":["boolean","null"],"description":"Reconciled against `era` on write: a piece from an era the app counts as vintage is marked vintage whatever you send."},"provenance":{"type":"array","items":{"type":"string"},"description":"How the piece was come by: Custom, Deadstock, Designer, Handmade, Preloved, Repaired, Reworked / Upcycled, Vintage. Unlike the other vocabularies this one drops what it cannot match rather than keeping it, and only the first 2 survive — so a third value, or a word outside the list, is accepted and then gone."},"department":{"type":["string","null"],"enum":["Men","Women","Teens","Unisex Adults","Boys","Girls","Unisex Kids",null],"description":"Who the garment is cut for. Refused with a 422 if outside the list, because every eBay clothing category requires it."},"material":{"type":["string","null"]},"cut":{"type":["string","null"]},"features":{"type":"array","items":{"type":"string"},"description":"Free-form construction and flaw notes. The app's own picker offers Single Stitch, Faded, Pin Holes, Rips, Stained; anything else is stored as sent."},"description":{"type":["string","null"]},"measurements":{"type":"object","description":"Inches. Null when unset.","properties":{"pit_to_pit":{"type":["number","null"],"exclusiveMinimum":0},"length":{"type":["number","null"],"exclusiveMinimum":0}},"additionalProperties":false},"quantity":{"type":"integer","minimum":1,"default":1},"status":{"type":"string","enum":["holding","listed","sold"],"description":"`sold` is refused with a 422 pointing at POST /items/mark-sold, which writes the sale that explains the money. Not settable on create — a new item is `holding`."},"intent":{"type":"string","enum":["resell","personal"],"default":"resell","description":"Why the piece is tracked. Orthogonal to `status` — a keeper still sits in `holding`. The two intents hold separate plan budgets. Anything outside the list is read as `resell`."},"listing_type":{"type":["string","null"],"description":"Fixed price or auction. Normalized onto Buy it Now, Auction; a value outside that list is kept verbatim rather than refused."},"listed_on":{"type":["string","null"],"description":"Where the piece is listed, as free text — `eBay`, `Depop`. Setting it is bookkeeping: delisting from the marketplace itself is the caller's job. Not settable on create."},"show_in_showroom":{"type":"boolean","description":"Whether the piece appears in this business's public showroom. Not settable on create."},"price_firmness":{"type":"string","enum":["flexible","firm","bundle"],"description":"How negotiable the asking price is: flexible (open to offers), firm (price is firm), bundle (deal on bundles). Seller-facing. Not settable on create."},"cogs_cents":{"type":["integer","null"],"minimum":0,"description":"What the piece cost to acquire. Integer cents, never a float. Reads 0 when the token's role may not see acquisition cost — redaction is indistinguishable from a genuinely free item, by design."},"asking_price_cents":{"type":["integer","null"],"minimum":0,"description":"What it is listed at. Integer cents, never a float."},"true_price_cents":{"type":["integer","null"],"minimum":0,"description":"The offer that is accepted without asking. Integer cents, never a float. Defaults to the asking price on create."},"floor_price_cents":{"type":["integer","null"],"minimum":0,"description":"The lowest price worth taking. Integer cents, never a float. Seller-facing; never shown to a buyer."},"bundle_id":{"type":["string","null"],"description":"The lot this piece was bought in."},"source_id":{"type":["string","null"],"description":"Who it was bought from."},"storage_location_id":{"type":["string","null"],"description":"Where it lives. Omitted on create, the item lands in the oldest location on its first place. A business with no location at all gets a 422."},"storage_place_id":{"type":["string","null"],"description":"The shelf, rack or bin inside that location."}},"additionalProperties":false},"Listing":{"type":"object","required":["platform","state","url","external_id","managed_by_yanni","listed_at","ended_at"],"properties":{"platform":{"type":"string","description":"The platform's name, as in `listed_on`: `eBay`, `Depop`, `Grailed`…"},"state":{"type":"string","enum":["live","draft","ended"],"description":"`draft` is a listing Yanni pushed to eBay and never published, so no buyer can reach it."},"url":{"type":["string","null"],"format":"uri","description":"Where a person can look at it, when known."},"external_id":{"type":["string","null"],"description":"The platform's own id for the listing, when known."},"managed_by_yanni":{"type":"boolean","description":"Yanni made this listing itself. Ending it ends it on the platform; recording over it is refused."},"listed_at":{"type":"string","format":"date-time"},"ended_at":{"type":["string","null"],"format":"date-time"}}},"ListingList":{"type":"object","required":["item_id","sku","listed_on","data"],"properties":{"item_id":{"type":"string"},"sku":{"type":"string"},"listed_on":{"type":["string","null"],"description":"The item's `listed_on`, after any change."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Listing"}}}},"RecordListing":{"type":"object","required":["platform"],"additionalProperties":false,"properties":{"platform":{"type":"string","maxLength":60,"description":"One platform's name. Known names are matched case-insensitively."},"url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"The listing's http(s) link. `null` clears it."},"external_id":{"type":["string","null"],"maxLength":255,"description":"The platform's id for the listing. `null` clears it."},"ended":{"type":"boolean","default":false,"description":"`true` records that the listing came down instead. Send no `url` or `external_id` with it."}}},"MarkSold":{"type":"object","required":["item_ids"],"properties":{"item_ids":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Items in your business that are not already sold."},"sold_price_cents":{"type":"integer","minimum":0,"description":"One price per item. Integer cents, never a float. **Omit it and each item sells at its own asking price** — an item with none falls back to 0, so send this when you know the number."},"use_asking_price":{"type":"boolean","description":"`true` forces asking prices even when `sold_price_cents` is given."},"sold_channel":{"type":"string","description":"Free text — `eBay`, `Flea market`, `Whatnot`."},"event_id":{"type":["string","null"],"description":"Attribute the sale to one of your events."}}},"MarkSoldResult":{"type":"object","required":["sale_id","items_sold"],"properties":{"sale_id":{"type":"string"},"items_sold":{"type":"integer","description":"How many items the sale moved."}}},"DepopReceipts":{"type":"object","required":["receipts"],"properties":{"receipts":{"type":"array","maxItems":50,"items":{"type":"object","required":["purchase_id","status","sold_at","items_amount_cents","product_ids"],"properties":{"purchase_id":{"type":"string","pattern":"^\\d{1,20}$","description":"Depop's id for the receipt."},"status":{"type":"string","description":"Depop's status. Refunded and cancelled receipts are passed over."},"sold_at":{"type":"string","format":"date-time"},"items_amount_cents":{"type":"integer","minimum":0,"description":"What the buyer paid for the pieces, before shipping. Integer cents, never a float."},"seller_amount_cents":{"type":["integer","null"],"description":"What Depop paid out after fees. The difference from `items_amount_cents` is booked as a Selling Fees expense on the sale."},"product_ids":{"type":"array","minItems":1,"items":{"type":"string","pattern":"^\\d{1,20}$"},"description":"Depop listing ids. Several on a bundle, which divides the amount evenly."}}}}}},"DepopSyncResult":{"type":"object","required":["recorded","double_sales","unmatched"],"properties":{"recorded":{"type":"integer","description":"Sales written by this call. A receipt already recorded counts as 0."},"double_sales":{"type":"array","items":{"type":"string"},"description":"SKUs Depop sold that had already sold in Yanni. Not recorded — somebody has a buyer to refund."},"unmatched":{"type":"array","items":{"type":"string"},"description":"Depop listing ids that are not a listing Yanni posted."}}},"DepopShop":{"type":"object","required":["complete","listings"],"properties":{"complete":{"type":"boolean","enum":[true],"description":"That `listings` is the whole shop. Absence ends a listing, so a partial list is refused."},"confirm_empty":{"type":"boolean","description":"End listings even though the shop has nothing on sale."},"listings":{"type":"array","maxItems":5000,"items":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","pattern":"^\\d{1,20}$","description":"Depop's listing id."},"status":{"type":"string","description":"Depop's status: `ONSALE`, `PURCHASED` or `MARKED_AS_SOLD`. One it has not seen is treated as still on sale."}}}}}},"DepopListingCheckResult":{"type":"object","required":["ended","needs_confirmation"],"properties":{"ended":{"type":"array","items":{"type":"object","required":["sku","reason"],"properties":{"sku":{"type":"string"},"reason":{"type":["string","null"],"enum":["removed_on_depop","marked_sold_on_depop",null],"description":"The review flag the piece now carries, or null when the listing just ended — the piece had sold, Depop was already off it, or it was flagged already."}}}},"needs_confirmation":{"type":"integer","description":"Live Depop listings this call did not end because the shop showed nothing on sale. 0 when it acted."}}},"Sale":{"type":"object","description":"**Do not reconstruct a sale's total by summing `line_items`.** A custom line — something charged for that was never an inventory item — has no line row at all, so it is inside `subtotal_cents` and invisible in `line_items`. The total is `subtotal_cents`; the lines tell you which pieces left.","required":["id","subtotal_cents","line_items","created_at"],"properties":{"id":{"type":"string"},"subtotal_cents":{"type":"integer","description":"What the sale charged, custom lines included. Integer cents, never a float."},"discount_cents":{"type":"integer"},"total_cogs_cents":{"type":"integer","description":"Reads 0 when your role may not see cost — redaction, not a free sale."},"profit_cents":{"type":"integer","description":"Redacted the same way as `total_cogs_cents`."},"card_fee_cents":{"type":"integer","description":"Buyer-paid card surcharge. Excluded from subtotal and profit."},"refunded_cents":{"type":"integer","description":"Cumulative. This column never records which item came back."},"refunded_at":{"type":["string","null"],"format":"date-time"},"payment_method":{"type":["string","null"],"description":"Cash, card, and the rest — free text."},"method":{"type":"string","description":"How the sale was recorded: `pos_scan`, `manual`, `invoice`, `integration`, `showroom`."},"customer_name":{"type":["string","null"]},"notes":{"type":["string","null"]},"client_ref":{"type":["string","null"],"description":"The caller's own reference, when one was given."},"event":{"type":["object","null"],"description":"Null unless the sale was attributed to an event.","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/SaleLineItem"}},"created_at":{"type":"string","format":"date-time"}}},"SaleLineItem":{"type":"object","required":["id","item_id","quantity","sold_price_cents","cogs_cents"],"properties":{"id":{"type":"string"},"item_id":{"type":"string"},"sku":{"type":["string","null"]},"title":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"string","format":"uri"},"description":"Signed URLs, valid 24 hours."},"quantity":{"type":"integer"},"sold_price_cents":{"type":"integer","description":"What this line charged. Integer cents, never a float."},"cogs_cents":{"type":"integer","description":"Reads 0 when your role may not see this piece's cost."}}},"StorageList":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StorageLocation"}}}},"StorageLocation":{"type":"object","required":["id","name","created_at","places"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"places":{"type":"array","description":"The shelves, racks or bins inside this location, oldest first.","items":{"type":"object","required":["id","name","created_at"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}}}}},"Business":{"type":"object","required":["id","name","plan","limits","usage","features","listing_defaults","token","role"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"plan":{"type":"string","description":"The subscription tier this business is on."},"limits":{"type":"object","description":"**Null means unlimited**, published as null rather than omitted so a client can tell no ceiling from a missing field.","required":["items","collection","seats","depop_run"],"properties":{"items":{"type":["integer","null"],"description":"Resell items."},"collection":{"type":["integer","null"],"description":"Personal items. A separate budget — a keeper does not spend the resell allowance."},"seats":{"type":["integer","null"]},"depop_run":{"type":["integer","null"],"description":"Pieces one bulk List on Depop run may hold. The browser extension refuses a longer run."}}},"usage":{"type":"object","required":["items","collection","sold_items","storage_locations"],"properties":{"items":{"type":"integer"},"collection":{"type":"integer"},"sold_items":{"type":"integer"},"storage_locations":{"type":"integer"}}},"features":{"type":"object","description":"What the plan includes.","required":["pos","sale_history","ai"],"properties":{"pos":{"type":"boolean","description":"Recording sales, POS/mark-sold included."},"sale_history":{"type":"boolean","description":"Reading sales."},"ai":{"type":"boolean","description":"The app's AI surfaces. Not reachable through this API."}}},"listing_defaults":{"type":"object","description":"What this business fills in for an item that answered none of these. Category, colour and department are the fields real inventory is most often missing, and a marketplace usually rejects a blank — so resolve against these rather than inventing your own default. `department` is not a marketplace field: it is how a menswear or womenswear category tree is picked, so an item missing it loses its whole category rather than one field.","required":["category","color","department"],"properties":{"category":{"type":"string","description":"One of Yanni's categories."},"color":{"type":"string","description":"A colour name."},"department":{"type":"string","enum":["Men","Women","Teens","Unisex Adults","Boys","Girls","Unisex Kids"],"description":"Who the piece is cut for."}}},"token":{"type":"object","description":"What THIS credential may do, as opposed to what the business can afford.","required":["name","scopes"],"properties":{"name":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["items:read","items:write","sales:read","sales:write","sales:sync","listings:sync"]}}}},"role":{"type":"string","enum":["owner","admin","member","seller"],"description":"The role of the person who created this token. It decides what the token may see — a `seller` token has no inventory access and reads costs as 0."}}},"ItemPage":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Item"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `?cursor=`. Null on the last page, never absent."},"has_more":{"type":"boolean"}}},"SalePage":{"type":"object","required":["data","next_cursor","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Sale"}},"next_cursor":{"type":["string","null"],"description":"Pass back as `?cursor=`. Null on the last page, never absent."},"has_more":{"type":"boolean"}}}}}}