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_capabilitiesconst response = await fetch("https://app.platecost.io/api/v1/get_capabilities", {
headers: { Authorization: `Bearer ${process.env.PLATECOST_API_TOKEN}` },
});
const capabilities = await response.json();import os, requests
response = requests.get(
"https://app.platecost.io/api/v1/get_capabilities",
headers={"Authorization": f"Bearer {os.environ['PLATECOST_API_TOKEN']}"},
)
capabilities = response.json()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.
Keep tokens out of version control and out of client-side code. They cannot be recovered — if one leaks, revoke it in Settings → API tokens.
Base URL and methods
https://app.platecost.io/api/v1/<tool_name>| Kind | Method | Input |
|---|---|---|
| Read | GET | Query parameters (?q=sysco&limit=25) |
| Read | POST | JSON body — useful when a filter is an array |
| Write | POST | JSON 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:
{
"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, …}:
{ "ok": true, "itemId": "…", "revision": 8 }A read larger than 96,000 characters comes back visibly cut rather than silently:
{
"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.
| Status | Meaning |
|---|---|
400 | The input failed validation, or the write was refused for a reason in error |
401 | Missing, unknown, revoked or expired token. Every authentication failure looks identical on purpose |
403 | The token's scope (read) or the owner's role forbids this |
404 | No such tool, or no such row in this organization — the two are indistinguishable, deliberately |
405 | A write reached with GET |
409 | An Idempotency-Key was reused with a different body |
413 | The request body is over the size ceiling |
429 | Rate limit exceeded |
A write that lost a race answers with the current row attached:
{
"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.
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:
{
"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
| Endpoint | What it answers |
|---|---|
GET /get_capabilities | Plan, limits, this token's scope, role and locations, live tool counts |
GET /get_platecost_guide?section=overview | The agent manual — data model, workflow, gotchas |
GET /search_invoices | q, vendorId, locationId, reviewStatus, from, to, limit, offset |
GET /get_invoice | One invoice with every line item |
GET /list_documents | Uploaded documents with pipeline status |
GET /read_document_text | The OCR text behind a document — the evidence for every extracted number |
GET /search_vendors · /search_items · /search_products | The catalog |
GET /get_price_history | itemId, months |
GET /compare_vendor_prices | Omit vendorIds for every vendor, or name two to eight |
GET /get_food_cost_report | Plate cost and food cost % per menu recipe, worst first |
GET /get_spending_analysis | Spend by vendor, category and storage type over a window |
GET /get_purchases_series | Purchases per calendar day, per ISO week and per vendor, with filters |
Writing
| Endpoint | What it does |
|---|---|
POST /approve_invoice · /flag_invoice · /reject_invoice | Move an invoice through review |
POST /update_invoice · /update_line_item · /delete_line_item | Fix what the extraction got wrong |
POST /reclassify_document | Re-label a document the classifier read wrongly |
POST /retry_document | Put a failed document back through the pipeline |
POST /create_recipe · /update_recipe · /add_recipe_ingredient | Recipes and their ingredients |
POST /update_item · /update_product · /merge_items · /merge_vendors | Catalog maintenance |
POST /propose_changes | Stage writes as a changeset for a person to approve |
POST /apply_changeset · /cancel_changeset | Apply 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.
| Endpoint | What it does |
|---|---|
GET /list_api_tokens | Every token in the organization: name, prefix, scope, status, last use. Never the token itself |
POST /mint_api_token | name, scope, expiresInDays. The plaintext is returned once |
POST /revoke_api_token | Effective on the very next request. Idempotent |
GET /get_api_usage | Requests per day per token, 30-day retention |
OpenAPI
GET https://app.platecost.io/api/v1/openapi.jsonAn 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.