Ir al contenido

Tickets

Esta página aún no está disponible en tu idioma.

Tickets are the core work-order record in BenchKey, each ticket represents one repair or service job. A ticket has a stable id (the order_id string), customer contact info, an optional device, a status that moves through your workflow, and a running payment total.

{
"object": "ticket",
"id": "TK-10042",
"internal_id": "10042",
"status": "Waiting for Parts",
"priority": null,
"device": "iPhone 15 Pro",
"ticket_type": null,
"is_mailin": false,
"location_id": "1",
"assigned_to": "alex@repairshop.com",
"queue_name": null,
"source": "walk-in",
"how_did_you_find_us": null,
"address": {
"line1": null,
"line2": null,
"city": null,
"state": null,
"postal_code": null
},
"customer": {
"object": "ticket_customer",
"name": "Jane Smith",
"email": "jane.smith@example.com",
"phone": "555-867-5309"
},
"custom_fields": {},
"is_business": false,
"business_company": null,
"business_reference": null,
"terms_accepted_at": null,
"amount_paid": {
"amount_cents": 8999,
"amount": "89.99",
"currency": "USD"
},
"lead_id": null,
"category_id": null,
"devices": [
{
"object": "ticket_device",
"id": "7291",
"asset_id": null,
"status": "In Repair",
"issue_reported": "Cracked screen",
"work_performed": "Replaced display assembly"
}
],
"due_at": null,
"closed_at": null,
"created_at": "2025-11-01T10:14:22.000Z",
"updated_at": "2025-11-03T16:45:00.000Z"
}
FieldTypeDescription
idstringCanonical order ID (stable across the ticket’s lifetime)
internal_idstring|nullInternal numeric ID, if your shop uses it
statusstring|nullCurrent workflow status (tenant-configurable)
prioritystring|nullPriority label, or null
devicestring|nullDevice description
ticket_typestring|nullTicket type label
is_mailinbooleantrue for mail-in / depot jobs
location_idstring|nullLocation this ticket belongs to
assigned_tostring|nullTech email assigned to the ticket
queue_namestring|nullName of the queue the ticket is currently in
sourcestring|nullHow the ticket arrived (walk-in, web, etc.)
how_did_you_find_usstring|nullReferral source recorded at intake
addressobjectCustomer mailing address fields (line1, line2, city, state, postal_code)
customerobjectDenormalized customer contact summary
custom_fieldsobjectTenant-defined New Ticket custom fields (key/value pairs; values are strings, numbers, booleans, or null)
is_businessbooleantrue for a B2B intake ticket (mail-in business gate)
business_companystring|nullCompany name recorded on a business intake
business_referencestring|nullCustomer’s own reference / PO number recorded on a business intake
terms_accepted_atstring|nullISO-8601 timestamp the customer accepted the shop’s terms, or null
amount_paidobject|nullSum of all payments recorded against the ticket, both payments taken in BenchKey and imported legacy payments
lead_idstring|nullID of the lead this ticket was converted from, if any
category_idstring|nullCheck-in category ID associated with the ticket, if any
deletedbooleanPresent (true) only on soft-deleted rows returned under include_deleted=true; visible tickets omit the field
deleted_atstring|nullISO-8601 soft-delete timestamp (present only alongside deleted)
hiddenbooleanPresent (true) only on hidden rows returned under include_hidden=true; visible tickets omit the field
devicesarrayAttached devices/assets (present on single-ticket reads)
due_atstring|nullISO-8601 due date
closed_atstring|nullISO-8601 timestamp the ticket was closed
created_atstringISO-8601 creation timestamp
updated_atstringISO-8601 last-updated timestamp

The devices array is included on GET /tickets/:id responses. List responses omit it for performance.

To discover the status strings your workflow accepts, use the Statuses discovery endpoint, it returns the tenant’s live status catalog (and queues).


GET /api/v1/tickets

Returns a cursor-paginated list of tickets, newest first. Soft-deleted and hidden tickets are excluded by default.

Scope required: tickets.read

ParameterTypeDescription
statusstringFilter by exact status value (e.g. "Waiting for Parts")
location_idstringFilter to a specific location
customer_emailstringFilter to one customer’s tickets by email address (case-insensitive). This is the reliable way to list a customer’s tickets, see note below.
customer_idintegerDeprecated. Resolves through an internal table that is never populated, so this filter always returns an empty list. Use customer_email instead. Still validated as a positive integer ≤ Number.MAX_SAFE_INTEGER; values above that return 400 invalid_field.
is_mailinbooleantrue for mail-in only, false for walk-in only. Accepts true/false/1/0/yes/no; any other value returns 400 invalid_filter
qstringSearch by order ID, customer name, email, phone, or device
updated_sincestringISO-8601 or Unix epoch, only tickets updated at or after this time
include_deletedbooleanInclude soft-deleted tickets (default: false). Accepts true/false/1/0/yes/no; any other value returns 400 invalid_filter
include_hiddenbooleanInclude hidden tickets (default: false). Accepts 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

