Skip to content

Leads

Leads are inbound prospects captured before they become customers, from the BenchKey lead widget, live chat, manual entry, or an import. A lead carries the contact information the prospect supplied, a workflow status (e.g. new, won, lost), a source, optional UTM attribution, and, once worked, links to the converted ticket and/or a matched customer record.

Archived leads are hidden from list responses by default. Pass include_archived=true to include them.

{
"object": "lead",
"id": "4821",
"status": "new",
"source": "lead_widget",
"channel": "widget",
"type": null,
"priority": null,
"urgency_tag": null,
"lead_category": null,
"lead_path": null,
"category_id": null,
"customer_name": "Alex Rivera",
"customer_email": "alex.rivera@example.com",
"customer_phone": "512-555-0142",
"contact_name": null,
"contact_email": null,
"contact_phone": null,
"assigned_to": null,
"ai_summary": "Customer has a cracked screen on an iPhone 15 Pro.",
"quoted_price": {
"amount_cents": 14900,
"amount": "149.00",
"currency": "usd"
},
"source_page_url": "https://yourshop.com/repair",
"source_referrer": null,
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "iphone-repair",
"utm_term": null,
"utm_content": null,
"first_response_at": null,
"linked_ticket_id": null,
"merged_into_lead_id": null,
"duplicate_of_lead_id": null,
"matched_customer_id": null,
"matched_ticket_customer_id": null,
"customer": null,
"converted_ticket_id": null,
"converted_at": null,
"last_activity_at": "2025-12-01T18:44:00.000Z",
"archived_at": null,
"created_at": "2025-12-01T18:44:00.000Z",
"updated_at": "2025-12-01T18:44:00.000Z"
}
FieldTypeDescription
idstringLead id
statusstring|nullWorkflow status: new, contacted, quoted, won, lost, etc. Discover the tenant’s live status registry with GET /statuses/leads
sourcestring|nullCapture source (e.g. lead_widget, manual)
channelstring|nullInbound channel: email, sms, phone, voicemail, widget, or manual
typestring|nullLead type (read-only, set by the app)
prioritystring|nullPriority level (read-only)
urgency_tagstring|nullUrgency label set by staff
lead_categorystring|nullLead category label
lead_pathstring|nullLead path/flow identifier, local or mail_in
category_idstring|nullRepair category id associated with the lead, represented as a string
customer_namestring|nullProspect’s name
customer_emailstring|nullProspect’s email
customer_phonestring|nullProspect’s phone
contact_namestring|nullSecondary contact name
contact_emailstring|nullSecondary contact email
contact_phonestring|nullSecondary contact phone
assigned_tostring|nullStaff member assigned to this lead
ai_summarystring|nullAI-generated summary of the lead
quoted_priceobject|nullQuoted price, { amount_cents, amount, currency }
source_page_urlstring|nullURL the lead was captured on
source_referrerstring|nullHTTP Referer at capture time
utm_sourcestring|nullUTM source
utm_mediumstring|nullUTM medium
utm_campaignstring|nullUTM campaign
utm_termstring|nullUTM term
utm_contentstring|nullUTM content
first_response_atstring|nullISO-8601 timestamp of the shop’s first response, the response-time SLA signal for reporting tools
linked_ticket_idstring|nullActive ticket this lead’s inbox is locked to (distinct from converted_ticket_id)
merged_into_lead_idstring|nullThe surviving lead this one was merged into. A merged lead reads status dead, use this pointer to reconcile instead of double-counting
duplicate_of_lead_idstring|nullThe original lead this one was marked a duplicate of
matched_customer_idstring|nullPublic customer email id this lead is linked to (dereferenceable via GET /customers/:id)
matched_ticket_customer_idstring|nullBenchKey’s internal all-tickets order_id customer-link value
customerobject|nullSummary of the linked customer, { object, id, name, email, phone }, or null if unlinked
converted_ticket_idstring|nullTicket id created when the lead was converted
converted_atstring|nullISO-8601 timestamp of conversion
last_activity_atstring|nullISO-8601 timestamp of last activity
archived_atstring|nullISO-8601 timestamp of archival, or null if active
created_atstringISO-8601 creation timestamp
updated_atstringISO-8601 last-updated timestamp

GET /api/v1/leads

Returns a cursor-paginated list of leads, newest first. Archived leads are excluded unless include_archived=true.

Scope required: leads.read

