Back to Docs

API Reference

Build your own UI with the full REST API. Complete control over the experience.

Interactive API Documentation

Try API endpoints directly in your browser with our Swagger UI. Test requests, see responses, and explore the full API.

Open Swagger UI →

Overview

The Dispatch Tickets API is a RESTful JSON API. All requests are made to:

https://api.dispatchtickets.com/v1

Use the API when you want:

  • Complete control over the UI/UX
  • To integrate ticketing into an existing app
  • Custom workflows that don't fit the portal model
  • Backend-only integrations (no customer-facing UI)

RESTful

Predictable resource-oriented URLs

JSON

All requests and responses in JSON

API Key Auth

Bearer token authentication

TypeScript SDK

Type-safe client available

Authentication

All API requests require authentication via Bearer token:

Authorization header
Authorization: Bearer sk_live_your_api_key

Get your API key from the dashboard under Settings → API Keys.

Keep your API key secret. Never expose it in client-side code. All API calls should be made from your backend.

SDK Authentication

Initialize SDK
import { DispatchTickets } from '@dispatchtickets/sdk';

const dispatch = new DispatchTickets({
  apiKey: process.env.DISPATCH_API_KEY!
});

cURL Authentication

cURL example
curl https://api.dispatchtickets.com/v1/brands \
  -H "Authorization: Bearer sk_live_your_api_key"

Brands

Brands (also called workspaces) are isolated containers for tickets. Each brand has its own customers, settings, and email identity.

List Brands

GET /v1/brands
const { data: brands } = await dispatch.brands.list();

// Response
[
  {
    "id": "br_abc123",
    "name": "Acme Support",
    "slug": "acme-support",
    "createdAt": "2024-01-01T00:00:00Z"
  }
]

Get Brand

GET /v1/brands/:id
const brand = await dispatch.brands.get('br_abc123');

Create Brand

POST /v1/brands
const brand = await dispatch.brands.create({
  name: 'New Brand',
  slug: 'new-brand' // Optional, auto-generated if omitted
});

Tickets

List Tickets

GET /v1/brands/:brandId/tickets
const { data: tickets, pagination } = await dispatch.tickets.list('br_abc123', {
  status: 'open',
  limit: 20,
  cursor: 'cursor_xyz' // For pagination
});