Filtering by customer: Use customer_email to list a customer’s tickets. The customer_id filter is deprecated, it resolves through a table that is never populated and will always return an empty list.

Terminal window
curl "https://app.benchkey.com/api/v1/tickets?status=Waiting%20for%20Parts&limit=10" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const params = new URLSearchParams({ status: "Waiting for Parts", limit: "10" });
const res = await fetch(
`https://app.benchkey.com/api/v1/tickets?${params}`,
{ headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } }
);
const { data, has_more, next_cursor } = await res.json();
{
"object": "list",
"data": [
{
"object": "ticket",
"id": "TK-10042",
"internal_id": "10042",
"status": "Waiting for Parts",
"priority": null,
"device": "iPhone 15 Pro",
"ticket_type": null,
"is_mailin": false,
"location_id": "1",
"assigned_to": "alex@repairshop.com",
"source": "walk-in",
"customer": {
"object": "ticket_customer",
"name": "Jane Smith",
"email": "jane.smith@example.com",
"phone": "555-867-5309"
},
"amount_paid": {
"amount_cents": 8999,
"amount": "89.99",
"currency": "USD"
},
"due_at": null,
"closed_at": null,
"created_at": "2025-11-01T10:14:22.000Z",
"updated_at": "2025-11-03T16:45:00.000Z"
}
],
"has_more": false,
"next_cursor": null
}

See Pagination for how to page through results.


GET /api/v1/tickets/:id

Returns a single ticket, including its attached devices array. The :id can be the canonical order_id, the ticket’s internal_id, or a queue ticket_number.

Scope required: tickets.read

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

Returns the ticket object including the devices array. Returns 404 if no ticket with that ID exists.


POST /api/v1/tickets

Creates a ticket via the same path the app uses, so queue routing, SLA timers, the audit log, and the ticket.created notification all fire. Returns 201 on success.

Scope required: tickets.write

An Idempotency-Key of 8 to 255 characters is required for ticket creation. Reuse it only for an identical retry. See Idempotency.

FieldRequiredDescription
customer_nameyesCustomer’s full name
customer_phoneyes*Phone number (*required if no customer_email)
customer_emailyes*Email address (*required if no customer_phone)
service_nameyesService/issue description (e.g. "Screen replacement")
device_namenoDevice model (e.g. "iPhone 15 Pro")
device_categorynoDevice category
category_idnoInternal category ID
task_typeno1 = mail-in, 2 = walk-in (default)
assigned_tonoTech email to assign the ticket to
queue_namenoName of the queue to route the ticket into
location_idnoLocation ID
imeinoDevice IMEI
serialnoDevice serial number
security_codenoDevice security code / PIN
notesnoAdditional notes
pricenoQuoted price (string, e.g. "149.99")
how_did_you_find_usnoReferral source
address1noCustomer street address line 1
address2noCustomer street address line 2
citynoCity
statenoState / province
zipnoPostal code
lead_idnoID of the lead this ticket was converted from
extra_devicesnoArray of additional device objects to attach to the ticket. Each item must include at least one of: device_name, serial, imei, issue, service_name. Optional fields: make, model, device_type. All values must be strings.
custom_fieldsnoObject of tenant-defined New Ticket custom field values, keyed by field_key. Values must be strings, numbers, booleans, or null. Blank keys and reserved object-prototype keys (__proto__, constructor, prototype) are rejected.
Terminal window
curl -X POST https://app.benchkey.com/api/v1/tickets \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-ticket-$(uuidgen)" \
-d '{
"customer_name": "Jane Smith",
"customer_phone": "555-867-5309",
"customer_email": "jane.smith@example.com",
"service_name": "Screen replacement",
"device_name": "iPhone 15 Pro",
"notes": "Cracked front glass, back intact"
}'
const res = await fetch("https://app.benchkey.com/api/v1/tickets", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
customer_name: "Jane Smith",
customer_phone: "555-867-5309",
customer_email: "jane.smith@example.com",
service_name: "Screen replacement",
device_name: "iPhone 15 Pro",
notes: "Cracked front glass, back intact",
}),
});
const ticket = await res.json(); // HTTP 201

Returns the ticket object with HTTP 201.


PATCH /api/v1/tickets/:id

Updates a ticket’s mutable fields. Send any subset of the fields below; omitted fields are left unchanged. An empty string ("") clears a field.

Side effects fire just as they would for a staff edit: changing assigned_to adds the assignee as a watcher, broadcasts the assignment notification, and reassigns open commission rows; changing customer/device/address keeps the queue in sync and logs the edit.

