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.
The lead object
Section titled “The lead object”{ "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"}| Field | Type | Description |
|---|---|---|
id | string | Lead id |
status | string|null | Workflow status: new, contacted, quoted, won, lost, etc. Discover the tenant’s live status registry with GET /statuses/leads |
source | string|null | Capture source (e.g. lead_widget, manual) |
channel | string|null | Inbound channel: email, sms, phone, voicemail, widget, or manual |
type | string|null | Lead type (read-only, set by the app) |
priority | string|null | Priority level (read-only) |
urgency_tag | string|null | Urgency label set by staff |
lead_category | string|null | Lead category label |
lead_path | string|null | Lead path/flow identifier, local or mail_in |
category_id | string|null | Repair category id associated with the lead, represented as a string |
customer_name | string|null | Prospect’s name |
customer_email | string|null | Prospect’s email |
customer_phone | string|null | Prospect’s phone |
contact_name | string|null | Secondary contact name |
contact_email | string|null | Secondary contact email |
contact_phone | string|null | Secondary contact phone |
assigned_to | string|null | Staff member assigned to this lead |
ai_summary | string|null | AI-generated summary of the lead |
quoted_price | object|null | Quoted price, { amount_cents, amount, currency } |
source_page_url | string|null | URL the lead was captured on |
source_referrer | string|null | HTTP Referer at capture time |
utm_source | string|null | UTM source |
utm_medium | string|null | UTM medium |
utm_campaign | string|null | UTM campaign |
utm_term | string|null | UTM term |
utm_content | string|null | UTM content |
first_response_at | string|null | ISO-8601 timestamp of the shop’s first response, the response-time SLA signal for reporting tools |
linked_ticket_id | string|null | Active ticket this lead’s inbox is locked to (distinct from converted_ticket_id) |
merged_into_lead_id | string|null | The 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_id | string|null | The original lead this one was marked a duplicate of |
matched_customer_id | string|null | Public customer email id this lead is linked to (dereferenceable via GET /customers/:id) |
matched_ticket_customer_id | string|null | BenchKey’s internal all-tickets order_id customer-link value |
customer | object|null | Summary of the linked customer, { object, id, name, email, phone }, or null if unlinked |
converted_ticket_id | string|null | Ticket id created when the lead was converted |
converted_at | string|null | ISO-8601 timestamp of conversion |
last_activity_at | string|null | ISO-8601 timestamp of last activity |
archived_at | string|null | ISO-8601 timestamp of archival, or null if active |
created_at | string | ISO-8601 creation timestamp |
updated_at | string | ISO-8601 last-updated timestamp |
List leads
Section titled “List leads”GET /api/v1/leadsReturns a cursor-paginated list of leads, newest first. Archived leads are excluded unless include_archived=true.
Scope required: leads.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
status | string | Filter by workflow status (e.g. new, won, lost) |
source | string | Filter by capture source |
channel | string | Filter by inbound channel |
customer_id | string | Filter 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. |
converted | boolean | true = only converted leads; false = only unconverted. Any other value returns 400 invalid_filter. |
q | string | Search by customer/contact name, email, or phone |
updated_since | string | ISO-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_archived | boolean | Include archived leads (default: false). Accepted values: true/false/1/0/yes/no. Any other value returns 400 invalid_filter |
limit | integer | Page size, 1–100 (default: 25) |
cursor | string | Opaque cursor from a previous response’s next_cursor |
curl "https://app.benchkey.com/api/v1/leads?status=new&limit=10" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”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();Response
Section titled “Response”{ "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.
Retrieve a lead
Section titled “Retrieve a lead”GET /api/v1/leads/:idFetches a single lead by id.
Scope required: leads.read
curl "https://app.benchkey.com/api/v1/leads/4821" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/leads/4821", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },});const lead = await res.json();Response
Section titled “Response”Returns the lead object. Returns 404 if no lead with that id exists.
Archived leads return 404 by default.
GET /:idapplies 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.
Create a lead
Section titled “Create a lead”POST /api/v1/leadsCreates 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
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
customer_name | one of three* | Prospect’s name |
customer_email | one of three* | Prospect’s email address |
customer_phone | one of three* | Prospect’s phone number |
channel | no | email, sms, phone, voicemail, widget, or manual (default: manual) |
category_id | no | Repair category id (positive integer; non-integer or non-positive values return 400 invalid_field) |
assigned_to | no | Staff user id to assign the lead to |
lead_category | no | Lead category label |
lead_path | no | Lead path/flow identifier |
notes | no | Internal note recorded on the lead |
* At least one of customer_name, customer_email, or customer_phone must be provided.
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" }'Node.js
Section titled “Node.js”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 201Response
Section titled “Response”Returns the created lead object with HTTP 201.
Update a lead
Section titled “Update a lead”PATCH /api/v1/leads/:idUpdates 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
Request body
Section titled “Request body”At least one field must be provided:
| Field | Description |
|---|---|
status | New workflow status (e.g. contacted, quoted, won, lost) |
assigned_to | Staff user id, or null to unassign |
category_id | Repair category id |
customer_name | Prospect’s name |
customer_email | Prospect’s email |
customer_phone | Prospect’s phone |
contact_name | Secondary contact name |
contact_email | Secondary contact email |
contact_phone | Secondary contact phone |
on_behalf_of | Name of person on whose behalf the contact is calling |
urgency_tag | Urgency label |
quoted_price | Quoted price in dollars (e.g. 149.00), or null to clear |
lead_category | Lead category label |
lead_path | Lead path/flow identifier |
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 }'Node.js
Section titled “Node.js”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();Response
Section titled “Response”Returns the updated lead object.
Archived leads return 404 on PATCH.
PATCH /:idapplies the same visibility rule as the list andGET /:id: an archived lead is treated as non-existent and returns404 not_found. To update an archived lead, first restore it viaPOST /:id/unarchive.
Archive a lead
Section titled “Archive a lead”POST /api/v1/leads/:id/archiveMoves a lead to the archived bucket. Archived leads are excluded from list responses unless include_archived=true.
Scope required: leads.write
curl -X POST "https://app.benchkey.com/api/v1/leads/4821/archive" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”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();Response
Section titled “Response”Returns the updated lead object with archived_at set.
Unarchive a lead
Section titled “Unarchive a lead”POST /api/v1/leads/:id/unarchiveRestores an archived lead to active status.
Scope required: leads.write
curl -X POST "https://app.benchkey.com/api/v1/leads/4821/unarchive" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”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();Response
Section titled “Response”Returns the updated lead object with archived_at set to null.
Convert a lead to a ticket
Section titled “Convert a lead to a ticket”POST /api/v1/leads/:id/convert-to-ticketConverts 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
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
service_name | yes | Description of the work/issue |
customer_name | no | Overrides the lead’s name on the ticket (defaults to lead’s name) |
customer_email | no | Customer email on the ticket |
customer_phone | no | Customer phone on the ticket |
device_name | no | Device description (e.g. “iPhone 15 Pro”) |
category_id | no | Repair category id |
task_type | no | 1 = mail-in, 2 = walk-in (default) |
assigned_to | no | Staff user id |
queue_name | no | Queue to route the ticket to |
price | no | Starting price on the ticket |
notes | no | Internal notes to copy to the ticket |
address1 | no | Street address (for mail-in) |
address2 | no | Address line 2 |
city | no | City |
state | no | State |
zip | no | Postal code |
trigger_kiosk_signature | no | true to trigger the kiosk signature flow after conversion (boolean) |
carry_over_attachments | no | true to copy the lead’s file attachments to the new ticket (boolean) |
send_mail_in_link | no | true to send a mail-in shipping label link to the customer (boolean) |
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" }'Node.js
Section titled “Node.js”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 idResponse
Section titled “Response”Returns the updated lead object. converted_ticket_id is set to the new ticket’s id.
Link a lead to a customer
Section titled “Link a lead to a customer”POST /api/v1/leads/:id/link-customerAttaches 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
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
customer_id | yes | Customer id, email, or phone to link |
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" }'Node.js
Section titled “Node.js”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 populatedResponse
Section titled “Response”Returns the updated lead object with customer populated.
Unlink a lead’s customer
Section titled “Unlink a lead’s customer”DELETE /api/v1/leads/:id/link-customerClears 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
curl -X DELETE "https://app.benchkey.com/api/v1/leads/4821/link-customer" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”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();Response
Section titled “Response”Returns the updated lead object with customer set to null.
Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_id | The :id is not a valid integer |
400 | invalid_query | A list filter parameter was supplied as an array or object instead of a single string value |
400 | missing_contact | POST body has none of customer_name, customer_email, or customer_phone |
400 | missing_field | A required field is absent (e.g. service_name for convert) |
400 | invalid_field | A 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) |
400 | no_updatable_fields | PATCH body contains no recognized fields |
400 | invalid_filter | A 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) |
404 | not_found | No lead with that id exists |
409 | conflict | Lead is already converted (cannot convert twice) |
403 | insufficient_scope | API key lacks leads.read or leads.write |
See Errors for the full error envelope format.