Leads
Send new requests into the lead inbox from your website or other systems, and read, assign, mark contacted, move forward or dismiss them.
View as MarkdownThe lead inbox holds new requests waiting for someone to reach out: website request forms, customer-portal requests and requests sent through the API.
When a lead comes in, TruTec runs the same intake as a web request form:
- matches the person and company to records you already have, or creates them;
- opens a job in the first open stage of your sales pipeline (and links the site from the address);
- routes it to a teammate using your lead routing settings (round-robin, territory and service rules);
- creates a "Call new lead" company follow-up due within your speed-to-lead window;
- emails the assigned teammate (you can turn this off per request).
Nothing is sent to the lead.
In TruTec Heavy workspaces a website request form can instead add a basic lead to the Leads pipeline (Request forms → Routing → "Where website requests go"): no company, contact or job until someone moves it to sales. Leads sent through this API always get the full intake above.
A lead's status is new, contacted, converted ("Moving forward"), dismissed or spam. TruTec updates it on its own: a logged call, email or meeting with the person (or the "Call new lead" follow-up being completed) makes it contacted, and the job leaving its first stage makes it Moving forward.
Note
Partner keys that act for several workspaces must also send firm_id (query string, or JSON body on writes).
The lead object#
| Field | Type | Description |
|---|---|---|
id |
integer | TruTec id. |
status, status_label |
string | See above. converted reads "Moving forward". |
name, email, phone, company, address |
string or null | What the person sent. |
services |
string or null | Requested services, as readable labels. |
source |
string or null | Form name, "Customer portal", or your source label. |
source_kind |
string | form, portal, api or manual. |
submitted_at |
string | When it came in (UTC). |
assigned_to_user_id, assigned_to |
integer, object | Who it went to ({id, name}). |
assignment_reason |
string or null | Why it went to them. |
first_contacted_at, first_contact_kind, first_contacted_by_user_id |
When and how someone first reached out (call, email, text, meeting, task, marked). |
|
contact_id, client_account_id, job_id, location_id |
integer or null | The records the lead created or matched. |
follow_up_task_id |
integer or null | The "Call new lead" company follow-up (GET /v1/account-tasks/{id}). |
answers |
array | [{label, value}], everything they told you. |
utm |
object | Campaign fields sent with the request. |
match_reasons |
array | Why an existing contact or company was matched. |
created_records |
object | Which records this lead created (contact, account, job, location). |
escalated_at |
string or null | When the owner was alerted that nobody had reached out. |
auto_spam |
boolean | The form flagged it as spam on its own. |
problem |
string or null | Set when the request was saved but its job could not be created. |
on_leads_pipeline |
boolean | TruTec Heavy: the lead waits on the Leads pipeline with no company or job yet (typed in with "Add lead", or a website request). Someone moves it to sales in TruTec, where the company is added; until then convert answers 409 no_job. |
pipeline_stage_id |
integer or null | Its Leads pipeline stage. |
estimated_value, customer_type, notes, next_follow_up_at |
What the team typed on a Leads pipeline lead. | |
status_changed_at, created_at, updated_at |
string | UTC timestamps with Z. |
Leads#
List leads#
GET /v1/leads
Permission: Read jobs
Newest first. With updated_after, oldest change first.
| Name | Type | Required | Description |
|---|---|---|---|
status |
string | No | all (default), open (new + contacted), new, contacted, converted, dismissed or spam. |
assigned_to_user_id |
integer | No | Only this teammate's leads. |
source_kind |
string | No | form, portal, api or manual. |
q |
string | No | Name, email, phone, company or address contains. |
updated_after |
string | No | ISO 8601. |
limit, offset |
integer | No | Paging (default 50, max 200). |
curl "https://trutec.ai/api/v1/leads?status=open" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 902,
"status": "new",
"status_label": "New",
"name": "Jordan Ellis",
"display_name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(512) 555-0199",
"company": "Lakeside Commons",
"address": "400 Lake Rd, Austin, TX 78701",
"services": "Sealcoating",
"source": "API",
"source_kind": "api",
"submitted_at": "2026-10-12T14:00:00Z",
"assigned_to_user_id": 118,
"assigned_to": {"id": 118, "name": "Dana Whitfield"},
"assignment_reason": "Round-robin",
"first_contacted_at": null,
"first_contact_kind": null,
"first_contacted_by_user_id": null,
"contact_id": 20500,
"client_account_id": 4901,
"job_id": 41901,
"location_id": 9411,
"follow_up_task_id": 3310,
"answers": [{"label": "Name", "value": "Jordan Ellis"}, {"label": "Services", "value": "Sealcoating"}],
"utm": {},
"match_reasons": [],
"created_records": {"contact": true, "account": true, "job": true},
"escalated_at": null,
"auto_spam": false,
"problem": null,
"status_changed_at": null,
"created_at": "2026-10-12T14:00:00Z",
"updated_at": "2026-10-12T14:00:00Z"
}
],
"pagination": {"total": 1, "limit": 50, "offset": 0}
}
Get a lead#
GET /v1/leads/{id}
Permission: Read jobs
Returns {"data": {lead}}. Errors: 404 Lead not found.
Create a lead#
POST /v1/leads
Permission: Write jobs
Use this for requests from your own website or another system. If you are syncing jobs you already have, use POST /v1/jobs instead (see Jobs); those are not new leads.
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Who is asking. |
email |
string | One of | At least an email or a phone. |
phone |
string | One of | 10 to 15 digits with area code. |
company |
string | No | Company or property name. |
address |
string | No | Property address. |
message |
string | No | What they need. Up to 3,000 characters. |
services |
array of strings | No | Job-type values from GET /v1/job-types, e.g. ["sealcoating", "striping"]. |
timing |
string | No | asap, 30_days, this_season, budgeting or unsure. |
property_type |
string | No | One of the workspace's property types. |
source |
string | No | Label shown in the inbox, e.g. "Spring mailer". Default API. |
utm |
object | No | Campaign fields such as utm_source, utm_campaign, gclid. |
notify |
boolean | No | Email the assigned teammate the new-lead alert. Default true. |
curl -X POST "https://trutec.ai/api/v1/leads" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Jordan Ellis",
"email": "[email protected]",
"phone": "(512) 555-0199",
"company": "Lakeside Commons",
"address": "400 Lake Rd, Austin, TX 78701",
"services": ["sealcoating"],
"message": "Lot needs a reseal before spring"
}'
Returns 201 with {"data": {lead}}. If something fails part-way, the lead is still saved with its answers and problem is set, so nobody loses a request.
Errors: 400 name is required, Send an email or a phone number so someone can reach them., Unknown services: ..., a bad timing or property_type, or unknown fields.
Mark a lead contacted#
POST /v1/leads/{id}/contacted
Permission: Write jobs
Someone reached out. A call, email or meeting is logged on the job and the company feed (with note); a text is noted on the person. The "Call new lead" follow-up closes.
| Name | Type | Required | Description |
|---|---|---|---|
how |
string | No | call, email, text, meeting or other (default). |
note |
string | No | What happened. |
Returns {"data": {lead}}. Errors: 409 Restore this lead first. for a dismissed or spam lead.
Assign a lead#
POST /v1/leads/{id}/assign
Permission: Write jobs
Hands the lead, its job and its open follow-up to user_id (an office teammate). A new lead that hasn't been contacted yet emails them the new-lead alert. Returns {"data": {lead}}.
Errors: 400 user_id is required; 404 when the user is not an office teammate in the workspace.
Move a lead forward#
POST /v1/leads/{id}/convert
Permission: Write jobs
Every lead already has a job. This moves that job to a later stage, with the same rules as POST /v1/jobs/{id}/move (see Jobs), and the inbox then shows the lead as "Moving forward".
| Name | Type | Required | Description |
|---|---|---|---|
status |
string | No | An open or won stage key from GET /v1/job-stages. Default: the next open stage after the one the lead came in at. |
confirm_schedule_completion |
boolean | No | Needed when the move would complete open work orders. |
curl -X POST "https://trutec.ai/api/v1/leads/902/convert" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"status": "bid_sent"}'
Returns {"data": {lead}, "job": {job}}.
Errors: 400 for a lost or excluded stage (close a lead with /dismiss, or move its job with /v1/jobs/{id}/move), or when there is no later open stage; 409 lead_closed, no_job or confirmation_required; 403 status_locked.
Dismiss a lead#
POST /v1/leads/{id}/dismiss
Permission: Write jobs
Takes the lead out of the inbox. Send {"spam": true} to mark it spam instead: the contact and company the lead created are archived too, when nothing else uses them. Its follow-up closes. Returns {"data": {lead}, "message": "..."}.
Restore a lead#
POST /v1/leads/{id}/restore
Permission: Write jobs
Back into the inbox, as new or (if someone already reached out) contacted; anything archived by marking it spam comes back. A submission the form flagged as spam on its own has no job yet: it runs through the full intake now and returns as a new lead, with replaced_lead_id naming the old one.
Edit or delete a lead#
A lead is the request as it came in, so it can't be edited or deleted, in the app or through the API. Change what it created instead: the person (PATCH /v1/contacts/{id}), the company (PATCH /v1/client-accounts/{id}) or the job (PATCH /v1/jobs/{id}), using the contact_id, client_account_id and job_id on the lead. To take a lead out of the inbox, dismiss it (or mark it spam).