Merge & search
Search the whole workspace like the search box in TruTec, and merge duplicate customers, contacts and sites after previewing what moves.
View as MarkdownSearch the workspace#
GET /v1/search?q=
Permission: Read jobs
The search box at the top of TruTec, as one call. It looks through jobs (name, job number, company, address, contact, Property ID, work order and accounting numbers), customers, contacts, sites, leads, proposals, invoices, logged notes and emails, projects and measurements, and returns the best few matches of each type. It sees what the person who created the API key sees: a member who only sees their own jobs gets only those.
| Name | Type | Required | Description |
|---|---|---|---|
q |
string | Yes | 2 to 120 characters. |
types |
string | No | Comma list to keep: job, client_account, contact, location, lead, proposal, invoice, note, project, measurement. |
curl "https://trutec.ai/api/v1/search?q=oak%20hills" -H "X-API-Key: sk_your_api_key_here"
{
"data": [
{"type": "job", "id": 70412, "title": "Oak Hills Plaza reseal", "subtitle": "Sealcoating · 1200 Commerce Dr",
"status": "proposal_sent", "app_url": "https://trutec.ai/jobs/70412"},
{"type": "client_account", "id": 3110, "title": "Oak Hills Property Group", "subtitle": "3 contacts · 12 jobs",
"status": null, "app_url": "https://trutec.ai/jobs/companies/3110"},
{"type": "location", "id": 812, "title": "Oak Hills Plaza", "subtitle": "1200 Commerce Dr",
"status": null, "app_url": "https://trutec.ai/jobs/locations/812"}
],
"query": "oak hills"
}
There is no paging: add words to narrow. Use the id with the matching resource (/v1/jobs/{id}, /v1/client-accounts/{id}, /v1/contacts/{id}, /v1/locations/{id}, ...).
Merge duplicates#
Two records for the same customer, person or site? Merge one into the other. The record in the URL is merged away; the one you name in the body stays. Everything on the merged record moves over: jobs, contacts, sites, notes and logged calls and emails, follow-ups, reminders, files, divisions, capital plan items, re-bid history and campaign recipients. Then:
- a merged customer or contact is deleted;
- a merged site is archived and points to the site it went into (its old Site ID keeps resolving).
Field values follow the app's defaults: the surviving record's value, unless it is blank (then the other one's). Lead details follow special rules (the further-along stage and the larger value win, tags and notes are combined). To pick the other side for a field, send choices.
Warning
A merge can't be undone through the API. Preview first, and confirm with whoever owns the data.
Merges run the app's duplicate-review merge as the key's user, who must be an office user (not a crew login).
Preview a merge#
GET /v1/client-accounts/{account_id}/merge-preview?into={keep_id}
GET /v1/contacts/{contact_id}/merge-preview?into={keep_id}
GET /v1/locations/{location_id}/merge-preview?into={keep_id}
Permission: Read jobs
Changes nothing.
{
"data": {
"keep": {"id": 3110, "name": "Oak Hills Property Group", "archived": false},
"merge_away": {"id": 3187, "name": "Oak Hills Property Grp", "archived": false},
"fields": [
{"field": "billing_address", "label": "Billing address", "keep_value": "PO Box 18, Austin TX",
"merge_away_value": "1200 Commerce Dr, Austin TX", "needs_choice": true, "default": "keep", "can_keep_both": false},
{"field": "billing_contact_email", "label": "Billing email", "keep_value": null,
"merge_away_value": "[email protected]", "needs_choice": false, "default": "drop", "can_keep_both": false}
],
"moves": [{"count": 4, "label": "jobs"}, {"count": 2, "label": "contacts"}],
"blockers": []
}
}
fields lists only the fields that differ. needs_choice: true means both records have a value; default says which side is kept unless you choose (keep = the surviving record's). Site previews add blockers (reasons the merge is refused, such as both sites carrying different Site IDs from a connected system) and invoiced_job_count.
Merge#
POST /v1/client-accounts/{account_id}/merge with into_client_account_id
POST /v1/contacts/{contact_id}/merge with into_contact_id
POST /v1/locations/{location_id}/merge with into_location_id
Permission: Write jobs + Delete records
| Name | Type | Required | Description |
|---|---|---|---|
into_client_account_id / into_contact_id / into_location_id |
integer | Yes | The record that stays. |
choices |
object | No | {field: "keep" \| "drop"} for fields from the preview. Custom fields are custom:<key>. On a contact, "phone": "both" keeps both numbers. |
move_invoiced_jobs |
boolean | No | Sites only: a site with invoiced jobs is refused unless this is true (check with accounting first). |
curl -X POST "https://trutec.ai/api/v1/client-accounts/3187/merge" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"into_client_account_id": 3110, "choices": {"billing_address": "drop"}}'
{
"data": {
"kept": {"id": 3110, "name": "Oak Hills Property Group", "archived": false},
"merged_id": 3187,
"merged_name": "Oak Hills Property Grp",
"message": "Merged ‘Oak Hills Property Grp’ into ‘Oak Hills Property Group’ — 4 jobs, 2 contacts moved."
}
}
Errors: 400 merging a record into itself, or an unknown field in choices; 403 when the key lacks Delete records or its user isn't an office user; 404 for either id outside the workspace; 409 when the app refuses the merge (for example the workspace has turned off deleting records, or a site has invoiced jobs), with its reason in message.