DeckBuild API
Read your jobs, versions, material takeoffs, estimates and proposals from your own tools, and get a signed notice the moment a proposal is sent or signed, or a version is sold. Build once against it and connect any CRM, spreadsheet, automation platform or AI agent.
Getting started
- A company admin opens Settings > Company > API in DeckBuild and clicks Create Key. Name it after the tool it is for, and tick only what that tool needs.
- Copy the key. DeckBuild shows it once.
- Make your first call:
curl https://api.deckbuild.com/company \
-H "Authorization: Bearer dbk_live_..."It answers with your company and the key's permissions. Then list jobs with GET /jobs. The machine-readable description of the whole API is at https://api.deckbuild.com/openapi.json, which code generators, Postman, automation platforms and AI agents read directly.
Keys and permissions
Send the key on every request as Authorization: Bearer dbk_live_.... Keys belong to the company, not a person, so they keep working when someone leaves. A key stops working when an admin revokes it, or after 180 days without a single request; Settings warns 30 days ahead. To rotate a key without downtime, create a new one, switch your tool to it, then revoke the old one.
Each key carries only the permissions ticked when it was made:
| Permission | What it reads |
|---|---|
jobs:readJobs | Jobs, versions, statuses and proposal status. No customer contact details. |
customers:readCustomer contact details | The customer's name, email, phone and job address on each job. |
estimates:readEstimates and takeoff | What the customer pays: sell price, tax, payment schedule, takeoff quantities and labor items. |
costs:readInternal costs | What you pay and keep: unit costs, labor rates, margin and commission. Give this only to tools you own. |
drawings:readDrawings | Drawing PDFs, no prices: the Plan View sent with a proposal. With Customer contact details also ticked, the full drawing set sent with a material order and the plot plan, because both print the job address. |
proposal_documents:readProposal documents | Download links for proposal PDFs and signed copies. Also needs Estimates and Customer contact details, because both are printed on them. |
material_orders:readMaterial order files | Material orders sent to suppliers and their material lists. Also needs Estimates, and Internal costs for orders sent with costs. |
Give a proposal tool estimates:read but not costs:read, and it sees what the customer pays without ever seeing your margin. A file needs the permission for everything printed on it, so a key can never read through a PDF what it could not read as data.
A key reads every job in its company, whatever per-person job visibility the company uses. Treat it like a password: keep it on a server, never in a web page or a public repository.
Requests and responses
- Base URL:
https://api.deckbuild.com. Every endpoint is aGET. - A single record comes back as
{ "data": { ... } }, a list as{ "data": [ ... ], "next_cursor", "has_more" }. - Money is in whole cents:
total_cents: 2150000is $21,500.00. Percentages run 0 to 100. - Times are ISO 8601 in UTC. Ids are UUIDs.
- A field the key may not read is
null. So is a field with nothing in it, like a customer with no phone on file. To tell the two apart, check the key's permissions withGET /company. - Every response carries
X-Request-Id. Quote it to support and we can find the call.
Staying in sync
Lists come 50 at a time (up to 200 with ?limit=). When has_more is true, pass next_cursor back as ?cursor=. To keep another system in step, ask for what changed since your last run with ?updated_since=, and page with ?order=oldest_first: a record edited while you page through moves to the end and is picked up later in the same pass, instead of being skipped.
// Run every few minutes, or when a webhook arrives.
const startedAt = new Date(); // 1. note the time FIRST
let cursor = null;
do {
const url = new URL("https://api.deckbuild.com/jobs");
url.searchParams.set("order", "oldest_first"); // 2. sync oldest first
url.searchParams.set("updated_since", lastSyncAt); // 3. what changed since last run
url.searchParams.set("include_archived", "true");
if (cursor) url.searchParams.set("cursor", cursor);
const page = await get(url);
for (const job of page.data) upsertById(job); // 4. save by id: a repeat is harmless
cursor = page.next_cursor;
} while (cursor);
// 5. next run starts a few minutes before this one did, so nothing in between is missed
lastSyncAt = new Date(startedAt.getTime() - 5 * 60 * 1000).toISOString();
// Do the same with GET /proposals for proposal changes.- A job's
updated_atmoves when the job or any of its versions is edited, and when it is archived or restored. - Sending, signing or declining a proposal moves the proposal's
updated_at, not the job's: sync proposals withGET /proposals. - Archived jobs drop out of normal lists. To see one go, sync with
?include_archived=trueand readarchived_at, or listen for thejob.archivedwebhook. - Webhooks tell you sooner. Use them to trigger a sync, and keep the sync as the safety net.
Errors
Errors share one shape: { "error": { "code", "message", "request_id" } }. Branch on code; the message is for people.
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_key | No Authorization header was sent. |
| 401 | invalid_key | The key is not one DeckBuild issued. |
| 401 | key_revoked | An admin revoked this key. |
| 401 | key_expired | The key went 180 days without a request. Create a new one. |
| 403 | api_disabled | API access is turned off for this company. Contact DeckBuild support. |
| 403 | insufficient_scope | The key lacks a permission this request needs. The message names it. |
| 402 | subscription_required | The API is part of every DeckBuild Pro plan, trials included, and this company's plan is not active. |
| 400 | invalid_request | A query parameter is not valid. The message says which. |
| 404 | not_found | No such record in your company. Another company's ids answer the same way. |
| 405 | method_not_allowed | The API is read-only. Only GET (and HEAD) are accepted. |
| 429 | rate_limited | Too many requests. Wait Retry-After seconds. |
| 503 | maintenance | DeckBuild is briefly down for maintenance. |
| 500 | internal_error | Something failed on our side. Retrying is safe. |
Rate limits
Each key may make 300 requests a minute, and all of a company's keys together 600. Two kinds of endpoint have a lower cap per key: 60 a minute to the ones that hand out download links, and 60 a minute (and 120 for the whole company) to takeoff, estimate and payment schedule, which calculate as they answer. This keeps one busy integration from slowing anyone else. Every response says where you stand: RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window resets). Over the limit you get 429 with Retry-After; wait that long and carry on.
Versioning
Our promise: fields, endpoints, events and error codes are only ever added, never renamed or removed. New DeckBuild features show up as new fields your integration can start reading whenever it likes, and nothing you built stops working.
So build to tolerate additions: ignore fields you do not know, and expect new values in lists like statuses, event types, document kinds and error codes. Every response names the contract it follows in a DeckBuild-Version header. If a change ever cannot keep this promise, it gets a new version date; existing keys keep the version they were made under, and a new key gets the new one.
Webhooks
Add a URL in Settings > Company > API > Webhooks and pick which events it hears. DeckBuild POSTs a small JSON notice to it within about a minute of the event:
| Event | Sent when |
|---|---|
job.created | A job is created |
job.updated | A job's name, number, status or customer details change |
job.archived | A job is archived |
proposal.sent | A proposal is sent to the customer |
proposal.viewed | The customer opens a proposal |
proposal.signed | The customer signs a proposal (online or on paper) |
proposal.completed | A proposal is fully signed by both sides |
proposal.declined | A proposal is marked declined |
payment.recorded | A deposit or progress payment is marked paid |
version.sold | A version is sold (its takeoff is frozen) |
{
"id": "b553d4d6-daa4-4d60-9c12-86d33f60abc6",
"company_id": "36df67a5-984b-4f43-95da-a2926842f918",
"type": "version.sold",
"created_at": "2026-10-12T01:13:46.204Z",
"data": {
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"proposal_id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"proposal_number": 1,
"status": "sold"
}
}- Notices carry IDs, never customer details or prices. Fetch what you need with your API key, under that key's permissions.
- Answer with any
2xxwithin 10 seconds, then do the work. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. - Ten failed deliveries in a row, counted across all events, switch the URL off; any success resets the count. While it is off, DeckBuild keeps its events for 30 days. Turn it back on in Settings and click Resend Missed to catch up.
- The same event can arrive twice. Drop duplicates by its
id, which is also thewebhook-idheader and stays the same on every retry. - Webhook URLs must use
https://and a public address.
Checking the signature
Every notice is signed per Standard Webhooks with your URL's secret (whsec_..., shown once in Settings), in the webhook-id, webhook-timestamp and webhook-signature headers. Libraries for most languages check it in one line. Check it on the raw body, and reject anything that fails:
import { Webhook } from "standardwebhooks"; // npm i standardwebhooks
const wh = new Webhook(process.env.DECKBUILD_WEBHOOK_SECRET); // whsec_...
export async function POST(request) {
const body = await request.text(); // the RAW body, before any JSON parsing
const headers = Object.fromEntries(request.headers);
let event;
try {
// Throws on a wrong signature, and on a timestamp more than 5 minutes
// old, which stops anyone replaying a captured notice.
event = wh.verify(body, headers);
} catch {
return new Response("bad signature", { status: 401 });
}
queueForLater(event); // answer fast; do the work after responding
return new Response("ok");
}Changing the secret. Click New Secret on the webhook in Settings. The old secret keeps working for 24 hours: until then each notice carries two signatures in webhook-signature, separated by a space, one per secret. The libraries accept a notice when either one matches, so put the new secret in your tool any time that day. After 24 hours only the new secret signs.
Recipes
Order materials when a version is sold
- Subscribe a webhook to
version.sold. A version is sold when its proposal is signed, and its takeoff is frozen at that moment, so the order matches what the customer bought. - On the notice, call
GET /versions/{version_id}/takeoff?purchase_list=true. It returns what a DeckBuild material order contains: no stock you already hold, no per-job fees, no zero quantities, with SKUs and suppliers on every line. - Add
?supplier=Lumberyardto split the list by supplier, using the supplier names on your takeoff.
Put a DeckBuild estimate into your CRM
GET /jobs?number=J-1042finds the job by its number;GET /jobs/{job_id}/versionslists each version with its proposals and their status.GET /proposals/{proposal_id}/estimategivescustomer_price: price, tax, total and the price of each group (decking, railing and so on).sourcesays whether it is the copy the customer was sent, or a draft priced live.GET /proposals/{proposal_id}/payment-schedulegives the progress payments, in cents.GET /proposals/{proposal_id}/documentsgives download links for the proposal and the Plan View drawing sent with it. The full drawing set (3D, elevations, connection details, post loads) comes with a material order, andGET /versions/{version_id}/plot-planreturns the plot plan as a PDF.
Security and support
- Download links last 5 minutes. A link works for whoever holds it until then, so do not store or share them; ask for fresh ones.
- The Try itbox below sends requests straight from your browser to the API. The key you paste stays in this page's memory: it is never saved, logged or sent anywhere else, and it is gone when you leave. Use a key with only the permissions you are trying out.
- Admins see every key's last use, its calls this month and where recent calls came from in Settings > API. Revoke a key the moment it may have leaked.
- Use of the API is covered by the DeckBuild terms. Questions or problems: info@deckbuild.com, with the
X-Request-Idif you have one.
Company
The company and key behind this request. Confirms a key works and shows what it may read. Needs no permission.
Example response
{
"data": {
"id": "36df67a5-984b-4f43-95da-a2926842f918",
"name": "Example Deck Co.",
"key": {
"id": "9b2d4f6a-1c3e-4a5b-8d7f-0e2c4a6b8d0f",
"name": "Estimating agent",
"scopes": [
"estimates:read",
"jobs:read"
],
"api_version": "2026-10-16"
}
}
}Jobs
List jobs. Most recently changed first, or oldest first for syncing. A job's updated_at moves when the job or any of its versions is edited, and when it is archived or restored. Archived jobs only appear with include_archived=true.
limit | query, optional | Page size, 1 to 200 (default 50) |
cursor | query, optional | next_cursor from the previous page |
updated_since | query, optional | Only records changed at or after this ISO 8601 time |
order | query, optional | newest_first (default) or oldest_first, the order to sync in |
status_id | query, optional | Only jobs with this status |
number | query, optional | Only the job with this job number (exact match) |
include_archived | query, optional | true: archived jobs too, with archived_at set |
Example response
{
"data": [
{
"id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"name": "Smith Deck",
"number": "J-1042",
"status": {
"id": "6a1f2c4e-8b0d-4e1a-9c3b-5d7e9f1a2b3c",
"name": "Sold",
"is_sold": true
},
"building_code": "2021",
"app_url": "https://deckbuild.com/jobs/4f8e5fa1-2706-4999-9f16-ead6266e067d",
"created_at": "2026-10-01T15:04:22.118Z",
"updated_at": "2026-10-14T19:41:07.552Z",
"archived_at": null,
"customer": {
"name": "Pat Smith",
"email": "pat.smith@example.com",
"phone": "515-555-0142",
"secondary_contact": null,
"address": {
"full": "1218 Grand Ave, Des Moines, IA 50309",
"street_number": "1218",
"street": "Grand Ave",
"city": "Des Moines",
"state": "IA",
"zip": "50309"
}
}
}
],
"next_cursor": null,
"has_more": false
}Get a job. customer is filled only for keys with customers:read. An archived job answers 404 unless include_archived=true.
job_id | in the path | The job's id |
include_archived | query, optional | true: archived jobs too, with archived_at set |
Example response
{
"data": {
"id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"name": "Smith Deck",
"number": "J-1042",
"status": {
"id": "6a1f2c4e-8b0d-4e1a-9c3b-5d7e9f1a2b3c",
"name": "Sold",
"is_sold": true
},
"building_code": "2021",
"app_url": "https://deckbuild.com/jobs/4f8e5fa1-2706-4999-9f16-ead6266e067d",
"created_at": "2026-10-01T15:04:22.118Z",
"updated_at": "2026-10-14T19:41:07.552Z",
"archived_at": null,
"customer": {
"name": "Pat Smith",
"email": "pat.smith@example.com",
"phone": "515-555-0142",
"secondary_contact": null,
"address": {
"full": "1218 Grand Ave, Des Moines, IA 50309",
"street_number": "1218",
"street": "Grand Ave",
"city": "Des Moines",
"state": "IA",
"zip": "50309"
}
}
}
}List job statuses. The company's statuses in display order, archived ones included and marked.
Example response
{
"data": [
{
"id": "6a1f2c4e-8b0d-4e1a-9c3b-5d7e9f1a2b3c",
"name": "Sold",
"position": 30,
"is_default": false,
"is_sold": true,
"archived": false
}
]
}Versions
List a job's versions. Oldest first, each with the proposals made from it.
job_id | in the path | The job's id |
Example response
{
"data": [
{
"id": "a17cf02c-db02-405e-b595-465755bb323e",
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"name": "Option A: 16x20 composite",
"parent_version_id": null,
"created_at": "2026-10-01T15:04:22.118Z",
"updated_at": "2026-10-12T01:13:46.000Z",
"drawing_updated_at": "2026-10-08T21:02:10.000Z",
"sold": {
"sold_at": "2026-10-12T01:13:46.000Z",
"proposal_id": "82361030-0244-4185-ac1d-18dfc8bebf77"
},
"app_url": "https://deckbuild.com/jobs/4f8e5fa1-2706-4999-9f16-ead6266e067d/versions/a17cf02c-db02-405e-b595-465755bb323e",
"proposals": [
{
"id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"number": 1,
"name": "Smith Deck, composite",
"status": "signed",
"sent_at": "2026-10-09T16:20:00.000Z",
"signed_at": "2026-10-12T01:13:45.000Z",
"total_cents": 2430000
}
]
}
]
}Get a version. With its proposals and whether it has been sold.
version_id | in the path | The version's id |
Example response
{
"data": {
"id": "a17cf02c-db02-405e-b595-465755bb323e",
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"name": "Option A: 16x20 composite",
"parent_version_id": null,
"created_at": "2026-10-01T15:04:22.118Z",
"updated_at": "2026-10-12T01:13:46.000Z",
"drawing_updated_at": "2026-10-08T21:02:10.000Z",
"sold": {
"sold_at": "2026-10-12T01:13:46.000Z",
"proposal_id": "82361030-0244-4185-ac1d-18dfc8bebf77"
},
"app_url": "https://deckbuild.com/jobs/4f8e5fa1-2706-4999-9f16-ead6266e067d/versions/a17cf02c-db02-405e-b595-465755bb323e",
"proposals": [
{
"id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"number": 1,
"name": "Smith Deck, composite",
"status": "signed",
"sent_at": "2026-10-09T16:20:00.000Z",
"signed_at": "2026-10-12T01:13:45.000Z",
"total_cents": 2430000
}
]
}
}Get a version's material takeoff. Every line, as the takeoff screen's CSV export has it. A sold version returns the takeoff it was sold with. Cost fields need costs:read.
version_id | in the path | The version's id |
supplier | query, optional | Only this supplier's lines. Repeat for several. |
inventory | query, optional | all (default), exclude, or only lines from your own stock |
purchase_list | query, optional | true: what a material order contains (no stock, no per-job fees, no zero quantities). Cannot be combined with inventory=only. |
Example response
{
"data": {
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"sold": true,
"lines": [
{
"line_key": "ln_3f9a1c27b84e60d5a2c1",
"group": "Lumber",
"description": "2x10 PT Joist",
"size": "2x10x16",
"color": null,
"sku": "PT21016",
"supplier": "Lumberyard",
"quantity": 24,
"unit": "ea",
"from_inventory": false,
"pieces_needed": null,
"pieces_per_package": null,
"edited": false,
"note": null,
"unit_cost_cents": 3150,
"extended_cost_cents": 75600,
"billable_cost_cents": 75600,
"taxable": true
},
{
"line_key": "ln_8e02d4b7c19f3a6e5b70",
"group": "Decking",
"description": "Trex Transcend grooved board",
"size": "1x6x20",
"color": "Spiced Rum",
"sku": null,
"supplier": "Lumberyard",
"quantity": 38,
"unit": "ea",
"from_inventory": false,
"pieces_needed": null,
"pieces_per_package": null,
"edited": true,
"note": null,
"unit_cost_cents": null,
"extended_cost_cents": null,
"billable_cost_cents": null,
"taxable": null
}
],
"totals": {
"line_count": 2,
"material_cost_cents": null,
"billable_material_cents": null,
"taxable_material_cost_cents": null,
"groups": [
{
"group": "Lumber",
"cost_cents": null
},
{
"group": "Decking",
"cost_cents": null
}
]
}
}
}List material orders sent to suppliers. Newest first, up to 50 (Send to Lumberyard in the app). The drawing file is the full drawing set as the rep exported it for the order; it needs drawings:read and customers:read, because its title block prints the job address; the material list needs costs:read too when the order was sent with costs; order_value_cents needs costs:read.
version_id | in the path | The version's id |
Example response
{
"data": [
{
"id": "c0ffee00-1234-4abc-8def-0123456789ab",
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"status": "sent",
"fulfillment": "delivery",
"delivery": {
"day": "2026-10-20",
"time": "Morning",
"location": "Driveway, left side"
},
"po_number": "PO-88213",
"note": null,
"suppliers": [
"Lumberyard"
],
"included_costs": false,
"included_inventory": false,
"line_count": 31,
"order_value_cents": null,
"recipients": [
{
"email": "orders@example-lumber.com",
"status": "delivered"
}
],
"created_at": "2026-10-12T15:30:00.000Z",
"completed_at": "2026-10-12T15:30:04.000Z",
"files": [
{
"kind": "material_list_csv",
"filename": "Smith Deck Material List.csv",
"url": "https://files.deckbuild.com/signed/...",
"expires_at": "2026-10-16T14:07:11.000Z"
}
]
}
]
}Get one material order. With fresh file links, under the same file rules as the list.
order_id | in the path | The material order's id |
Example response
{
"data": {
"id": "c0ffee00-1234-4abc-8def-0123456789ab",
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"status": "sent",
"fulfillment": "delivery",
"delivery": {
"day": "2026-10-20",
"time": "Morning",
"location": "Driveway, left side"
},
"po_number": "PO-88213",
"note": null,
"suppliers": [
"Lumberyard"
],
"included_costs": false,
"included_inventory": false,
"line_count": 31,
"order_value_cents": null,
"recipients": [
{
"email": "orders@example-lumber.com",
"status": "delivered"
}
],
"created_at": "2026-10-12T15:30:00.000Z",
"completed_at": "2026-10-12T15:30:04.000Z",
"files": [
{
"kind": "material_list_csv",
"filename": "Smith Deck Material List.csv",
"url": "https://files.deckbuild.com/signed/...",
"expires_at": "2026-10-16T14:07:11.000Z"
}
]
}
}Download a version's plot plan. The Plot Plan sheet as a PDF, built from the saved plot plan when you ask, the same sheet the Plot Plan page downloads. The response body is the PDF, not a link. Needs customers:read too, because the sheet prints the job address. A version with no plot plan drawn answers 404.
version_id | in the path | The version's id |
Proposals
List proposals. Every proposal in the company, most recently changed first. Sending, signing or declining a proposal moves the proposal's updated_at, not the job's, so sync proposals here.
limit | query, optional | Page size, 1 to 200 (default 50) |
cursor | query, optional | next_cursor from the previous page |
updated_since | query, optional | Only records changed at or after this ISO 8601 time |
order | query, optional | newest_first (default) or oldest_first, the order to sync in |
status | query, optional | Only proposals with this status |
Example response
{
"data": [
{
"id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"number": 1,
"name": "Smith Deck, composite",
"status": "signed",
"sent_at": "2026-10-09T16:20:00.000Z",
"signed_at": "2026-10-12T01:13:45.000Z",
"total_cents": 2430000,
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"current_revision": 2,
"display_mode": "lump_sum",
"valid_until": "2026-11-08",
"deposit_paid_at": "2026-10-13T14:00:00.000Z",
"app_url": "https://deckbuild.com/jobs/4f8e5fa1-2706-4999-9f16-ead6266e067d/proposals/82361030-0244-4185-ac1d-18dfc8bebf77",
"created_at": "2026-10-02T13:10:00.000Z",
"updated_at": "2026-10-13T14:00:00.000Z"
}
],
"next_cursor": null,
"has_more": false
}List a job's proposals. In proposal-number order, with status, dates and the total as last sent.
job_id | in the path | The job's id |
Example response
{
"data": [
{
"id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"number": 1,
"name": "Smith Deck, composite",
"status": "signed",
"sent_at": "2026-10-09T16:20:00.000Z",
"signed_at": "2026-10-12T01:13:45.000Z",
"total_cents": 2430000,
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"current_revision": 2,
"display_mode": "lump_sum",
"valid_until": "2026-11-08",
"deposit_paid_at": "2026-10-13T14:00:00.000Z",
"app_url": "https://deckbuild.com/jobs/4f8e5fa1-2706-4999-9f16-ead6266e067d/proposals/82361030-0244-4185-ac1d-18dfc8bebf77",
"created_at": "2026-10-02T13:10:00.000Z",
"updated_at": "2026-10-13T14:00:00.000Z"
}
]
}Get a proposal. Status, dates and the total as last sent. total_cents needs estimates:read.
proposal_id | in the path | The proposal's id |
Example response
{
"data": {
"id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"number": 1,
"name": "Smith Deck, composite",
"status": "signed",
"sent_at": "2026-10-09T16:20:00.000Z",
"signed_at": "2026-10-12T01:13:45.000Z",
"total_cents": 2430000,
"job_id": "4f8e5fa1-2706-4999-9f16-ead6266e067d",
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"current_revision": 2,
"display_mode": "lump_sum",
"valid_until": "2026-11-08",
"deposit_paid_at": "2026-10-13T14:00:00.000Z",
"app_url": "https://deckbuild.com/jobs/4f8e5fa1-2706-4999-9f16-ead6266e067d/proposals/82361030-0244-4185-ac1d-18dfc8bebf77",
"created_at": "2026-10-02T13:10:00.000Z",
"updated_at": "2026-10-13T14:00:00.000Z"
}
}Get a proposal's price breakdown. Sent proposals answer frozen, as the customer saw them; drafts are priced live. internal (costs, margin, commission) needs costs:read.
proposal_id | in the path | The proposal's id |
Example response
{
"data": {
"proposal_id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"version_id": "a17cf02c-db02-405e-b595-465755bb323e",
"source": "sent",
"revision": 2,
"priced_at": "2026-10-09T16:20:00.000Z",
"customer_price": {
"list_price_cents": 2270000,
"discount_cents": 0,
"sell_price_cents": 2270000,
"tax_cents": 0,
"total_cents": 2270000,
"total_with_options_cents": 2430000,
"groups": [
{
"group": "Framing",
"price_cents": 820000
},
{
"group": "Decking",
"price_cents": 960000
},
{
"group": "Railing",
"price_cents": 490000
}
],
"discounts": [],
"upgrades": [
{
"name": "Hidden fasteners",
"price_cents": 160000,
"selected": true
}
],
"labor": [
{
"name": "Framing",
"quantity": 320,
"group": "Framing"
}
],
"other_costs": [
{
"name": "Building permit",
"kind": "permit_fee",
"group": "Fees"
}
]
},
"internal_costs": null
}
}Get a proposal's payment schedule. Progress payments as amounts in cents, resolved against the total the customer was shown (total_with_options_cents), with paid dates.
proposal_id | in the path | The proposal's id |
Example response
{
"data": {
"proposal_id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"source": "sent",
"total_cents": 2430000,
"milestones": [
{
"position": 1,
"label": "Deposit",
"kind": "percent",
"percent": 30,
"amount_cents": 729000,
"paid_at": "2026-10-13T14:00:00.000Z"
},
{
"position": 2,
"label": "Balance on completion",
"kind": "remaining",
"percent": null,
"amount_cents": 1701000,
"paid_at": null
}
],
"financing": {
"plan_name": "12 months same as cash",
"lender": "Wisetack",
"paid_to": "builder",
"apr_pct": 0,
"term_months": 12,
"down_payment_cents": 230000,
"financed_cents": 2200000,
"monthly_cents": 183333,
"schedule": [
{
"position": 1,
"label": "Down payment, due at signing",
"payer": "homeowner",
"amount_cents": 230000
},
{
"position": 2,
"label": "At completion",
"payer": "lender",
"amount_cents": 2200000
}
]
}
}
}Get download links for a proposal's PDFs. As the customer received them; links expire after 5 minutes. The drawing is the Plan View page sent with the proposal; the full drawing set (3D, elevations, connection details, post loads) comes with a material order, and the plot plan from GET /versions/{version_id}/plot-plan. The drawing needs drawings:read. The proposal PDF and signed copies need proposal_documents:read, estimates:read and customers:read, because prices and the customer's details are printed on them. The AI rendering the customer saw (rendering_image) and, when it was made on a photo of their house, that photo (rendering_before_photo) follow the same rule as the proposal PDF. Files a key may not have are left out.
proposal_id | in the path | The proposal's id |
Example response
{
"data": {
"proposal_id": "82361030-0244-4185-ac1d-18dfc8bebf77",
"revision": 2,
"documents": [
{
"kind": "drawing_pdf",
"filename": "Smith Deck, composite Drawing.pdf",
"url": "https://files.deckbuild.com/signed/...",
"expires_at": "2026-10-16T14:07:11.000Z"
}
]
}
}Try it
Paste a key and send a real request from this page. The key stays in this page only and is gone when you leave.
curl https://api.deckbuild.com/company \
-H "Authorization: Bearer YOUR_KEY"