Proposal budgets
A proposal budget prices a proposal and sets the structure of the project it becomes. These endpoints write a live proposal’s budget one row at a time, through the same code as the budget editor in Drum. To read the whole budget, use GET /proposals/:id.
In the budget editor, the rows under a deliverable are called tasks. The API calls them items.
Money is integer cents, tax exclusive. You send cents in fields ending _cents. Results show money as an object: {"cents": 120000, "currency": "AUD", "formatted": "$1,200.00"}. Quantities are decimals.
A proposal template’s budget is written with the same row shapes, on its own paths: see Proposal templates.
Endpoints
All paths start with /api/v1/proposals/:proposal_id/budget.
| Method and path | Does |
|---|---|
POST .../phases |
Adds a phase |
PATCH .../phases/:id |
Changes a phase |
DELETE .../phases/:id |
Deletes a phase. Its deliverables stay, with no phase |
POST .../deliverables |
Adds a deliverable, copies one, or adds deliverables from a project template |
PATCH .../deliverables/:id |
Changes a deliverable |
DELETE .../deliverables/:id |
Deletes a deliverable with its items and their lines |
POST .../items |
Adds an item (a task) to a deliverable |
PATCH .../items/:id |
Changes an item, or moves it to another deliverable |
DELETE .../items/:id |
Deletes an item with its lines |
POST .../resources |
Adds a resource line (time by role or person) to an item |
PATCH .../resources/:id |
Changes a resource line |
DELETE .../resources/:id |
Deletes a resource line |
POST .../costs |
Adds a cost line (an expense) to an item |
PATCH .../costs/:id |
Changes a cost line |
DELETE .../costs/:id |
Deletes a cost line |
How the budget fits together
Budget
├── Phases labels that group deliverables
└── Deliverables one list, in order; each may name a phase
└── Items the tasks of the deliverable
├── Resource lines hours or days of a role or person, at a rate
└── Cost lines expenses, at a billable rate
- A phase is a label. A deliverable names at most one phase with
budget_phase_id. Deleting a phase keeps its deliverables. - A deliverable becomes a deliverable of the project when the proposal is converted. Its
budget_typeisfixed_price(the default) ortime_and_materials. - An item becomes a task. Its
kinddecides where its value comes from (see below). - Resource lines and cost lines sit under an item. They make up the item’s internal budget, and for a dynamic item its price as well.
Fixed and dynamic items
Every item has two values. contract_value is the sell price the client sees. internal_budget is what the work is budgeted to cost inside the business.
kind |
contract_value |
internal_budget |
|---|---|---|
fixed, with no lines |
quantity × billable_rate |
The same amount |
fixed, with lines |
quantity × billable_rate |
The sum of its resource and cost lines |
dynamic |
The sum of its resource and cost lines | The same amount |
non_financial |
Zero | Zero |
- A fixed item is a lump sum until it has lines. Once it has lines, its price and its budget can differ.
- A dynamic item takes its value from its lines. With no lines it is worth zero, and the budget reports an
empty_dynamic_itemwarning. - A
non_financialitem is an internal process (item_type: process_only). It has no price and takes no lines.
Switching kind. Changing a dynamic item to fixed sets quantity to 1, quantity_unit to per_item, and billable_rate_cents to the current total of its lines (0 when it has none). Send quantity, quantity_unit or billable_rate_cents in the same request to use your own values instead. Changing a fixed item to dynamic clears its own quantity and rate: its value comes from its lines from then on.
How the budget reaches the fees
A budget write does not change the proposal’s fee stages and lines directly. When the proposal uses live pricing (see Set the pricing layout), the fees follow the budget a moment after each write. Otherwise they change only when someone updates them in Drum.
Permissions
| Request | API permission | Drum role permission |
|---|---|---|
POST |
api_create |
update_budgets |
PATCH |
api_update |
update_budgets |
DELETE |
api_delete |
update_budgets |
The token’s user must also be able to read the proposal’s opportunity or project. Otherwise the answer is 404 proposal_not_found. See Which proposals you can reach. The proposal builder is not needed.
Rules for every row
- Send the fields at the top level of the JSON body. A key that the row type does not take is refused with
422 invalid_inputand thatfield. PATCHchanges only the keys you send.nullclears only the fields marked “Can be null” below. Any othernullis422 invalid_input.positionplaces the row in its list.1is first. A position past the end puts the row last. Without it, a new row goes last.- A row of another budget is not found, with the code of its type, for example
404 budget_deliverable_not_found. - A finalised budget (approved or rejected) refuses every write with
409 budget_finalised. - The budget must exist. A proposal with no budget answers
404 proposal_budget_not_found. Add the budget in Drum. - New children ride along on a create only. A new deliverable can carry
items, and a new item can carryresourcesandcosts. On aPATCH, a child list is refused with422 invalid_input: add or change each child at its own path. - Writes are not idempotent. These creates take no
Idempotency-Key. After a timeout, read the proposal before you retry.
Results
A POST returns 201, and a PATCH returns 200, with the row as the budget document shows it:
{
"proposal_id": 18342,
"row_type": "budget_item",
"record_kind": "budget_item",
"row": {
"id": 66120,
"record_kind": "budget_item",
"kind": "fixed",
"name": "Site survey",
"item_type": "task",
"position": 1,
"description": null,
"delivery_task_name": null,
"quantity": 2.0,
"quantity_unit": "per_item",
"billable_rate": {"cents": 60000, "currency": "AUD", "formatted": "$600.00"},
"contract_value": {"cents": 120000, "currency": "AUD", "formatted": "$1,200.00"},
"internal_budget": {"cents": 120000, "currency": "AUD", "formatted": "$1,200.00"},
"resources": [],
"costs": []
},
"warnings": []
}
Adding deliverables from a project template also returns rows, every new deliverable. row is the first.
A DELETE returns 200:
{"proposal_id": 18342, "row_type": "budget_item", "id": 66120, "deleted": true, "permanently_deleted": true, "warnings": []}
warnings holds messages about the budget after the write. Each is {code, message}, with count, subject_id or subject_ids where they apply.
The budget document
GET /proposals/:id returns the budget under budget. Each row has record_kind.
| Budget field | Meaning |
|---|---|
id, name, description |
As recorded |
status |
draft, submitted, approved or rejected. Approved and rejected budgets cannot change |
rate_card, custom_rate_card |
{id, name} of the rate card and of the proposal’s own rate card, or null |
currency |
ISO 4217 code |
phases |
Phase rows |
deliverables |
Deliverable rows, in order, each with its items |
| Row | Fields |
|---|---|
Phase (budget_phase) |
id, name, description, colour, position |
Deliverable (budget_deliverable) |
id, name, description, colour, budget_type, position, budget_phase_id, items |
Item (budget_item) |
id, kind, name, item_type, position, description, delivery_task_name, quantity, quantity_unit, billable_rate, contract_value, internal_budget, resources, costs. quantity, quantity_unit and billable_rate are null unless the item is fixed |
Resource line (budget_item_resource) |
id, resource_type ({id, name} or null), preferred_member ({id, name} or null), quantity, quantity_unit, rate, total, source_rate_card ({id, name}, or null for a typed rate), notes, position |
Cost line (budget_item_cost) |
id, cost_type ({id, name}), name, description, quantity, quantity_unit, billable_rate, position |
Budget warnings
code |
Meaning |
|---|---|
empty_dynamic_item |
Dynamic items with no lines, so they are worth zero. subject_ids lists them |
no_billable_path |
The budget has no billable task or event, so the proposal has no priced lines |
proposal_template_drift |
The linked project template has deliverables that the budget does not. subject_ids are their project template deliverable ids, which from_project_template_deliverable_ids takes |
Phases
| Field | Required on create | Rules |
|---|---|---|
name |
Yes | Text |
description |
No | Text. Can be null |
colour |
No | gray, red, orange, yellow, blue, green or indigo. Default: a colour no other phase uses. Can be null |
position |
No | 1 is first |
POST /api/v1/proposals/18342/budget/phases
{"name": "Concept", "colour": "blue"}
Deleting a phase keeps its deliverables, with no phase.
Deliverables
| Field | Required on create | Rules |
|---|---|---|
name |
Yes | Text |
description |
No | Text. Can be null |
colour |
No | red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink or rose. Can be null |
budget_type |
No | fixed_price (default) or time_and_materials |
budget_phase_id |
No | A phase of this budget. null removes the phase |
budget_phase_name |
No | The name of a phase of this budget, in place of budget_phase_id |
position |
No | 1 is first, in the budget’s one list of deliverables |
items |
No | Create only. New items, each with the item fields below |
POST /api/v1/proposals/18342/budget/deliverables
{
"name": "Design development",
"budget_phase_name": "Concept",
"items": [
{"name": "Site survey", "kind": "fixed", "quantity": 1, "quantity_unit": "per_item", "billable_rate_cents": 150000},
{"name": "Concept drawings", "resources": [{"resource_type_id": 77, "quantity": 24}]}
]
}
Deleting a deliverable deletes its items and their lines.
Copy a deliverable
Send only copy_from_id, a deliverable of this budget, and optionally position. Drum copies it with its items and lines, names it with “(Copy)”, and places it after the original, as Duplicate does in the editor. Change the copy by its new id afterwards.
POST /api/v1/proposals/18342/budget/deliverables
{"copy_from_id": 40210}
Add deliverables from a project template
Send from_project_template_deliverable_ids, a list of project template deliverable ids, and optionally budget_phase_id or budget_phase_name, and position. Drum adds one deliverable for each, with its tasks, resources and costs from the template, as Add from template does in the editor. They are added in the project template’s order, as one block at position. The result has every new deliverable in rows.
Project template deliverable ids come from the proposal_template_drift warning, or from the project template endpoints. An id that is not found is 404 project_template_deliverable_not_found.
include_quote_sections is refused on a live proposal (422 invalid_input).
Items
| Field | Required on create | Rules |
|---|---|---|
budget_deliverable_id |
Yes | The deliverable to add the item to. On a PATCH, a deliverable of the same budget moves the item there |
name |
Yes | Text |
kind |
No | fixed, dynamic or non_financial. Default on create: dynamic, or non_financial when item_type is process_only. On a PATCH, the item keeps its kind unless you send one |
item_type |
No | task (default) or event for a fixed or dynamic item. process_only for a non-financial item |
quantity |
No | Fixed items only. A number |
quantity_unit |
No | Fixed items only. hour, day or per_item |
billable_rate_cents |
No | Fixed items only. Integer cents for each unit |
description |
No | Text. Can be null |
delivery_task_name |
No | The task this item goes into on the project. Can be null |
position |
No | 1 is first, within the deliverable |
resources, costs |
No | Create only. New resource and cost lines, with the line fields below. Not accepted on a non-financial item |
quantity, quantity_unit or billable_rate_cents on an item that is not fixed is 422 invalid_input. Send kind: "fixed" with them.
POST /api/v1/proposals/18342/budget/items
{"budget_deliverable_id": 40210, "name": "Site survey", "kind": "fixed", "quantity": 2, "billable_rate_cents": 60000}
PATCH /api/v1/proposals/18342/budget/items/66120
{"kind": "dynamic"}
An item that has lines cannot become non_financial (422 item_has_lines). Delete its lines first.
Resource lines
A resource line is time for a role, optionally a named person, at a rate.
| Field | Required on create | Rules |
|---|---|---|
budget_item_id |
Yes (create only) | The item to add the line to |
resource_type_id |
No | A role (resource type), from the account catalog. Leave it out for a lump sum line with no role |
preferred_member_id |
No | A current member of the account, from GET /members. Ignored on a lump sum line. Can be null |
quantity |
No | A number. Default 0 |
quantity_unit |
No | hour (default), day or per_item |
rate_cents |
No | A typed rate in integer cents. Leave it out to take the rate card’s rate |
notes |
No | Text. Can be null |
position |
No | 1 is first, within the item |
Rates. When a line’s role or person changes and you send no rate_cents, Drum takes the rate from the budget’s rate cards: the proposal’s own rate card first, and a person’s rate before a role’s. source_rate_card shows the card the rate came from. A typed rate_cents sets source_rate_card to null, so a later rate card change leaves the line alone. The line total is quantity × rate.
POST /api/v1/proposals/18342/budget/resources
{"budget_item_id": 66125, "resource_type_id": 77, "preferred_member_id": 5501, "quantity": 16}
A line cannot be added to a non-financial item (422 item_process_only).
Cost lines
A cost line is an expense, such as a subcontractor fee or travel, at a billable rate.
| Field | Required on create | Rules |
|---|---|---|
budget_item_id |
Yes (create only) | The item to add the line to |
cost_type_id |
Yes | From GET /cost_types |
name |
Yes | Text |
description |
No | Text. Can be null |
quantity |
No | A number. Default 0 |
quantity_unit |
No | per_item (default), hour or day |
billable_rate_cents |
No | Integer cents for each unit. Default 0 |
position |
No | 1 is first, within the item |
The line total is quantity × billable_rate.
POST /api/v1/proposals/18342/budget/costs
{"budget_item_id": 66125, "cost_type_id": 610, "name": "Geotechnical report", "quantity": 1, "billable_rate_cents": 450000}
Errors
Error bodies are {"error", "code"}, with field on input and lookup errors. See Errors. Nothing is saved unless the whole request succeeds.
| Status | code |
When |
|---|---|---|
403 |
none | {"error": "Insufficient API permissions"}: the API permission for the method is missing |
403 |
not_authorized |
The role lacks update_budgets |
404 |
proposal_not_found |
The proposal does not exist, is deleted, is a template, or cannot be read |
404 |
proposal_budget_not_found |
The proposal has no budget |
404 |
budget_phase_not_found, budget_deliverable_not_found, budget_item_not_found, budget_item_resource_not_found, budget_item_cost_not_found |
The row, or the parent you named, is not in this budget |
404 |
resource_type_not_found, cost_type_not_found, member_not_found, project_template_deliverable_not_found |
A referenced record cannot be used |
409 |
budget_finalised |
The budget is approved or rejected |
422 |
missing_input, invalid_input |
A required field is missing, a value is not valid, or a key is not accepted |
422 |
item_has_lines, item_process_only |
A rule about non-financial items refused the change |
422 |
validation_failed |
The row would not save. errors lists why |