Pular para o conteúdo

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.

{
"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"
}
FieldTypeDescription
idstringThe customer’s lowercased email address
display_namestringThe customer’s name as stored on their most recent ticket
emailstringLowercased canonical email
emailsarrayArray containing the email (reserved for future multi-email support)
phonestring|nullThe customer’s primary phone (the phone flagged primary when they have several), or null if none
phonesarrayAll of the customer’s phone numbers, primary first, deduplicated. Holds one entry for single-phone customers
phone_detailsarrayLabeled phone rows, { phone, label, is_primary } per entry. Present only when the customer has saved multiple/labeled phones; single-phone customers omit the field
addressobjectMailing address fields
ticket_countintegerNumber of tickets associated with this customer
is_businessbooleanWhether this customer is flagged as a B2B account
languagestring|nullPreferred communications language (e.g. "en", "es"), or null. Honor this when sending your own comms
created_atstringISO-8601 timestamp of the customer’s first ticket
updated_atstringISO-8601 timestamp of the customer’s most recent activity

GET /api/v1/customers

Returns a cursor-paginated list of customers ordered by most recent activity first.

Scope required: customers.read

ParameterTypeDescription
qstringSearch by name, email, or phone
updated_sincestringISO-8601 or Unix epoch, only customers with ticket activity at or after this timestamp (full sub-second precision honored)
limitintegerPage size, 1–100 (default: 25)
cursorstringOpaque cursor from a previous response’s next_cursor
Terminal window
curl "https://app.benchkey.com/api/v1/customers?limit=10&q=smith" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
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();
{
"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.


GET /api/v1/customers/:id

Returns 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

Terminal window
curl "https://app.benchkey.com/api/v1/customers/jane.smith%40example.com" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
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();

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:

  1. 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.
  2. GET /api/v1/customers/:email for each new email and cache it locally. The email is the customer’s stable ID, so the cache key never drifts.
  3. Keep the cache fresh with polling, not re-fetching: GET /api/v1/customers?updated_since=<last sync> returns only customers whose records changed, and the customer.updated webhook 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.


POST /api/v1/customers

Creates 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

FieldRequiredDescription
nameyesCustomer’s full name
emailyesEmail address, the customer’s id across this API
phonenoPhone number
address1noStreet address line 1
address2noStreet address line 2
citynoCity
statenoState / province
zipnoPostal code
Terminal window
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"
}'
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 returned
const customer = await res.json();

Returns the customer object. HTTP 201 for a new customer, 200 if the email already existed.


PATCH /api/v1/customers/:id

Updates 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

At least one of the following fields must be included:

FieldDescription
nameNew full name
emailNew email address (renames the customer’s id)
phoneNew phone number
address1Street address line 1
address2Street address line 2
cityCity
stateState / province
zipPostal code
Terminal window
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" }'
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();

Returns the updated customer object.


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.

GET /api/v1/customers/:email/consent

Scope required: customers.read

Terminal window
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"
}
}
FieldDescription
channels.email / channels.smsLatest consent event for the channel (status, source, occurred_at), or null if the customer was never asked
marketing_email.sendableWhether this customer passes the affirmative-consent and suppression checks for marketing email; this is not the campaign eligibility verdict
marketing_email.reasongranted, 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.

POST /api/v1/customers/:email/consent

Scope 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.

FieldRequiredTypeDescription
statusyesstringgranted or revoked
channelnostringemail (default) or sms
sourcenostringWhere the consent was collected, e.g. "signup_form" (max 80 chars; defaults to "api")
notenostringInternal note for the audit trail (max 500 chars; not returned by the GET)
Terminal window
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.


HTTP statusCodeMeaning
400invalid_queryA list filter parameter was supplied as an array or object instead of a single scalar value
400invalid_filterupdated_since is not a valid ISO-8601 timestamp or Unix epoch (seconds or milliseconds)
400invalid_idThe :id is not a valid email address
400nothing_to_updatePATCH body contains no recognized fields
400invalid_fieldA 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)
400missing_fieldA required field (name or email) is absent
404not_foundNo customer with that email exists
403insufficient_scopeAPI key lacks customers.read or customers.write

See Errors for the full error envelope format.

Status do sistema