Tickets Live
Tickets are conversations with channel = "ticket". Most operations (list, assign, transfer, status change) use the standard Conversations API. This page covers the ticket-specific extras: create from a server-side event, send a reply that goes out as email, and export.
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. Requires visitorEmail. Optional department routing via groupId.
| Field | Type | Description |
|---|---|---|
| visitorEmailrequired | string | Customer email. We upsert the visitor by email. |
| visitorNameoptional | string | Customer display name. |
| subjectrequired | string | Ticket subject. 1 to 200 chars. |
| bodyrequired | string | First message body (plain text). |
| bodyHtmloptional | string | First message body as sanitized HTML. |
| groupIdoptional | uuid | Department (optional). Routes to that queue. |
| assigneeIdoptional | uuid | Pre-assign to a specific agent. |
| tagsoptional | string[] | Initial tags. |
| sendEmailoptional | boolean | When true, sends the body to the visitor as outbound email. Defaults to false (creates a ticket without notifying). |
{
"visitorEmail": "[email protected]",
"visitorName": "Jane Doe",
"subject": "Refund request",
"body": "Hi, I would like a refund for order #4567.",
"groupId": "uuid",
"tags": ["billing"]
}Code samples
curl -X POST 'https://app.message.com/api/v1/tickets' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: ticket-from-stripe-cs_123' \
-d '{"visitorEmail":"[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',
'Idempotency-Key': 'ticket-from-stripe-cs_123'
},
body: JSON.stringify({
visitorEmail: '[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}",
"Idempotency-Key": "ticket-from-stripe-cs_123",
},
json={
"visitorEmail": "[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",
"Idempotency-Key" => "ticket-from-stripe-cs_123",
})
req.body = { visitorEmail: "[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\r\nIdempotency-Key: ticket-from-stripe-cs_123",
"content" => json_encode([
"visitorEmail" => "[email protected]",
"subject" => "Refund request",
"body" => "..."
]),
],
]);
$ticket = json_decode(file_get_contents("https://app.message.com/api/v1/tickets", false, $ctx), true);Reply to a ticket (email out)
Sends a reply that goes out as email to the ticket visitor via our outbound ESP (Resend). Same shape as conversations/:id/messages, but the message is delivered via SMTP rather than displayed in a widget.
{
"body": "Hi Jane, we have issued a refund. Expect 3-5 business days.",
"bodyHtml": "<p>Hi Jane, we have issued a refund. Expect 3-5 business days.</p>",
"attachments": []
}If the visitor email is on the suppression list, the reply is recorded but not delivered. The message deliveryStatus field reflects this.
Export ticket
Generates a CSV (or JSON) of the full conversation timeline including messages, notes, status changes, and assignment history. Returns a signed URL valid for 15 minutes.
Merge tickets
Merges another ticket into this one. Both ticket message timelines are interleaved by timestamp; the secondary ticket becomes a system note pointing to the primary. Useful when one customer emails twice about the same issue.
Errors
| Code | When |
|---|---|
400 invalid_body | Required fields missing or malformed. |
403 forbidden | Agent lacks ticket-channel scope. |
404 not_found | Ticket does not exist in this workspace. |
409 conflict | Idempotency key reused with a different body. |
422 suppression_active | Visitor email is on the suppression list and reply attempted with sendEmail: true. |
Common pitfalls
- Forgetting
sendEmail. On creation it defaults tofalse; you have to opt in to send the body as outbound email. This prevents accidental notifications when seeding tickets from CRM events. - Sending HTML without a plain-text fallback. The
bodyfield is always required. Mail clients render the plain-text version when HTML is blocked. - Treating tickets as a separate database. They are conversations. Listing, filtering, and routing live on Conversations.