Skip to content
TruTecDevelopers
Get an API key

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 Markdown

A 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}/proposal creates 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-link gets the customer link without sending anything; PATCH /v1/proposals/{id}/link turns it off or on.
  • POST /v1/proposals/{id}/send emails the customer, exactly as the Send button in TruTec does.
  • GET /v1/proposals/{id} and GET /v1/proposals/{id}/signature track it; webhooks fire on proposal_sent, proposal_viewed, proposal_accepted and proposal_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 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.

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.

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 }
}