Quote requests (RFQs)
Ask suppliers and subcontractors for prices on an estimate's materials, equipment, trucking and sub work, record their quotes, compare them and lock the winner into the bid.
View as MarkdownA quote request (RFQ) asks one or more vendors for prices on items an estimate buys: materials, rented equipment, trucking and subcontractor work. Each vendor gets a private link to a quote page (no account needed) where they enter prices; you can also record a quote they gave by phone. Accepting a quote saves it as the vendor's price and locks it onto the estimate's lines, so the bid re-prices.
The loop.
GET /v1/jobs/{job_id}/procurementshows what the job's estimate buys and which vendors are set up to quote each item.POST /v1/estimates/{estimate_id}/rfqs(orPOST /v1/jobs/{job_id}/rfqs) creates a draft. Nothing is sent.POST /v1/rfqs/{id}/sendemails each vendor their quote link. Status becomessent.- Quotes come back on the vendor's page, or you record them with
POST /v1/rfqs/{id}/responses. Status becomesanswered. GET /v1/rfqs/{id}/levelingcompares them;POST /v1/rfq-responses/{id}/acceptlocks the winner into the bid. Status becomesclosedonce every item has an accepted price.- Optionally,
POST /v1/rfq-responses/{id}/notify-acceptedemails the winning vendor.
Vendors (suppliers, rental houses and subcontractor firms) are managed with /v1/catalog/vendors. A vendor can be asked to quote an item when it supplies that kind of item, already has a price for it, or is a subcontractor (for sub work).
Writes act as the user who created the API key, with that user's estimating role (owner, admin or estimator). A member's key never acts as anyone else. Requests email from that user's connected mailbox when they have one (Authentication).
Warning
Four calls on this page reach people outside your workspace: POST /v1/rfqs/{id}/send and POST /v1/rfqs/send-digest email (or text) vendors, POST /v1/rfqs/{id}/text-link texts one, and POST /v1/rfq-responses/{id}/notify-accepted emails one. Confirm before you call them, especially from an AI assistant. Everything else only changes your workspace.
Note
Partner keys that act on behalf of several workspaces must also pass firm_id (query string, or JSON body on writes).
The quote request object#
| Field | Type | Description |
|---|---|---|
id |
integer | |
estimate_id, job_id |
integer | Where it belongs. |
title |
string | |
status |
string | draft, sent, answered or closed. |
bid_due_on |
string or null | YYYY-MM-DD. Quotes are accepted through the end of this day. |
sent_at |
string or null | Last send. |
items |
array | What is being priced: type (material, equipment, trucking, subcontractor), catalog_item_id, name, quantity, unit; line_item_id + trade for subcontractor work quoted for one estimate line; vendor_ids when each item goes to particular vendors. |
vendor_ids |
array | Everyone asked. |
response_count |
integer | Quotes received. |
scope_html |
string or null | The scope note vendors read. Single request only. |
recipients |
array | Per vendor: email, phone, sent_at, delivered, viewed_at, responded_at, locked_after_submit (they submitted, so their page is read-only). Single request only. |
responses |
array | Every quote (below). Single request only. |
created_at, updated_at |
string |
A quote (responses[]): id (use it to accept), vendor_id, vendor_name, price and unit (single-item requests), item_prices (multi-item: type, catalog_item_id, price, unit, notes), notes, submitted_via (office or link), has_bid_pdf, received_at, accepted_at, accepted_items.
Read#
GET /v1/rfqs lists requests, newest first. Filters: estimate_id, job_id, status, updated_after (then oldest change first), limit, offset. Permission: Read jobs.
GET /v1/rfqs/{id} returns one with recipients and quotes.
GET /v1/estimates/{estimate_id}/rfqs lists an estimate's requests plus quotes_received: every quote back, grouped by item and ranked cheapest first (rank, price, unit, response_id, accepted, used_in_estimate, vendor_notified). mixed_units: true means the quotes use different units, so the ranking isn't comparable.
GET /v1/rfqs/{id}/leveling lays one request out side by side: items down, vendors across, each vendor's newest price, the cheapest, outlier (more than 20% over the cheapest), missing and the accepted quote. The key's user needs an estimating role.
curl "https://trutec.ai/api/v1/estimates/9840/rfqs" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 812, "estimate_id": 9840, "job_id": 70412, "title": "RFQ — Northgate Plaza",
"status": "answered", "bid_due_on": "2026-10-20", "sent_at": "2026-10-13T16:02:11Z",
"items": [{"type": "material", "catalog_item_id": 311, "name": "Asphalt sealer",
"quantity": 1450.0, "unit": "gal", "vendor_ids": [55, 61]}],
"vendor_ids": [55, 61], "response_count": 2,
"created_at": "2026-10-13T15:58:40Z", "updated_at": "2026-10-14T09:12:30Z"
}
],
"pagination": {"total": 1, "limit": 50, "offset": 0},
"quotes_received": [
{
"type": "material", "catalog_item_id": 311, "name": "Asphalt sealer", "quantity": 1450.0, "unit": "gal",
"has_accepted_quote": false, "mixed_units": false,
"quotes": [
{"rank": 1, "rfq_id": 812, "response_id": 4410, "vendor_id": 61, "vendor_name": "Valley Coatings Supply",
"price": 4.62, "unit": "gal", "accepted": false, "used_in_estimate": false, "vendor_notified": false},
{"rank": 2, "rfq_id": 812, "response_id": 4402, "vendor_id": 55, "vendor_name": "Ridge Materials",
"price": 4.85, "unit": "gal", "accepted": false, "used_in_estimate": false, "vendor_notified": false}
]
}
]
}
What a job can request#
GET /v1/jobs/{job_id}/procurement
Permission: Read jobs (the key's user needs pricing access and an estimating role of owner, admin or estimator)
Returns the job's estimates; quotable_items (what the chosen estimate buys, with summed quantity, the line_item_ids using it and the vendor_ids set up to quote it); catalog (up to 50 matches for type / q, each with its eligible vendor_ids); the vendors book; and the job's rfqs. Query: estimate_id (default: the newest), type, q.
Create a quote request#
POST /v1/estimates/{estimate_id}/rfqs
Permission: Write jobs (role: owner, admin or estimator)
Creates a draft. Nothing is sent.
| Name | Type | Required | Description |
|---|---|---|---|
items |
array | Yes | 1–100 items: type, catalog_item_id, optional quantity, unit, line_item_id (subcontractor items only: quote that estimate line on its own, with its quantity and internal scope), vendor_ids. |
vendor_ids |
array | No | The vendors to ask. Defaults to everyone named on the items. |
title |
string | No | Defaults to "RFQ — estimate name". |
scope_html |
string | No | The scope note. {{vendor_name}}, {{job_name}}, {{address}} and {{bid_due}} fill in per vendor. Defaults to your saved request email. |
bid_due_on |
string or null | No | YYYY-MM-DD. Leave it out for your workspace's default due date; null for none. |
Give vendor_ids on every item or on none. Items with vendor_ids go only to those vendors; without, every vendor sees every item. Each vendor must be set up to quote the items it gets.
curl -X POST "https://trutec.ai/api/v1/estimates/9840/rfqs" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"items": [{"type": "material", "catalog_item_id": 311, "quantity": 1450, "unit": "gal",
"vendor_ids": [55, 61]}],
"bid_due_on": "2026-10-20"}'
Returns 201 with the request.
Errors: 400 items must be a list of 1 to 100 items to quote, Give vendor_ids on every item, or on none of them, and the app's reason when a vendor isn't set up to quote an item. 404 Estimate not found, Catalog item … not found, Vendor not found: …, Line item not found.
From a job#
POST /v1/jobs/{job_id}/rfqs
Same body, plus estimate_id (default: the job's newest estimate; a job with none gets a blank draft estimate) and client_request_id (a UUID). With client_request_id, repeating the call with the same body returns the first request with 200 and "replayed": true; reusing it for a different body is 409. The request is logged on the job. Returns 201 with data, estimate_id and replayed.
Send#
Email the vendors#
POST /v1/rfqs/{id}/send
Permission: Write jobs (role: owner, admin or estimator)
Emails each vendor on the request their quote link, from the key user's connected mailbox when they have one (replies go back to them). Vendors with no email but a cell number are texted the link when your workspace has texting set up. Marks the request sent and logs who sent it (the quote page shows them as the contact).
| Name | Type | Description |
|---|---|---|
subject |
string | Defaults to your saved request email's subject, else "Quote request — title — company". |
message_html |
string | Replaces the scope note. |
Returns data (the request), sent (per vendor: channel email or text, to, delivered, quote_url), skipped (vendor_id, reason: no email on file, no items assigned) and sent_from.
One email per vendor for several requests#
POST /v1/rfqs/send-digest with rfq_ids (up to 50) and optional subject. Emails each vendor once, with a section and link per request they are on. Returns data (the requests), sent and skipped.
Text one vendor#
POST /v1/rfqs/{id}/text-link with vendor_id and optional phone (saved on the vendor first). Texts that vendor their link. Returns phone and quote_url.
Errors: 400 That phone number does not look right., No cell number on file for this vendor — add one first., Texting is not enabled on this workspace yet. 502 when the text could not be sent.
Quotes#
Record a quote#
POST /v1/rfqs/{id}/responses
Permission: Write jobs (role: owner, admin or estimator)
For a quote a vendor gave by phone or email. Multi-item requests take item_prices; single-item requests take price.
| Name | Type | Description |
|---|---|---|
vendor_id |
integer | Required. |
item_prices |
array | type, catalog_item_id, price, optional unit, notes. Items not on the request (or not assigned to that vendor) are dropped. |
price |
number | Single-item requests. |
unit, notes |
string |
Returns 201 with the quote.
Accept a quote#
POST /v1/rfq-responses/{response_id}/accept
Permission: Write jobs (role: owner, admin or estimator)
The quoted price becomes the vendor's saved price for the item and is locked onto the estimate's matching lines, which re-price. A subcontractor quote for one estimate line is locked on that line only. Another vendor's accepted price on the same item is replaced (last pick wins). Nothing is emailed.
Optional items (type, catalog_item_id, line_item_id) picks lines from a multi-item quote; without it every line the vendor priced is accepted. Returns data (the quote), accepted (with price_id and estimate_lines_updated), skipped (with reason), rfq_status.
Use a price on the estimate only#
POST /v1/rfq-responses/{response_id}/use-price
Puts the quoted price on the estimate's matching lines without accepting the quote: no saved vendor price, and the request stays open. Optional items as above. All-or-nothing: a quote whose unit doesn't match the estimate line is refused with the reason.
Tell the vendor they won#
POST /v1/rfq-responses/{response_id}/notify-accepted
Emails the vendor that their quote was accepted, only after it was. Uses your saved acceptance email unless you send subject and/or message (plain text). A multi-item request names the item (type, catalog_item_id, line_item_id). Returns delivered, email, item_name, price, unit.
Line scope for subcontractors#
The scope a subcontractor reads for a line comes from the line's internal_scope, set with PATCH /v1/estimates/{id}/line-items/{line_item_id} (Estimates).