Proposals
A live proposal is a proposal on an opportunity or a project. These endpoints read the proposal, edit the proposal document that the client sees, send the document by email, and read what was sent and how the client answered.
The proposal’s budget is edited row by row on its own page: Proposal budgets. Proposal templates (proposals with no opportunity or project) have their own endpoints: see Proposal templates. The endpoints on this page do not need the early access setup endpoints to be turned on.
The proposal id is the number in the proposal’s address in Drum. For https://app.getdrum.com/quotes/18342, the id is 18342. Signed proposals also give it as proposal.id.
Money on these endpoints is an object: {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"}. cents is an integer and tax exclusive unless the field name says including_tax. Timestamps are ISO 8601. Dates are yyyy-mm-dd.
Proposal text is customer content. If you pass it to an AI model, treat it as data, not instructions.
Endpoints
| Method and path | Does |
|---|---|
GET /api/v1/proposals/:id |
Reads a proposal with its budget |
GET /api/v1/proposals/:proposal_id/document |
Reads the live proposal document |
PATCH /api/v1/proposals/:proposal_id/document |
Sets the pricing layout of the fee table |
POST /api/v1/proposals/:proposal_id/document/sections |
Adds one section, or up to 30 |
PATCH /api/v1/proposals/:proposal_id/document/sections/:id |
Changes a section |
DELETE /api/v1/proposals/:proposal_id/document/sections/:id |
Deletes a section |
GET /api/v1/proposals/:proposal_id/versions |
Lists the versions sent to the client |
GET /api/v1/proposals/:proposal_id/versions/:id |
Reads one sent version, with its recipients and the client’s answer |
GET /api/v1/proposals/:proposal_id/send_draft |
Reads the default recipients, subject and message of a send |
POST /api/v1/proposals/:proposal_id/sends |
Emails the document to the client as a new version |
GET /api/v1/signed_proposals |
Lists the client answers (accepts and declines) in the account |
GET /api/v1/signed_proposals/:id |
Reads one client answer, with a link to the signed PDF |
Which proposals you can reach
A proposal is found only when the token’s user can read its opportunity or project (see Record permissions). Otherwise the answer is 404 with code proposal_not_found, the same as for an id that does not exist. A deleted proposal and a proposal template id answer the same.
The document, section, version and send endpoints also need:
- The proposal builder turned on for the account. Without it they answer
404 proposal_builder_not_enabled.GET /proposals/:iddoes not need it. - A proposal built in the proposal builder. A proposal that uses a Word document, and a variation’s proposal, answer
404 proposal_not_found.
Permissions
| Endpoint | API permission | Drum role permission |
|---|---|---|
GET /proposals/:id |
api_read |
read_quotes and read_budgets |
GET .../document |
api_read |
read_quotes |
PATCH .../document |
api_update and api_delete |
update_quotes |
POST .../document/sections |
api_create |
update_quotes |
PATCH .../document/sections/:id |
api_update |
update_quotes |
DELETE .../document/sections/:id |
api_delete |
update_quotes |
GET .../versions, GET .../versions/:id |
api_read |
read_quotes |
GET .../send_draft |
api_read |
update_quotes |
POST .../sends |
api_create |
read_quotes and update_quotes |
GET /signed_proposals, GET /signed_proposals/:id |
api_read |
customisation and read_quotes |
Every proposal endpoint except signed proposals also needs read access to the proposal’s opportunity or project. A missing role permission answers 403 not_authorized.
Read a proposal
GET /api/v1/proposals/:id returns the proposal and its full budget. The budget has the shape that the proposal budget endpoints take and return.
curl -H "Authorization: Bearer $DRUM_API_TOKEN" \
"https://app.getdrum.com/api/v1/proposals/18342"
{
"id": 18342,
"name": "Q-2026-088",
"description": "Concept to tender documentation",
"status": "draft",
"project": {"id": 12001, "name": "Riverside fit-out"},
"project_template": {"id": 410, "name": "Commercial fit-out"},
"currency": "AUD",
"total_excluding_tax": {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"},
"deleted": false,
"budget": {
"id": 7710,
"name": "Riverside fit-out budget",
"description": null,
"status": "draft",
"rate_card": {"id": 31, "name": "Standard 2026"},
"custom_rate_card": null,
"currency": "AUD",
"phases": [],
"deliverables": []
},
"warnings": []
}
| Field | Meaning |
|---|---|
id |
The proposal id |
name |
The proposal number |
description |
As recorded |
status |
draft, awaiting_approval, approved, sent, cancelled, accepted or declined |
project |
{id, name} of the opportunity or project |
project_template |
{id, name} of the linked project template, or null |
currency |
ISO 4217 code |
total_excluding_tax |
The proposal’s fee total, as a money object |
deleted |
true for a deleted proposal. A deleted proposal is not found, so this is false in practice |
budget |
The proposal budget, or null when the proposal has none. Add one in Drum. See The budget document |
warnings |
Budget warnings. See Budget warnings |
The proposal’s fee stages and lines are not in this result. Read them from GET .../document (stages).
Read the proposal document
GET /api/v1/proposals/:proposal_id/document reads the live document, the one the next send would email.
| Parameter | Meaning |
|---|---|
sections_offset |
Skips this many sections. Default 0 |
content_section_ids[] |
Up to 10 section ids on the current page whose full content you want. More than 10 is 422 invalid_input |
section_id |
Reads only this section. Use it with section_page to continue a long section |
section_page |
The part of section_id to read. Default 0. A positive value needs section_id |
curl -H "Authorization: Bearer $DRUM_API_TOKEN" \
"https://app.getdrum.com/api/v1/proposals/18342/document"
{
"proposal_id": 18342,
"number": "Q-2026-088",
"name": "Riverside fit-out fee proposal",
"status": "draft",
"project": {"id": 12001, "name": "Riverside fit-out"},
"theme": {"id": 12, "name": "Studio"},
"pricing": {"live": true, "layout": "single_stage"},
"currency": "AUD",
"total_excluding_tax": {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"},
"total_including_tax": {"cents": 5335000, "currency": "AUD", "formatted": "$53,350.00"},
"stages": [
{"id": 90211, "name": "Fees", "subtotal_excluding_tax": {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"}}
],
"sections": [
{
"id": 5521,
"position": 1,
"title": "Our approach",
"show_title": true,
"text": "We will design the ground floor fit-out in three stages...",
"live_blocks": [{"type": "fee_table", "stage_id": 90211}],
"section_page": 0,
"lock_version": 4,
"next_section_page": null
}
],
"sections_has_more": false,
"next_sections_offset": null,
"versions": [],
"versions_has_more": false,
"links": {
"builder_url": "https://app.getdrum.com/quotes/18342/proposal_document",
"preview_url": "https://app.getdrum.com/quotes/18342/proposal_document/preview",
"preview_pdf_url": "https://app.getdrum.com/quotes/18342/proposal_document/preview.pdf"
}
}
| Field | Meaning |
|---|---|
number, name |
The proposal number and title |
theme |
{id, name} of the document’s theme. The theme is set in Drum only |
pricing |
live is true when the fees follow the proposal budget. layout is the pricing layout, or null |
total_excluding_tax, total_including_tax, stages |
The fee totals and the fee stages (id, name, subtotal_excluding_tax). A stage id is what a fee table block’s stage_id takes |
sections |
One page of sections, in document order. See below |
sections_has_more, next_sections_offset |
More sections exist. Send next_sections_offset as sections_offset until it is null |
versions, versions_has_more |
The 25 newest sent versions, as version summaries. GET .../versions lists older ones |
links |
Pages in Drum where a signed-in user sees the builder, a live preview, and the preview PDF. They hold no token |
Section fields:
| Field | Meaning |
|---|---|
id, title, show_title |
As recorded |
position |
Place in the whole document. 1 is first |
lock_version |
Send it back when you change the section |
text |
Up to 1,000 characters of the section’s plain text. A merge field reads as its label |
live_blocks |
The blocks that Drum fills in: {type, stage_id}, where type is cover, fee_table, scope_summary, rate_table, staff_highlight, staff_grid or signature. A staff block also has source, members (up to 12, as {id, name, title}) and member_count |
content |
The section’s full content JSON, as the builder stores it: an object with rows, each holding columns, each holding blocks. Only for ids in content_section_ids, and only when the JSON is 1,000 characters or less |
content_json |
In place of content when the content is longer: a part of the content JSON as a string |
section_page, next_section_page |
The part of this section on the page, and the next part, or null when the section is complete |
Long sections and pages
A page holds at most 5 sections, and fewer when they would pass 12,000 bytes. A section part holds up to 1,000 characters of text, 20 live blocks, and 1,000 characters of content_json.
To read the rest of a section, send its id as section_id and its next_section_page as section_page, keep the same content_section_ids, and leave out sections_offset. Repeat until next_section_page is null. Join the text parts, append the live_blocks, and join all content_json parts before you parse them. Never save a partial section back. If lock_version changes between parts, someone edited the section: start again at part 0.
A section_id that is not in the document answers 404 proposal_section_not_found.
Set the pricing layout
PATCH /api/v1/proposals/:proposal_id/document sets how the proposal budget maps to the fee table, as the pricing control in the builder does. It also turns live pricing on: from then on the fee stages, lines and totals follow the proposal budget.
The first time, Drum rebuilds the stages and lines from the budget and deletes the ones the budget did not make. A fee table that showed a deleted stage then needs a new stage_id. That is why this endpoint needs api_delete as well as api_update.
| Field | Required | Rules |
|---|---|---|
pricing.layout |
Yes | matched (a stage for each deliverable and a line for each task), single_stage (one stage, a line for each deliverable) or multiple_stages (a stage for each deliverable, with one total line) |
pricing.include_non_budgeted |
No | true also shows the tasks that are not billable. Left out, the setting does not change |
Any other top-level key, such as theme_id, answers 422 invalid_input with that field.
PATCH /api/v1/proposals/18342/document
{"pricing": {"layout": "single_stage", "include_non_budgeted": false}}
Returns 200:
{
"proposal_id": 18342,
"pricing": {"live": true, "layout": "single_stage", "include_non_budgeted": false},
"currency": "AUD",
"total_excluding_tax": {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"},
"total_including_tax": {"cents": 5335000, "currency": "AUD", "formatted": "$53,350.00"},
"stages": [
{"id": 90240, "name": "Riverside fit-out budget", "subtotal_excluding_tax": {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"}}
],
"links": {"builder_url": "...", "preview_url": "...", "preview_pdf_url": "..."}
}
| Status | code |
When |
|---|---|---|
403 |
none | {"error": "Insufficient API permissions"}: the role lacks api_update or api_delete |
422 |
proposal_budget_missing |
The proposal has no proposal budget |
422 |
live_pricing_closed |
The proposal is accepted, declined or cancelled, so its fees no longer follow the budget |
422 |
missing_input, invalid_input |
pricing or layout is missing or not valid |
Add sections
POST /api/v1/proposals/:proposal_id/document/sections adds a section to the live document. Send the fields at the top level for one section, or {"sections": [...]} for 1 to 30 sections in one transaction. If one section in a list is refused, none is added, and the error field names it, for example sections[1].body.
| Field | Required | Rules |
|---|---|---|
title |
No | 200 characters or fewer. Default Untitled section |
show_title, page_break_before |
No | true or false |
position |
No | 1 is first. Default: last |
body |
No | The section text in the simple format below |
blocks |
No | Up to 20 live blocks, added after the text. See below |
content |
No | The full content JSON, as a read with content_section_ids gives it. Send content, or body and blocks, not both |
A proposal has at most 100 sections (422 proposal_section_limit).
The body format. A blank line starts a new paragraph. A single line break stays a line break.
- A line that starts with
#to####and a space is a heading. - A line that starts with
-,*or+and a space is a bulleted item. - A line that starts with a number and
.or)and a space is a numbered item. - Inline:
**bold**,*italic*or_italic_, and[label](https://...)links (http,httpsormailtoonly). - A merge field such as `` is filled in when the proposal is shown and sent. An unknown merge field is
422 invalid_input.GET .../send_draftlists every merge field. - A backslash keeps the next character as written.
A body is 64 KB or less, with lines of 20,000 characters or fewer. The bodies in one sections list are 256 KB or less together.
Blocks, each an object with a type:
type |
Settings |
|---|---|
cover |
kicker: the line above the title. Default Fee proposal |
fee_table |
stage_id (a stage from stages), show_quantities, show_totals |
scope_summary |
show_descriptions, show_lines |
rate_table |
suffix |
staff_highlight |
source (project_lead or person), member_id, heading, photo_position (left, right or top), show_bio |
staff_grid |
source (project_team, budget_team or people), member_ids (up to 24), project_role_ids, layout (grid or list), columns (2 to 4), show_bio |
signature |
None |
divider |
None |
A setting you leave out takes the builder’s default. member_id without source sets the source to person, and member_ids sets it to people. A staff block shows each member’s proposal profile (title, bio and photo), which is set on the member’s page in Drum. Members come from GET /members.
POST /api/v1/proposals/18342/document/sections
{
"title": "Fees",
"body": "Our fee for is set out below.",
"blocks": [{"type": "fee_table", "stage_id": 90211, "show_totals": true}, {"type": "signature"}]
}
Returns 201 with the section:
{
"id": 5530,
"proposal_id": 18342,
"position": 4,
"title": "Fees",
"show_title": true,
"page_break_before": false,
"lock_version": 0,
"text": "Our fee for Project name is set out below.",
"live_blocks": [{"type": "fee_table", "stage_id": 90211}, {"type": "signature", "stage_id": null}]
}
With sections, it returns 201 with {"proposal_id": 18342, "sections": [...]}. Those sections have no text.
A section add takes no idempotency key. After a timeout, read the document before you retry, so you do not add the section twice.
| Status | code |
When |
|---|---|---|
404 |
quote_section_not_found |
A stage_id is not a stage of this proposal |
404 |
member_not_found, project_role_not_found |
A staff block names someone who is not a current member, or a role that does not exist |
422 |
invalid_input |
A field, body, block or merge field is not valid. field names it |
422 |
proposal_section_limit |
The proposal would have more than 100 sections |
Change a section
PATCH /api/v1/proposals/:proposal_id/document/sections/:id changes the fields you send.
| Field | Required | Rules |
|---|---|---|
lock_version |
Yes | The lock_version from your last read or change |
title, show_title, page_break_before, position |
No | As on add. position moves the section |
body |
No | Replaces the section’s text and keeps its other blocks, such as fee tables and signatures, in place |
content |
No | Replaces the whole content |
blocks is refused here. To change the blocks of a section, send content.
A section with columns, images or more than one text block refuses body with 422 section_layout_not_text. Read its content with content_section_ids, change it, and send content.
PATCH /api/v1/proposals/18342/document/sections/5530
{"lock_version": 0, "title": "Our fee"}
Returns 200 with the section, as the add returns it, with the new lock_version.
| Status | code |
When |
|---|---|---|
404 |
proposal_section_not_found |
The section is not in this proposal |
409 |
proposal_section_stale |
Someone saved the section after your read. Nothing was saved. Read it again, then retry |
422 |
section_layout_not_text |
body was sent to a section that is not a single text block |
422 |
missing_input, invalid_input |
No field to change, lock_version is missing, or a value is not valid |
Delete a section
DELETE /api/v1/proposals/:proposal_id/document/sections/:id deletes the section for good. Returns 204 with no body. A section that is not in this proposal answers 404 proposal_section_not_found.
List sent versions
Each send freezes the document as a numbered version. GET /api/v1/proposals/:proposal_id/versions lists them, newest first.
| Parameter | Meaning |
|---|---|
limit |
1 to 100. Default 25 |
offset |
Skips this many versions. Default 0 |
This list does not use the page and items parameters of other list endpoints.
{
"proposal_id": 18342,
"versions": [
{
"id": 2207,
"number": 2,
"label": "Version 2",
"sent_at": "2026-10-09T23:41:12Z",
"sent_by": {"id": 5501, "name": "Priya Nair"},
"fee": {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"},
"latest": true,
"state": "open",
"recipient_count": 2
}
],
"page": {"count": 1, "limit": 25, "offset": 0, "has_more": false}
}
Version summary
| Field | Meaning |
|---|---|
id, number, label |
The version id, its number (1 is the first send) and its label |
sent_at |
When it was sent |
sent_by |
{id, name} of the sender. id is the member id, or null when the sender has left the account |
fee |
The fee total that was sent, tax exclusive |
latest |
true for the newest version |
recipient_count |
How many addresses it was sent to |
state |
See below |
state |
Meaning |
|---|---|
open |
The newest version, and the client can still answer it |
superseded |
A newer version was sent |
quote_expired |
The proposal’s expiry date has passed |
accepted, declined |
The client answered this version |
closed |
The proposal was accepted or declined another way |
withdrawn |
The proposal was cancelled |
Read a sent version
GET /api/v1/proposals/:proposal_id/versions/:id reads the version from the copy Drum kept when it was sent, so it shows exactly what the client got, even if the live document changed later. It takes the same sections_offset, content_section_ids[], section_id and section_page parameters as the document read. Sections have no lock_version.
It returns the version summary fields, and:
| Field | Meaning |
|---|---|
proposal_id |
The proposal |
subject, message |
The email as sent, with merge fields filled in |
pdf_url, pdf_status |
A link to the PDF that expires after 5 minutes, and ready. While the PDF is being made, pdf_url is null and pdf_status is pending: read again in a minute |
snapshot_sha256 |
A hash of the sent copy |
currency, total_excluding_tax, total_including_tax, stages, sections, sections_has_more, next_sections_offset |
As in the document read, from the sent copy |
recipients |
One row for each address (see below) |
response |
The client’s answer, or null |
Recipient fields: id, role (to, cc or bcc), email, name, signatory (true for the one person who may accept or decline online), status (sending, sent, delivered, opened, viewed, bounced, failed or revoked), sent_at, delivered_at, bounced_at, delivery_error, opens_count, first_viewed_at, last_viewed_at, views_count, downloads_count and revoked_at.
Response fields: id (the signed proposal id), kind (accepted or declined), responded_at, signer_name, signer_position, decline_reason and signed_pdf_status (none for a decline, ready, or pending).
A version of another proposal answers 404 proposal_version_not_found.
Read the send draft
GET /api/v1/proposals/:proposal_id/send_draft returns what a send uses when you leave fields out, and the merge fields that would show empty.
{
"to": ["contact:3307"],
"recipient_options": [
{"value": "contact:3307", "label": "Jane Smith <jane@acme.example>", "group": "Proposal contact"},
{"value": "contact:3312", "label": "Tom Lee <tom@acme.example>", "group": "Client contacts"},
{"value": "accounts@acme.example", "label": "Acme Corporation <accounts@acme.example>", "group": "Client company"}
],
"subject": "Proposal : ",
"message": "Dear ,\n\nPlease find our proposal for attached. You can also read and accept it online with the link below.\n\nKind regards,\n",
"blank_merge_fields": [{"key": "project.address", "label": "Project location", "where": "Fees"}],
"merge_fields": [{"key": "client.name", "label": "Client name"}]
}
| Field | Meaning |
|---|---|
to |
The default To recipients |
recipient_options |
Addresses you can send to. value is contact:<id> for a contact, or an email address. group is Proposal contact, Project contacts, Client contacts or Client company. Empty when the token’s user may not read contacts |
subject, message |
The default subject and message, with their merge fields not yet filled in |
blank_merge_fields |
Merge fields used in the document or the default email that have no value now: key, label, and where they are used |
merge_fields |
Every merge field: key and label |
Send the proposal
POST /api/v1/proposals/:proposal_id/sends emails the live document to the client as a new version, as Send in the builder does. It sets the proposal’s status to sent, freezes the document and the budget as they are now, and emails each recipient a link and the PDF.
The email goes to people outside Drum and cannot be recalled. Check the recipients before you call it.
An Idempotency-Key header is required. Without one, the answer is 422 idempotency_key_required. A retry with the same key and the same body returns 200, the header Idempotent-Replayed: true and the first version, and sends nothing. The same key with a different body is 409 idempotency_key_reused. Use a new key for every send. See Idempotency keys.
| Field | Required | Rules |
|---|---|---|
to |
Yes | At least one recipient |
cc, bcc |
No | Lists of recipients |
signatory |
No | One recipient: the person who may accept or decline online. Drum adds them as a To recipient. Default: the To recipient with the proposal contact’s address, else the first To recipient |
subject |
No | One line, 200 characters or fewer. Default: the subject of the send draft |
message |
No | Plain text, 5,000 characters or fewer. Default: the message of the send draft |
A recipient is {"email": "...", "name": "..."}, {"contact_id": 3307}, or just an email address as a string. A contact must be a contact of the proposal, its opportunity or project, or its client company, and the token’s user must be able to read contacts. A contact with no email given uses the contact’s own address and name. To, Cc and Bcc together hold at most 20 addresses. An address that appears twice gets one email.
subject and message can hold merge fields, such as ``. An unknown merge field is 422 unknown_merge_field.
curl -X POST "https://app.getdrum.com/api/v1/proposals/18342/sends" \
-H "Authorization: Bearer $DRUM_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2d9e-4b7a-4a51-9a0e-1d2b3c4d5e6f" \
-d '{
"to": [{"contact_id": 3307}],
"cc": ["tom@acme.example"],
"subject": "Proposal for "
}'
Returns 201 with the new version, in the same shape as GET .../versions/:id, plus:
| Field | Meaning |
|---|---|
replayed |
true when this is the result of an earlier send with the same key |
warnings |
merge_field_blank for each merge field that showed empty, and duplicate_recipient_removed when a repeated address was dropped. Each is {code, message}, and may have count |
The emails go out shortly after the call returns. Read the version again to follow each recipient’s status.
Sending limits. A user can send to 50 recipients an hour, and an account to 200 recipients a day. Over a limit the answer is 429 send_rate_limited with a Retry-After header.
| Status | code |
When |
|---|---|---|
403 |
not_authorized |
The role lacks read_quotes or update_quotes |
404 |
contact_not_found |
A contact_id is not a contact of this proposal, or the token’s user cannot read contacts |
404 |
idempotent_record_not_found |
A replay of a send that the token’s user can no longer read |
409 |
idempotency_key_reused, retry_later |
See Idempotency keys |
422 |
idempotency_key_required |
No Idempotency-Key header |
422 |
quote_awaiting_approval |
The proposal waits for approval. Approve it in Drum first |
422 |
quote_closed |
The proposal is accepted, declined or cancelled |
422 |
proposal_document_empty |
The document has no sections |
422 |
proposal_fee_table_stage_missing |
A fee table names a stage the proposal no longer has |
422 |
proposal_staff_highlight_missing |
A staff highlight block has no one to show, for example no project lead is set |
422 |
recipient_required, invalid_email, invalid_name, invalid_signatory, too_many_recipients |
The recipients are missing or not valid. field names the entry, for example to[0].email |
422 |
invalid_subject, message_too_long, unknown_merge_field |
The subject or message is not valid |
429 |
send_rate_limited |
A sending limit was reached |
List signed proposals
GET /api/v1/signed_proposals lists the client answers (accepts and declines) to every proposal sent from the account, newest first, as Settings, Signed proposals lists them. It does not check access to each opportunity or project: the customisation and read_quotes permissions show every answer in the account.
| Parameter | Meaning |
|---|---|
kind |
accepted or declined |
proposal_id |
Answers to one proposal |
date_from, date_to |
Answered on or after, or on or before, this day in the account’s time zone. date_to before date_from is 422 invalid_input |
limit |
1 to 100. Default 25 |
offset |
Skips this many rows. Default 0 |
{
"signed_proposals": [
{
"id": 914,
"kind": "accepted",
"proposal": {"id": 18342, "number": "Q-2026-088", "name": "Riverside fit-out fee proposal"},
"version": {"id": 2207, "number": 2},
"responded_at": "2026-10-12T04:05:33Z",
"signer_name": "Jane Smith",
"client_name": "Acme Corporation",
"fee": {"cents": 4850000, "currency": "AUD", "formatted": "$48,500.00"},
"signed_pdf_status": "ready"
}
],
"count": 1,
"limit": 25,
"offset": 0,
"has_more": false
}
count is the number of rows on this page, not a total. signed_pdf_status is none for a decline, ready when the signed PDF exists, and pending while Drum makes it.
Read a signed proposal
GET /api/v1/signed_proposals/:id returns the list fields, and:
| Field | Meaning |
|---|---|
time_zone |
The time zone the answer was recorded in |
signer_position, client_company |
What the signer entered |
recipient_email, recipient_role |
The address and role the answer came from |
decline_reason |
The reason given for a decline, or null |
price_changed |
true when the proposal’s fee changed after this version was sent |
changes_proposal |
{id, number, name} of the draft that holds changes made after the send, or null |
acceptance_text, consent_text |
The wording the client agreed to |
email_delivered_at |
When the email with this version was delivered |
snapshot_sha256, pdf_sha256, signed_pdf_sha256, record_sha256 |
Hashes of the sent copy, its PDF, the signed PDF and the record |
record_intact |
true when the record still matches record_sha256 |
signed_pdf_url |
A link to the signed PDF that expires after 5 minutes, or null |
No token, IP address or user agent is in the result. The CSV export on the Settings screen has them.
An id from another account, or one that does not exist, answers 404 signed_proposal_not_found.
Errors
Error bodies are {"error", "code"}, with field on input and lookup errors. See Errors. These apply to every endpoint on this page:
| Status | code |
When |
|---|---|---|
403 |
none | {"error": "Insufficient API permissions"}: the API permission for the method is missing |
403 |
not_authorized |
A Drum role permission is missing |
404 |
proposal_not_found |
The proposal does not exist, is deleted, is a template, cannot be read, or (on document endpoints) is not a builder proposal |
404 |
proposal_builder_not_enabled |
The account does not have the proposal builder |
422 |
missing_input, invalid_input |
A required input is missing or a value is not valid |
422 |
validation_failed |
The record would not save. errors lists why |