Base URL: https://www.yanni.app/api/v1
Use the www host. yanni.app redirects to www.yanni.app, and HTTP clients drop the Authorization header across a cross-host redirect — a call to the bare domain arrives unauthenticated and is refused with a 401, however valid your token.
The complete contract — every field, parameter and vocabulary — is the OpenAPI document. It is generated from the code that enforces it, so point a client generator at it. This page is the tour.
Quickstart
Mint a token (below), then ask what it is allowed to do:
export YANNI_TOKEN=ysk_live_... curl -H "Authorization: Bearer $YANNI_TOKEN" \ https://www.yanni.app/api/v1/business
List your inventory, up to 200 items a page:
curl -H "Authorization: Bearer $YANNI_TOKEN" \ "https://www.yanni.app/api/v1/items?limit=200&status=all"
Every list answers { data, next_cursor, has_more }. Pass next_cursor back as ?cursor= until it is null. Sold items are hidden unless you pass status=all or status=sold.
Tokens
Tokens are minted in the app under Settings → API tokens, by an owner or admin. Name it after what will use it and tick the scopes it needs.
The secret is shown once.Only a fingerprint of it is stored, so nobody can show it to you again, us included. Lose it and revoke it; revoking takes effect on the token's next request.
A token is bound to one business and inherits the permissions of the person who created it. Demote them and the token is demoted with them; remove them from the business and it stops working. Send it on every request:
Authorization: Bearer ysk_live_...
Scopes
| Scope | Grants |
|---|---|
items:read | Read inventory, measurements and photos |
items:write | Create items, and change ones that exist — list, unlist, re-price |
sales:read | Read recorded sales and their line items |
sales:write | Record sales. This books revenue, so items:write alone cannot |
sales:sync | Record sales a marketplace reported for listings Yanni posted there — nothing else |
listings:sync | Keep Yanni's record of the listings it posted in step with the marketplace, and flag what left it for review |
items:write cannot book revenue. Listing, re-pricing and unlisting are inventory; recording a sale writes money into your books and needs sales:write. Give an agent the narrower scope unless it is genuinely selling for you.
Endpoints
Relative to https://www.yanni.app/api/v1. Parameters and bodies are in the OpenAPI document.
| Method | Path | What | Scope |
|---|---|---|---|
| GET | /items | List inventory | items:read |
| POST | /items | Create an item | items:write |
| POST | /items/mark-sold | Record that items sold | sales:write |
| POST | /integrations/depop/sales | Record what sold on Depop | sales:sync |
| POST | /integrations/depop/listings/check | Find what left Depop without a sale | listings:sync |
| GET | /integrations/depop/listings/to-end | Depop listings to take down | listings:sync |
| POST | /integrations/depop/listings/ended | Report Depop listings taken down | listings:sync |
| GET | /items/{id} | Read one item | items:read |
| PATCH | /items/{id} | Change one item | items:write |
| GET | /items/{id}/listings | Where one item is listed | items:read |
| POST | /items/{id}/listings | Record a listing, or end one | items:write |
| GET | /sales | List sales | sales:read |
| GET | /storage | List storage locations and their places | items:read |
| GET | /business | Plan, limits, usage and this token's scopes | any valid token |
Fields are snake_case, money is integer cents, timestamps are ISO 8601 UTC. Items can be addressed by id or by SKU (YNI-0042). Cost and profit are redacted by the token creator's role, and redaction reads as 0, not as a missing field.
Writing safely
Send an Idempotency-Key on every write. The same key with the same body replays the first response instead of doing it twice, so a retry after a timeout cannot create a second item. Keys are remembered for 24 hours.
curl -X POST https://www.yanni.app/api/v1/items \
-H "Authorization: Bearer $YANNI_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "title": "Harley single stitch tee", "size": "L", "asking_price_cents": 8500 }'PATCH changes only the fields you name; null clears one. An unknown field is a 422 naming it, so a typo fails loudly rather than doing nothing.
AI agents (MCP)
Yanni is also an MCP server, so Claude, Cursor and other MCP clients can use your inventory as tools without writing any code. It uses the same tokens, scopes, limits and errors as the API above, and every change an agent makes is logged as coming through MCP.
https://www.yanni.app/api/mcp
Claude Code and other clients that connect to a remote server directly:
claude mcp add --transport http yanni https://www.yanni.app/api/mcp \ --header "Authorization: Bearer ysk_live_..."
Claude Desktop only starts local programs from its config file, so it reaches Yanni through the mcp-remote bridge. The token goes in env, where other programs on the machine cannot read it off the command line, and the bridge is pinned to one version:
{
"mcpServers": {
"yanni": {
"command": "npx",
"args": ["-y", "mcp-remote@0.14.3", "https://www.yanni.app/api/mcp",
"--header", "Authorization:${YANNI_AUTH}"],
"env": { "YANNI_AUTH": "Bearer ysk_live_..." }
}
}
}An agent sees only the tools its token's scopes allow:
| Tool | What | Scope |
|---|---|---|
business_summary | Who you are acting for: the business, its plan and limits, how many items it holds, and which scopes this token has. | any valid token |
search_inventory | Search the inventory, newest first, and get back a compact row per item: id, sku, title, brand, category, size, status, intent, asking_price_cents, cogs_cents, listed_on, photo_count and created_at. | items:read |
get_item | One item in full, by id or SKU, with every field and signed photo URLs valid for 24 hours. | items:read |
create_item | Create one item in holding. | items:write |
update_item | Change fields on one item, by id or SKU. | items:write |
get_listings | Where one item is listed, by id or SKU: every listing it has had, live and ended, with the platform, its link and the platform's id where known. | items:read |
record_listing | Record that an item was listed on a platform, with the listing's url and external_id if you have them. | items:write |
mark_sold | Record that items sold: one sale, dated now, of up to 25 items. | sales:write |
get_sales | Recorded sales, newest first, with their line items. | sales:read |
list_storage | Where the business keeps stock: its storage locations and the shelves, racks or bins inside each. | items:read |
Grant the least you can. An agent that adds stock needs items:read and items:write. Only an agent that records sales needs sales:write, and one sale is at most 25 items. Your client asks before update_item, record_listing and mark_soldunless you have told it to always allow them, so don't, for those three. Ending a listing Yanni put on eBay itself takes it down on eBay.
Tool output is data, not instructions.Sale notes and buyer names can be typed by people outside your business. If you build an agent on Yanni, don't let text it reads there tell it what to do.
Rate limits
300 requests per 60 seconds, per token. Past that, every endpoint answers 429 with retry_after_seconds in the body and these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds until the window rolls, never below 1. |
RateLimit-Limit | The ceiling. |
RateLimit-Remaining | What is left in the window, floored at 0. |
RateLimit-Reset | Seconds until the window rolls. |
Sleep that long and carry on. A refused request is not processed, so a 429 on a write means it did not happen — retry under the same Idempotency-Key.
The window is fixed, not sliding: it opens on your first request and closes 60 seconds later. Each token has its own budget, and Settings shows how much of it each one has used this minute. Page at limit=200 rather than requesting items one at a time, and poll /sales?since= every few minutes — there are no webhooks.
Errors
Every failure has one shape. Branch on code, never on message — the message is for a person reading a log and may be reworded.
{ "error": { "code": "forbidden", "message": "...", "required_scope": "items:write" } }| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, unknown, revoked or expired token, or its creator is no longer a member. Which of the four is deliberately not said. |
| 403 | forbidden | Valid token, wrong scope or wrong role. Carries required_scope when a scope is what was missing. |
| 404 | not_found | No such row in your business. Never 403 — a 403 would confirm the id names something real. |
| 409 | conflict | The state moved under you: already sold, or an idempotency key still in flight. |
| 422 | validation | Bad parameter or body. Carries field. |
| 402 | plan_limit_exceeded | Your plan does not include this, or you are at its item limit. Carries limit and usage, or required_plan. |
| 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. |
| 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. |
What it won't do
- No photo upload. Items created through the API arrive with
photos: []and are photographed in the app. - No
DELETE.Agents get prompt-injected, and destructive operations should be bounded by the API's shape rather than a model's judgement. Deleting is done by a person, in the app. - No webhooks, OAuth or SDKs yet. Poll, use a token, and generate a client from the OpenAPI document.
/v1 is versioned in the path. Fields may be added without notice; nothing is removed or renamed inside v1, so ignore fields you don't recognise.
Stuck, or want something the API doesn't do? Write in.