Opportunities
An opportunity is pipeline work that has not been won yet. Drum stores opportunities as projects with project_type: "opportunity", so they share most fields with projects. Converting an opportunity creates a delivery project.
Money is tax exclusive. deal_won_date and deal_lost_date are full timestamps. Other dates are yyyy-mm-dd.
GET /api/v1/opportunities
Lists the opportunities the token’s user can read. Deleted opportunities are left out. Paginated (see Pagination).
| Parameter | Meaning |
|---|---|
project_number |
Exact opportunity number. Surrounding spaces are ignored |
status |
Status id or array of ids. all turns the filter off |
project_status |
One status id |
lead_score |
Lead score id |
assigned_to |
Member id or array of ids |
team |
Team id or array of ids |
opportunity_tag_list |
A tag name, or an array of tag names. An opportunity must carry every tag |
estimated_start_date |
Estimated start on or after this date |
estimated_end_date |
Estimated start on or before this date |
due_date_from, due_date_to |
Due on or after, or on or before, this date (the whole day is included). Not a yyyy-mm-dd date: 422 invalid_input |
Each row is compact:
[
{
"id": 12001,
"name": "Riverside fit-out",
"project_number": "OPP-2026-014",
"description": "Ground floor refurbishment",
"currency": "AUD",
"project_type": "opportunity",
"estimated_start_date": "2026-10-01",
"estimated_end_date": "2026-12-15",
"enquiry_date": "2026-08-20",
"target_date": null,
"project_status": {"id": 301, "name": "Proposal Sent", "complete_status": false},
"client": {"id": 789, "name": "Acme Corporation", "type": "Company"},
"team": {"id": 42, "name": "Fit-out"},
"amount": 48500.0,
"lead_score": {"id": 51, "name": "Warm"},
"planned": false
}
]
amount (the deal value) appears only with view_opportunity_financial_values. due_date appears only when the account uses opportunity due dates.
GET /api/v1/opportunities/:id
Returns one opportunity with its deliverables and tasks, contacts, addresses and custom fields, in the project shape. These fields are specific to opportunities:
| Field | Meaning |
|---|---|
estimated_start_date, estimated_end_date, enquiry_date, target_date |
Dates, in place of the project dates |
deal_won_date, deal_lost_date |
Timestamps, set when the opportunity moved to a won or lost status |
due_date |
When the account uses opportunity due dates |
amount, amount_cents |
The deal value, as a decimal and in cents. Only with view_opportunity_financial_values |
estimated_deposit |
Shown under the project money rules |
invoicing_structure |
payment_on_delivery, monthly, or null |
lead_score |
{id, name}, or left out |
colour |
The opportunity colour, or null |
default_rate_card |
{id, name}, or null |
project_templates |
[{id, name}] |
members |
[{id, name, project_role}]. project_role is null for a member with no role |
assigned_users |
Deprecated. The rows of members without the role. Use members |
opportunity_tags |
Tag names |
variation_opportunity |
true when the opportunity sells a variation on an existing project. See Variation opportunities |
variation |
Only on a variation opportunity. See Variation opportunities |
projects |
[{id, project_number, name}]: delivery projects created from this opportunity. name is left out for a project the token’s user cannot read. Empty until it is converted |
A delivery project id, a deleted opportunity, an id from another account, or an opportunity hidden from the token’s user returns 404 with an empty body.
POST /api/v1/opportunities
Creates an opportunity as the New Opportunity form does. Send fields flat or under opportunity.
name and client_company_id are required. You can also send the PATCH fields below (except add_opportunity_tags, remove_opportunity_tags, expected_project_status_id, project_role_id and member_ids), plus:
| Field | Rules |
|---|---|
opportunity_tags |
Array of up to 50 tag names of up to 100 characters |
role_members |
Array of up to 50 {project_role_id, member_ids}. Sets the members of each role. project_role_id: null sets members with no role |
location_id |
Id of a location. Needed when the opportunity template numbers opportunities by location |
Fields you leave out take the form’s defaults:
| Field | Default |
|---|---|
project_status_id |
The default opportunity status |
enquiry_date |
Today, in the account’s time zone |
project_template_ids |
The default opportunity templates |
lead_score_id, invoicing_structure |
The account defaults, when the token’s user has view_opportunity_financial_values |
| Members | The token’s user, when the account has no opportunity roles |
The templates add their deliverables, and the opportunity gets its number and custom fields, as in Drum.
Send an Idempotency-Key header to make retries safe (see Idempotency keys).
Returns 201 with the opportunity in the show shape. A token user who can read only assigned opportunities, and is not a member of the new one, gets {"id", "project_number", "visible": false}.
PATCH /api/v1/opportunities/:id
Changes the fields you send. Send fields flat or under opportunity. A field of the wrong type is refused with 422 invalid_input and field.
| Field | Rules |
|---|---|
name |
Text, up to 255 characters |
description |
Text |
project_status_id |
A status of the opportunity template (see project statuses, type=opportunity) |
expected_project_status_id |
The status you last read. If the opportunity is now on another status, the request is refused with 409 stale_state and nothing changes |
client_company_id |
A company the token’s user can read |
team_id |
Team id, or null |
amount_cents, estimated_deposit_cents |
Integer cents, zero or more. Needs view_opportunity_financial_values |
invoicing_structure |
payment_on_delivery, monthly, or null. Needs view_opportunity_financial_values |
lead_score_id |
Lead score id, or null. Needs view_opportunity_financial_values |
colour |
A colour name from Drum’s colour picker, or null |
enquiry_date, target_date, estimated_start_date, estimated_end_date |
yyyy-mm-dd, or null or "" to clear |
due_at |
ISO 8601 timestamp, or null or "" to clear. Without a zone it is read in the account’s time zone |
default_rate_card_id |
An account rate card that Drum offers for projects |
project_template_ids |
Array of active opportunity template ids. Replaces the templates. Refused once the opportunity has time or costs |
project_role_id, member_ids |
member_ids replaces the members of the role project_role_id (or of members with no role when it is left out or null). Other roles keep their members |
add_opportunity_tags, remove_opportunity_tags |
Arrays of up to 50 tag names. Other tags are kept |
referrer_company_id |
Company id, or null |
referrer_contact_id |
Contact id, or null. When there is no referrer company, the contact’s company becomes the referrer company |
Moving to a won status sets deal_won_date, and a lost status sets deal_lost_date. A won status does not create a project. Use convert for that.
On a variation opportunity, PATCH cannot set a won status: convert it instead. A lost status declines the variation’s proposal and rejects the variation in the same request, and needs update_quotes as well (update_budgets when the variation has no proposal). A committed variation cannot be lost this way: 422 validation_failed. Once a variation opportunity is won or lost, its status changes only through its proposal.
PATCH /api/v1/opportunities/12001
{
"opportunity": {
"project_status_id": 302,
"expected_project_status_id": 301,
"add_opportunity_tags": ["fit-out"]
}
}
Returns 200 with the opportunity in the show shape.
opportunity_tag_list is no longer accepted and returns 422 invalid_input. Use add_opportunity_tags and remove_opportunity_tags.
Errors for create and update
| Status | code |
When |
|---|---|---|
403 |
none | The token’s user cannot create or update this opportunity |
403 |
financials_not_permitted |
A money field or lead_score_id was sent without view_opportunity_financial_values |
404 |
not_found |
The opportunity does not exist, is deleted, or is hidden |
404 |
project_status_not_found, lead_score_not_found, client_company_not_found, team_not_found, default_rate_card_not_found, project_template_not_found, project_role_not_found, member_not_found, referrer_company_not_found, referrer_contact_not_found, location_not_found |
A referenced record does not exist in the account, is deleted, or does not fit the field. field names the input |
409 |
stale_state |
expected_project_status_id does not match |
409 |
idempotency_key_reused |
The idempotency key was used with a different body |
422 |
missing_input |
name or client_company_id is missing on create |
422 |
invalid_input |
A field could not be read |
422 |
invalid_project_status, invalid_lead_score |
The record exists but is not one this field takes |
422 |
variation_opportunity |
A won status on a variation opportunity, or any status change once it is won or lost |
422 |
location_required |
The template numbers by location and no location_id was sent |
422 |
validation_failed |
The opportunity would not save. errors lists why |
Convert an opportunity
POST /api/v1/opportunities/:id/convert creates a delivery project from the opportunity and marks the opportunity won, in one transaction. Send fields flat or under conversion. All fields are optional.
A variation opportunity converts differently and takes other fields. See Variation opportunities.
To convert, the token’s user needs the opportunity update rights and create_projects. A user with read_quotes and accept_quotes can instead convert through an accepted proposal of the opportunity (the one named by proposal_id, or by default the last accepted one), without the other two.
| Field | Default |
|---|---|
project_status_id |
The project template’s default status. Must be a status of the project template (project statuses, type=project) |
name, description, team_id, location_id |
The opportunity’s values |
start_date, end_date |
Empty |
budget_cents |
See below. Needs view_opportunity_financial_values |
proposal_id |
The last accepted proposal of the opportunity |
structure_type |
budget builds deliverables and tasks from the proposal budget. proposal builds them from the proposal’s sections and lines |
expected_project_status_id |
When sent and the opportunity is on another status: 409 stale_state |
allow_additional_project |
false. Send true to create another project when one already exists |
What a convert does:
- Creates the project with a new project number. With an accepted proposal, the project’s structure and budget come from it, and the proposal is linked to the project.
- Marks the client company as a client.
- Moves the opportunity to its won status and sets
deal_won_date, unless it is already won. - Adds the opportunity’s members to the project with the first of the account’s project roles. Adds the token’s user too, but only when they could not otherwise read the project.
POST /api/v1/opportunities/12001/convert
{"project_status_id": 410, "expected_project_status_id": 301}
Returns 201 with a new project, or 200 when a project already exists:
{
"created": true,
"project": {"id": 20488, "name": "Riverside fit-out", "project_number": "2026-088", "project_type": "project", "opportunity_id": 12001},
"opportunity": {
"id": 12001,
"project_number": "OPP-2026-014",
"project_status": {"id": 305, "name": "Won", "sales_status_type": "won"},
"deal_won_date": "2026-09-10T00:00:00.000Z",
"variation_opportunity": false
}
}
project holds the full project shape plus opportunity_id (shortened above). It holds only id and project_number when the token’s user cannot read the project.
Retries. When the opportunity already has a delivery project, convert changes nothing and returns 200 with created: false and that project. Treat 201 and 200 as success. This makes a retry safe without an idempotency key.
Budget. budget_cents is stored as the project’s budget and as the opportunity’s deal value. New projects use the account’s default currency. If the opportunity’s currency is different, budget_cents is refused with 422 currency_mismatch. The project’s total_budget is built from its tasks, so it stays 0.0 until tasks are added.
Events. A convert sends three webhook events: project.created for the new project, and project.updated and opportunity.won for the opportunity. A replay sends none.
Convert errors
Error bodies are {"error", "code"}, with field on input errors. Every error leaves the account unchanged.
| Status | code |
When |
|---|---|---|
403 |
none | No opportunity update rights or no create_projects, and no right to accept the proposal |
403 |
financials_not_permitted |
budget_cents without view_opportunity_financial_values |
404 |
not_found |
The opportunity does not exist, is deleted, or is hidden |
404 |
project_status_not_found, team_not_found, location_not_found, proposal_not_found |
A referenced record does not exist in the account |
409 |
stale_state |
expected_project_status_id does not match |
409 |
retry_later |
Another request holds the opportunity. Retry with backoff |
429 |
rate_limited |
More than 30 converts for the account this minute. See Retry-After |
422 |
invalid_input |
A field could not be read, or expected_total_cents or won_status_id was sent for an opportunity that is not a variation opportunity |
422 |
currency_mismatch |
See Budget above |
422 |
invalid_project_status |
The status is not a status of the project template |
422 |
not_convertible |
Not an opportunity, deleted, no company client, or no template |
422 |
proposal_not_accepted, variation_proposal |
proposal_id is not accepted, or is a variation proposal |
422 |
won_status_missing |
The opportunity template has no won status |
422 |
project_role_missing |
The account has no project role |
422 |
location_required |
The project template numbers by location and there is no location |
422 |
validation_failed |
The project or opportunity would not save |
429 |
none | The request rate limit. See Retry-After |
Variation opportunities
A variation opportunity (variation_opportunity: true) sells a variation on a project that already exists. Converting it creates no project. Instead it accepts the variation’s proposal, which approves the variation, commits it to its project and marks the opportunity won.
GET /opportunities/:id shows what a convert would do in variation:
"variation": {
"id": 611,
"name": "Extra loading dock",
"status": "submitted",
"project": {"id": 20488, "project_number": "2026-088", "name": "Riverside fit-out"},
"proposal": {"id": 9120, "number": "Q-0412", "status": "sent"},
"total_cents": 1850000,
"currency": "AUD",
"convertible": true,
"blocked_code": null,
"blocked_reason": null
}
| Field | Meaning |
|---|---|
status |
The variation’s status: draft, submitted, approved, rejected or partially_approved |
project |
The project the variation changes. name is left out when the token’s user cannot read it |
proposal |
The variation’s proposal, or null when there is none or the token’s user lacks read_quotes |
total_cents, currency |
The proposal total, tax exclusive. Only with view_opportunity_financial_values and read_quotes |
convertible |
true when a convert would succeed |
blocked_code, blocked_reason |
Why it would not, or null |
Fields for a variation convert:
| Field | Rules |
|---|---|
expected_total_cents |
Required when the token’s user has view_opportunity_financial_values: the total_cents you read. If the proposal total has changed, the convert is refused with 409 stale_state. Refused with 403 financials_not_permitted from a user without that permission |
won_status_id |
A won status of the opportunity template. Default: the status the proposal sets |
expected_project_status_id |
As above |
proposal_id |
Optional. Must be the variation’s proposal |
The fields that build a new project (project_status_id, name, description, team_id, start_date, end_date, location_id, budget_cents, allow_additional_project, structure_type) are refused with 422 invalid_input.
It needs read_quotes and accept_quotes. A user with the opportunity update rights but no proposal to accept gets the variation_proposal_missing error.
The response is 200 with created: false. project is the project the variation changed, opportunity has variation_opportunity: true, and variation shows the variation after the convert. Converting again once it is done returns the same response and changes nothing.
Errors specific to a variation convert, all 422 unless shown:
code |
When |
|---|---|
missing_input |
expected_total_cents was not sent by a user with view_opportunity_financial_values |
opportunity_won, opportunity_lost |
The opportunity is already won or lost. Change the proposal’s status in Drum instead |
variation_project_missing |
The variation’s project has been deleted |
variation_proposal_missing |
The variation has no proposal |
proposal_already_accepted |
The proposal is accepted already |
proposal_closed |
The proposal is declined or cancelled |
variation_rejected |
The variation is rejected |
won_status_missing |
The opportunity template has no won status |
invalid_won_status |
won_status_id is not a won status of the opportunity |
404 won_status_not_found, 404 proposal_not_found |
The status is not in the account, or proposal_id is not the variation’s proposal |
409 stale_state |
The status or the proposal total has changed since you read it |
Deliverables and tasks
These endpoints edit one deliverable or task of an opportunity at a time:
POST /api/v1/opportunities/:opportunity_id/deliverables
PATCH /api/v1/opportunities/:opportunity_id/deliverables/:id
DELETE /api/v1/opportunities/:opportunity_id/deliverables/:id
POST /api/v1/opportunities/:opportunity_id/tasks
PATCH /api/v1/opportunities/:opportunity_id/tasks/:id
DELETE /api/v1/opportunities/:opportunity_id/tasks/:id
Send the row’s fields at the top level. A field you leave out does not change, and null clears an optional field.
| Row | Fields |
|---|---|
| Deliverable | name, description, position, colour, start_date, end_date, project_phase_id, rate_card_id, allow_time_tracking. On PATCH also status and percentage_complete. On POST, from_project_template_deliverable_ids instead adds one deliverable for each template deliverable, with its tasks and budget lines, and takes only project_phase_id and position beside it |
| Task | deliverable_id (required on POST; on PATCH it moves the task), name, item_type, priority, visible, allow_time_tracking, position, status, start_date, due_date, note. On PATCH also member_ids |
position is a 1-based place among the row’s siblings, or {"after": id} or {"before": id}.
An opportunity is priced in its proposal budget, not on its tasks, so budget fields (budget_type, quantity, quantity_unit, rate_cents, contract_value_cents, resource_type_id) are refused with 422 invalid_input.
A write returns {"rows": [row]}. Each row has record_kind (deliverable or task), and money (budget, rate, contract_value) only for a user who can read budgets. A DELETE returns {"record_kind", "id", "deleted": true}.
Changing a row needs the opportunity update rights or update_budgets. A deliverable’s rate_card_id also needs update_budgets. Deleting needs the opportunity update rights or delete_budgets. Error codes include opportunity_not_found, deliverable_not_found, task_not_found, project_phase_not_found, rate_card_not_found, member_not_found, project_template_deliverable_not_found and deliverable_has_tracked_time.
Permissions
| Endpoint | Needs |
|---|---|
GET /opportunities |
api_read, and read_all_opportunities, read_team_opportunities or read_assigned_opportunities |
GET /opportunities/:id |
api_read, and read access to the opportunity |
POST /opportunities |
api_create, create_opportunities |
PATCH /opportunities/:id |
api_update, and update_all_opportunities, update_team_opportunities or update_assigned_opportunities |
POST /opportunities/:id/convert |
api_create, and either the update rights above and create_projects, or read_quotes and accept_quotes with an accepted proposal. A variation opportunity needs read_quotes and accept_quotes |