Tickets Live
Tickets are conversations with channel = "ticket". Most operations (list, assign, transfer, status change) use the standard Conversations API. This page covers the one ticket-specific extra: creating a ticket from a server-side event. Reply, export, and transcript actions all reuse the Conversations API.
Inbound email arriving at one of your inbound addresses creates a ticket automatically. Use POST /tickets only when the source is a backend event (a Stripe failed-charge, a CRM hand-off, an internal escalation).
Create a ticket
Creates a ticket and, if the visitor does not exist yet, also creates the visitor record. Admin/supervisor always; a plain agent needs canTransfer. Field names differ from the widget-facing visitor concept: this endpoint uses customerEmail, not visitorEmail.
| Field | Type | Description |
|---|---|---|
| customerEmailrequired | string | Customer email. We match-or-create the visitor by email. |
| customerNameoptional | string | Customer display name. Defaults to the email's local part. |
| subjectrequired | string | Ticket subject, 1 to 200 characters. |
| bodyrequired | string | First message body, 1 to 10,000 characters. |
| departmentIdoptional | uuid | Department (optional). Falls back to the workspace default so the ticket is never left un-routed. |
{
"customerEmail": "[email protected]",
"customerName": "Jane Doe",
"subject": "Refund request",
"body": "Hi, I would like a refund for order #4567.",
"departmentId": "uuid"
}Code samples
curl -X POST 'https://app.message.com/api/v1/tickets' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT' \
-H 'Content-Type: application/json' \
-d '{"customerEmail":"[email protected]","subject":"Refund request","body":"..."}'const res = await fetch('https://app.message.com/api/v1/tickets', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
customerEmail: '[email protected]',
subject: 'Refund request',
body: 'Hi, I would like a refund...'
})
});
const ticket = await res.json();import requests
r = requests.post(
"https://app.message.com/api/v1/tickets",
headers={"Authorization": f"Bearer {token}"},
json={
"customerEmail": "[email protected]",
"subject": "Refund request",
"body": "...",
},
)
ticket = r.json()require "net/http"
require "json"
uri = URI("https://app.message.com/api/v1/tickets")
req = Net::HTTP::Post.new(uri, {
"Authorization" => "Bearer #{token}",
"Content-Type" => "application/json",
})
req.body = { customerEmail: "[email protected]", subject: "Refund request", body: "..." }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
ticket = JSON.parse(res.body)<?php
$ctx = stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer $token\r\nContent-Type: application/json",
"content" => json_encode([
"customerEmail" => "[email protected]",
"subject" => "Refund request",
"body" => "..."
]),
],
]);
$ticket = json_decode(file_get_contents("https://app.message.com/api/v1/tickets", false, $ctx), true);Replying to a ticket
There is no /tickets/:id/reply endpoint. A ticket reply is a plain
Send a transcript copy
Different from a reply: this emails a read-only rendered copy of the conversation so far to an arbitrary address, not a new customer-facing message. Works on a conversation in any status, useful for sending a partial transcript mid-thread.
{
"to": "[email protected]",
"subject": "Your conversation with Acme support"
}There is no suppression check documented on this endpoint specifically; see Suppressions for how the outbound-message path (the actual reply, above) handles a suppressed address.
Export a ticket
Not a POST, and not ticket-specific: this is the same conversation-export endpoint every channel uses. JSON only for now (?format=json, the default); requesting any other format returns 501 format_not_implemented. Returns the full record directly in the response body, not a signed URL.
There is no merge
Merging two tickets isn't possible today. There is no merge endpoint and no code path that combines two conversations into one. If a customer emails twice about the same issue, close the duplicate manually and reference the other ticket number in a note.
Errors
| Code | When |
|---|---|
400 invalid_body | Required fields missing or malformed. |
401 Unauthorized | Missing or invalid token. |
403 forbidden | Agent role doesn't allow manual ticket creation (needs admin, supervisor, or agent with canTransfer). |
402 | Workspace plan does not include the tickets feature. |
500 site_bootstrap_failed | First-ever ticket on a workspace with no site yet, and the synthetic site couldn't be created. |
Common pitfalls
- Sending
visitorEmailinstead ofcustomerEmail. The field is named differently on this endpoint than the widget-facing visitor APIs. It will 400. - Looking for a reply, export, or merge action under
/tickets/:id/*. None exist. Reply is a plain message send, export is the conversation-level GET, and merge doesn't exist at all. - Treating tickets as a separate database. They are conversations. Listing, filtering, and routing live on Conversations.