Developers

The Yanni API

Read and change your inventory from a script or an AI agent, without a browser session.

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

ScopeGrants
items:readRead inventory, measurements and photos
items:writeCreate items, and change ones that exist — list, unlist, re-price
sales:readRead recorded sales and their line items
sales:writeRecord sales. This books revenue, so items:write alone cannot
sales:syncRecord sales a marketplace reported for listings Yanni posted there — nothing else
listings:syncKeep 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.

MethodPathWhatScope
GET/itemsList inventoryitems:read
POST/itemsCreate an itemitems:write
POST/items/mark-soldRecord that items soldsales:write
POST/integrations/depop/salesRecord what sold on Depopsales:sync
POST/integrations/depop/listings/checkFind what left Depop without a salelistings:sync
GET/integrations/depop/listings/to-endDepop listings to take downlistings:sync
POST/integrations/depop/listings/endedReport Depop listings taken downlistings:sync
GET/items/{id}Read one itemitems:read
PATCH/items/{id}Change one itemitems:write
GET/items/{id}/listingsWhere one item is listeditems:read
POST/items/{id}/listingsRecord a listing, or end oneitems:write
GET/salesList salessales:read
GET/storageList storage locations and their placesitems:read
GET/businessPlan, limits, usage and this token's scopesany 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:

ToolWhatScope
business_summaryWho 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_inventorySearch 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_itemOne item in full, by id or SKU, with every field and signed photo URLs valid for 24 hours.items:read
create_itemCreate one item in holding.items:write
update_itemChange fields on one item, by id or SKU.items:write
get_listingsWhere 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_listingRecord that an item was listed on a platform, with the listing's url and external_id if you have them.items:write
mark_soldRecord that items sold: one sale, dated now, of up to 25 items.sales:write
get_salesRecorded sales, newest first, with their line items.sales:read
list_storageWhere 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:

HeaderMeaning
Retry-AfterSeconds until the window rolls, never below 1.
RateLimit-LimitThe ceiling.
RateLimit-RemainingWhat is left in the window, floored at 0.
RateLimit-ResetSeconds 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" } }
HTTPCodeMeaning
401unauthorizedMissing, unknown, revoked or expired token, or its creator is no longer a member. Which of the four is deliberately not said.
403forbiddenValid token, wrong scope or wrong role. Carries required_scope when a scope is what was missing.
404not_foundNo such row in your business. Never 403 — a 403 would confirm the id names something real.
409conflictThe state moved under you: already sold, or an idempotency key still in flight.
422validationBad parameter or body. Carries field.
402plan_limit_exceededYour plan does not include this, or you are at its item limit. Carries limit and usage, or required_plan.
429rate_limitedOver 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.
502marketplace_errorA 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.