// Response
{
  "data": [
    {
      "id": "tkt_xyz789",
      "ticketNumber": "TKT-1042",
      "title": "Need help with my order",
      "status": "open",
      "priority": "normal",
      "customer": {
        "id": "cus_def456",
        "email": "[email protected]",
        "name": "Jane Doe"
      },
      "customFields": {},
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  ],
  "pagination": {
    "hasMore": true,
    "nextCursor": "cursor_abc"
  }
}

Get Ticket

GET /v1/brands/:brandId/tickets/:id
const ticket = await dispatch.tickets.get('br_abc123', 'tkt_xyz789');

Create Ticket

POST /v1/brands/:brandId/tickets
const ticket = await dispatch.tickets.create('br_abc123', {
  title: 'Need help with my order',
  body: 'My order #12345 has not arrived yet.',
  customerEmail: '[email protected]',
  customerName: 'Jane Doe', // Optional
  priority: 'normal', // 'low' | 'normal' | 'high' | 'urgent'
  customFields: {
    orderId: '12345',
    orderTotal: 99.99
  }
});

Update Ticket

PATCH /v1/brands/:brandId/tickets/:id
const ticket = await dispatch.tickets.update('br_abc123', 'tkt_xyz789', {
  status: 'pending',
  priority: 'high',
  assigneeId: 'usr_abc123',
  customFields: {
    resolution: 'Shipped replacement'
  }
});

Close Ticket

POST /v1/brands/:brandId/tickets/:id/close
await dispatch.tickets.close('br_abc123', 'tkt_xyz789', {
  resolution: 'Resolved - replacement shipped'
});

Comments

List Comments

GET /v1/brands/:brandId/tickets/:ticketId/comments
const { data: comments } = await dispatch.tickets.listComments(
  'br_abc123',
  'tkt_xyz789'
);

// Response
[
  {
    "id": "cmt_abc123",
    "body": "Thanks for reaching out! Let me look into this.",
    "authorType": "STAFF",
    "authorName": "Support Team",
    "authorId": "usr_xyz789",
    "createdAt": "2024-01-15T11:00:00Z"
  }
]

Add Comment

POST /v1/brands/:brandId/tickets/:ticketId/comments
const comment = await dispatch.tickets.addComment('br_abc123', 'tkt_xyz789', {
  body: 'Your order has shipped! Tracking: 1Z999...',
  authorType: 'STAFF', // 'STAFF' | 'CUSTOMER' | 'SYSTEM'
  authorName: 'Support Team',
  authorId: 'usr_xyz789', // Optional
  internal: false // Set true for internal notes
});

Author Types

TypeUse ForSends Email
STAFFAgent repliesYes (to customer)
CUSTOMERCustomer repliesYes (to agents)
SYSTEMAutomated messagesNo

Customers

List Customers

GET /v1/brands/:brandId/customers
const { data: customers } = await dispatch.customers.list('br_abc123', {
  search: '[email protected]'
});

Get Customer

GET /v1/brands/:brandId/customers/:id
const customer = await dispatch.customers.get('br_abc123', 'cus_def456');

// Response
{
  "id": "cus_def456",
  "email": "[email protected]",
  "name": "Jane Doe",
  "ticketCount": 5,
  "createdAt": "2024-01-01T00:00:00Z"
}

Get Customer Tickets

GET /v1/brands/:brandId/customers/:id/tickets
const { data: tickets } = await dispatch.customers.listTickets(
  'br_abc123',
  'cus_def456'
);

Attachments

Attachments use presigned URLs. Upload directly to cloud storage without going through our API.

Get Upload URL

Request upload URL
const { uploadUrl, attachmentId } = await dispatch.attachments.getUploadUrl(
  'br_abc123',
  'tkt_xyz789',
  {
    filename: 'screenshot.png',
    contentType: 'image/png',
    size: 102400 // bytes
  }
);

// Upload file directly to the presigned URL
await fetch(uploadUrl, {
  method: 'PUT',
  body: fileBuffer,
  headers: { 'Content-Type': 'image/png' }
});

// Confirm upload
await dispatch.attachments.confirmUpload('br_abc123', 'tkt_xyz789', attachmentId);

Get Download URL

Get download URL
const { downloadUrl } = await dispatch.attachments.getDownloadUrl(
  'br_abc123',
  'tkt_xyz789',
  'att_abc123'
);

// URL is valid for 15 minutes

Pagination & Filtering

List endpoints support cursor-based pagination and filtering:

Pagination and filtering
const { data, pagination } = await dispatch.tickets.list('br_abc123', {
  // Pagination
  limit: 20,           // 1-100, default 20
  cursor: 'cursor_xyz', // From previous response

  // Filtering
  status: 'open',       // 'open' | 'pending' | 'closed'
  priority: 'high',     // 'low' | 'normal' | 'high' | 'urgent'
  assigneeId: 'usr_abc123',
  customerId: 'cus_def456',

  // Search
  search: 'order #12345',

  // Date range
  createdAfter: '2024-01-01T00:00:00Z',
  createdBefore: '2024-01-31T23:59:59Z',

  // Sorting
  sort: 'createdAt',    // Field to sort by
  order: 'desc'         // 'asc' | 'desc'
});

// Paginate through all results
let allTickets = [];
let cursor = undefined;

do {
  const { data, pagination } = await dispatch.tickets.list('br_abc123', {
    cursor,
    limit: 100
  });

  allTickets.push(...data);
  cursor = pagination.nextCursor;
} while (cursor);

Error Handling

The API returns standard HTTP status codes. Error responses include a code and message:

Error response format
{
  "error": {
    "code": "TICKET_NOT_FOUND",
    "message": "Ticket with ID tkt_xyz789 not found",
    "details": {} // Additional context if available
  }
}

Status Codes

CodeMeaning
200Success
201Created
400Bad Request - Invalid parameters
401Unauthorized - Invalid or missing API key
403Forbidden - No access to this resource
404Not Found - Resource doesn't exist
429Too Many Requests - Rate limited
500Internal Server Error

SDK Error Handling

Handle errors in SDK
import { DispatchError } from '@dispatchtickets/sdk';

try {
  const ticket = await dispatch.tickets.get('br_abc123', 'tkt_invalid');
} catch (error) {
  if (error instanceof DispatchError) {
    console.error(error.code);    // 'TICKET_NOT_FOUND'
    console.error(error.message); // 'Ticket with ID...'
    console.error(error.status);  // 404
  }
}