A cost is a supplier bill or an internal expense recorded against a project. Every cost is allocated to one task of the project and carries at least one line item. Drum derives the cost total from those line items.
All endpoints sit under /api/v1/ and need an account-scoped bearer token:
Authorization: Bearer <api-token>
See API authentication, permissions and rate limits for the permissions each endpoint needs and the rate limits. Dates are ISO 8601 (yyyy-mm-dd); created_at, updated_at and approved_at are full ISO 8601 timestamps. Money is an integer number of cents and is tax exclusive. Reference lists for valid ids are in the Reference data API article.
GET /api/v1/projects/:project_id/costs
Lists the costs on every task of the project, newest issued date first, paginated with page and items (default 25, max 100). A cost raised by a variation is listed only once that variation is approved.
Filters: start_date, end_date, status (draft, final, approved, or planned), task_id, deliverable_id, cost_type_id, billing_status, billable. Pass include_line_items=true to add line_items to every row.
For one release, the list filter also accepts the deprecated name project_procedure_id. Prefer deliverable_id.
GET /api/v1/projects/:project_id/costs/:id
Returns one cost of the project. include_line_items=true adds its line items. Totals need view_project_metrics; markup fields need update_costs or approve_costs.
POST /api/v1/projects/:project_id/costs
Creates a cost and its line items in one transaction. Send fields flat or nested under a cost key. The project must be a kept delivery project the token user can read.
Required: task_id, cost_type_id, name, line_items (at least one), and exactly one of company_id or member_id.
Optional: description, reference, invoice_number, issued_date, due_date, status (draft default, final, or planned; approved is refused), billable, deliverable_id, fixed_markup_cents, percentage_markup.
Each line item needs name, quantity, and price_cents. Optional line fields: finance_account_id, tax_rate_id, line_type (line or heading).
For one release, POST also accepts the deprecated names account_user_id (for the staff member) and project_procedure_id (for the deliverable). Prefer member_id and deliverable_id. If you send both a current name and its deprecated alias with different values, the request answers 422 invalid_input with the field of the alias. A missing member answers 404 member_not_found; a missing deliverable answers 404 deliverable_not_found.
Response 201 matches the show shape and always includes line_items. Each cost has member_id and deliverable_id. For one release it also sends the deprecated copies account_user_id and project_procedure_id with the same values.
PATCH /api/v1/projects/:project_id/costs/:id
Updates one cost on the project. Send only the fields that should change (flat or nested under a cost key). Fields not sent keep their value; null clears an optional field. Response matches the show shape and always includes line_items.
A cost that belongs to an expense claim takes name, description, supplier, status, tax rule and total from the claim (and markup for an allowance). Changing one of those answers 422 with cost_on_expense_claim — change the claim in Drum instead. Other fields (reference, cost type, billable, dates, task, item markups) still update as on any cost.
Unparseable values (for example a non-integer cents field or a bad date) answer 422 invalid_input rather than being coerced to zero or blank.
Errors
Error bodies are {"error": "...", "code": "..."}. Common codes include task_not_found, cost_type_not_found, company_not_found, member_not_found, deliverable_not_found, finance_account_not_found, tax_rate_not_found, missing_input, invalid_input, supplier_or_resource_required, conflicting_markup, cost_on_expense_claim, and validation_failed.
Idempotency codes: 422 invalid_input when field is idempotency_key (header longer than 255 characters or control characters), 409 idempotency_key_reused, 409 retry_later, 409 idempotent_record_deleted, 404 idempotent_record_not_found.
Retries
Send an Idempotency-Key header (1 to 255 characters, a new value for each new cost) to make a retry safe. The MCP tool create_cost uses the same rule through its idempotency_key input.
The first request with a key creates the cost and answers
201.A request with the same key and the same body creates nothing, raises no approval and pushes no bill. It answers
200with the first cost and the headerIdempotent-Replayed: true. When the cost qualifies for an automatic bill and has no bill,warningssays that the bill may not have been created: the push of the first request failed, or it still runs. Check the cost in Drum.The same key with a different body answers
409withidempotency_key_reused.A request that fails stores no key, so a retry with the same key runs again.
A key is kept for 24 hours for each token user. After that, the same key creates a new cost. When two requests with the same expired key arrive at the same time, the second answers
409withretry_later.
Project visibility
A project the token user cannot read answers 404 on create, not 403. GET on the collection and on a single cost keep the older behaviour: a project outside the account is 404, but a project inside the account that the token user cannot read is 403. Treat both as not available to this token.
Webhooks and approvals
A created cost emits cost.created. A cost created with status: "final" starts the approval workflow when the account has cost approvals configured. status: "approved" is refused with invalid_input; approve the cost in Drum after creating it as final.