ParameterTypeDescription
statusstringFilter by workflow status (e.g. new, won, lost)
sourcestringFilter by capture source
channelstringFilter by inbound channel
customer_idstringFilter to leads matched to a specific customer. Accepts the customer’s email address (the public customer id) or a positive integer order-id. Blank or whitespace-only values return 400 invalid_field.
convertedbooleantrue = only converted leads; false = only unconverted. Any other value returns 400 invalid_filter.
qstringSearch by customer/contact name, email, or phone
updated_sincestringISO-8601 or Unix epoch, only leads updated at or after this time. Arrays or objects return 400 invalid_query; non-parseable strings return 400 invalid_filter
include_archivedbooleanInclude archived leads (default: false). Accepted values: true/false/1/0/yes/no. Any other value returns 400 invalid_filter
limitintegerPage size, 1–100 (default: 25)
cursorstringOpaque cursor from a previous response’s next_cursor
Terminal window
curl "https://app.benchkey.com/api/v1/leads?status=new&limit=10" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch(
"https://app.benchkey.com/api/v1/leads?status=new&limit=10",
{ headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } }
);
const { data, has_more, next_cursor } = await res.json();
{
"object": "list",
"data": [
{
"object": "lead",
"id": "4821",
"status": "new",
"source": "lead_widget",
"channel": "widget",
"customer_name": "Alex Rivera",
"customer_email": "alex.rivera@example.com",
"customer_phone": "512-555-0142",
"quoted_price": { "amount_cents": 14900, "amount": "149.00", "currency": "usd" },
"matched_customer_id": null,
"customer": null,
"converted_ticket_id": null,
"converted_at": null,
"archived_at": null,
"created_at": "2025-12-01T18:44:00.000Z",
"updated_at": "2025-12-01T18:44:00.000Z"
}
],
"has_more": false,
"next_cursor": null
}

See Pagination for how to page through results.


GET /api/v1/leads/:id

Fetches a single lead by id.

Scope required: leads.read

Terminal window
curl "https://app.benchkey.com/api/v1/leads/4821" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch("https://app.benchkey.com/api/v1/leads/4821", {
headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },
});
const lead = await res.json();

Returns the lead object. Returns 404 if no lead with that id exists.

Archived leads return 404 by default. GET /:id applies the same visibility filter as the list: archived leads are hidden unless you pass ?include_archived=true. This keeps the single-record path consistent with what the list exposes.


POST /api/v1/leads

Creates an inbound lead. At least one of customer_name, customer_email, or customer_phone is required. Fires the same follow-up-sequence enrollment, auto-assignment, and customer auto-match the app performs for a staff-created lead.

Scope required: leads.write

FieldRequiredDescription
customer_nameone of three*Prospect’s name
customer_emailone of three*Prospect’s email address
customer_phoneone of three*Prospect’s phone number
channelnoemail, sms, phone, voicemail, widget, or manual (default: manual)
category_idnoRepair category id (positive integer; non-integer or non-positive values return 400 invalid_field)
assigned_tonoStaff user id to assign the lead to
lead_categorynoLead category label
lead_pathnoLead path/flow identifier
notesnoInternal note recorded on the lead

* At least one of customer_name, customer_email, or customer_phone must be provided.

Terminal window
curl -X POST https://app.benchkey.com/api/v1/leads \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-d '{
"customer_name": "Alex Rivera",
"customer_email": "alex.rivera@example.com",
"customer_phone": "512-555-0142",
"channel": "widget",
"notes": "Cracked screen, iPhone 15 Pro"
}'
const res = await fetch("https://app.benchkey.com/api/v1/leads", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
},
body: JSON.stringify({
customer_name: "Alex Rivera",
customer_email: "alex.rivera@example.com",
customer_phone: "512-555-0142",
channel: "widget",
notes: "Cracked screen, iPhone 15 Pro",
}),
});
const lead = await res.json(); // HTTP 201

Returns the created lead object with HTTP 201.


PATCH /api/v1/leads/:id

Updates one or more mutable fields on a lead. A status change drives the follow-up-sequence lifecycle (enrollment or cancellation) just as a staff edit would. Only fields included in the request body are changed.

Scope required: leads.write

At least one field must be provided:

FieldDescription
statusNew workflow status (e.g. contacted, quoted, won, lost)
assigned_toStaff user id, or null to unassign
category_idRepair category id
customer_nameProspect’s name
customer_emailProspect’s email
customer_phoneProspect’s phone
contact_nameSecondary contact name
contact_emailSecondary contact email
contact_phoneSecondary contact phone
on_behalf_ofName of person on whose behalf the contact is calling
urgency_tagUrgency label
quoted_priceQuoted price in dollars (e.g. 149.00), or null to clear
lead_categoryLead category label
lead_pathLead path/flow identifier
Terminal window
curl -X PATCH "https://app.benchkey.com/api/v1/leads/4821" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-d '{ "status": "quoted", "quoted_price": 149.00 }'
const res = await fetch("https://app.benchkey.com/api/v1/leads/4821", {
method: "PATCH",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "quoted", quoted_price: 149.00 }),
});
const lead = await res.json();

Returns the updated lead object.

Archived leads return 404 on PATCH. PATCH /:id applies the same visibility rule as the list and GET /:id: an archived lead is treated as non-existent and returns 404 not_found. To update an archived lead, first restore it via POST /:id/unarchive.


POST /api/v1/leads/:id/archive

