Companies and contacts
A company is a client, a supplier, or both. A contact is a person. A contact can be linked to any number of companies, opportunities and projects.
The web company form, the contact modal, the MCP tools and these endpoints run the same operations, so the same rules apply everywhere.
Endpoints
| Method and path | Does |
|---|---|
GET /api/v1/companies |
Lists companies |
GET /api/v1/companies/:id |
Reads one company |
POST /api/v1/companies |
Creates a company, or returns the one that already has the name |
PATCH /api/v1/companies/:id |
Changes a company’s fields |
POST /api/v1/companies/:id/merge |
Merges duplicate companies into this one |
GET /api/v1/contacts |
Lists contacts |
GET /api/v1/contacts/:id |
Reads one contact with its links |
POST /api/v1/contacts |
Creates a contact, or returns the one that already has the email or mobile |
PATCH /api/v1/contacts/:id |
Changes a contact’s fields, and adds or changes links |
DELETE /api/v1/contacts/:id |
Deletes a contact |
PATCH /api/v1/contacts/:id/restore |
Restores a deleted contact |
PATCH /api/v1/{companies,opportunities,projects}/:record_id/contacts/:id |
Links a contact to the record, or changes the link |
DELETE /api/v1/{companies,opportunities,projects}/:record_id/contacts/:id |
Removes the link |
Permissions
Contacts use the company permissions of the Drum role. Timesheet-only members cannot use any of these endpoints.
| Endpoint | Needs |
|---|---|
GET companies and contacts |
api_read, read_companies |
POST /companies, POST /contacts |
api_create, create_companies |
PATCH /companies/:id, PATCH /contacts/:id |
api_update, update_companies |
POST /companies/:id/merge |
api_create and api_delete, update_companies and delete_companies |
DELETE /contacts/:id |
api_delete, delete_companies |
PATCH /contacts/:id/restore |
api_update, update_companies |
PATCH a contact link |
api_update, read access to the contact, and permission to update the record |
DELETE a contact link |
api_delete, read access to the contact, and permission to update the record |
Updating the record means update_companies for a company, the opportunity update permissions for an opportunity, and the project update permissions for a project. See Authentication and permissions.
A company or contact the token’s user cannot read answers 404, the same as one that does not exist.
The company object
{
"id": 789,
"trading_name": "Acme Corporation",
"legal_name": "Acme Corporation Pty Ltd",
"finance_name": "Acme Corp",
"client_code": "ACME",
"client_number": 118,
"company_type": "client",
"email": "accounts@acme.example",
"phone": "+61 2 5550 1234",
"website": "https://acme.example",
"company_status": {"id": 7, "name": "Active"},
"supplier_status": null,
"invoice_payment_terms": 30,
"invoice_payment_terms_type": "calendar_days",
"company_tags": ["Builder"],
"created_at": "2025-03-02T01:12:44Z",
"updated_at": "2026-08-30T22:05:10Z"
}
| Field | Meaning |
|---|---|
trading_name |
The name shown in Drum |
legal_name |
The registered name, or null |
finance_name |
The name of the linked contact in the accounting system, or null. Read only |
client_code, client_number |
The account’s client identifiers, or null |
company_type |
client, supplier or both |
website |
Read only. Set it in Drum |
company_status |
{id, name} |
supplier_status |
{id, name}, or null |
invoice_payment_terms |
Days until an invoice is due, or null |
invoice_payment_terms_type |
calendar_days, business_days or day_of_following_month, or null |
company_tags |
Tag names, in alphabetical order |
The tax number is never returned. Addresses, custom fields and contacts are not part of the company object.
List companies
GET /api/v1/companies lists the kept companies, ordered by trading name. Paginated (see Pagination).
| Parameter | Meaning |
|---|---|
search |
Matches the start of words in the trading name, legal name, email or client number |
company_type |
client, supplier or both. Exact match, so supplier leaves out companies typed both. Any other value returns an empty list |
status |
Company status id. A value that is not a number returns 422 invalid_input |
company_tag_list |
A tag name, or an array of tag names. A company must carry every tag |
curl -H "Authorization: Bearer $DRUM_API_TOKEN" \
"https://app.getdrum.com/api/v1/companies?search=acme&company_type=client"
Returns 200 with an array of company objects. Without read_companies it returns 403 {"error": "Forbidden"}.
Read a company
GET /api/v1/companies/:id returns one company object.
A deleted company, an id from another account, or a company the token’s user cannot read returns 404 {"error": "Not found", "code": "not_found"}.
Create a company
POST /api/v1/companies creates a company. Send the fields under company.
| Field | Required | Rules |
|---|---|---|
trading_name |
Yes | Up to 255 characters. Not blank |
legal_name, email, phone |
No | Up to 255 characters each. The email is stored in lower case |
tax_number |
No | An ABN or other tax number. Drum keeps the digits only, up to 18 of them. Never returned |
company_status_id |
No | A company status of the account. Default: the account’s default company status |
company_type |
No | client (default), supplier or both |
client_code |
No | Letters and digits only, unique in the account. Stored in upper case |
client_number |
No | A whole number of 1 or more, unique in the account |
invoice_payment_terms |
No | Whole days, 0 to 3650 |
invoice_payment_terms_type |
No | calendar_days, business_days or day_of_following_month |
company_tags |
No | Array of up to 50 tag names of up to 100 characters |
When the account numbers its clients or gives them codes, a company with a client status that is sent without a client_number or client_code gets the next one.
Send the fields nested under company. Flat fields are read for the company’s own columns, but company_tags is dropped when sent flat.
POST /api/v1/companies
{
"company": {
"trading_name": "Harbour Joinery",
"legal_name": "Harbour Joinery Pty Ltd",
"email": "accounts@harbour.example",
"company_type": "supplier",
"company_tags": ["Joinery"]
}
}
Returns 201 with the company object and "matched_existing": false.
Matching by name. When a kept company already has the trading_name as its trading name or its legal name (any case, surrounding spaces ignored), Drum creates nothing. It returns 200 with that company and "matched_existing": true, and none of its fields change. This makes a retry safe without an idempotency key. To change the matched company, send a PATCH.
Addresses, custom fields, contacts, the website and the accounting system contact are set in Drum, not through this endpoint.
Change a company
PATCH /api/v1/companies/:id changes the fields you send. Send the fields under company.
The fields are those of create, except company_tags. Instead:
| Field | Rules |
|---|---|
add_company_tags |
Array of tag names to add. Other tags are kept |
remove_company_tags |
Array of tag names to remove, matched in any case |
As on create, the tag fields are dropped when sent flat, so nest them under company.
nullclearslegal_name,email,phone,tax_number,client_code,client_number,invoice_payment_termsandinvoice_payment_terms_type.trading_name,company_status_idandcompany_typecannot be cleared.- Clearing
client_numberorclient_codeon a company with a client status is refused with422 invalid_inputwhile the account generates that identifier. - A company can carry at most 50 tags.
PATCH /api/v1/companies/789
{"company": {"phone": "+61 2 5550 9999", "add_company_tags": ["Preferred"]}}
Returns 200 with the company object.
Errors for create and change
| Status | code |
When |
|---|---|---|
403 |
not_authorized |
No create_companies (create) or update_companies (change) |
404 |
company_not_found |
The company :id is deleted, in another account, or hidden from the token’s user |
404 |
company_status_not_found |
company_status_id is not a status of the account |
409 |
company_name_taken |
A create matched a company the token’s user cannot read |
422 |
missing_input |
trading_name is missing on create |
422 |
invalid_input |
A value could not be read, such as an unknown company_type or a blank trading_name. field names it |
422 |
validation_failed |
The company would not save, such as a client_code another company has. errors lists why |
Merge companies
POST /api/v1/companies/:id/merge moves the records of duplicate companies onto the company :id, then deletes the duplicates. Clients and suppliers can both be merged.
| Field | Required | Rules |
|---|---|---|
source_company_ids |
Yes | Array of the duplicate company ids. Send it flat or under merge |
POST /api/v1/companies/789/merge
{"source_company_ids": [815, 816]}
What moves to the kept company:
- Projects and opportunities whose client, invoice-to company or referrer is a duplicate, deleted ones included.
- Contracts whose primary contractor is a duplicate.
- Invoices that are not approved and not synced.
- Costs, purchase orders, expense claim items and staff credit cards that are not synced to the accounting system.
- Beats.
What stays on the deleted duplicates:
- Records already synced to Xero, MYOB or QuickBooks, and approved invoices.
- Contacts, addresses, notes and custom fields. Link the contacts you need to the kept company first.
The kept company becomes both when the merge brings client and supplier records together. A kept company that becomes the client of a moved project or opportunity gets the account’s client status, and a client number or code when the account uses them.
The merge runs in one transaction, so a failed merge changes nothing. A successful merge sends one company.merged webhook event (see Events).
Returns 200:
{
"company": {"id": 789, "trading_name": "Acme Corporation", "company_type": "both"},
"merged_companies": [
{"id": 815, "trading_name": "Acme", "discarded_at": "2026-10-13T04:10:00.000Z"},
{"id": 816, "trading_name": "ACME Pty Ltd", "discarded_at": "2026-10-13T04:10:00.000Z"}
],
"moved": {
"projects": 4,
"invoices": 1,
"contracts": 0,
"costs": 12,
"purchase_orders": 2,
"expense_claim_items": 0,
"staff_credit_cards": 0,
"beats": 0
}
}
company is the full company object (shortened above). moved counts the records moved, by kind.
A merge cannot be undone through the API.
Merge errors
| Status | code |
When |
|---|---|---|
403 |
none | No api_create or no api_delete ({"error": "Insufficient API permissions"}) |
403 |
not_authorized |
No update_companies or no delete_companies |
404 |
company_not_found |
The company :id (field: company_id) or a source company (field: source_company_ids) does not exist or is hidden |
422 |
missing_input |
source_company_ids is missing or empty |
422 |
invalid_input |
An id is not a number, or the company is in its own source_company_ids |
422 |
company_deleted |
The company :id is deleted. Restore it in Drum first |
422 |
source_company_deleted |
A source company is already deleted |
The contact object
{
"id": 307,
"first_name": "Mia",
"last_name": "Chen",
"name": "Mia Chen",
"title": "Accounts manager",
"email": "mia.chen@acme.example",
"mobile_phone": "0412 555 307",
"office_phone": null,
"description": null,
"deleted": false,
"links": [
{
"id": 9001,
"record_kind": "company",
"record": {"id": 789, "name": "Acme Corporation"},
"role": null,
"description": "Accounts",
"pinned": true,
"primary_financial_contact": true
}
],
"created_at": "2026-10-07T01:00:00Z",
"updated_at": "2026-10-07T01:00:00Z"
}
| Field | Meaning |
|---|---|
name |
First and last name together |
last_name |
null when blank |
description |
Free-text notes about the contact |
deleted |
true for a deleted contact |
links |
The contact’s links. Not in the list endpoint |
Each link:
| Field | Meaning |
|---|---|
id |
The link’s id |
record_kind |
company, opportunity or project |
record |
{id, name} of the record |
role |
owner, primary_contractor, contractor, or null |
description |
The contact’s role on this record, as free text, or null |
pinned |
true when the contact is pinned on the record |
primary_financial_contact |
true for the company’s primary financial contact. Always false on other kinds |
Links are ordered pinned first, then in the order of the contact cards in Drum. A link to a record the token’s user cannot read, or to a contract, is left out.
List contacts
GET /api/v1/contacts lists contacts in last name, then first name order. Paginated. Each row is a contact object without links.
| Parameter | Meaning |
|---|---|
search |
Matches the start of words in the first name, last name, email and phones |
company_status |
Contacts linked to a company with this company status id |
deleted |
true lists deleted contacts only. Otherwise deleted contacts are left out |
curl -H "Authorization: Bearer $DRUM_API_TOKEN" \
"https://app.getdrum.com/api/v1/contacts?search=chen"
Without read_companies this returns 403 {"error": "Forbidden", "code": "not_authorized"}.
Read a contact
GET /api/v1/contacts/:id returns one contact object with its links. A deleted contact is returned too, with "deleted": true.
A contact in another account, or one the token’s user cannot read, returns 404 contact_not_found.
Create a contact
POST /api/v1/contacts creates a contact. Send the fields flat or under contact.
| Field | Required | Rules |
|---|---|---|
first_name |
Yes | Up to 255 characters. Not blank |
last_name, title, office_phone |
No | Up to 255 characters each |
email, mobile_phone |
No | Up to 255 characters each. Each is unique in the account, deleted contacts included |
description |
No | Up to 10,000 characters |
link |
No | One link to make in the same request (see below) |
idempotency_key |
No | 1 to 255 characters. Send it at the same level as the other fields: under contact when you nest them |
link fields:
| Field | Required | Rules |
|---|---|---|
record_kind |
Yes | company, opportunity or project |
record_id |
Yes | The record’s id. It must be a kept record the token’s user can read and update |
role |
No | owner, primary_contractor, contractor, or null |
description |
No | Up to 10,000 characters |
pinned |
No | true or false |
primary_financial_contact |
No | true or false. A company link only. Setting it clears it on the company’s other contacts |
Any other key inside link returns 422 invalid_input.
POST /api/v1/contacts
{
"contact": {
"first_name": "Liam",
"last_name": "Ng",
"email": "liam@acme.example",
"link": {"record_kind": "company", "record_id": 789, "description": "Site manager"},
"idempotency_key": "crm-contact-55120"
}
}
Returns 201 with the contact object, its links, "matched_existing": false and "replayed": false.
Matching by email or mobile. When a kept contact already has the email (any case), or failing that the mobile phone, Drum creates nothing. It returns 200 with that contact and "matched_existing": true. None of the contact’s fields change, but link is still made to it. To change the matched contact, send a PATCH.
When a deleted contact has the email or mobile phone, the request returns 409 contact_deleted, and the message names the deleted contact’s id. Restore that contact instead of creating a new one.
Idempotency key. A retry with the same idempotency_key and the same fields creates nothing and returns 200 with the first contact and "replayed": true. This key is a body field, not the Idempotency-Key header. The other rules match Idempotency keys.
Change a contact
PATCH /api/v1/contacts/:id changes the fields you send. Send the fields flat or under contact. null clears an optional field. first_name cannot be cleared.
links adds links, or changes the link a record already has, in the same save:
| Field | Rules |
|---|---|
links |
Array of up to 20 links, with the fields of link above. Each record may appear once |
links never removes a link. Links you leave out are kept, and a link field you leave out keeps its value. To remove a link, use DELETE on the link.
Changing a contact field needs update_companies. A request that sends only links needs the update permission of each linked record instead.
PATCH /api/v1/contacts/307
{"title": "Finance director", "links": [{"record_kind": "opportunity", "record_id": 12001, "pinned": true}]}
Returns 200 with the contact object and its links. A deleted contact returns 404 contact_not_found.
Delete and restore a contact
DELETE /api/v1/contacts/:id deletes the contact, as Delete does in Drum. Its links stay, so a restored contact is back on its records. Returns 204 with no body. A contact that is already deleted returns 404 contact_not_found.
PATCH /api/v1/contacts/:id/restore restores a deleted contact. Returns 200 with the contact object. A contact that is not deleted returns 404 contact_not_found.
Link a contact to a record
PATCH /api/v1/companies/:record_id/contacts/:id links the contact :id to the company. The same path under opportunities or projects links it to an opportunity or a project. PUT works the same way.
Body fields are all optional: role, description, pinned and primary_financial_contact (company only), with the rules of link above. Send them flat or under link. A field you leave out keeps its value.
PATCH /api/v1/opportunities/12001/contacts/307
{"role": "owner", "pinned": true}
Returns 201 for a new link, or 200 when the record already had the contact and the link changed:
{
"contact_id": 307,
"id": 9002,
"record_kind": "opportunity",
"record": {"id": 12001, "name": "Riverside fit-out"},
"role": "owner",
"description": null,
"pinned": true,
"primary_financial_contact": false
}
The path must match the kind of record. An opportunity id under projects returns 404 project_not_found. A deleted contact cannot be linked (404 contact_not_found).
Remove a link
DELETE /api/v1/companies/:record_id/contacts/:id removes the link between the contact and the company, as Un-assign does in Drum. The same path works under opportunities and projects. The contact itself stays, even when it has no other link. A deleted contact can be unlinked.
Returns 204 with no body. A contact that is not linked to the record returns 404 contact_link_not_found.
Contact errors
| Status | code |
When |
|---|---|---|
403 |
not_authorized |
The token’s user lacks the contact permission, or the update permission of a linked record |
404 |
contact_not_found |
The contact does not exist, is in another account, is hidden, or (on a change or link) is deleted |
404 |
company_not_found, opportunity_not_found, project_not_found |
The linked record does not exist, is deleted, is hidden, or is a different kind |
404 |
contact_link_not_found |
A DELETE of a link that does not exist |
409 |
contact_deleted |
A create matched a deleted contact by email or mobile phone |
409 |
contact_exists |
A create matched a contact the token’s user cannot read |
409 |
idempotency_key_reused, retry_later, idempotent_record_deleted |
See Idempotency keys |
422 |
missing_input |
first_name, or a link’s record_id, is missing |
422 |
invalid_input |
A value could not be read, such as an unknown record_kind or role, primary_financial_contact on a record that is not a company, more than 20 links, or the same record twice. field names it |
422 |
validation_failed |
The contact would not save. errors lists why |