WFDASH-WEBAPP/docs/api-mcp-layer.md
Dave 3091ac94de docs(quote-assistant): runbook accuracy, MCP tool listed in API docs
- Runbook: pre-migration settings don't persist (not "saves fail"),
  eligible statuses, 15 lines per quote, audit attribution, clone/template
  behaviour, supplier kept, doubts dot, confidence guidance (Medium is the
  default; raise to High if reps often change AI prices).
- quotes.assistant_fill added to docs/api-mcp-layer.md,
  docs/audit/api-mcp-coverage.md and the Giellie skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:42:44 +02:00

57 KiB
Raw Permalink Blame History

WFDASH API + MCP Layer

This document describes the public HTTP surface intended for external automation (n8n workflows, custom AI agents, the local "Hermes" agent, etc.). Everything below is implemented in this repo and exercised by the Vitest suite under src/app/api/.

The README still lists an older subset of this surface. This file is the source of truth. If you add or rename an endpoint or MCP tool, update both files in the same commit.


1. Environment

Add to .env.local:

NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io
PB_ADMIN_EMAIL=david.els1@outlook.com
PB_ADMIN_PASSWORD=1234567890
AGENT_API_KEY=123456
ORDER_ITEMS_COLLECTION=order_items   # or `order_items_v2` after migration

