Skip to content

Messages

Messages in BenchKey are organized into conversations, one thread per customer. Each conversation contains one or more messages sent or received via SMS, email, the customer portal, or voice. You can read conversations and their messages, and send new SMS or email messages on any ticket.

{
"object": "conversation",
"id": 4201,
"customer_id": 8812,
"customer_email": "jane.smith@example.com",
"customer_name": "Jane Smith",
"last_inbound_at": "2025-11-04T09:12:00.000Z",
"last_outbound_at": "2025-11-03T16:45:00.000Z",
"unread_count": 1,
"assigned_user_id": null,
"created_at": "2025-10-28T11:00:00.000Z",
"updated_at": "2025-11-04T09:12:00.000Z"
}
FieldTypeDescription
idintegerConversation ID
customer_idinteger|nullLinked customer ID, or null for unlinked threads
customer_emailstring|nullCustomer email at time of last message
customer_namestring|nullCustomer display name
last_inbound_atstring|nullISO-8601 timestamp of the last inbound message
last_outbound_atstring|nullISO-8601 timestamp of the last outbound message
unread_countintegerNumber of unread inbound messages
assigned_user_idinteger|nullUser ID this conversation is assigned to, or null
created_atstringISO-8601 creation timestamp
updated_atstringISO-8601 last-activity timestamp
{
"object": "message",
"id": 91042,
"conversation_id": 4201,
"channel": "sms",
"direction": "outbound",
"body": "Hi Jane! Your phone is ready for pickup.",
"subject": null,
"sender": "alex@repairshop.com",
"sent_from_ticket_id": "TK-10042",
"external_uuid": null,
"in_reply_to": null,
"media_urls": null,
"attachments": null,
"created_at": "2025-11-03T16:45:00.000Z"
}
FieldTypeDescription
idintegerMessage ID
conversation_idintegerParent conversation
channelstringsms, email, portal, or voice
directionstringinbound (customer → shop) or outbound (shop → customer)
bodystring|nullMessage text content
subjectstring|nullEmail subject line (email channel only)
senderstring|nullSender identifier (staff email or customer contact)
sent_from_ticket_idstring|nullTicket ID this message was sent from
external_uuidstring|nullProvider message ID (Twilio SID, etc.)
in_reply_tostring|nullMessage-ID this email replied to
media_urlsarray|nullMMS media URLs, if any
attachmentsarray|nullEmail attachment metadata, if any
created_atstringISO-8601 send/receive timestamp

GET /api/v1/messages/conversations

Returns conversations sorted by most-recently-updated first. GET /api/v1/messages is an alias for this endpoint.

Scope required: messages.read

ParameterTypeDescription
customer_idintegerFilter to a specific customer’s conversations
limitintegerPage size, 1–100 (default: 25)
cursorstringOpaque cursor from a previous response’s next_cursor
Terminal window
curl "https://app.benchkey.com/api/v1/messages/conversations?customer_id=8812&limit=10" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const params = new URLSearchParams({ customer_id: "8812", limit: "10" });
const res = await fetch(
`https://app.benchkey.com/api/v1/messages/conversations?${params}`,
{ headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } }
);
const { data, has_more, next_cursor } = await res.json();
{
"object": "list",
"data": [
{
"object": "conversation",
"id": 4201,
"customer_id": 8812,
"customer_email": "jane.smith@example.com",
"customer_name": "Jane Smith",
"last_inbound_at": "2025-11-04T09:12:00.000Z",
"last_outbound_at": "2025-11-03T16:45:00.000Z",
"unread_count": 1,
"assigned_user_id": null,
"created_at": "2025-10-28T11:00:00.000Z",
"updated_at": "2025-11-04T09:12:00.000Z"
}
],
"has_more": false,
"next_cursor": null
}

See Pagination for how to page through results.


GET /api/v1/messages/conversations/:id

Returns a single conversation by ID.

Scope required: messages.read

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

Returns the conversation object. Returns 404 if not found.


GET /api/v1/messages/conversations/:id/messages

Returns messages in a conversation, newest first.

Scope required: messages.read

