# WFDASH API — Quote Create + Customer Lookup Reference > What the WFDASH REST/MCP API actually exposes today for the two > tasks you keep hitting during intake: creating a quote, and > finding an existing customer. --- ## Quote create — three flavours There are three different quote-creation endpoints. Pick by **where the data came from**, not by what feels closest: ### 1. `quotes.create` (walk-in) — **the one for your intake** ``` POST /api/v1/quotes/create ``` - Use when: there's no inbound RFQ email. Operator types it in, you extracted items from a PDF / phone-call / WhatsApp, or you're driving the quote from the in-store counter PWA. - Body has `customer` (id OR name), `company` (id OR name), and `items[]`. **Names are resolved case-insensitive exact** — if there's no match, a new customer / company row is created with empty defaults. So passing a name is safe; passing the wrong name silently creates a duplicate. - Returns `{ quote, customer, company, items, quote_number }` on 201. - Status default is `"new"`. `quote_number` is auto-generated unless you supply one (operator-supplied conflicts return 409). - MCP wrapper: `mcp__wfdash-mcp__quotes__create`. Same body shape. ### 2. `quotes.ingest_from_email` (inbound RFQ email) ``` POST /api/v1/quotes/ingest-from-email ``` - Use when: the customer actually emailed in. Returns immediately with `ai_status="pending"`; the background processor runs Claude against the body + attachments to extract items. Don't extract items yourself — let the pipeline do it. - MCP wrapper: `mcp__wfdash-mcp__quotes__ingest_from_email`. ### 3. `quotes.create_from_email` (legacy, **avoid**) ``` POST /api/v1/quotes/create-from-email ``` - Caller passes a pre-extracted `extracted_items[]`. Only use this if you're calling on behalf of a parser that already did the extraction. **Wrong tool if you don't have an inbound email** — it requires `subject` + parsed body and silently rewrites `email_message_id` to a `synth-…@wfdash.local` sentinel. - MCP wrapper: `mcp__wfdash-mcp__quotes__create_from_email`. --- ## Customer lookup — fuzzy search by name / company ### `customers.list` — substring search ``` GET /api/v1/customers ``` Query params: | Param | Type | What it does | |-----------|------|--------------| | `search` | string | Substring match on **customer name / email / phone**, OR'd together; also matches **rep name / rep email** (the user that owns this customer). One query covers all four fields. Case-sensitive PB `~` operator. | | `rep` | string | Exact filter on the rep relation. Accepts a 15-char PB user id OR a user name / email (case-insensitive). | | `page` | number | 1-based, default 1 | | `perPage` | number | default 50, hard cap 100 | | `sort` | string | PB sort expression, default `-created` | | `expand` | string | Comma-separated, e.g. `"company,rep"` to inline relations | Returns `{ items, totalItems, totalPages, page, perPage }`. MCP wrapper: `mcp__wfdash-mcp__customers__list`. Same params. ### `customers.get` — fetch by id ``` GET /api/v1/customers/{id} ``` For when you already have the PB id and just need the full row + relations. MCP wrapper: `mcp__wfdash-mcp__customers__get`. ### What we DO NOT have today - **No direct "filter customers by company name"** on `customers.list`. The `search` param doesn't search across the `company` relation. Two workarounds: 1. Pass `expand: "company"` on the list call, then filter the returned `items[]` client-side by company name. 2. First `companies.list({search: ""})` to get the company id, then iterate the matching customers client-side. (The `customers.list` `search` filter does not accept a company relation id today.) - **No dedicated `customers.lookup` endpoint** — `customers.list` with a small `perPage` (e.g. 5) is the closest equivalent. - **No fuzzy / Levenshtein matching.** It's PB's literal substring `~` operator. "AB" won't match "ABC Trading" if you typed it wrong-case, and partial-token matches like "Bear" against "Bearings Co" will work — but "Bearings" against "Bear" works too (substring is bidirectional). Typos are not forgiven. --- ## The intake pattern that works Given what we have, the cleanest customer-resolution flow at intake is: ```ts // 1. Try the most specific lookup first. const hit = await customers.list({ search: customerName, expand: "company", perPage: 5, }); // 2. If the caller ALSO gave a company name, narrow client-side. const filtered = companyName ? hit.items.filter(c => c.expand?.company?.company === companyName) : hit.items; // 3. If 0 hits → create via customers.create (auto-creates company // if company name doesn't resolve either). Use quotes.create with // the names directly — it has the same auto-create behaviour, so // you don't need a separate customers.create call unless you want // to backfill email/phone/etc. ``` For quote creation specifically, **just pass `customer` and `company` as names to `quotes.create`** — the service layer resolves them by case-insensitive exact match and creates missing rows. You only need the lookup step when: - You're filling in `email` / `phone` from a contact form. - You want to verify the customer exists before showing the operator a "is this the right John Smith?" confirmation. - You're merging two customer records. --- ## Companies (related, since you mentioned name + company) ### `companies.list` — substring search ``` GET /api/v1/companies ``` Query params: `page`, `perPage`, `search` (substring on `company.company` / address / vendor), `sort`. Same shape as `customers.list`. MCP wrapper: `mcp__wfdash-mcp__companies__list`. ### `companies.get` — fetch by id ``` GET /api/v1/companies/{id} ``` MCP wrapper: `mcp__wfdash-mcp__companies__get`. ### `companies.create` / `companies.update` Free-text company name, optional address / vendor / coordinates. Same auto-resolve-and-create-on-no-match behaviour as customers.