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 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

POST/api/v1/ticketsAuth: Bearer

Creates a ticket and, if the visitor does not exist yet, also creates the visitor record. Requires visitorEmail. Optional department routing via groupId.

FieldTypeDescription
visitorEmailrequiredstringCustomer email. We upsert the visitor by email.
visitorNameoptionalstringCustomer display name.
subjectrequiredstringTicket subject. 1 to 200 chars.
bodyrequiredstringFirst message body (plain text).
bodyHtmloptionalstringFirst message body as sanitized HTML.
groupIdoptionaluuidDepartment (optional). Routes to that queue.
assigneeIdoptionaluuidPre-assign to a specific agent.
tagsoptionalstring[]Initial tags.
sendEmailoptionalbooleanWhen true, sends the body to the visitor as outbound email. Defaults to false (creates a ticket without notifying).
Body
{
  "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
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":"..."}'
JavaScript
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();
Python
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()
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",
  "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
<?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)

POST/api/v1/tickets/:id/replyAuth: Bearer

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

POST/api/v1/tickets/:id/exportAuth: Bearer

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

POST/api/v1/tickets/:id/mergeAuth: Bearer

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

CodeWhen
400 invalid_bodyRequired fields missing or malformed.
403 forbiddenAgent lacks ticket-channel scope.
404 not_foundTicket does not exist in this workspace.
409 conflictIdempotency key reused with a different body.
422 suppression_activeVisitor email is on the suppression list and reply attempted with sendEmail: true.

Common pitfalls

  • Forgetting sendEmail. On creation it defaults to false; 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 body field 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.