ParameterTypeDescription
limitintegerPage size, 1–100 (default: 25)
cursorstringOpaque cursor from a previous response’s next_cursor
Terminal window
curl "https://app.benchkey.com/api/v1/messages/conversations/4201/messages?limit=25" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch(
"https://app.benchkey.com/api/v1/messages/conversations/4201/messages",
{ headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } }
);
const { data, has_more, next_cursor } = await res.json();
{
"object": "list",
"data": [
{
"object": "message",
"id": 91043,
"conversation_id": 4201,
"channel": "sms",
"direction": "inbound",
"body": "Thanks! I'll be there in an hour.",
"subject": null,
"sender": "+15558675309",
"sent_from_ticket_id": null,
"external_uuid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"in_reply_to": null,
"media_urls": null,
"attachments": null,
"created_at": "2025-11-04T09:12:00.000Z"
},
{
"object": "message",
"id": 91042,
"conversation_id": 4201,
"channel": "sms",
"direction": "outbound",
"body": "Hi Jane! Your phone is ready for pickup.",
"subject": null,
"sender": "alex@repairshop.com",
"sent_from_ticket_id": "TK-10042",
"external_uuid": null,
"in_reply_to": null,
"media_urls": null,
"attachments": null,
"created_at": "2025-11-03T16:45:00.000Z"
}
],
"has_more": false,
"next_cursor": null
}

GET /api/v1/messages/:id

Returns a single message by ID.

Scope required: messages.read

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

Returns the message object. Returns 404 if not found.


POST /api/v1/messages/send

Sends an SMS or email on a ticket. Uses the same path the BenchKey app uses, so suppression lists, SMS consent checks, per-tenant rate limits, and the conversation log all apply automatically.

Returns 202 Accepted on success. A 202 means the message was dispatched to the send pipeline, delivery is asynchronous and subject to carrier or email provider acceptance.

Scope required: messages.write

Send an Idempotency-Key header to make retries safe. See Idempotency.

FieldRequiredDescription
ticket_idyesThe ticket to send the message on (e.g. "TK-10042")
channelyes"sms" or "email"
contentyes (SMS)Message text for SMS (alias: body, max 1,600 chars)
toyes (SMS)Recipient phone number (SMS)
bodyyes (email)Email body text (alias: content, max 100,000 chars)
subjectnoEmail subject line (email only; BenchKey derives one from the ticket if omitted)
tono (email)Recipient email address. If supplied, it must exactly match the ticket’s customer email (case-insensitive); mismatches return 400 invalid_field. Omit to use the ticket’s customer email automatically.
Terminal window
curl -X POST https://app.benchkey.com/api/v1/messages/send \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: msg-ready-TK-10042-$(uuidgen)" \
-d '{
"ticket_id": "TK-10042",
"channel": "sms",
"to": "+15558675309",
"content": "Hi Jane! Your phone is ready for pickup."
}'
Terminal window
curl -X POST https://app.benchkey.com/api/v1/messages/send \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: email-ready-TK-10042-$(uuidgen)" \
-d '{
"ticket_id": "TK-10042",
"channel": "email",
"to": "jane.smith@example.com",
"subject": "Your repair is ready",
"body": "Hi Jane,\n\nYour iPhone 15 Pro is repaired and ready for pickup. See you soon!\n\n- Alex"
}'
const res = await fetch("https://app.benchkey.com/api/v1/messages/send", {
method: "POST",
headers: {
Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
ticket_id: "TK-10042",
channel: "sms",
to: "+15558675309",
content: "Hi Jane! Your phone is ready for pickup.",
}),
});
const result = await res.json(); // HTTP 202
{
"object": "message_send_result",
"ok": true,
"ticket_id": "TK-10042",
"channel": "sms"
}

HTTP statusCodeMeaning
400missing_fieldA required field is absent (ticket_id, channel, content, to, body)
400invalid_fieldA field value is invalid or too long; or to (email) does not match the ticket’s customer email
400conflicting_aliasesBoth content and body were provided but their trimmed values differ
400invalid_channelchannel is not "sms" or "email"
400invalid_id:id or customer_id is not a valid positive integer, or exceeds 2,147,483,647
400invalid_queryA list filter parameter was supplied as an array or object instead of a single scalar value
400invalid_cursorcursor is malformed, contains an impossible calendar value (e.g. month 99, hour 25), or was issued by a different endpoint
404not_foundTicket or conversation not found for this tenant
422send_failedMessage was rejected by the send pipeline (suppression, invalid contact, missing consent)
429(rate limit)Per-tenant send rate limit exceeded
403insufficient_scopeAPI key lacks messages.read or messages.write

See Errors for the full error envelope format.

System status