message.comDevelopers

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

POST/api/v1/ticketsAuth: Bearer

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.

FieldTypeDescription
customerEmailrequiredstringCustomer email. We match-or-create the visitor by email.
customerNameoptionalstringCustomer display name. Defaults to the email's local part.
subjectrequiredstringTicket subject, 1 to 200 characters.
bodyrequiredstringFirst message body, 1 to 10,000 characters.
departmentIdoptionaluuidDepartment (optional). Falls back to the workspace default so the ticket is never left un-routed.
Body
{
  "customerEmail": "[email protected]",
  "customerName": "Jane Doe",
  "subject": "Refund request",
  "body": "Hi, I would like a refund for order #4567.",
  "departmentId": "uuid"
}

Code samples

cURL
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":"..."}'
JavaScript
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();
Python
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()
Ruby
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
<?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

POST/api/v1/conversations/:id/messagesAuth: Bearer
on the ticket-channel conversation, same as chat: see Messages for the body shape. Sending into a ticket conversation is what triggers the outbound email.

Send a transcript copy

POST/api/v1/conversations/:id/email-transcriptAuth: Bearer

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.

Body
{
  "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

GET/api/v1/conversations/:id/exportAuth: Bearer

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

CodeWhen
400 invalid_bodyRequired fields missing or malformed.
401 UnauthorizedMissing or invalid token.
403 forbiddenAgent role doesn't allow manual ticket creation (needs admin, supervisor, or agent with canTransfer).
402Workspace plan does not include the tickets feature.
500 site_bootstrap_failedFirst-ever ticket on a workspace with no site yet, and the synthetic site couldn't be created.

Common pitfalls

  • Sending visitorEmail instead of customerEmail. 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.