Customers
Este conteúdo não está disponível em sua língua ainda.
Customers in BenchKey are identified by their email address, the email is the customer’s stable id across this resource. A customer record is an aggregate view of all tickets that share a given email, so a customer appears as soon as their first ticket is created.
The customer object
Section titled “The customer object”{ "object": "customer", "id": "jane.smith@example.com", "display_name": "Jane Smith", "email": "jane.smith@example.com", "emails": ["jane.smith@example.com"], "phone": "555-867-5309", "phones": ["555-867-5309", "512-555-0100"], "phone_details": [ { "phone": "555-867-5309", "label": "Mobile", "is_primary": true }, { "phone": "512-555-0100", "label": "Work", "is_primary": false } ], "address": { "line1": "123 Main St", "line2": null, "city": "Austin", "state": "TX", "postal_code": "78701" }, "ticket_count": 4, "is_business": false, "language": "en", "created_at": "2024-03-01T14:22:00.000Z", "updated_at": "2025-11-15T09:10:44.000Z"}| Field | Type | Description |
|---|---|---|
id | string | The customer’s lowercased email address |
display_name | string | The customer’s name as stored on their most recent ticket |
email | string | Lowercased canonical email |
emails | array | Array containing the email (reserved for future multi-email support) |
phone | string|null | The customer’s primary phone (the phone flagged primary when they have several), or null if none |
phones | array | All of the customer’s phone numbers, primary first, deduplicated. Holds one entry for single-phone customers |
phone_details | array | Labeled phone rows, { phone, label, is_primary } per entry. Present only when the customer has saved multiple/labeled phones; single-phone customers omit the field |
address | object | Mailing address fields |
ticket_count | integer | Number of tickets associated with this customer |
is_business | boolean | Whether this customer is flagged as a B2B account |
language | string|null | Preferred communications language (e.g. "en", "es"), or null. Honor this when sending your own comms |
created_at | string | ISO-8601 timestamp of the customer’s first ticket |
updated_at | string | ISO-8601 timestamp of the customer’s most recent activity |
List customers
Section titled “List customers”GET /api/v1/customersReturns a cursor-paginated list of customers ordered by most recent activity first.
Scope required: customers.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
q | string | Search by name, email, or phone |
updated_since | string | ISO-8601 or Unix epoch, only customers with ticket activity at or after this timestamp (full sub-second precision honored) |
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/customers?limit=10&q=smith" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/customers?limit=10&q=smith", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const { data, has_more, next_cursor } = await res.json();Response
Section titled “Response”{ "object": "list", "data": [ { "object": "customer", "id": "jane.smith@example.com", "display_name": "Jane Smith", "email": "jane.smith@example.com", "emails": ["jane.smith@example.com"], "phone": "555-867-5309", "phones": ["555-867-5309"], "address": { "line1": "123 Main St", "line2": null, "city": "Austin", "state": "TX", "postal_code": "78701" }, "ticket_count": 4, "is_business": false, "language": null, "created_at": "2024-03-01T14:22:00.000Z", "updated_at": "2025-11-15T09:10:44.000Z" } ], "has_more": false, "next_cursor": null}See Pagination for how to page through results.
Retrieve a customer
Section titled “Retrieve a customer”GET /api/v1/customers/:idReturns a single customer. The :id is the customer’s lowercased email address. URL-encode the email when embedding it in a path (e.g. jane.smith%40example.com).
Scope required: customers.read
curl "https://app.benchkey.com/api/v1/customers/jane.smith%40example.com" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const email = "jane.smith@example.com";const res = await fetch( `https://app.benchkey.com/api/v1/customers/${encodeURIComponent(email)}`, { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const customer = await res.json();Response
Section titled “Response”Returns the customer object. Returns 404 if no customer with that email exists.
Joining customers to tickets, invoices, and estimates
Section titled “Joining customers to tickets, invoices, and estimates”There is no expand parameter, and you rarely need one. Ticket, invoice, and estimate objects all inline the customer’s identity as a nested customer object (name, email, phone), which covers display and record-matching without extra calls. When you need the full customer record (address, language, marketing status), fetch it once per unique customer:
- Collect unique emails from the list page you just fetched, customers repeat heavily across tickets and invoices, so the fan-out is per customer, not per row.
GET /api/v1/customers/:emailfor each new email and cache it locally. The email is the customer’s stable ID, so the cache key never drifts.- Keep the cache fresh with polling, not re-fetching:
GET /api/v1/customers?updated_since=<last sync>returns only customers whose records changed, and thecustomer.updatedwebhook event pushes changes to you in near-real-time.
This pattern is deliberate: a customer’s full record on this API carries aggregate fields (like all_tickets) with their own visibility rules, so embedding it inside every ticket row would bloat list responses and slow the common case to serve the rare one.
Create a customer
Section titled “Create a customer”POST /api/v1/customersCreates a new customer record. If a customer with the given email already exists, that existing customer is returned with HTTP 200, no duplicate is created. A genuinely new customer returns HTTP 201.
Scope required: customers.write
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
name | yes | Customer’s full name |
email | yes | Email address, the customer’s id across this API |
phone | no | Phone number |
address1 | no | Street address line 1 |
address2 | no | Street address line 2 |
city | no | City |
state | no | State / province |
zip | no | Postal code |
curl -X POST https://app.benchkey.com/api/v1/customers \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "name": "Jane Smith", "email": "jane.smith@example.com", "phone": "555-867-5309", "address1": "123 Main St", "city": "Austin", "state": "TX", "zip": "78701" }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/customers", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Jane Smith", email: "jane.smith@example.com", phone: "555-867-5309", address1: "123 Main St", city: "Austin", state: "TX", zip: "78701", }),});// 201 = created, 200 = existing customer returnedconst customer = await res.json();Response
Section titled “Response”Returns the customer object. HTTP 201 for a new customer, 200 if the email already existed.
Update a customer
Section titled “Update a customer”PATCH /api/v1/customers/:idUpdates one or more fields on an existing customer. Only fields provided in the request body are changed. To rename a customer’s email, pass the new email, all tickets are updated and the customer’s id changes to the new email.
Scope required: customers.write
Request body
Section titled “Request body”At least one of the following fields must be included:
| Field | Description |
|---|---|
name | New full name |
email | New email address (renames the customer’s id) |
phone | New phone number |
address1 | Street address line 1 |
address2 | Street address line 2 |
city | City |
state | State / province |
zip | Postal code |
curl -X PATCH "https://app.benchkey.com/api/v1/customers/jane.smith%40example.com" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "phone": "512-555-0100" }'Node.js
Section titled “Node.js”const email = "jane.smith@example.com";const res = await fetch( `https://app.benchkey.com/api/v1/customers/${encodeURIComponent(email)}`, { method: "PATCH", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ phone: "512-555-0100" }), });const customer = await res.json();Response
Section titled “Response”Returns the updated customer object.
Marketing consent
Section titled “Marketing consent”This endpoint exposes recorded consent and the affirmative-consent verdict for marketing email. Campaign eligibility uses a separate check. Consent is stored as an append-only event log per channel (email, sms), the current state is the latest event, and history is never edited.
Retrieve consent state
Section titled “Retrieve consent state”GET /api/v1/customers/:email/consentScope required: customers.read
curl "https://app.benchkey.com/api/v1/customers/jane.smith%40example.com/consent" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"{ "object": "customer_consent", "customer_id": "jane.smith@example.com", "channels": { "email": { "status": "granted", "source": "signup_form", "occurred_at": "2026-07-01T15:02:11.000Z" }, "sms": null }, "marketing_email": { "sendable": true, "reason": "granted", "consented_at": "2026-07-01T15:02:11.000Z" }}| Field | Description |
|---|---|
channels.email / channels.sms | Latest consent event for the channel (status, source, occurred_at), or null if the customer was never asked |
marketing_email.sendable | Whether this customer passes the affirmative-consent and suppression checks for marketing email; this is not the campaign eligibility verdict |
marketing_email.reason | granted, no_consent, revoked, or suppressed |
For marketing_email, a suppression (bounce, complaint, or unsubscribe) takes precedence over granted consent, and no_consent is not sendable. Campaigns use separate opt-out eligibility rules and may allow no_consent when their other checks pass. Do not treat this API verdict as campaign eligibility or as a determination of your legal obligations.
Record a consent event
Section titled “Record a consent event”POST /api/v1/customers/:email/consentScope required: customers.write
An Idempotency-Key of 8 to 255 characters is required for both email and SMS consent events. Retain the same key when retrying the same evidence event.
| Field | Required | Type | Description |
|---|---|---|---|
status | yes | string | granted or revoked |
channel | no | string | email (default) or sms |
source | no | string | Where the consent was collected, e.g. "signup_form" (max 80 chars; defaults to "api") |
note | no | string | Internal note for the audit trail (max 500 chars; not returned by the GET) |
curl -X POST "https://app.benchkey.com/api/v1/customers/jane.smith%40example.com/consent" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: consent-jane-signup-001" \ -d '{ "status": "granted", "channel": "email", "source": "signup_form" }'Returns 201 with the recorded event:
{ "object": "consent_event", "id": 7, "customer_id": "jane.smith@example.com", "channel": "email", "status": "granted", "source": "signup_form", "occurred_at": "2026-07-10T00:15:42.000Z"}Every event is attributed to the calling API key in the audit log. Recording revoked does not remove history, it appends a new event that becomes the current state. Both routes return 404 for an email that isn’t a customer on this tenant.
Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_query | A list filter parameter was supplied as an array or object instead of a single scalar value |
400 | invalid_filter | updated_since is not a valid ISO-8601 timestamp or Unix epoch (seconds or milliseconds) |
400 | invalid_id | The :id is not a valid email address |
400 | nothing_to_update | PATCH body contains no recognized fields |
400 | invalid_field | A field value is invalid, malformed email, or name is the reserved value "Walk In" (a system placeholder that cannot be used as a real customer name) |
400 | missing_field | A required field (name or email) is absent |
404 | not_found | No customer with that email exists |
403 | insufficient_scope | API key lacks customers.read or customers.write |
See Errors for the full error envelope format.