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.

The API is read-only: it never changes anything in DeckBuild. Every request reads only your own company's data, enforced by the database itself, the same way the app is. It is included in every DeckBuild Pro plan, trials too.

Getting started

  1. 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.
  2. Copy the key. DeckBuild shows it once.
  3. 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:

PermissionWhat it reads
jobs:read
Jobs
Jobs, versions, statuses and proposal status. No customer contact details.
customers:read
Customer contact details
The customer's name, email, phone and job address on each job.
estimates:read
Estimates and takeoff
What the customer pays: sell price, tax, payment schedule, takeoff quantities and labor items.
costs:read
Internal costs
What you pay and keep: unit costs, labor rates, margin and commission. Give this only to tools you own.
drawings:read
Drawings
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:read
Proposal documents
Download links for proposal PDFs and signed copies. Also needs Estimates and Customer contact details, because both are printed on them.
material_orders:read
Material 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 a GET.
  • A single record comes back as { "data": { ... } }, a list as { "data": [ ... ], "next_cursor", "has_more" }.
  • Money is in whole cents: total_cents: 2150000 is $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 with GET /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_at moves 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 with GET /proposals.
  • Archived jobs drop out of normal lists. To see one go, sync with ?include_archived=true and read archived_at, or listen for the job.archived webhook.
  • 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.

StatusCodeMeaning
401missing_keyNo Authorization header was sent.
401invalid_keyThe key is not one DeckBuild issued.
401key_revokedAn admin revoked this key.
401key_expiredThe key went 180 days without a request. Create a new one.
403api_disabledAPI access is turned off for this company. Contact DeckBuild support.
403insufficient_scopeThe key lacks a permission this request needs. The message names it.
402subscription_requiredThe API is part of every DeckBuild Pro plan, trials included, and this company's plan is not active.
400invalid_requestA query parameter is not valid. The message says which.
404not_foundNo such record in your company. Another company's ids answer the same way.
405method_not_allowedThe API is read-only. Only GET (and HEAD) are accepted.
429rate_limitedToo many requests. Wait Retry-After seconds.
503maintenanceDeckBuild is briefly down for maintenance.
500internal_errorSomething 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:

EventSent when
job.createdA job is created
job.updatedA job's name, number, status or customer details change
job.archivedA job is archived
proposal.sentA proposal is sent to the customer
proposal.viewedThe customer opens a proposal
proposal.signedThe customer signs a proposal (online or on paper)
proposal.completedA proposal is fully signed by both sides
proposal.declinedA proposal is marked declined
payment.recordedA deposit or progress payment is marked paid
version.soldA 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 2xx within 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 the webhook-id header 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

  1. 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.
  2. 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.
  3. Add ?supplier=Lumberyard to split the list by supplier, using the supplier names on your takeoff.

Put a DeckBuild estimate into your CRM

  1. GET /jobs?number=J-1042 finds the job by its number; GET /jobs/{job_id}/versions lists each version with its proposals and their status.
  2. GET /proposals/{proposal_id}/estimate gives customer_price: price, tax, total and the price of each group (decking, railing and so on). source says whether it is the copy the customer was sent, or a draft priced live.
  3. GET /proposals/{proposal_id}/payment-schedule gives the progress payments, in cents.
  4. GET /proposals/{proposal_id}/documents gives 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, and GET /versions/{version_id}/plot-plan returns 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-Id if you have one.

Company

GET/companyNeeds no permission

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

GET/jobsNeeds jobs:read

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.

limitquery, optionalPage size, 1 to 200 (default 50)
cursorquery, optionalnext_cursor from the previous page
updated_sincequery, optionalOnly records changed at or after this ISO 8601 time
orderquery, optionalnewest_first (default) or oldest_first, the order to sync in
status_idquery, optionalOnly jobs with this status
numberquery, optionalOnly the job with this job number (exact match)
include_archivedquery, optionaltrue: 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/jobs/{job_id}Needs jobs:read

Get a job. customer is filled only for keys with customers:read. An archived job answers 404 unless include_archived=true.

job_idin the pathThe job's id
include_archivedquery, optionaltrue: 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"
      }
    }
  }
}
GET/job-statusesNeeds jobs:read

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

GET/jobs/{job_id}/versionsNeeds jobs:read

List a job's versions. Oldest first, each with the proposals made from it.

job_idin the pathThe 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/versions/{version_id}Needs jobs:read

Get a version. With its proposals and whether it has been sold.

version_idin the pathThe 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/versions/{version_id}/takeoffNeeds estimates:read

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_idin the pathThe version's id
supplierquery, optionalOnly this supplier's lines. Repeat for several.
inventoryquery, optionalall (default), exclude, or only lines from your own stock
purchase_listquery, optionaltrue: 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
        }
      ]
    }
  }
}
GET/versions/{version_id}/material-ordersNeeds material_orders:read, estimates:read

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_idin the pathThe 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/material-orders/{order_id}Needs material_orders:read, estimates:read

Get one material order. With fresh file links, under the same file rules as the list.

order_idin the pathThe 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"
      }
    ]
  }
}
GET/versions/{version_id}/plot-planNeeds drawings:read, customers:read

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_idin the pathThe version's id

Proposals

GET/proposalsNeeds jobs:read

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.

limitquery, optionalPage size, 1 to 200 (default 50)
cursorquery, optionalnext_cursor from the previous page
updated_sincequery, optionalOnly records changed at or after this ISO 8601 time
orderquery, optionalnewest_first (default) or oldest_first, the order to sync in
statusquery, optionalOnly 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
}
GET/jobs/{job_id}/proposalsNeeds jobs:read

List a job's proposals. In proposal-number order, with status, dates and the total as last sent.

job_idin the pathThe 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/proposals/{proposal_id}Needs jobs:read

Get a proposal. Status, dates and the total as last sent. total_cents needs estimates:read.

proposal_idin the pathThe 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/proposals/{proposal_id}/estimateNeeds estimates:read

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_idin the pathThe 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/proposals/{proposal_id}/payment-scheduleNeeds estimates:read

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_idin the pathThe 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/proposals/{proposal_id}/documentsNeeds one of drawings:read, proposal_documents:read

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_idin the pathThe 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"