Moves a lead to the archived bucket. Archived leads are excluded from list responses unless include_archived=true.

Scope required: leads.write

Terminal window
curl -X POST "https://app.benchkey.com/api/v1/leads/4821/archive" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch("https://app.benchkey.com/api/v1/leads/4821/archive", {
method: "POST",
headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },
});
const lead = await res.json();

Returns the updated lead object with archived_at set.


POST /api/v1/leads/:id/unarchive

Restores an archived lead to active status.

Scope required: leads.write

Terminal window
curl -X POST "https://app.benchkey.com/api/v1/leads/4821/unarchive" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch("https://app.benchkey.com/api/v1/leads/4821/unarchive", {
method: "POST",
headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },
});
const lead = await res.json();

Returns the updated lead object with archived_at set to null.


POST /api/v1/leads/:id/convert-to-ticket

Converts an inbound lead into a work ticket. Fires the full conversion workflow, order numbering, audit log, queue routing, and any kiosk/mail-in side effects. The lead is atomically claimed so a repeated call cannot create a second ticket.

Returns the updated lead, whose converted_ticket_id is the newly created ticket’s id.

Scope required: leads.write

FieldRequiredDescription
service_nameyesDescription of the work/issue
customer_namenoOverrides the lead’s name on the ticket (defaults to lead’s name)
customer_emailnoCustomer email on the ticket
customer_phonenoCustomer phone on the ticket
device_namenoDevice description (e.g. “iPhone 15 Pro”)
category_idnoRepair category id
task_typeno1 = mail-in, 2 = walk-in (default)
assigned_tonoStaff user id
queue_namenoQueue to route the ticket to
pricenoStarting price on the ticket
notesnoInternal notes to copy to the ticket
address1noStreet address (for mail-in)
address2noAddress line 2
citynoCity
statenoState
zipnoPostal code
trigger_kiosk_signaturenotrue to trigger the kiosk signature flow after conversion (boolean)
carry_over_attachmentsnotrue to copy the lead’s file attachments to the new ticket (boolean)
send_mail_in_linknotrue to send a mail-in shipping label link to the customer (boolean)
Terminal window
curl -X POST "https://app.benchkey.com/api/v1/leads/4821/convert-to-ticket" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-d '{
"service_name": "Cracked screen replacement",
"device_name": "iPhone 15 Pro",
"price": "149.00"
}'
const res = await fetch("https://app.benchkey.com/api/v1/leads/4821/convert-to-ticket", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
},
body: JSON.stringify({
service_name: "Cracked screen replacement",
device_name: "iPhone 15 Pro",
price: "149.00",
}),
});
const lead = await res.json();
// lead.converted_ticket_id is the new ticket id

Returns the updated lead object. converted_ticket_id is set to the new ticket’s id.


POST /api/v1/leads/:id/link-customer

Attaches the lead to an existing customer record. The internal matcher resolves customer_id by numeric id, email address, or phone number. Returns the updated lead with customer populated.

Scope required: leads.write

FieldRequiredDescription
customer_idyesCustomer id, email, or phone to link
Terminal window
curl -X POST "https://app.benchkey.com/api/v1/leads/4821/link-customer" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-d '{ "customer_id": "alex.rivera@example.com" }'
const res = await fetch("https://app.benchkey.com/api/v1/leads/4821/link-customer", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
},
body: JSON.stringify({ customer_id: "alex.rivera@example.com" }),
});
const lead = await res.json();
// lead.customer is now populated

Returns the updated lead object with customer populated.


DELETE /api/v1/leads/:id/link-customer

Clears the lead’s customer match, useful to undo a wrong link or treat the lead as brand-new. Returns the updated lead with customer and matched_customer_id cleared.

Scope required: leads.write

Terminal window
curl -X DELETE "https://app.benchkey.com/api/v1/leads/4821/link-customer" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch("https://app.benchkey.com/api/v1/leads/4821/link-customer", {
method: "DELETE",
headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },
});
const lead = await res.json();

Returns the updated lead object with customer set to null.


HTTP statusCodeMeaning
400invalid_idThe :id is not a valid integer
400invalid_queryA list filter parameter was supplied as an array or object instead of a single string value
400missing_contactPOST body has none of customer_name, customer_email, or customer_phone
400missing_fieldA required field is absent (e.g. service_name for convert)
400invalid_fieldA field value is invalid (e.g. malformed email, negative quoted_price, non-integer customer_id filter, or non-positive-integer category_id on create/update/convert)
400no_updatable_fieldsPATCH body contains no recognized fields
400invalid_filterA filter value is invalid: updated_since is not a valid timestamp, converted is not a recognized boolean, or include_archived is not a recognized boolean (true/false/1/0/yes/no)
404not_foundNo lead with that id exists
409conflictLead is already converted (cannot convert twice)
403insufficient_scopeAPI key lacks leads.read or leads.write

See Errors for the full error envelope format.

System status