Contacts
Read and write the people you work with, their phones and emails, their roles at customers and sites, and who is on each job.
View as MarkdownA contact is a person: a property manager, an HOA board member, a facilities director, an accounts-payable clerk. Each contact can have:
- a primary company (
client_account_id, see Customers & sites); - several phone numbers and emails (channels), one primary of each kind;
- roles at other companies and sites (links), with start and end dates so history survives a handoff;
- preferences (preferred channel, do-not-contact, birthday, tags) and the workspace's custom fields (see Custom fields).
A job has a list of people on the job. The primary person fills the job's own contact_name, contact_email and contact_phone. Extra people can be marked as additional customers who get their own proposal copy.
Note
Partner keys that act for several workspaces must also send firm_id (query string, or JSON body on writes).
Response format#
Lists return {"data": [...], "pagination": {"total", "limit", "offset"}}. limit defaults to 50 (maximum 200). Single records return {"data": {...}}. Timestamps are UTC ISO 8601 with a Z (2026-10-12T14:03:22Z); dates are YYYY-MM-DD. Errors are {"error": "...", "code": "..."}; unknown body fields are a 400 that lists them in fields.
The contact object#
| Field | Type | Description |
|---|---|---|
id |
integer | TruTec id. |
name |
string or null | Full name. |
display_name |
string | Name, or company when there is no name. |
title |
string or null | Job title. |
contact_type |
string or null | The workspace's own category (Property Manager, Billing, ...). |
company |
string or null | Company name shown for the person. |
client_account_id |
integer or null | Primary company. |
client_account |
object or null | {id, name} of that company. |
email, phone |
string or null | The primary email and phone. |
notes |
string or null | The pinned note on the person. |
assigned_user_id |
integer or null | Salesperson for this person. null means the company's salesperson. |
preferred_channel |
string or null | call, text or email. |
do_not_contact, do_not_contact_reason |
boolean, string | Don't call, text or email this person. |
birthday |
string or null | YYYY-MM-DD. |
tags |
array of strings | Free tags. |
custom_fields |
object | Custom field values by key. |
archived_at, created_at, updated_at |
string or null | UTC timestamps. |
GET /v1/contacts/{id} and every write also return:
| Field | Description |
|---|---|
channels |
Every phone and email: id, ref, kind (phone / email), label (mobile, work, office, home, fax, other), value, is_primary, do_not_contact. |
links |
Companies and sites: kind (company / location), id, name, roles, is_primary, current, started_on, ended_on. |
jobs |
Jobs the person is on: job_id, name, status, role, is_primary, bills_separately. |
Contacts#
List contacts#
GET /v1/contacts
Permission: Read jobs
People A to Z. With updated_after, oldest change first, so you can page forward with the last row's updated_at.
| Name | Type | Required | Description |
|---|---|---|---|
q |
string | No | Name, email, phone, company or title contains this text. |
email |
string | No | Exact email, any case. Matches any of the person's emails, so use it to look someone up before creating them. |
client_account_id |
integer | No | People whose primary company this is. For everyone with a role there, use GET /v1/client-accounts/{id}/contacts. |
job_id |
integer | No | People on this job. |
contact_type |
string | No | Exact category. |
include_archived |
boolean | No | Include archived people. |
updated_after |
string | No | ISO 8601. |
limit, offset |
integer | No | Paging. |
curl "https://trutec.ai/api/v1/[email protected]" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 20417,
"name": "Jamie Rivera",
"display_name": "Jamie Rivera",
"title": "Community manager",
"contact_type": "Property Manager",
"company": "Maple Ridge HOA",
"client_account_id": 4812,
"client_account": {"id": 4812, "name": "Maple Ridge HOA"},
"email": "[email protected]",
"phone": "(512) 555-0142",
"notes": null,
"assigned_user_id": null,
"preferred_channel": "text",
"do_not_contact": false,
"do_not_contact_reason": null,
"birthday": null,
"tags": ["board"],
"custom_fields": {},
"archived_at": null,
"created_at": "2026-09-14T15:30:02Z",
"updated_at": "2026-10-12T14:03:22Z"
}
],
"pagination": {"total": 1, "limit": 50, "offset": 0}
}
Errors: 403 API key lacks job read permission; 400 for a bad updated_after.
Get a contact#
GET /v1/contacts/{id}
Permission: Read jobs
One person (archived or not) with channels, links and jobs.
{
"data": {
"id": 20417,
"name": "Jamie Rivera",
"company": "Maple Ridge HOA",
"client_account_id": 4812,
"email": "[email protected]",
"phone": "(512) 555-0142",
"channels": [
{"id": 5521, "ref": "5521", "kind": "phone", "label": "mobile", "value": "(512) 555-0142", "is_primary": true, "do_not_contact": false},
{"id": 5522, "ref": "5522", "kind": "email", "label": "work", "value": "[email protected]", "is_primary": true, "do_not_contact": false}
],
"links": [
{"kind": "company", "id": 4812, "name": "Maple Ridge HOA", "roles": ["property_manager"], "is_primary": true, "current": true, "started_on": null, "ended_on": null},
{"kind": "location", "id": 9307, "name": "Clubhouse lot", "roles": ["site_contact"], "is_primary": false, "current": true, "started_on": "2025-03-01", "ended_on": null}
],
"jobs": [
{"job_id": 41872, "name": "Maple Ridge HOA reseal", "status": "lead", "role": "billing", "is_primary": true, "bills_separately": false}
]
}
}
The example is shortened; every field of the contact object is present.
Errors: 404 Contact not found (also for another workspace's contact).
Create a contact#
POST /v1/contacts
Permission: Write jobs
Send at least one of name, company, email or phone.
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | No | Max 200 characters. |
title |
string | No | Job title. |
contact_type |
string | No | Category. Max 80 characters. |
company |
string | No | Company name. Linked to the customer with that name, or a new customer is created, as in the app. |
client_account_id |
integer | No | Primary company by id. Wins over company. |
email, phone |
string | No | Primary email and phone. More can be added with Add a phone or email. |
notes |
string | No | The pinned note. |
assigned_user_id |
integer | No | Salesperson for this person (an office user). |
preferred_channel |
string | No | call, text or email. |
do_not_contact |
boolean | No | Don't reach out. Add do_not_contact_reason. |
birthday |
string | No | YYYY-MM-DD. |
tags |
array of strings | No | Free tags. |
custom_fields |
object | No | Values by field key. |
Contacts have no external_ref. To avoid duplicates, look the person up first with GET /v1/contacts?email=.
curl -X POST "https://trutec.ai/api/v1/contacts" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Jamie Rivera",
"email": "[email protected]",
"phone": "(512) 555-0142",
"client_account_id": 4812,
"title": "Community manager",
"preferred_channel": "text"
}'
Returns 201 with {"data": {contact with channels, links and jobs}}.
Errors:
400Add at least a name, company, email, or phone number.,email does not look like an email address,Pick call, text or email., an unknown or invalid custom field, or unknown body fields404client_account_id not found in this workspace,assigned_user_id does not match a user in this workspace
Update a contact#
PATCH /v1/contacts/{id}
Permission: Write jobs
Takes the same fields as create; only the ones you send change. client_account_id: null removes the primary company. When the name, email, phone or company changes, every job the person is on picks up the new details, the same as saving the contact in the app.
curl -X PATCH "https://trutec.ai/api/v1/contacts/20417" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"phone": "(512) 555-0100", "tags": ["board", "billing"]}'
Returns {"data": {contact}}. Same errors as create.
Archive or delete a contact#
DELETE /v1/contacts/{id}
Permission: Write jobs and Delete records
Archives the person (204). They drop out of lists but keep their history; POST /v1/contacts/{id}/restore brings them back.
Add ?hard=true to delete permanently. Jobs keep their own contact fields. The person's own notes and files are deleted; notes that are also on their company's feed stay there.
Errors: 403 API key lacks delete permission; 403 with "deletion_locked": true when the workspace has turned off deleting records; 404.
Restore a contact#
POST /v1/contacts/{id}/restore
Permission: Write jobs
Un-archives the person. Returns {"data": {contact}}.
Phones and emails#
The primary phone and email of each person are also their phone and email fields. Editing those fields moves the primary entry; these endpoints manage the full list.
Each entry has a ref. For a person whose phones and emails have never been edited, entries are read from the phone / email text and have id: null and a ref like v-phone-0; the first edit saves them all and gives them ids.
Add a phone or email#
POST /v1/contacts/{id}/channels
Permission: Write jobs
| Name | Type | Required | Description |
|---|---|---|---|
kind |
string | Yes | phone or email. |
value |
string | Yes | The number or address. Phone numbers are formatted for you. |
label |
string | No | mobile, work, office, home, fax or other. |
is_primary |
boolean | No | Make it the primary of its kind. The first of each kind is primary. |
curl -X POST "https://trutec.ai/api/v1/contacts/20417/channels" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"kind": "phone", "value": "512-555-0177", "label": "mobile"}'
{"data": {"id": 5530, "ref": "5530", "kind": "phone", "label": "mobile", "value": "(512) 555-0177", "is_primary": false, "do_not_contact": false}}
Errors: 400 kind must be phone or email, That email doesn’t look right., Type the whole phone number.; 409 That one is already on file.
Update a phone or email#
PATCH /v1/contacts/{id}/channels/{ref}
Permission: Write jobs
Send any of value, label, is_primary (true makes it primary) and do_not_contact. Returns {"data": {channel}}.
Remove a phone or email#
DELETE /v1/contacts/{id}/channels/{ref}
Permission: Write jobs
Removes one entry (204). If it was primary, the next one of that kind becomes primary. This is an edit of the contact, so it needs only Write jobs.
Roles at companies and sites#
Set a role at a company or site#
POST /v1/contacts/{id}/links
Permission: Write jobs
Records that the person works with a company or a site. Sending the same company or site again replaces the roles.
| Name | Type | Required | Description |
|---|---|---|---|
client_account_id |
integer | One of | The company. |
location_id |
integer | One of | The site. |
roles |
array | No | Any of decision_maker, property_manager, site_contact, billing, accounts_payable, board_member, other. Empty means "works with them". |
started_on |
string | No | YYYY-MM-DD. |
is_primary |
boolean | No | Make this their primary company (or main site). A person with no primary company gets the first company linked. |
curl -X POST "https://trutec.ai/api/v1/contacts/20417/links" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"location_id": 9307, "roles": ["site_contact"]}'
Returns 201 with {"data": [links]}: all of the person's links.
Errors: 400 Send exactly one of client_account_id or location_id, unknown roles; 404 for a company or site outside the workspace.
End or remove a role#
DELETE /v1/contacts/{id}/links/{kind}/{target_id}
Permission: Write jobs
kind is company or location. With ?end=true the link is kept as history ("no longer works there", ended today or on ended_on=YYYY-MM-DD); without it the link is removed. Ending or removing their primary company picks another current company, or none. Returns 204.
People on a job#
The people on a job, with their role on it, is_primary and bills_separately.
List the people on a job#
GET /v1/jobs/{job_id}/contacts
Permission: Read jobs
Primary first, then by role.
{
"data": [
{
"contact_id": 20417,
"job_id": 41872,
"role": "billing",
"is_primary": true,
"bills_separately": false,
"created_at": "2026-10-12T14:03:22Z",
"contact": {"id": 20417, "name": "Jamie Rivera", "display_name": "Jamie Rivera", "title": "Community manager", "contact_type": "Property Manager", "email": "[email protected]", "phone": "(512) 555-0142", "client_account_id": 4812, "archived_at": null, "updated_at": "2026-10-12T14:03:22Z"}
}
]
}
Add a person to a job#
POST /v1/jobs/{job_id}/contacts
Permission: Write jobs
Link a saved person with contact_id, or create one by sending name / email / phone / company (and optionally title, contact_type). The first person on a job becomes primary and fills the job's contact fields; the job is linked to the person's company if it has none.
| Name | Type | Required | Description |
|---|---|---|---|
contact_id |
integer | One of | A saved, non-archived person. |
name, email, phone, company, title, contact_type |
string | One of | A new person (a name or an email at least). |
role |
string | No | billing, onsite, decision_maker, property_manager, estimator, operations, or your own text. |
is_primary |
boolean | No | Make them the job's primary contact. |
bills_separately |
boolean | No | Add them as an additional customer: the job is bid to them too and each gets a proposal copy. The job must already have a main customer. |
curl -X POST "https://trutec.ai/api/v1/jobs/41872/contacts" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"contact_id": 20417, "role": "billing"}'
Returns 201 with {"data": {person on the job}}, or 200 when the person was already on the job.
Errors: 400 Archived contacts cannot be linked to jobs. Restore it first., Add the job’s main customer first, then add more customers.; 404 for a job or contact outside the workspace.
Change a person's role on a job#
PATCH /v1/jobs/{job_id}/contacts/{contact_id}
Permission: Write jobs
Send role, is_primary: true (makes them primary; the job's contact fields follow), or bills_separately (turn "additional customer" on or off; the job's activity log records it). Returns {"data": {person on the job}}.
Remove a person from a job#
DELETE /v1/jobs/{job_id}/contacts/{contact_id}
Permission: Write jobs
Unlinks the person; the contact itself stays. If they were primary, the next person becomes primary and fills the job's contact fields. Returns 204.
Notes and history#
A person's notes feed and their full history are in Notes & tasks: GET /v1/contacts/{id}/notes, POST /v1/contacts/{id}/notes and GET /v1/contacts/{id}/timeline.