Skip to content
TruTecDevelopers
Get an API key

Leads

Send new requests into the lead inbox from your website or other systems, and read, update, assign, mark contacted, convert or dismiss them.

View as Markdown

The 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:

  1. matches the person and company to records you already have, or creates them;
  2. opens a job in the first open stage of your sales pipeline (and links the site from the address);
  3. routes it to a teammate using your lead routing settings (round-robin, territory and service rules);
  4. creates a "Call new lead" company follow-up due within your speed-to-lead window;
  5. 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, unless the workspace uses the lead pipeline below.

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.

The lead pipeline (leads stay in the inbox until Convert)#

A workspace can turn on Leads stay in the Lead inbox until you Convert them (Request forms → Lead routing → Lead pipeline; owners and admins). The response's lead_pipeline field says whether it is on. With it on:

  • A new lead, from a form, this API or "+ New lead" in the app, gets no job, site or new company. It is matched to a contact and company you already have when one fits (contact_id, client_account_id), routed and alerted as usual.
  • Stages: new → contacted → engaged. Exits: nurture (needs a date; the lead shows under New again when the date comes), unqualified (needs a reason), do_not_contact (also flags the linked contact). Change them with PATCH /v1/leads/{id}.
  • A qualification checklist (qualification): work type, site address and timing, plus any job fields the workspace requires on new jobs (required_job_fields).
  • Convert is the only way to Qualified (status converted, status_label "Qualified"). One call closes the lead and creates the job in the first deal stage, with its site, company and contact. It is retry-safe: converting a lead that already has its job returns that job with created: false, never a second job or HubSpot deal. See Convert a lead.

Every change fires lead events (lead_created, lead_updated, lead_status_changed, lead_converted), whoever made it: the app, this API or an automation.

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.
external_ref string or null Your id for the lead (e.g. the HubSpot lead id). Unique per workspace. With Settings → HubSpot → "Sync leads with HubSpot" on, TruTec's own sync uses hubspot:lead:<id> (or hubspot:contact:<id> where the HubSpot account has no Leads object); a lead you post with your own external_ref never gets a second HubSpot Lead.
status, status_label string See above. converted reads "Moving forward", or "Qualified" with the lead pipeline on.
stage object {status, label, is_open, is_qualified, nurture_due}.
lead_pipeline boolean The workspace keeps leads in the inbox until Convert.
qualification object {work_types, work_type_labels, site_address, timing, timing_label, timing_date, job_fields, required_job_fields, complete, missing}. Until someone fills it in, it shows what the request said. required_job_fields lists {key, label, type, options, required} the workspace's New Job form insists on; answer them in job_fields.
unqualified_reason, unqualified_note string or null Set while the lead is unqualified.
nurture_until string or null When a nurture lead comes back (UTC).
job_number, job_external_ref, converted_at string or null The job Convert made: its number (e.g. J-1204), your id for it, and when.
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.
last_touch_at string or null The last call, email, text or meeting logged on the 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, engaged, nurture), closed (everything else), or one status: new, contacted, engaged, nurture, converted, unqualified, do_not_contact, dismissed, spam.
external_ref string No The lead carrying your id.
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.
external_ref string No Your id for the lead, up to 120 characters. Makes the call an upsert: a lead that already carries it is updated with the fields you send (200, created: false; name is then optional); otherwise the lead is created (201, created: 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}, "created": true}. If something fails part-way, the lead is still saved with its answers and problem is set, so nobody loses a request. With the lead pipeline on, no job is made until Convert.

Update a lead#

PATCH /v1/leads/{id}

Permission: Write jobs

Send only what changes. Everything is checked first; when anything is refused, nothing changes.

Name Type Required Description
status string No new, contacted, engaged, nurture, unqualified, do_not_contact, dismissed or spam. converted (Qualified) only comes from Convert.
nurture_until string With nurture When to check back: YYYY-MM-DD (8 am workspace time) or ISO 8601.
reason string With unqualified not_our_work, outside_area, too_small, price_shopper, duplicate or other (or its label, e.g. "Too small").
note string No A note with the stage change.
assigned_to_user_id integer No Hand it to an office teammate (they get the new-lead email while it is new).
assigned_to_email string No The same, by the teammate's login email.
work_types array of strings No Checklist: job-type values from GET /v1/job-types.
site_address string No Checklist: the site.
timing string No Checklist: asap, within_2_weeks, within_30_days, this_season or before_date.
timing_date string With before_date YYYY-MM-DD.
job_fields object No Answers to qualification.required_job_fields, by key.
notes string No Notes on the lead.
follow_up_at string No Next follow-up, ISO 8601 or YYYY-MM-DD.
external_ref string or null No Your id for the lead.
name, email, phone, company, address, source, utm No The person's details, as on create.
curl -X PATCH "https://trutec.ai/api/v1/leads/902" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"status": "nurture", "nurture_until": "2026-11-15", "note": "Budget opens in spring"}'

Returns {"data": {lead}}.

Errors: 400 bad values or unknown fields; 404 the lead or teammate isn't in the workspace; 409 use_convert, lead_converted (a Qualified lead has its job) or external_ref_taken; 422 date_required (nurture) or reason_required (unqualified).

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. With the lead pipeline on, the touch is logged on the lead itself (its activity) and a new lead becomes contacted.

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.

Convert a lead#

POST /v1/leads/{id}/convert

Permission: Write jobs

With the lead pipeline on, this is the one-step Convert, the same as the button in the inbox. In one transaction the lead closes as Qualified and the job is created in the first deal stage of the sales pipeline: the company (their company name, else the person's name) and contact are found or created, the site comes from the address, the job types from work_types, the salesperson is the lead's assignee, and the source, first touch and campaign are copied onto the job. Then the usual new-job steps run: job number, HubSpot deal (when your HubSpot connection creates deals) and webhooks.

The lead's checklist must be complete. Send any missing answers in the same call:

Name Type Required Description
work_types array of strings If not on the lead Job-type values.
site_address string If not on the lead The site.
timing string If not on the lead asap, within_2_weeks, within_30_days, this_season or before_date.
timing_date string With before_date YYYY-MM-DD.
job_fields object If the workspace requires them Answers to qualification.required_job_fields, by key.
job_external_ref string No Your id for the new job (e.g. the HubSpot deal id), saved as the job's external_ref.
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 '{"work_types": ["sealcoating"], "site_address": "400 Lake Rd, Austin, TX 78701", "timing": "within_30_days"}'

Returns 201 with {"data": {lead}, "job": {job}, "created": true, "job_id": 41901, "job_number": "J-1204", "lead_external_ref": "...", "job_external_ref": "..."}. Retry-safe: calling it again for a lead that already has its job returns that job with 200 and created: false. Nothing is made twice.

Errors: 409 lead_closed (restore it first), job_external_ref_taken or jobs_from_workman; 422 qualification_incomplete with missing (the checklist items still empty); 400 bad values or unknown fields.

With the lead pipeline off, 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. This also reopens unqualified and do_not_contact leads (the contact's do-not-contact flag set by the lead is cleared). 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.

Delete a lead#

A lead is the request as it came in, so it can't be deleted, in the app or through the API. Update its stage, assignee, checklist or details, and change what it created through its own endpoints: 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).