Proposals
Create the customer proposal for an estimate, edit its wording, email it to the customer, and track when it's viewed, accepted and signed.
View as MarkdownA proposal is the customer-facing document an estimate becomes: cover, scope, pricing, terms and an acceptance page, at a private link the customer opens in a browser (and as a PDF). TruTec tracks when it's sent, viewed, accepted or declined, and who signed.
Lifecycle. draft → sent → viewed → accepted or rejected. void is a proposal the office withdrew (a revised one replaces it).
POST /v1/estimates/{id}/proposalcreates the proposal from the estimate, or refreshes it with the estimate's current prices. Nothing is sent.PATCH /v1/proposals/{id}edits the title, addressee, valid-until date, wording and sections (About us, scope, terms, custom sections, pages, deposit, photos).POST /v1/proposals/{id}/share-linkgets the customer link without sending anything;PATCH /v1/proposals/{id}/linkturns it off or on.POST /v1/proposals/{id}/sendemails the customer, exactly as the Send button in TruTec does.GET /v1/proposals/{id}andGET /v1/proposals/{id}/signaturetrack it; webhooks fire onproposal_sent,proposal_viewed,proposal_acceptedandproposal_declined(Webhooks & events).
When the customer accepts, TruTec records who signed and moves the job to the stage your workspace maps acceptance to, the same as in the app. Writes act as the user who created the API key, with that user's permissions (Authentication).
Warning
POST /v1/proposals/{id}/send is the only call on this page that reaches a customer. It emails them from your workspace and can start your workspace's automatic follow-up emails. Confirm the recipient before you call it, especially from an AI assistant.
Timestamps are UTC ISO 8601 with a Z. Money is in dollars.
Note
Partner keys that act on behalf of several workspaces must also pass firm_id (query string, or JSON body on writes).
The proposal object#
| Field | Type | Description |
|---|---|---|
id |
integer | TruTec id. |
estimate_id |
integer or null | The estimate it was made from. |
job_id |
integer | |
status |
string | draft, sent, viewed, accepted, rejected or void. |
title |
string | The proposal's title. |
client_name, client_email, client_company |
string or null | Who it's addressed to. Send uses client_email unless you give another address. |
valid_until |
string or null | YYYY-MM-DD. |
share_url |
string or null | The customer's link. Null when the link is turned off or expired. |
is_customer_copy |
boolean | A copy of the job's proposal for an additional customer. |
approval_status |
string | not_required, pending (waiting on an owner before it can be sent), approved or rejected. |
sent_at, first_viewed_at, last_viewed_at, accepted_at, rejected_at |
string or null | |
view_count |
integer | Times the customer opened it. |
created_at, updated_at |
string | |
totals |
object | subtotal, tax, total, optional_total, deposit. The numbers the customer sees (frozen once sent). Single-proposal responses only. |
content |
object | The wording, as HTML: cover_html, intro_html, scope_html, terms_html, closing_html. Single-proposal responses only. |
signature |
object | The signature status object below. Single-proposal responses only. |
pdf_url |
string | GET it with your key for the PDF. Single-proposal responses only. |
link |
object | The customer link: has_link, enabled (not turned off), expires_on (or null) and active (the customer can open it today). Single-proposal responses only. |
sections |
object | The editable sections as the customer sees them (see Edit the sections). Single-proposal responses only. |
List proposals#
GET /v1/proposals
Permission: Read jobs
Most recently changed first. With updated_after, sorted by updated_at, oldest first, so you can page forward.
| Name | Type | Required | Description |
|---|---|---|---|
estimate_id |
integer | No | Only this estimate's proposals. |
job_id |
integer | No | Only this job's. |
status |
string | No | One of the statuses above. |
updated_after |
string | No | ISO 8601 timestamp. |
limit |
integer | No | Default 50, maximum 200. |
offset |
integer | No | Default 0. |
curl "https://trutec.ai/api/v1/proposals?status=viewed" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 3312,
"estimate_id": 9840,
"job_id": 70412,
"status": "viewed",
"title": "Northgate Plaza Sealcoat",
"client_name": "Pat Sample",
"client_email": "[email protected]",
"client_company": "Northgate Property Group",
"valid_until": "2026-11-05",
"share_url": "https://trutec.ai/portal/v3/z2eY7SFuWzWC2L02BeDVhc5b33Ud4kja",
"is_customer_copy": false,
"approval_status": "not_required",
"sent_at": "2026-10-06T19:31:10Z",
"first_viewed_at": "2026-10-06T21:02:44Z",
"last_viewed_at": "2026-10-08T15:17:03Z",
"view_count": 3,
"accepted_at": null,
"rejected_at": null,
"created_at": "2026-10-02T15:12:09Z",
"updated_at": "2026-10-08T15:17:03Z"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0 }
}
Errors: 400 status must be one of …. 403 API key lacks job read permission.
Get a proposal#
GET /v1/proposals/{id}
Permission: Read jobs
curl "https://trutec.ai/api/v1/proposals/3312" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": {
"id": 3312,
"estimate_id": 9840,
"job_id": 70412,
"status": "accepted",
"title": "Northgate Plaza Sealcoat",
"client_name": "Pat Sample",
"client_email": "[email protected]",
"client_company": "Northgate Property Group",
"valid_until": "2026-11-05",
"share_url": "https://trutec.ai/portal/v3/z2eY7SFuWzWC2L02BeDVhc5b33Ud4kja",
"approval_status": "not_required",
"sent_at": "2026-10-06T19:31:10Z",
"first_viewed_at": "2026-10-06T21:02:44Z",
"last_viewed_at": "2026-10-09T13:10:51Z",
"view_count": 4,
"accepted_at": "2026-10-09T13:12:47Z",
"rejected_at": null,
"totals": { "subtotal": 48500.0, "tax": 0.0, "total": 48500.0, "optional_total": 1850.0, "deposit": null },
"pdf_url": "/api/v1/proposals/3312/pdf",
"content": {
"cover_html": "<p>Thank you for the opportunity to review this project.</p>",
"intro_html": "<p>We pair experienced crews with modern equipment…</p>",
"scope_html": "<p>The scope below is based on measured site conditions…</p>",
"terms_html": "<ul><li>Pricing is based on the scope, quantities, and site conditions described in this proposal.</li></ul>",
"closing_html": "<p>To move forward, please review and approve the authorization section.</p>"
},
"signature": {
"proposal_id": 3312,
"proposal_status": "accepted",
"accepted_at": "2026-10-09T13:12:47Z",
"rejected_at": null,
"signer_name": "Pat Sample",
"signer_email": "[email protected]",
"signed_at": "2026-10-09T13:12:47Z",
"envelope": null
},
"is_customer_copy": false,
"created_at": "2026-10-02T15:12:09Z",
"updated_at": "2026-10-09T13:12:47Z"
}
}
Errors: 404 Proposal not found.
Create or refresh an estimate's proposal#
POST /v1/estimates/{id}/proposal
Permission: Write jobs (the key's user needs pricing access and an estimating role of owner, admin or estimator)
The sheet's Proposal button. The first call creates the proposal from the estimate, filled with your workspace's default wording, terms, clauses and template, addressed to the job's contact, valid for 30 days, with a customer link ready. Later calls refresh its prices and lines from the estimate (a sent or accepted proposal keeps the version the customer saw). For a change-order estimate it makes the change-order document. Nothing is sent. No body.
curl -X POST "https://trutec.ai/api/v1/estimates/9841/proposal" \
-H "X-API-Key: sk_your_api_key_here"
Returns 201 with the proposal (the single-proposal shape).
Errors: 404 Estimate not found. 409 with the reason when the estimate can't go to a proposal yet.
Update a proposal#
PATCH /v1/proposals/{id}
Permission: Write jobs (the key's user needs pricing access)
Only the fields you send change. HTML is cleaned the same way the proposal builder cleans it. A proposal that is out for signature can't be edited (409).
| Name | Type | Description |
|---|---|---|
title |
string | |
client_name, client_email, client_company |
string | Who it's addressed to. |
valid_until |
string or null | YYYY-MM-DD. |
cover_html, intro_html, scope_html, terms_html, closing_html |
string or null | Wording, as HTML. |
accent_color |
string | Hex color, for example #2563eb. |
curl -X PATCH "https://trutec.ai/api/v1/proposals/3312" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"title": "Northgate Plaza — Sealcoat & Restripe", "valid_until": "2026-12-31"}'
Returns 200 with the proposal.
Errors: 400 valid_until must be a date (YYYY-MM-DD) or null, Unknown fields: …. 404 Proposal not found. 409 while out for signature.
Edit the sections#
The same PATCH changes the proposal's sections, exactly as the proposal builder saves them. Nothing is sent, but a proposal that's already sent shows the new wording at its link. Read the current values in sections on GET /v1/proposals/{id}: each shows the proposal's own wording, or the company default when the proposal has none.
| Name | Type | Description |
|---|---|---|
about_us |
object | title, body_html. An empty string returns that part to the company default. |
scope |
object | The scope of work section: title, intro, notes_html. |
terms |
object | title, body_html, footer, and items: the option rows the customer sees, [{text, on}], which replace the whole list. reset: true drops this proposal's own terms and follows the company's again. |
custom_sections |
array | Your own sections (up to 32), [{id, title, body, items: [{text, on}], enabled}]. Replaces the whole list: keep a section's id to update it, leave a section out to remove it. |
section_order |
array | Section ids in order: overview, about, scope, service_info, pricing, photos, authorization, terms, acceptance and custom:<id>. |
pages |
object | Which pages print, page id to true or false (cover, video, overview, about, scope, service_info, pricing, photos, authorization and more). Only the pages you name change. |
deposit_percent |
number or null | 25 = a 25% deposit. Shown on the proposal and used by deposit invoices. |
payment_due_days |
integer or null | Days until an invoice is due (the proposal's payment terms). |
photo_ids |
array | Job photos to show on the proposal (photos on the proposal's job). [] = every photo marked for reports. |
photo_layout |
string | detail or grid. |
show_photos |
boolean | Show the photos section. |
display |
object | On/off switches: show_firm_logo, show_client_logo, show_preparer_card, show_scope_pin_details, show_quantity_column, show_unit_price_column, show_line_numbers, show_item_numbers, tax_in_item_prices, lump_sum_pricing, group_lines_separately, show_photo_titles, show_photo_captions, show_photo_dates. |
about_us.body_html, scope.notes_html and terms.body_html are the same text as intro_html, scope_html and terms_html; send one or the other, not both. A line's own wording on the proposal is changed on the estimate line (Estimates).
curl -X PATCH "https://trutec.ai/api/v1/proposals/3312" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"deposit_percent": 25,
"terms": {"items": [
{"text": "Work is weather permitting; rain days move the start date.", "on": true},
{"text": "Owner to remove vehicles before 7am.", "on": true}
]},
"pages": {"photos": false}
}'
Errors: 400 for a part the section doesn't take (terms does not take: …), an unknown page or display switch, or a deposit outside 0–100. 404 Photo not found on this job.
The customer link#
The proposal's private link is share_url. These two calls get it and control it without emailing anyone; to email it, use Send a proposal.
Get the customer link#
POST /v1/proposals/{id}/share-link
Permission: Write jobs (the key's user needs pricing access)
The builder's Copy link: creates the link when the proposal has none and returns it in url (with the proposal in data). The proposal is not marked sent. A voided proposal goes back to draft. regenerate: true replaces the link; the old link stops working.
curl -X POST "https://trutec.ai/api/v1/proposals/3312/share-link" \
-H "X-API-Key: sk_your_api_key_here"
{
"url": "https://trutec.ai/portal/v3/z2eY7SFuWzWC2L02BeDVhc5b33Ud4kja",
"data": {"id": 3312, "status": "draft", "share_url": "https://trutec.ai/portal/v3/z2eY7SFuWzWC2L02BeDVhc5b33Ud4kja", "...": "..."}
}
Errors: 409 approval_required when the proposal is over the user's send-approval limit (the owners are asked to approve it; ask again once approved), 409 while it's out for signature.
Turn the link off or on#
PATCH /v1/proposals/{id}/link
Permission: Write jobs
| Name | Type | Description |
|---|---|---|
enabled |
boolean | false turns the link off (the customer sees it's no longer available); true turns the same link back on. |
expires_on |
string or null | The day the link stops working (YYYY-MM-DD), or null for never. |
Returns the proposal; its link object shows the result. Moving valid_until later also moves an earlier link expiry with it.
Apply a proposal template#
POST /v1/proposals/{id}/apply-template
Permission: Write jobs
Applies one of the workspace's saved proposal templates (layout and pages) to a draft proposal.
| Name | Type | Required | Description |
|---|---|---|---|
template_id |
string | Yes | From GET /v1/proposal-templates. |
Returns 200 with the proposal. Errors: 404 That proposal template is no longer available. 409 when the proposal isn't a draft.
Send a proposal#
POST /v1/proposals/{id}/send
Permission: Write jobs (the key's user needs pricing access)
Emails the customer. The same as Send in the proposal builder: it re-checks the price, emails the customer a link to the live proposal (from the sending user's connected mailbox when the workspace requires it), freezes the version the customer sees, marks the proposal sent, logs the email on the job, and starts the workspace's follow-up sequence if one is set up. Sending again re-sends the link.
| Name | Type | Required | Description |
|---|---|---|---|
to_email |
string | No | Defaults to the proposal's client_email. |
cc |
array of strings | No | Up to 10 addresses. |
subject |
string | No | Defaults to your workspace's proposal email subject. Merge fields such as {{job_name}} work. |
message |
string | No | Defaults to your workspace's proposal email. {{link}} places the proposal link. |
curl -X POST "https://trutec.ai/api/v1/proposals/3312/send" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"cc": ["[email protected]"]}'
{
"data": {
"id": 3312,
"status": "sent",
"sent_at": "2026-10-12T16:40:02Z",
"share_url": "https://trutec.ai/portal/v3/z2eY7SFuWzWC2L02BeDVhc5b33Ud4kja"
},
"sent_to": "[email protected]"
}
(Response trimmed: data is the full proposal.)
When the total is over the sending user's approval limit, nothing is emailed: the workspace's owners are asked to approve it and you get
{
"error": "This proposal is over the sending user's approval limit. The workspace owners were asked to approve it; send again once approved.",
"code": "approval_required",
"amount": 48500.0
}
with status 409. Send again once approval_status is approved.
Errors: 400 A valid recipient email is required., cc must be a list of email addresses. 409 approval_required; 409 when the proposal is out for signature or the sending user has no connected mailbox and the workspace requires one. 502 when the email could not be delivered (nothing changes on the proposal).
Download the PDF#
GET /v1/proposals/{id}/pdf
Permission: Read jobs
Returns application/pdf, the same document as Download PDF in TruTec.
curl -o proposal.pdf "https://trutec.ai/api/v1/proposals/3312/pdf" \
-H "X-API-Key: sk_your_api_key_here"
Errors: 404 Proposal not found. 422 when an attached page can't be rendered. 503 when PDF rendering is briefly unavailable; retry.
Signature status#
GET /v1/proposals/{id}/signature
Permission: Read jobs
Who accepted and when, and the latest e-signature request.
{
"data": {
"proposal_id": 3312,
"proposal_status": "sent",
"accepted_at": null,
"rejected_at": null,
"signer_name": null,
"signer_email": null,
"signed_at": null,
"envelope": {
"id": 811,
"provider": "trusign",
"status": "delivered",
"status_detail": null,
"recipient_name": "Pat Sample",
"recipient_email": "[email protected]",
"sent_at": "2026-10-12T16:41:00Z",
"delivered_at": "2026-10-12T16:41:02Z",
"completed_at": null,
"declined_at": null,
"voided_at": null,
"signed_document_available": false
}
}
}
envelope.status is created, scheduled, sent, delivered, completed, declined or voided. envelope is null when no signature request was sent (customers can still accept in the proposal itself; signer_name and signed_at show that). Signature requests are sent from TruTec.
Proposal templates and clauses#
List proposal templates#
GET /v1/proposal-templates
Permission: Read jobs
The workspace's saved proposal templates.
{
"data": [
{ "id": "kq3X9sT1bQe", "name": "Commercial", "description": "Cover, scope, pricing, terms" }
],
"pagination": { "total": 1, "limit": 1, "offset": 0 }
}
List proposal clauses#
GET /v1/proposal-clauses
Permission: Read jobs
The reusable terms clauses. Clauses with is_default are added to every new proposal; the rest are picked per proposal in TruTec.
{
"data": [
{ "id": "cl-4e1a9b0c2d3f", "title": "Weather", "text": "Work is weather permitting and may be rescheduled.", "bold": false, "is_default": true }
],
"pagination": { "total": 1, "limit": 1, "offset": 0 }
}