To change status, use Change status instead, status changes are workflow transitions, not field patches.

Scope required: tickets.write

At least one of the following fields must be included:

FieldDescription
assigned_toTech email (empty string or null to unassign)
customer_nameCustomer’s full name
customer_emailCustomer’s email address
customer_phoneCustomer’s phone number
deviceDevice description
address1Street address line 1
address2Street address line 2
cityCity
stateState / province
zipPostal code
Terminal window
curl -X PATCH "https://app.benchkey.com/api/v1/tickets/TK-10042" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-d '{ "assigned_to": "alex@repairshop.com", "device": "iPhone 15 Pro Max" }'
const res = await fetch("https://app.benchkey.com/api/v1/tickets/TK-10042", {
method: "PATCH",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
},
body: JSON.stringify({ assigned_to: "alex@repairshop.com" }),
});
const ticket = await res.json();

Returns the updated ticket object.


POST /api/v1/tickets/:id/status

Transitions a ticket to a new status. Uses the same path the app uses, so every side effect fires: queue routing (mail-in arrival auto-moves), the status-history/activity log, portal phase sync, and any status-triggered notifications (e.g. the package-received email/SMS when a mail-in arrives).

Statuses are tenant-configurable, send a status string from your shop’s workflow. Discover the valid strings with GET /statuses. A status the tenant’s workflow rejects is returned as a 400.

Scope required: tickets.write

Send an Idempotency-Key header to make retries safe.

FieldRequiredDescription
statusyesThe new status (e.g. "Ready for Pickup")
Terminal window
curl -X POST "https://app.benchkey.com/api/v1/tickets/TK-10042/status" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: status-TK-10042-ready-$(date +%s)" \
-d '{ "status": "Ready for Pickup" }'
const res = await fetch(
"https://app.benchkey.com/api/v1/tickets/TK-10042/status",
{
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ status: "Ready for Pickup" }),
}
);
const ticket = await res.json();

Returns the updated ticket object.


POST /api/v1/tickets/:id/notes

Adds a note to a ticket. Uses the same path the app uses, so @mention notifications, bin-location auto-detection, and the note.added automation event all fire.

Set customer_visible: true to share the note on the customer portal and email it to the customer. Customer-visible notes are rate-limited upstream; if the limit is hit the API returns 429 with a Retry-After header (seconds to wait before retrying).

Scope required: tickets.write

Send an Idempotency-Key header to make retries safe.

FieldRequiredDescription
noteyesThe note text (must be a string; a present non-string value returns 400 invalid_field, absent/empty returns 400 missing_field)
customer_visiblenoBoolean, share on the customer portal and email the customer (default: false). Must be a JSON boolean (true/false); non-boolean values return 400 invalid_field
Terminal window
curl -X POST "https://app.benchkey.com/api/v1/tickets/TK-10042/notes" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: note-TK-10042-$(uuidgen)" \
-d '{
"note": "Screen ordered from supplier. ETA 2 days.",
"customer_visible": false
}'
const res = await fetch(
"https://app.benchkey.com/api/v1/tickets/TK-10042/notes",
{
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
note: "Screen ordered from supplier. ETA 2 days.",
customer_visible: false,
}),
}
);
const note = await res.json(); // HTTP 201

Returns a note object with HTTP 201:

{
"object": "ticket_note",
"id": "4812",
"ticket_id": "TK-10042",
"note": "Screen ordered from supplier. ETA 2 days.",
"author": "alex@repairshop.com",
"customer_visible": false,
"pinned": false,
"created_at": "2025-11-03T16:45:00.000Z"
}

HTTP statusCodeMeaning
400invalid_queryA list filter parameter (including updated_since) was supplied as an array or object instead of a single scalar value
400invalid_filterA boolean filter (is_mailin, include_deleted, include_hidden) was supplied with an unrecognized value. Accepted: true/false/1/0/yes/no
400missing_fieldA required field is absent (customer_name, service_name, status, note)
400missing_contactNeither customer_phone nor customer_email was provided
400invalid_fieldA field value is invalid (e.g. non-string value for a string field like customer_name, malformed email, non-integer customer_id)
400no_updatable_fieldsPATCH body contains no recognized updatable fields
400invalid_requestThe internal route rejected the request (e.g. status not in tenant workflow)
404not_foundNo ticket with that ID exists for this tenant
409conflictThe request conflicts with the ticket’s current state
422create_failedTicket creation could not be completed
429rate_limitedCustomer-visible note rate limit exceeded; includes Retry-After header (seconds)
403insufficient_scopeAPI key lacks tickets.read or tickets.write

See Errors for the full error envelope format.

Estado del sistema