API Reference

One HTTP endpoint per tool, generated from the same registry the MCP server serves

The PlateCost REST API is a mirror of the MCP tool registry, not a second product. Every tool the MCP server exposes is also an HTTP endpoint at https://app.platecost.io/api/v1/<tool_name>, taking the same input, enforcing the same role and scope rules, and returning the same result. There is one definition behind both doors, so they cannot describe different contracts.

That is why the endpoints are named after verbs rather than resources: search_invoices, get_food_cost_report, update_item. The list of them is generated — see the OpenAPI document for the authoritative set.

Authentication

Every request carries a bearer token minted in Settings → API tokens. Tokens start with pc_ and the plaintext is shown once.

curl -H "Authorization: Bearer pc_YOUR_TOKEN" \
  https://app.platecost.io/api/v1/get_capabilities

A token is bound to one organization and inherits the role and location grants of the person who minted it. A read-scoped token is refused every write endpoint with 403. Authentication is per request and stateless, so a revoked token stops working on the very next call.

Base URL and methods

Text
https://app.platecost.io/api/v1/<tool_name>
KindMethodInput
ReadGETQuery parameters (?q=sysco&limit=25)
ReadPOSTJSON body — useful when a filter is an array
WritePOSTJSON body. GET is refused with 405, so a write can never be triggered by an <img> tag

Start with GET /api/v1/get_capabilities. It answers what the token may do, the plan, the organization's limits and the live tool counts in one call.

Result shapes

Success is always a JSON object.

A list read answers with a named rows key and a total beside it:

GET /api/v1/search_vendors?q=syscoJSON
{
  "total": 3,
  "returned": 3,
  "offset": 0,
  "note": "All 3 matching rows are shown.",
  "vendors": [
    { "id": "…", "name": "Sysco", "itemCount": 412 }
  ]
}

total counts the whole match set regardless of limit, so an empty page is distinguishable from the end of the list. Page with limit and offset.

A write answers {"ok": true, …}:

POST /api/v1/update_itemJSON
{ "ok": true, "itemId": "…", "revision": 8 }

A read larger than 96,000 characters comes back visibly cut rather than silently:

JSON
{
  "truncated": true,
  "originalChars": 402911,
  "note": "You are reading a PREFIX — do not conclude anything is absent from it. …",
  "preview": "{\"total\":812,\"returned\":812,…"
}

preview is a JSON prefix, not the endpoint's own fields. Re-call with a narrower filter.

Errors

Every failure is {"ok": false, "error": "…"} with a status that says why.

StatusMeaning
400The input failed validation, or the write was refused for a reason in error
401Missing, unknown, revoked or expired token. Every authentication failure looks identical on purpose
403The token's scope (read) or the owner's role forbids this
404No such tool, or no such row in this organization — the two are indistinguishable, deliberately
405A write reached with GET
409An Idempotency-Key was reused with a different body
413The request body is over the size ceiling
429Rate limit exceeded

A write that lost a race answers with the current row attached:

409-shaped conflict on a writeJSON
{
  "ok": false,
  "error": "Someone else changed this item while you were editing it.",
  "conflict": {
    "currentRow": { "id": "…", "revision": 9 },
    "conflictingFields": ["name"]
  }
}

Re-apply your change onto conflict.currentRow and retry with its revision. Every write takes the revision you read, which is what makes a lost update impossible rather than unlikely.

Idempotency

Writes honour an Idempotency-Key header. The first success under a key is stored and replayed for a repeat of the same request; the same key with a different body is a 409. Failures are never stored — the retry you are about to make is exactly the one that should be allowed through.

Shell
curl -X POST https://app.platecost.io/api/v1/approve_invoice \
  -H "Authorization: Bearer pc_YOUR_TOKEN" \
  -H "Idempotency-Key: nightly-approve-2026-09-08" \
  -H "content-type: application/json" \
  -d '{"invoiceId":"…","revision":3}'

Reads take no key: replaying a stored read would serve stale data as if it were fresh.

Rate limits

600 requests per minute per token, in a fixed one-minute window, shared with the MCP door — one counter, two doors.

Responses carry X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit the answer is 429 with Retry-After and:

JSON
{
  "ok": false,
  "error": "Rate limit exceeded: 600 requests per minute per token. Retry in 24s.",
  "retryAfterSeconds": 24
}

The limit is per token, so a second workload wants a second token rather than a share of the first. get_capabilities reports the number under limits.restRequestsPerMinutePerToken.

Endpoints worth knowing

The full list is generated; these are the ones most integrations start with.

Reading

EndpointWhat it answers
GET /get_capabilitiesPlan, limits, this token's scope, role and locations, live tool counts
GET /get_platecost_guide?section=overviewThe agent manual — data model, workflow, gotchas
GET /search_invoicesq, vendorId, locationId, reviewStatus, from, to, limit, offset
GET /get_invoiceOne invoice with every line item
GET /list_documentsUploaded documents with pipeline status
GET /read_document_textThe OCR text behind a document — the evidence for every extracted number
GET /search_vendors · /search_items · /search_productsThe catalog
GET /get_price_historyitemId, months
GET /compare_vendor_pricesOmit vendorIds for every vendor, or name two to eight
GET /get_food_cost_reportPlate cost and food cost % per menu recipe, worst first
GET /get_spending_analysisSpend by vendor, category and storage type over a window
GET /get_purchases_seriesPurchases per calendar day, per ISO week and per vendor, with filters

Writing

EndpointWhat it does
POST /approve_invoice · /flag_invoice · /reject_invoiceMove an invoice through review
POST /update_invoice · /update_line_item · /delete_line_itemFix what the extraction got wrong
POST /reclassify_documentRe-label a document the classifier read wrongly
POST /retry_documentPut a failed document back through the pipeline
POST /create_recipe · /update_recipe · /add_recipe_ingredientRecipes and their ingredients
POST /update_item · /update_product · /merge_items · /merge_vendorsCatalog maintenance
POST /propose_changesStage writes as a changeset for a person to approve
POST /apply_changeset · /cancel_changesetApply or discard a staged changeset

Write endpoints apply immediately. Staging is opt-in and is what propose_changes is for — see Writes apply immediately.

Token management

These are REST-only: they exist here with a write token and are hidden from the MCP tool list, because a tool that returns a long-lived credential has no business appearing in the list an agent browses.

EndpointWhat it does
GET /list_api_tokensEvery token in the organization: name, prefix, scope, status, last use. Never the token itself
POST /mint_api_tokenname, scope, expiresInDays. The plaintext is returned once
POST /revoke_api_tokenEffective on the very next request. Idempotent
GET /get_api_usageRequests per day per token, 30-day retention

OpenAPI

Text
GET https://app.platecost.io/api/v1/openapi.json

An OpenAPI 3.1 document generated from the same schemas the MCP tools advertise — one path per tool, with GET on reads and POST on writes. Point a client generator at it rather than hand-writing types; when the registry grows, the document grows with it.