The MCP endpoint and every /api/v1/* route (except /api/v1/health) require the agent bearer token:

Authorization: Bearer <AGENT_API_KEY>

Rotate AGENT_API_KEY in this file plus every consumer when a key leaks.


2. REST endpoints (/api/v1/*)

All routes return JSON. All PATCH / POST bodies are JSON unless noted. Pagination defaults to page=1, perPage=50 where relevant.

2.1 Health / discovery

Method Path Auth Notes
GET /api/v1/health none { ok, service, time, version, env }

2.2 Orders

Method Path Body / params Notes
GET /api/v1/orders ?page&perPage&status&search Status filter is informational; items drive workflow. Expands company,customer,quote.
POST /api/v1/orders/from-quote { "quote_id": "..." } Creates order + items; idempotent on existing order_for_quote.
GET /api/v1/orders/{id} — Order detail with item rows.
GET /api/v1/orders/{id}/matched-attachments — Returns attachment matches against quote items.

2.3 Order items

Method Path Body Notes
PATCH /api/v1/order-items/{id}/status { item_status?, drv_status?, notes?, problem_notes? } Accepts legacy driver_status alias.

2.4 Quotes

Method Path Body / params Notes
GET /api/v1/quotes/lookup ?q=... Fuzzy lookup by quote number, ref, customer.
GET /api/v1/quotes/next-number — Atomic Q-YYYY-MM-NNN from quote_counters.
POST /api/v1/quotes/{id}/clone — Clone a quote + items.
POST /api/v1/quotes/{id}/save-as-template { name } Save a quote as a template.
POST /api/v1/quotes/{id}/create-from-template { template_id } New quote from a template.
POST /api/v1/quotes/{id}/retry-email — Resend the outbound quote email.
POST /api/v1/quotes/{id}/send-email { template? } Send the outbound quote email.
GET /api/v1/quotes/{id}/pdf — Render the quote PDF.
POST /api/v1/quotes/{id}/re-price { items: [...] } Bulk re-price quote items.
GET /api/v1/quotes/{id}/attachments ?direction=inbound|outbound|generated|all List quote-level attachments.
POST /api/v1/quotes/{id}/attachments/upload multipart file Upload a quote-level attachment (admin).
GET /api/v1/quotes/{id}/items/{itemId}/match — Match a quote item to a price-list row.
GET /api/v1/quotes/{id}/items/{itemId}/lookup — Suggest matches for an item.
POST /api/v1/quotes/items/rewrite { items: [...] } AI rewrite item descriptions.

2.5 Quote item attachments

Method Path Body Notes
POST /api/v1/quotes/items/{id}/attachments/upload multipart file New manual-upload endpoint (source = "manual").

2.6 Sprint

Method Path Body Notes
POST /api/v1/sprint/items/{id}/claim { ttl_minutes? } Claim a sprint row (30-min default lock).
GET /api/v1/sprint/items/{id}/attachments — List attachments for a quote item.
POST /api/v1/sprint/items/{id}/attachments/upload multipart file Upload a pricing proof (operator or AI).
GET /api/v1/sprint/attachment-counts ?quote_item_ids=... Bulk counts for sprint badges.

2.7 Companies

Method Path Body Notes
GET /api/v1/companies ?page&perPage&search&sort Filter on company / vendor substring.
POST /api/v1/companies { company, address?, vendor?, coordinates?: {lon,lat} } company required.
GET /api/v1/companies/{id} — Single row.
PATCH /api/v1/companies/{id} partial of create body
DELETE /api/v1/companies/{id} — PB cascade rules apply.

2.8 Customers

Method Path Body Notes
GET /api/v1/customers ?page&perPage&search&sort&expand expand=company,rep to inline.
POST /api/v1/customers { customer, email?, alternative_email?, buyer_email?, number?, company?, rep?, coordinates?, description? } customer required. company / rep accept id or name.
GET /api/v1/customers/{id} ?expand
PATCH /api/v1/customers/{id} partial Empty string unsets company / rep.
DELETE /api/v1/customers/{id} — Cascade rules apply.

2.9 Suppliers

Method Path Body Notes
GET /api/v1/suppliers ?page&perPage&search&sort coordinates is the legacy "lat, lng" text.
POST /api/v1/suppliers { suppliername, email?, suppliernumber?, supplieraddress?, supply_tags?, coordinates?, closingtime? }
GET /api/v1/suppliers/{id} —
PATCH /api/v1/suppliers/{id} partial
DELETE /api/v1/suppliers/{id} — Cascade rules apply.

2.10 Collection pool / map

Method Path Body Notes
GET /api/v1/collection-pool ?page&perPage&status Pool status filter.
POST /api/v1/collection-map/items/{id}/assign { driver_id } Assign a pool row to a driver.

2.11 Purchase orders

Method Path Body Notes
POST /api/v1/purchase-orders/{id}/dispatch { channel: "email"|"print" } Dispatch PO.
POST /api/v1/purchase-orders/{id}/send-email { to?, cc?, body? } Send PO email.
GET /api/v1/purchase-orders/{id}/send-email/outcome — Outcome of last send.
GET /api/v1/purchase-orders/{id}/pdf — Render PO PDF.
GET /api/v1/purchase-orders/stale ?days=... List POs untouched for N days.
POST /api/v1/purchase-orders/{id}/attachments/upload multipart/form-data field file Append a free-form attachment to additional_attachments. Max 25 MB.
DELETE /api/v1/purchase-orders/{id}/attachments/{filename} — Remove a single additional-attachment token.

2.12 Stock purchases

Method Path Body Notes
POST /api/v1/stock-purchases/{id}/attachments/upload multipart/form-data field file Append a free-form attachment (supplier invoice, packing slip) to stock_purchases.additional_attachments. Max 25 MB.
DELETE /api/v1/stock-purchases/{id}/attachments/{filename} — Remove a single additional-attachment token.

2.13 Users / file proxy

Method Path Body Notes
GET /api/v1/users/lookup ?q=... User search.
GET /api/v1/files/{collection}/{recordId}/{filename} — Auth-aware file proxy for collections whose rules reject the client.

3. MCP endpoint (/api/mcp)

JSON-RPC over HTTP. Same Authorization bearer as /api/v1/*.

3.1 Methods

  • tools/list — returns the full tool list (23 tools as of writing).
  • tools/call — { params: { name, arguments } }.

3.2 Tools

Orders / pool / sprint

Tool Purpose
orders.list List orders by order_number substring.
orders.create_from_quote Create order + items from a quote.
collection_pool.list List pool rows by status.
order_items.update_status Update item_status / drv_status / notes.
order_items.confirm_collected Mark one or more customer order_items as collected without requiring a driver assignment. The in-UI /collection-pool page only exposes "Mark Collected" after a drv_id is bound; this tool is the agent-side equivalent (mirrors stock_purchase_items.mark_collected on the stock side). Eligible from item_status ∈ {collection_pool, assigned}; per-row independence; bumps qty_collected + stamps drv_collected_at + appends a category: "collection" audit note to notes. Added 2026-08-11 to close the gap surfaced when goods physically arrived at the warehouse but no driver had ever been assigned.

Quotes (email pipeline)

Tool Purpose
quotes.create_from_email Create quote + customer + items from parsed email. Idempotent on message_id. Use attachments.upload_for_quote per file after this returns.
quotes.append_reply Append a structured reply. Matches In-Reply-To then Q-YYYY-MM-NNN in subject.
quotes.find_by_message_id Look up by RFC 5322 Message-ID.
attachments.upload_for_quote Upload one attachment to a quote (base64, ≤25 MB).
bulk_update_quote_items Patch up to 100 quote items in one call (whitelisted fields only).

Companies / customers / suppliers (full CRUD)

Tool Purpose
companies.list / .get / .create / .update / .delete Full CRUD.
customers.list / .get / .create / .update / .delete Full CRUD. company / rep accept id or name.
suppliers.list / .get / .create / .update / .delete Full CRUD.

Stock purchases / POs (attachments)

Tool Purpose
purchase_orders.add_attachment Upload one free-form attachment to a PO (base64, ≤25 MB).
stock_purchases.add_attachment Upload one free-form attachment (supplier invoice, packing slip, photo of damaged stock) to a stock purchase (base64, ≤25 MB). Appends to additional_attachments; does NOT touch the versioned-PDF documents ledger.

Stock purchases (line-level writes — added 2026-08-07)

Tool Purpose
stock_purchase_items.update Patch whitelisted pricing + catalogue fields on a single stock_purchase_item (cost_price, sku, description, notes, unit, supplier_sku, quantity). Hand-edit guard on cost_price > 0.
stock_purchase_items.bulk_update Batch patch (≤100 rows). Same field whitelist + hand-edit guard as .update. Per-row independence; dry_run: true returns identical shape with status: "dry_run" per row.
stock_purchase_items.assign_driver Bind a line to a driver (drv_id). Transitions line status null → assigned_to_driver or collection_pool → assigned_to_driver.
stock_purchase_items.mark_collected Mark a line as physically delivered. Status → collected, bumps quantity_received. Does NOT book into stock — that's stock_purchases.receive. Header auto-flips to received when every line on the PO has quantity_received >= quantity.

Stock items (catalogue)

Tool Purpose
stock_items.update Patch catalogue fields on a master stock_item (sku, description, notes, unit, selling_price, markup). For inventory qty use stock_items.adjust.
stock_items.set_pricing Set cost_price / selling_price / markup on existing stock_items in one batch (max 100). The POS till reads selling_price, not markup — set both. Cost changes are audited in stock_ledger with the actor; quantities are not changed (use stock_items.adjust).

3.3 Tool input shapes (condensed)

quotes.create_from_email (required: subject):

{
  "from_email": "buyer@acme.co.za",
  "from_name": "Acme Procurement",
  "subject": "RFQ — magnetic drill",
  "body_plain": "...",
  "body_html": "...",
  "message_id": "<unique-id@mail>",
  "in_reply_to": "<reply@mail>",
  "received_at": "2026-07-09T10:00:00Z",
  "company_hint": "Acme",
  "extracted_items": [
    { "description": "Magnetic drill", "quantity": 1, "unit": "ea", "notes": "" }
  ],
  "attachments": []
}

attachments.upload_for_quote (required: quote_id, filename, base64):

{
  "quote_id": "abc123",
  "filename": "spec.pdf",
  "content_type": "application/pdf",
  "size": 482113,
  "base64": "JVBERi0xLjQK..."
}

stock_purchase_items.update (required: id, patch):

{
  "id": "15-char-line-id",
  "patch": {
    "cost_price": 145.50,
    "sku": "HYD68-20",
    "supplier_sku": "SUP-HYD-068"
  },
  "force_overwrite": false
}

Whitelist: cost_price, sku, description, notes, unit, supplier_sku, quantity. Anything else returns forbidden_field. Lines with cost_price > 0 already return already_priced unless force_overwrite: true.

stock_purchase_items.bulk_update (required: items):

{
  "items": [
    { "id": "line-a", "patch": { "sku": "HYD68-20" } },
    { "id": "line-b", "patch": { "cost_price": 230.00 }, "force_overwrite": true }
  ],
  "dry_run": false
}

Returns { updated, skipped, errors, results: [{id, status, reason?, message?, previous?, item?}] }. Cap is 100 rows per call.

stock_purchase_items.assign_driver (required: id, drv_id):

{ "id": "15-char-line-id", "drv_id": "15-char-driver-id", "assigned_at": "2026-08-07T08:00:00Z" }

stock_purchase_items.mark_collected (required: id):

{ "id": "15-char-line-id", "quantity_collected": 4 }

Returns { item, previous_quantity_received, new_quantity_received, header_status_after, header_flipped }. header_flipped: true when this call completed the parent PO (auto-flip from sent → received).

stock_items.update (required: id, patch):

{
  "id": "15-char-stock-item-id",
  "patch": {
    "sku": "HYD68-20",
    "selling_price": 285.00,
    "markup": 35
  }
}

Whitelist: sku, description, notes, unit, selling_price, markup. Anything else returns reason: "forbidden_field". For inventory qty use stock_items.adjust. Returns {ok: true, item, previous} on success or {ok: false, reason, message, field?} on rejection.

stock_items.set_pricing (required: updates; actor.id is required at runtime and must be a 15-char user id — it is written to stock_ledger.created_by, a relation to users; actor.name defaults to the id):

{
  "updates": [
    { "id": "15-char-stock-item-id", "cost_price": 100, "selling_price": 180, "markup": 80 },
    { "id": "15-char-stock-item-id", "markup": 50 }
  ],
  "actor": { "id": "user-id", "name": "Dave" }
}

1–100 updates per call; keys limited to id, cost_price, selling_price, markup (anything else is an error naming the key). The POS till reads selling_price, not markup, so set both consistently (selling = cost × (1 + markup/100)). Cost changes are audited in stock_ledger. Returns {results: [...], summary: {total, succeeded, failed}}; per-row failures (not_found, validation_error, pb_error) are returned, not thrown. stock_items.create also accepts optional cost_price, selling_price and markup; a cost with neither selling price nor markup gets the app default markup (80%).

bulk_update_quote_items (required: items):

{
  "items": [
    {
      "id": "15-char-quote-item-id",
      "patch": {
        "cost_price": 1200.00,
        "selling_price": 1800.00,
        "supplier": "15-char-supplier-id",
        "pricing_source": "ai",
        "pricing_notes": "Auto-priced from Acme catalogue 2026-07-09"
      },
      "force_overwrite": false
    }
  ],
  "dry_run": false
}

Whitelist: cost_price, selling_price, description, sku, unit, supplier, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited. Anything else returns reason: "forbidden_field" per row. Pre-priced rows (cost_price > 0 AND selling_price > 0) are skipped with reason: "already_priced" unless force_overwrite: true. Cap is 100 rows per call.

companies.create:

{ "company": "Acme", "address": "1 Road", "vendor": "V-1", "coordinates": { "lon": 28.04, "lat": -26.20 } }

customers.create:

{
  "customer": "Acme Procurement",
  "email": "buyer@acme.co.za",
  "alternative_email": "ops@acme.co.za",
  "buyer_email": "ap@acme.co.za",
  "number": "+27 11 555 0100",
  "company": "Acme",
  "rep": "David",
  "coordinates": { "lon": 28.04, "lat": -26.20 },
  "description": "Industrial buyer"
}

suppliers.create:

{
  "suppliername": "Acme Supplies",
  "email": "sales@acme.co.za",
  "suppliernumber": "+27 11 555 0200",
  "supplieraddress": "2 Road",
  "supply_tags": "tools,fasteners",
  "coordinates": "-26.20, 28.04",
  "closingtime": "17:00"
}

3.4 Response envelopes

Success:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{ "type": "text", "text": "{\"...\":\"...\"}" }]
  }
}

Error:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": { "code": -32601, "message": "Method not found" }
}

Auth failures return HTTP 401 for /api/v1/* but land in the JSON-RPC error envelope for /api/mcp (per the in-tree test suite).


4. Known quirks

  • orders.list has no status filter. The orders.status column was removed; status lives on items now. The MCP orders.list tool only supports search / order_number substring.
  • Quote-level attachments via the REST route (/api/v1/quotes/{id}/attachments/upload) and via the MCP tool (attachments.upload_for_quote) are independent code paths. New agents should prefer the MCP tool.
  • Quote-item attachments have two upload routes — the new one at /api/v1/quotes/items/{id}/attachments/upload (source "manual"), and the sprint one at /api/v1/sprint/items/{id}/attachments/upload (source "operator_proof" / "agent_proof"). Use the sprint route for pricing proofs; use the quotes route for raw attachments coming in with the original RFQ.
  • customers.create / customers.update accept company and rep as either ids or case-insensitive names. An unresolved name leaves the relation empty (no error) so the operator can wire it up later.
  • suppliers.coordinates is the legacy "lat, lng" text field. Don't migrate it to {lon,lat} — supplier-map.ts still parses the text form.

5. Testing the surface

Unit/API tests:

npm run test:run -- src/app/api

End-to-end (boots Next dev on :3100):

export E2E_BASE_URL=http://localhost:3100
export E2E_AGENT_API_KEY=$AGENT_API_KEY
npm run e2e

The Playwright suite intentionally skips env-driven specs when the keys aren't set, so a clean checkout reports passed, N skipped.


5. MCP tool catalogue (auto-generated)

This catalogue is auto-generated from the tools array in src/app/api/mcp/route.ts by scripts/mcp-doc-gen.py. There are 144 tools as of this run. Re-run the script after adding or renaming a tool.

Grouped by resource family. The full description is in the table column; the description text the agent sees is preserved verbatim.

Orders

Tool Description
orders.list List orders with optional filters. Pass PB filter syntax in filter (e.g. created >= "2026-08-05" && created < "2026-08-06") and sort (e.g. -created, order_number). search is a substring match on order_number; customer accep...
orders.get Fetch a single order by id. Pass expand (e.g. customer,company,quote) to inline relations.
orders.create_from_quote Create a new order and its order_items from an existing quote. The standard pipeline for turning a customer-accepted quote into an operational order. Use this when the operator says 'convert Q-2026-08-014 to an order' or 'place the order...
orders.create Create a standalone order WITHOUT going through a quote. Use for counter sales, walk-ins, internal transfers, or any flow where there's no upstream quote record. Mirrors the REST POST /api/v1/orders/create endpoint. Required: customer_id...
orders.split Split an order into a continuation order. Caller passes the order_item IDs to move; unlisted items stay on the parent. Mirrors the REST POST /api/v1/orders/{id}/split endpoint. Semantics: - Parent order keeps its number; moved items stay...
orders.archive Browse the historical-orders archive (Delivered + Cancelled). Returns the same shape as the operator's /orders/archive page: paginated historical orders with company/customer/quote expanded, matched on Delivered + Cancelled item_stat...
orders.cancel Soft-cancel an order. Mirrors the REST POST /api/v1/orders/{id}/cancel endpoint. Flips every line item's item_status to cancelled so the order falls out of the active workflow tabs and surfaces on /orders/archive under 'Cancelled'. P...
orders.review_save Atomically commit the operator's review-time edits to an order header AND any line-item patches. Mirrors the REST POST /api/v1/orders/{id}/review-save endpoint semantics. Three independent row-level operations may run on a single save: -...
orders.stats Aggregate orders + their line items over a date range. Backed by getOrderStats. Returns { total, buckets[], range } where buckets groups by day, customer, or supplier. from and to are YYYY-MM-DD (inclusive lower, exclusiv...
orders.daily_summary One-shot summary for a single day: placed / delivered / invoiced counts and totals, pending item count, and the top 3 customers by placed-order count. Backed by getDailyOrderSummary. Use this at the start of the day to answer "what did...
orders.mark_invoiced Mark an order as invoiced. Mirrors the REST POST /api/v1/orders/{id}/mark_invoiced endpoint. Flips order.invoiced = true ONLY when every line item has item_status === "invoiced". Refuses with 409 if any item is not yet invoiced; the ...
orders.mark_delivered Mark every eligible line item on the order as delivered, stamping delivered_at and delivered_date. Mirrors the REST POST /api/v1/orders/{id}/mark_delivered endpoint. Eligible statuses: `ready_delivery | collected | with_driver | ch...
orders.update Patch whitelisted header fields on an order. Mirrors the REST PATCH /api/v1/orders/{id} endpoint. Whitelist (from ORDER_PATCHABLE_FIELDS): notes, contact_name, description, rep, email_cc, email_bcc, quote_reference_text, ...
orders.set_quote Link an EXISTING order to an EXISTING quote. Mirrors the REST POST /api/v1/orders/{id}/quote endpoint. Why this exists: only createOrderFromQuote writes the orders.quote relation today, so walk-in orders leave it null. When Hermes di...
orders.quote_match_sweep Run quote auto-detection over orders that have never been attempted. Mirrors the REST POST /api/v1/orders/quote-match/sweep endpoint. Why this exists: orders created from now on are matched automatically at intake. This is the catch-up p...
orders.add_note REPLACE the notes field on an order with text. Thin wrapper over orders.update (patch: { notes: text }). The notes field is a free-text header field, distinct from the per-line order_items.notes JSON audit feed. If you want t...
orders.add_attachment Upload a single attachment (proof, drawing, signed PO, supplier quote) to an order. The file is stored on the order's OWN order_attachements multi-file field (PB-side misspelling kept in sync with the live schema) — the same field the ...
orders.list_attachments List the attachments associated with an order. Merges two sources: (a) files written to the order's own order_attachements multi-file field — operator uploads via orders.add_attachment and counter-sale photos from the in-store PWA; a...
orders.sprint_counts Counts of sprint-pricing rows grouped by claim status (unclaimed / claimed-by-me / claimed-by-other / resolved). Used to render sprint badges in the operator UI and to decide whether an agent should claim a row. Wraps `GET /api/v1/orders...
orders.find_duplicates Find probable duplicate orders. Wraps GET /api/v1/orders/duplicates. Use this BEFORE creating a new order from a customer's RFQ so the operator doesn't accidentally double-order. Three pass kinds are supported: - order_number — same ...
orders.find_by_order_number Exact-match lookup for one order by its order_number. Returns the matching Order record or null if not found. Use this when you need a deterministic is there already an order for this order number? check. The substring search `or...
orders.find_by_email_message_id Exact-match lookup for one order by its email_message_id (RFC 5322 Message-ID of the inbound email that originated it). Returns the matching Order record or null if not found. Use this for the `is there already an order for this in...
orders.review Read-only review bundle for one order: header, items grouped by supplier, PO readiness, attachment matches against quote items, and any pending decisions. Wraps GET /api/v1/orders/[id]/review. Use this BEFORE generating POs or splittin...
orders.matched_attachments Match the order's quote attachments against each order_item by description / SKU overlap. Wraps GET /api/v1/orders/[id]/matched-attachments. Returns { item_id → [attachment_filename, ...] } so the agent can decide which proof applies...

Order items

Tool Description
order_items.update_status Update an order_item's item_status and/or drv_status fields. Use this when the operator needs to flip a single row through the workflow without bulk-pricing or per-line invoice reconciliation (order_items.update is the patch tool f...
order_items.confirm_collected Confirm collection of one or more customer order_items WITHOUT requiring a driver assignment. Mirrors the agent-side role that the stock-side stock_purchase_items.mark_collected already plays: marks item_status → collected and `drv...
order_items.create Add a new line item to an existing order. Mirrors the REST POST /api/v1/orders/{id}/items endpoint. selling_price auto-derives from cost_price * (1 + markup / 100) when omitted. Per-row failures never abort the whole call (returns `c...
order_items.update Patch fields on an order_item for invoice reconciliation and catalogue relabelling. Whitelist: cost_price, selling_price, quantity, sku, unit, category, description, supplier, markup, markup_percent, pricing_source, pricing_notes, matche...
order_items.list List order_items (the line items under each order). Useful for finding pending/PB-ready items, getting one supplier's items, etc. Pass PB filter syntax in filter.
order_items.get Fetch a single order_item by id. Pass expand to inline order, purchase_order, category, supplier.

Quotes

Tool Description
quotes.create_from_email Create a new quote (with customer/company/items) from a parsed email payload. Idempotent on message_id. from_email is optional — if missing (e.g. an email with no From: header), the quote is linked to a sentinel 'Unknown Sender' custom...
quotes.create Create a quote from scratch (walk-in, no source email). Use this when the operator types 'make me a quote for John' on the dashboard or via the agent — quotes.create_from_email is the wrong tool because it requires a subject/parsed-e...
quotes.ingest_from_email Fast ingestion front-door of the split AI pipeline. Same shape as quotes.create_from_email, but does NOT extract items synchronously — the new quote is stamped ai_status = "pending" and returned immediately so the caller doesn't time...
quotes.process_pending Cron-style processor. Picks up to limit quotes with ai_status = "pending" | "failed", runs each through Claude against the email body + attachment text, writes quote_items, and flips status. Returns counts (processed/skipped/failed...
quotes.re_run_ai Manual retry of the AI pipeline for a single quote. Resets ai_status via the atomic claim, then re-runs Claude. Pass wipe_items: false to keep the existing quote_items rows (the AI's new items will be appended); default true dele...
quotes.append_reply Append a structured reply to an existing quote. Matches by In-Reply-To header first, then by Q-YYYY-MM-NNN in the subject.
quotes.find_by_message_id Look up an existing quote by the inbound email's RFC 5322 Message-ID header. Use this when the operator (or another agent) needs to correlate an inbound email back to a quote that was already created from it — e.g. to attach a reply, app...
quotes.list List quotes. Pass PB filter syntax in filter (e.g. status="new"). Default sort -created.
quotes.get Fetch a single quote by id. Pass expand (e.g. customer,company,items).
quotes.update Patch fields on a quote header. Whitelist: company_letterhead, quote_date, quote_reference, rfq_number, validity_days, notes, delivery_note, markup, email_cc, email_bcc, claimed_by, email_subject, email_sender, ...
quotes.send_email Front-door for the outbound email send pipeline. Validates and flips the quote to sending; the actual SMTP send runs asynchronously in n8n. selectedAttachmentIds is a list of PB file tokens from quotes.outbound_attachments (operato...
quotes.retry_email Retry the outbound email send for a quote currently in mail_failed. Use this when SMTP credentials have been rotated, the customer's MX changed, or the original send bounced. The quote must already be in mail_failed (409 if not) — fo...
quotes.clone Clone a real quote into a new real quote with a fresh quote_number and status="new". The source quote is untouched. Useful for re-quoting the same items to a different customer.
quotes.create_from_template Create a new real quote from a template (is_template=true quote). The template is not modified — a fresh quote is created with a new Q-YYYY-MM-NNN number, status="new", and the template's customer / company / letterhead / items cop...
quotes.save_as_template Snapshot an existing quote as a reusable template. The original quote is not modified; a new is_template=true row is created.
quotes.split Split a quote into multiple quotes by grouping items. Body is { source_quote_id, groups: [{ name, item_ids: string[] }] }. Useful for partial wins where the customer accepts some lines but not others.
quotes.partial_approve Mark a subset of quote items as approved (or rejected) on a quote. Body: { quote_id, decisions: [{ item_id, decision }] }. Useful when the customer accepts some lines but not others.
quotes.submit Flip a new quote to approval_pending once every line is priced. Idempotent — re-submitting an already-approval_pending or partial_approval_pending quote is a no-op (returns statusChanged: false). Gated: every line on the quote ...
quotes.re_price Re-run pricing on every quote_item under a quote. Whitelist of patchable fields is the same as bulk_update_quote_items. Returns the per-row result list.
quotes.assistant_fill Run the AI quote assistant on one quote now ({ quote_id }): fills unpriced lines from matching POS items as purple, unreviewed drafts and notes the closest items on unmatched lines. Only quotes with status new / follow_up / claimed, never templates; at most 15 lines per run. Problems come back in error (not thrown). Never marks lines done, changes the quote status or sends anything. Wraps fillQuote (same as POST /api/v1/quotes/[id]/assistant/run).
quotes.next_number Reserve the next quote number (Q{yy}{mm}{NNN}). Atomic counter; gaps are possible if the caller never persists the resulting draft.
quotes.lookup Free-text lookup over quotes for the order page's quote-picker dropdown. Substring match across quote_number, quote_reference, rfq_number, customer name, company name. Excludes templates by default.
quotes.list_attachments List all attachments on a quote (the generated PDF, original RFQ email attachments, operator-uploaded proofs, outbound attachments). Newest first. Returns up to 50 per page.
quotes.get_pdf Retrieve the rendered quote PDF as a base64 string in the MCP response. The PDF is auto-rendered if one doesn't already exist on the quote. Pass letterhead to force CNL or ISHERWOOD; defaults to the quote's stored company_letterhead....

Quote items

Tool Description
quote_items.list List quote_items rows. Pass PB filter syntax in filter (e.g. quote="<quote_id>", sku~"M16", supplier="<id>"). Default sort -created.
quote_items.get Fetch a single quote_items row by id. Pass expand to inline relations (e.g. quote,supplier).
quote_items.create Create a single quote_items row on an existing quote. Use this AFTER quotes.create_from_email when the AI extraction missed a line, or to seed items manually. Mirrors the body shape of the REST POST /api/v1/quote_items. Notes: - `q...
quote_items.update Patch a single quote_items row. Patch whitelist is identical to bulk_update_quote_items: cost_price, selling_price, description, sku, unit, supplier, category, markup, pricing_source, pricing_notes, matched_price_list_id, user_edited...
quote_items.populate_insights Deterministically compute and write quote_items.ai_item_insights from PB. Skips the n8n AI round-trip — Hermes/Giellie's go-to tool for backfilling the per-item insights blob across the board. Two modes: - pass quote_id → populates e...

Attachments

Tool Description
attachments.upload_for_quote Upload a single attachment (PDF, image, Word, Excel, CSV, plain text, or any other binary up to 25 MB) to an existing quote. The caller (n8n) should call this once per attachment AFTER quotes.create_from_email returns the new quote id....

Companies

Tool Description
companies.list List companies, optionally filtered by name or vendor substring.
companies.get Fetch a single company by id.
companies.create Create a new company. company is the display name and is required. coordinates is a { lon, lat } object (PB geoPoint) — pass null/omit if unknown.
companies.update Patch fields on a company. Only the fields present in patch are written.
companies.delete Delete a company. PB's relation cascade rules apply — a company with linked customers will fail to delete unless those customers are deleted first (or the relation's cascadeDelete is true).

Customers

Tool Description
customers.list List customers, optionally filtered by name / email / phone substring. Returns the row with company and rep fields as relation ids; pass expand: "company,rep" to receive the expanded company + user records inline. Filters: - `searc...
customers.get Fetch a single customer by id. Pass expand to inline company / rep records.
customers.create Create a new customer. customer (display name) is required. company and rep accept either a record id OR a name — names are resolved by case-insensitive lookup. If the name doesn't match, the relation is left empty so the operator ...
customers.update Patch fields on a customer. Like customers.create, company and rep accept either id or name. Pass company: "" or rep: "" to unset a relation.
customers.delete Delete a customer. Cascade rules apply — customers linked to quotes or orders will fail to delete unless those rows are removed first.

Suppliers

Tool Description
suppliers.list List suppliers, optionally filtered by name / email / phone / tags substring. coordinates is the legacy "lat, lng" text field (suppliers pre-date the geopoint migration).
suppliers.get Fetch a single supplier by id.
suppliers.create Create a new supplier. suppliername is required. coordinates is the legacy "lat, lng" string — keep suppliers on the old format for now (it's what supplier-map.ts parses).
suppliers.update Patch fields on a supplier.
suppliers.delete Delete a supplier. Cascade rules apply — suppliers linked to purchase orders / order items / quote items will fail to delete unless those rows are removed first.

Purchase orders

Tool Description
purchase_orders.add_attachment Upload a single additional file (supplier quote, drawing, NDA) to an existing PO. The file travels with the outbound email alongside the versioned PO PDF. Call once per file. Max 25 MB per file.
purchase_orders.list List purchase_orders. Pass PB filter syntax in filter (e.g. status="sent"). Default sort -created.
purchase_orders.get Fetch a single purchase_order by id. Pass expand to inline supplier.
purchase_orders.send_email Front-door for the outbound email send pipeline on a customer PO. Validates and flips the PO to sending; the actual SMTP send runs asynchronously in n8n.
purchase_orders.retry_email Retry the outbound email send for a PO currently in mail_failed. Use this when SMTP credentials have been rotated, the supplier's MX changed, or the original send bounced (e.g. quota exceeded). The PO must already be in mail_failed (...
purchase_orders.stale List draft purchase_orders older than olderThan minutes. Used by n8n's WFDASH — Purchase Order Send (Cron) workflow to pick up POs that need nudging.
purchase_orders.record_send_outcome n8n callback fired after an outbound email send resolves. Flips the PO to sent (success) or mail_failed (failure) and records the SMTP error message. Both fields are required.
purchase_orders.scan Triage scan for the po-agent workflow (see docs/agents/purchase-order-agent.md). Walks BOTH stock_purchases and purchase_orders in one round-trip, runs per-line diagnostics on each (lines_missing_sku, lines_missing_cost, lines_mi...
purchase_orders.diff Read-only price/SKU/qty diff for a supplier document against a PO. Wraps stock_purchases.match_and_receive_from_invoice with confirm=false so it returns the full MatchReport (verdict, per-line LineComparison[] with qty_delta / co...
purchase_orders.record_diff_report Persist the full MatchReport from purchase_orders.diff (or any source) onto the PO's notes field as a structured JSON entry. The entry uses category price_diff_report so future UIs can filter on it. The caller must pass `actor: { i...

Stock purchases

Tool Description
stock_purchases.list List stock_purchases. status filter accepts any value from STOCK_PURCHASE_STATUS_VALUES (draft | sending | sent | mail_failed | received | cancelled); an unknown status yields 0 results rather than 400. Supplier is auto-expanded; per-r...
stock_purchases.get Fetch a single stock_purchase plus its line items. 404 when the id is missing.
stock_purchases.create Create a draft stock_purchase with one or more line items. purchase_number (PO{yy}{6d-seq}STK) is generated server-side. The supplier id must already exist in PocketBase. Returns { purchase, items }.
stock_purchases.next_number Reserve the next stock-purchase number (PO{yy}{6d-seq}STK). Atomic counter; gaps are possible if the caller never persists the resulting draft.
stock_purchases.send Mark a draft stock_purchase as sent. v1 just flips the status flag; the actual SMTP send (when wired) lives behind send_stock_purchase_email. Returns the updated purchase or a structured error when the purchase is not in draft.
stock_purchases.receive Mark a sent stock_purchase as received, booking each line's qty into the linked stock_item and writing a stock_ledger row per line. Optional items array supports partial receives via per-line quantity_received overrides. Idempo...
stock_purchases.send_email Front-door for the outbound email send on a stock purchase. Composes the to/cc/bcc/subject/message, flips the header to sending, and triggers the n8n SMTP workflow. Returns the purchase on success or a structured failure on pre-send va...
stock_purchases.retry_email Retry the email send for a stock_purchase currently in mail_failed. Flips the header back to sending, clears the previous error, and re-runs the n8n send workflow.
stock_purchases.add_attachment Upload a single free-form attachment (supplier invoice, packing slip, photo of damaged stock) to an existing stock purchase. The file is appended to stock_purchases.additional_attachments. Call once per file. Max 25 MB per file. The ve...
stock_purchases.match_and_receive_from_invoice Buyer-side reconciliation: take a parsed supplier document (invoice or delivery note) and the matching PO number, run deterministic line-by-line matching (sku → supplier_sku → description overlap), and either dry-run or commit the receiv...

Stock purchase items

Tool Description
stock_purchase_items.update Patch whitelisted fields on a single stock_purchase_item: cost_price, sku, description, notes, unit, supplier_sku, quantity. For quantity_received use stock_purchases.receive. For per-line status use assign_driver / mark_collected. Hand-...
stock_purchase_items.list List stock_purchase_items (the line items under each stock purchase). Useful for finding PO lines missing SKU or cost, or scanning all uncollected lines for one supplier. Pass PB filter syntax in filter (e.g. `cost_price = 0 && purchas...
stock_purchase_items.bulk_update Batch patch stock_purchase_items (≤100 rows). Same field whitelist as stock_purchase_items.update. Hand-edit guard on cost_price. Per-row independence — one row's failure never affects another. dry_run returns the same shape with status:...
stock_purchase_items.assign_driver Bind a stock_purchase_item to a driver. Transitions the line status null → assigned_to_driver or collection_pool → assigned_to_driver. Wrong_status (409) when the line is already past the assignment step.
stock_purchase_items.mark_collected Mark a stock_purchase_item as physically delivered. Transitions the line status to collected and bumps quantity_received. Does NOT book into stock — that requires the invoice-receive path. quantity_collected defaults to `quantity - q...

Stock items

Tool Description
stock_items.list List stock_items. Supports substring search on name/sku/description, low-stock filter (current_qty <= min_qty AND is_active = true), pagination, and expand (defaults to supplier).
stock_items.get Fetch a single stock_item by id. The response includes the item (expand=supplier) and a paginated recent-ledger slice (ledger.page, ledger.items, etc.). Pass ledger=false to skip the ledger fetch.
stock_items.create Create a new stock_item (a POS/till item). This is the DEFAULT place to put a product and its prices — use it unless the user explicitly asked for the price list / catalogue. The till reads selling_price from this collection and nothin...
stock_items.adjust Record a manual stock adjustment. Inserts a stock_ledger row and updates current_qty on the parent item. change_qty is signed (positive = in, negative = out); zero is rejected. reason must be one of STOCK_REASON_VALUES.
stock_items.update Patch catalogue fields on a master stock_item: sku, description, notes, unit, selling_price, markup. For inventory qty use stock_items.adjust. Returns {ok: true, item, previous} on success or {ok: false, reason, message, field?} on rejec...
stock_items.set_pricing Set cost price / selling price / markup on existing stock_items in one batch (max 100 per call). The POS till reads selling_price (NOT markup), so set both consistently: selling = cost × (1 + markup/100). Cost changes are audited in st...

Collection pool

Tool Description
collection_pool.list List collection_pool records (warehouse-side goods awaiting driver assignment) by status. Use this when the operator asks 'what's on the loading floor?' or 'show me everything waiting for a driver'. Pass status to narrow by `pending|...

Collection confirm

Tool Description
collection_confirm.list List every order_item and stock_purchase_item currently in the 'collected' status (the buyer-side collection_confirm queue). Returns a unified row shape with source: 'customer' | 'stock' so the caller can filter / process each kind sep...
collection_confirm.upload_invoice Upload a supplier invoice image (or PDF) and attach it to one or more order_items' order_item_attachments file field. Use this as the second step of the buyer-side collection-confirm flow: (1) `stock_purchases.match_and_receive_from_in...
collection_confirm.confirm_order_items Mark a batch of order_items as confirmed (item_status collected → ready_invoice). Stamps the audit fields (notes, order_item_details) with the supplier invoice number + date and the agent's identity. Optional cost_overrides pat...

Sprint

Tool Description
sprint.list List the sprint (pending, no-PO) items. Use this as the source of truth for what a buyer still needs to process.
sprint.attachment_counts Return a { id → count } map of pricing-proof attachments per sprint item, suitable for showing a paperclip badge in a list view.
sprint.item_attachments List the pricing-proof attachments on a single sprint item (the supplier PDFs, screenshots, and pasted emails an agent previously uploaded). Use this BEFORE picking a sprint row so you can see what evidence is already there, and BEFORE m...
sprint.claim Race-safe claim on a sprint item. Sets claim_status=claimed, claimed_by=<caller>, claimed_at=now, claim_expires_at=now+30min. Returns 409 with the current claim if another agent already owns it.

Buyer tasks

Tool Description
buyer_tasks.list List the buyer-tasks feed for a tab. tab is one of needs_po, po_draft, po_sent. Returns { groups: [...] }.
buyer_tasks.generate_po Generate a purchase_order for a (orderId, supplierId) pair and link the supplied items to it. Returns the new purchaseOrderId.
buyer_tasks.mark_ready Mark a (orderId, supplierId) group as ready for collection. note is required (non-empty) for the audit trail.

Price list

Tool Description
price_list.add_items ⚠️ DO NOT USE BY DEFAULT. This writes to the price_list catalogue, which the POS does NOT read — nothing here ever reaches the till. Only call this when the user has explicitly asked for the price list / catalogue / supplier price list...

Users

Tool Description
users.lookup Look up PB users for audit-stamp actor: { id, name } fields. Returns the full sorted list [{id, name, email}] — no passwords, tokens, or other sensitive fields. Use this BEFORE calling tools that require an actor identity (`order_ite...

Files

Tool Description
files.fetch Fetch a single file from PocketBase and return it as base64 in the MCP response. Wraps GET /api/v1/files/[collection]/[recordId]/[filename]. Use this to retrieve a generated quote PDF, a supplier invoice scan, a delivery-note signed co...

Dashboard

Tool Description
dashboard.daily_summary Single-call daily snapshot of the order pipeline: counts by item_status (pending, collection_pool, assigned, collected, delivered, invoiced), top suppliers by open volume, value totals, etc. Wraps GET /api/v1/dashboard. Use this when...

Misc

Tool Description
audit_log.get Change history for one quote, order or delivery note — who changed what and when, newest first. Each entry lists per-field changes (from → to), the actor and source (web / mcp / n8n / agent). Includes summary counts: quote PDFs gener...
bulk_update_quote_items Patch many quote_items rows in a single MCP call (one JSON-RPC round-trip instead of N PATCHes to PB REST). Use this for AI auto-fill, supplier switches, or any bulk pricing update. Each row is independent — one row's failure never aff...
stock.fulfill Fulfill order items from stock. CREATE mode (order_id + order_item_id + stock_item_id + qty + description + selling_price + cost_price) creates a new order_item and decrements stock in one call. UPDATE mode (order_item_id...
stock.surplus_book Book surplus from a collected order_item into a stock_item. Auto-picks the matching stock_item when stock_item_id is omitted.
ai_insights.rerun Re-run the n8n AI Insights workflow on a single quote or order. Snapshot-once lifecycle — use this when new catalogue data has appeared.
ai_settings.get Read the current AI configuration. Returns the redacted record (api_key is empty, api_key_set indicates whether one is configured).
ai_settings.update Patch the AI configuration. Body shape: { provider?: "anthropic" | "minimax" | "", api_key?: string (omit to keep, "" to clear), base_url?: string, model_id?: string, max_tokens?: number (256-8192), enabled?: boolean }. Returns the updat...
tools.discovery Returns the tool list as a flat array (no JSON-RPC envelope). Equivalent to tools/list but useful for clients that can't easily build a JSON-RPC request.
drivers.list_stops List today's driver stops with statuses and assigned items. Wraps GET /api/v1/driver/stops. Returns [{ stop_id, status, customer, address, items, ... }]. Use this to plan a driver's run or to find a stop the operator is asking about ...
drivers.update_stop_status Record a driver-side action on a stop (collected, problem). Wraps PATCH /api/v1/driver/stops/[id]/status. For action: "collected" on a pickup, qty_collected is required. For action: "problem", problem_type is required (one of `...
stock_quick_draft.search Search the quick-draft index for SKUs / descriptions / suppliers. Wraps GET /api/v1/stock-quick-draft/search. Returns ranked hits with last-known cost and supplier. Use this BEFORE adding a line so the agent can pre-fill cost + supplier.
stock_quick_draft.add_line Add one line to an in-progress quick-draft PO. Wraps POST /api/v1/stock-quick-draft/add-line. The draft persists between calls until the operator hits 'Commit'. Use this when building up a stock PO one line at a time. Pass `stockItemId...
stock_quick_draft.suppliers List suppliers with quick-draft history (last ordered, line count, total spend). Wraps GET /api/v1/stock-quick-draft/suppliers. Use this to pick a supplier when the catalogue doesn't have a clear default.
delivery_notes.list List delivery notes. Reads the delivery_notes collection via the admin PB client. Supports status filter, order_id filter, and pagination. Newest-first. Use this when the operator asks 'show me outstanding delivery notes' or 'what'...
delivery_notes.set_order Link an EXISTING delivery note to an EXISTING order. Mirrors the REST POST /api/v1/delivery-notes/{id}/order endpoint. Why this exists: delivery_notes.order is a relation to orders but has no setter in the service layer — only the cr...
delivery_notes.create Create a counter-pickup delivery note (Path B — no upstream order). Wraps POST /api/v1/delivery-notes. Body: customer (PB id, required), items[] (required, ≥1 with description + quantity), optional company, notes, createdBy. ...
delivery_notes.cancel Cancel a delivery note. Wraps POST /api/v1/delivery-notes/[id]/cancel. Body: optional setById (PB user id), setByName, and note for the audit trail. Use when a DN was created in error or the customer cancels the pickup.
delivery_notes.mark_invoiced Flip a delivery note to invoiced after the customer's invoice is raised. Wraps POST /api/v1/delivery-notes/[id]/mark-invoiced. Idempotent; returns the updated DN record.
delivery_notes.mark_complete Flip a delivery note to completed when goods are physically out the door. Wraps POST /api/v1/delivery-notes/[id]/mark-complete. Idempotent.
delivery_notes.regenerate_pdf Force a fresh PDF render for a delivery note. Wraps POST /api/v1/delivery-notes/[id]/regenerate-pdf. Use after editing DN items when the operator wants the printed copy to reflect the latest state. Returns { ok, pdfSaved, filename }.
delivery_notes.create_quote Create a follow-up quote from a delivery note's items (e.g. when the customer wants a formal price for goods they took on account). Wraps POST /api/v1/delivery-notes/[id]/create-quote. Returns { id, quote_number } for the new quote.
delivery_notes.set_customer_po Stamp the customer's PO reference onto a delivery note for their AP team. Wraps POST /api/v1/delivery-notes/[id]/customer-po. Use when the customer emails a PO number after the goods have already been delivered.
delivery_notes.set_item_quantity Update the quantity on a single delivery-note line item. Wraps PATCH /api/v1/delivery-notes/items/[id]/quantity. Triggers a PDF regeneration. Returns { item, pdfRegenerated }.
delivery_notes.combine Merge multiple delivery notes for the same order into one (the survivor keeps its number; the others are cancelled). Wraps POST /api/v1/delivery-notes/combine. Use when the warehouse accidentally split a customer's pickup into multiple...

Alphabetical index

ai_insights.rerun, ai_settings.get, ai_settings.update, attachments.upload_for_quote, audit_log.get, bulk_update_quote_items, buyer_tasks.generate_po, buyer_tasks.list, buyer_tasks.mark_ready, collection_confirm.confirm_order_items, collection_confirm.list, collection_confirm.upload_invoice, collection_pool.list, companies.create, companies.delete, companies.get, companies.list, companies.update, customers.create, customers.delete, customers.get, customers.list, customers.update, dashboard.daily_summary, delivery_notes.cancel, delivery_notes.combine, delivery_notes.create, delivery_notes.create_quote, delivery_notes.list, delivery_notes.mark_complete, delivery_notes.mark_invoiced, delivery_notes.regenerate_pdf, delivery_notes.set_customer_po, delivery_notes.set_item_quantity, delivery_notes.set_order, drivers.list_stops, drivers.update_stop_status, files.fetch, order_items.confirm_collected, order_items.create, order_items.get, order_items.list, order_items.update, order_items.update_status, orders.add_attachment, orders.add_note, orders.archive, orders.cancel, orders.create, orders.create_from_quote, orders.daily_summary, orders.find_by_email_message_id, orders.find_by_order_number, orders.find_duplicates, orders.get, orders.list, orders.list_attachments, orders.mark_delivered, orders.mark_invoiced, orders.matched_attachments, orders.quote_match_sweep, orders.review, orders.review_save, orders.set_quote, orders.split, orders.sprint_counts, orders.stats, orders.update, price_list.add_items, purchase_orders.add_attachment, purchase_orders.diff, purchase_orders.get, purchase_orders.list, purchase_orders.record_diff_report, purchase_orders.record_send_outcome, purchase_orders.retry_email, purchase_orders.scan, purchase_orders.send_email, purchase_orders.stale, quote_items.create, quote_items.get, quote_items.list, quote_items.populate_insights, quote_items.update, quotes.append_reply, quotes.assistant_fill, quotes.clone, quotes.create, quotes.create_from_email, quotes.create_from_template, quotes.find_by_message_id, quotes.get, quotes.get_pdf, quotes.ingest_from_email, quotes.list, quotes.list_attachments, quotes.lookup, quotes.next_number, quotes.partial_approve, quotes.process_pending, quotes.re_price, quotes.re_run_ai, quotes.retry_email, quotes.save_as_template, quotes.send_email, quotes.split, quotes.submit, quotes.update, sprint.attachment_counts, sprint.claim, sprint.item_attachments, sprint.list, stock.fulfill, stock.surplus_book, stock_items.adjust, stock_items.create, stock_items.get, stock_items.list, stock_items.set_pricing, stock_items.update, stock_purchase_items.assign_driver, stock_purchase_items.bulk_update, stock_purchase_items.list, stock_purchase_items.mark_collected, stock_purchase_items.update, stock_purchases.add_attachment, stock_purchases.create, stock_purchases.get, stock_purchases.list, stock_purchases.match_and_receive_from_invoice, stock_purchases.next_number, stock_purchases.receive, stock_purchases.retry_email, stock_purchases.send, stock_purchases.send_email, stock_quick_draft.add_line, stock_quick_draft.search, stock_quick_draft.suppliers, suppliers.create, suppliers.delete, suppliers.get, suppliers.list, suppliers.update, tools.discovery, users.lookup