# Ticketdesk API Skill

## Overview

Use this skill to interact with the **Ticketdesk AI helpdesk API** — create and manage tickets, configure inboxes, build and train AI agents, set tags, manage automations, and more.

**Base URL:** `https://api.ticketdesk.ai/v1`

## Authentication

All requests require a Bearer token in the `Authorization` header.

```bash
Authorization: Bearer {TICKETDESK_API_KEY}
```

Get your API key at: https://app.ticketdesk.ai/settings/api-keys

**In code (Node.js/TypeScript):**

```typescript
const BASE_URL = 'https://api.ticketdesk.ai/v1';
const HEADERS = {
  Authorization: `Bearer ${process.env.TICKETDESK_API_KEY}`,
  'Content-Type': 'application/json',
};
```

---

## Tickets

### List Tickets

```http
GET /tickets
```

**Query Parameters:**

| Parameter | Type   | Description                                                      | Default |
| --------- | ------ | ---------------------------------------------------------------- | ------- |
| offset    | number | Pagination offset (max 10000)                                    | 0       |
| limit     | number | Results per page (1–100)                                         | 100     |
| search    | string | Search keyword (max 50 chars)                                    | —       |
| user_id   | number | Filter by assigned user ID                                       | —       |
| sort      | array  | Sort array: `[{ id: "created_at", desc: true }]`                 | []      |
| filters   | array  | Filter array: `[{ id: "state", operator: "eq", value: "open" }]` | []      |

**Example:**

```typescript
const res = await fetch(`${BASE_URL}/tickets?limit=20&offset=0`, {
  headers: HEADERS,
});
const data = await res.json();
```

---

### Create Ticket

```http
POST /tickets
```

**Required fields:** `subject`, `requester`

**Body:**

```typescript
{
  subject: string;               // 2–200 chars (required)
  requester: {                   // (required)
    name?: string;
    email?: string;              // email format
    phone?: string;
    first_name?: string;
    last_name?: string;
    avatar?: string;
  };
  description?: string;
  state?: "open" | "closed";
  priority?: "low" | "medium" | "high" | "urgent";
  source?: "email" | "web" | "api" | "chat" | "form";
  tags?: string[];               // array of tag names
  user_id?: number;              // assigned agent user ID
  inbox_id?: number;
  participants?: string[];       // array of email addresses
  attachments?: Array<{
    path: string;                // (required)
    name?: string;
    size?: number;
    type?: string;
  }>;                            // max 10 attachments
}
```

**Example — create a basic ticket:**

```typescript
const res = await fetch(`${BASE_URL}/tickets`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    subject: 'My login is broken',
    requester: { name: 'Jane Doe', email: 'jane@example.com' },
    priority: 'high',
    tags: ['login', 'bug'],
    source: 'api',
  }),
});
const ticket = await res.json();
```

---

### Get Ticket

```http
GET /tickets/:id
```

```typescript
const res = await fetch(`${BASE_URL}/tickets/42`, { headers: HEADERS });
const ticket = await res.json();
```

---

### Update Ticket (full)

```http
PUT /tickets/:id
```

Body accepts the same fields as `POST /tickets`. Replaces the ticket record.

---

### Patch Ticket (partial update)

```http
PATCH /tickets/:id
```

Pass only the fields you want to update.

**Common partial updates:**

```typescript
// Change priority
await fetch(`${BASE_URL}/tickets/42`, {
  method: 'PATCH',
  headers: HEADERS,
  body: JSON.stringify({ priority: 'urgent' }),
});

// Assign to a user
await fetch(`${BASE_URL}/tickets/42`, {
  method: 'PATCH',
  headers: HEADERS,
  body: JSON.stringify({ user_id: 7 }),
});

// Set tags
await fetch(`${BASE_URL}/tickets/42`, {
  method: 'PATCH',
  headers: HEADERS,
  body: JSON.stringify({ tags: ['billing', 'refund'] }),
});
```

---

### Set Ticket State (open / close)

```http
PATCH /tickets/:id/state
```

```typescript
{
  state: "open" | "closed";
  state_reason?: "completed" | "not_planned" | "duplicate";
}
```

**Example — close a ticket:**

```typescript
await fetch(`${BASE_URL}/tickets/42/state`, {
  method: 'PATCH',
  headers: HEADERS,
  body: JSON.stringify({ state: 'closed', state_reason: 'completed' }),
});
```

---

### Bulk Ticket Actions

```http
PATCH /tickets/bulk
```

Apply an action to up to 1000 tickets at once.

```typescript
{
  ticket_ids: number[];  // 1–1000 IDs (required)
  data: {
    user_id?: number;
    inbox_id?: number;
    priority?: "low" | "medium" | "high" | "urgent";
    source?: "email" | "web" | "api" | "chat" | "form";
    state?: "open" | "closed";
    tags?: string[];
  };
}
```

**Example — bulk close tickets:**

```typescript
await fetch(`${BASE_URL}/tickets/bulk`, {
  method: 'PATCH',
  headers: HEADERS,
  body: JSON.stringify({
    ticket_ids: [1, 2, 3, 4, 5],
    data: { state: 'closed', state_reason: 'completed' },
  }),
});
```

---

### Delete Ticket

```http
DELETE /tickets/:id
```

---

## Ticket Activities (Replies & Notes)

Activities are the messages on a ticket — replies to the customer, internal notes, or system events.

### List Activities on a Ticket

```http
GET /activities/:ticket_id
```

Query params: `offset`, `limit`, `search`

---

### Add Activity (Reply or Note)

```http
POST /activities
```

**Required:** `ticket_id`, `type`, `content`

```typescript
{
  ticket_id: number;             // (required)
  type: "reply" | "note" | "system" | "queued";  // (required)
  content: string;               // min 1 char (required)
  is_draft?: boolean;            // default false
  attachments?: Array<{
    path: string;
    name?: string;
    size?: number;
    type?: string;
  }>;
}
```

**Example — send a reply:**

```typescript
await fetch(`${BASE_URL}/activities`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    ticket_id: 42,
    type: 'reply',
    content: 'Hi Jane, we are looking into your login issue. Please hold on.',
  }),
});
```

**Example — add an internal note:**

```typescript
await fetch(`${BASE_URL}/activities`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    ticket_id: 42,
    type: 'note',
    content:
      'Checked logs — seems like a cache issue. Escalating to backend team.',
  }),
});
```

---

### Update Activity

```http
PUT /activities/:id
```

---

### Delete Activity

```http
DELETE /activities/:id
```

---

## Inboxes

Inboxes route incoming emails to tickets. Each inbox has a unique email address and can be connected to an AI agent.

### List Inboxes

```http
GET /inboxes
```

Query params: `offset`, `limit`, `search`

---

### Create Inbox

```http
POST /inboxes
```

**Required:** `name`, `email`

```typescript
{
  name: string;          // 1–60 chars (required)
  email: string;         // 2–60 chars, becomes the @ticketdesk.ai address (required)
  prefix?: string;       // ticket subject prefix (max 10 chars)
  canned_id?: number;    // default canned response ID
  sender_id?: number;    // custom sender/SMTP ID for outgoing email
  agent_id?: number;     // AI agent ID to auto-respond to incoming tickets
  prompts?: Array<{      // inbox-level prompt overrides (max 10)
    name: string;        // 1–60 chars
    content: string;     // 1–5000 chars
  }>;
}
```

**Example:**

```typescript
const res = await fetch(`${BASE_URL}/inboxes`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    name: 'Your Company Support',
    email: 'yourcompany', // becomes yourcompany@ticketdesk.ai
    agent_id: 1, // connect AI agent
    prefix: 'SUP',
  }),
});
const inbox = await res.json();
```

> **Note:** The `email` field is the local part only (before `@ticketdesk.ai`).

---

### Check Inbox Email Availability

```http
GET /inboxes/availability?alias=yourcompany
```

---

### Get Inbox

```http
GET /inboxes/:id
```

---

### Update Inbox

```http
PUT /inboxes/:id
```

Same body as `POST /inboxes`.

---

### Delete Inbox

```http
DELETE /inboxes/:id
```

---

## AI Agents

AI agents respond automatically to tickets using LLMs. They can be trained with knowledge sources.

### List AI Agents

```http
GET /agents
```

Query params: `offset`, `limit`, `search`

---

### Create AI Agent

```http
POST /agents
```

**Required:** `name`, `prompt`

```typescript
{
  name: string;                  // agent display name (required)
  prompt: string;                // system prompt / personality (max 8192 chars, required)
  provider?: string;             // LLM provider (default: "default" = Cloudflare Workers AI)
  model?: string;                // model name (default: "@cf/meta/llama-3.3-70b-instruct-fp8-fast")
  api_key?: string;              // API key if using a custom provider (OpenAI, Anthropic, etc.)
  max_token?: number;            // max output tokens (default: 512)
  temperature?: number;          // 0–1, creativity (default: 0.7)
  confidence?: number;           // 0–1, confidence threshold (default: 0.4)
}
```

**Supported providers and models:**

| Provider    | Model examples                                |
| ----------- | --------------------------------------------- |
| `default`   | `@cf/meta/llama-3.3-70b-instruct-fp8-fast`    |
| `openai`    | `gpt-4o`, `gpt-4-turbo`, `gpt-3.5-turbo`      |
| `anthropic` | `claude-3-5-sonnet-20241022`, `claude-3-opus` |
| `google`    | `gemini-1.5-pro`, `gemini-1.5-flash`          |

**Example:**

```typescript
const res = await fetch(`${BASE_URL}/agents`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    name: 'Support Bot',
    prompt:
      "You are a helpful customer support agent for YourCompany. Always be polite and concise. If you don't know the answer, escalate to a human agent.",
    temperature: 0.5,
    confidence: 0.5,
  }),
});
const agent = await res.json();
```

---

### Get Agent

```http
GET /agents/:id
```

---

### Update Agent

```http
PUT /agents/:id
```

Same body as `POST /agents`.

---

### Delete Agent

```http
DELETE /agents/:id
```

---

## Training Sources

Add knowledge to an AI agent by attaching training sources. Supported types: URLs, sitemaps, plain text, PDFs, audio, and YouTube videos.

### List Sources for an Agent

```http
GET /sources?agent_id={agent_id}
```

---

### Add Sources

```http
POST /sources
```

**Required:** `agent_id`, `sources`

```typescript
{
  agent_id: number;              // (required)
  sources: Array<{               // 1–100 sources per request (required)
    type: "url" | "sitemap" | "text" | "pdf" | "audio" | "youtube";  // (required)
    url?: string;                // for url, sitemap, pdf, audio, youtube
    name?: string;               // display name
    text?: string;               // for type="text" (max 8192 chars)
  }>;
  config?: {                     // optional URL scraping config
    contentSelector?: string;    // CSS selector (default: "body")
    ignoreTags?: string[];        // HTML tags to ignore
    removeExternalLinks?: boolean;  // default: true
    relativeToAbsoluteLinks?: boolean; // default: true
    removeDataImages?: boolean;  // default: true
  };
}
```

**Example — add a website URL:**

```typescript
await fetch(`${BASE_URL}/sources`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    agent_id: 1,
    sources: [
      { type: 'url', url: 'https://yourcompany.com/faq', name: 'FAQ Page' },
    ],
  }),
});
```

**Example — add a sitemap:**

```typescript
await fetch(`${BASE_URL}/sources`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    agent_id: 1,
    sources: [{ type: 'sitemap', url: 'https://yourcompany.com/sitemap.xml' }],
  }),
});
```

**Example — add plain text:**

```typescript
await fetch(`${BASE_URL}/sources`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    agent_id: 1,
    sources: [
      {
        type: 'text',
        name: 'Refund Policy',
        text: 'We offer a 30-day money-back guarantee on all purchases. To request a refund, contact support@yourcompany.com.',
      },
    ],
  }),
});
```

**Example — add a YouTube video:**

```typescript
await fetch(`${BASE_URL}/sources`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    agent_id: 1,
    sources: [
      {
        type: 'youtube',
        url: 'https://www.youtube.com/watch?v=XXXXXXXXX',
        name: 'Product Demo',
      },
    ],
  }),
});
```

---

### Get Source

```http
GET /sources/:id?download=false
```

---

### Update Source Content

```http
PATCH /sources/:id
```

```typescript
{
  text: string; // min 1, max 8192 chars (required)
}
```

---

### Delete Source

```http
DELETE /sources/:id
```

---

### Test URL Scraping

```http
POST /sources/test
```

Preview how a URL will be scraped before adding it as a source.

```typescript
{
  url: string;           // URI to test (required)
  config?: {
    contentSelector?: string;
    ignoreTags?: string[];
    removeExternalLinks?: boolean;
    relativeToAbsoluteLinks?: boolean;
    removeDataImages?: boolean;
  };
}
```

---

## Automations

Automations are rules attached to an AI agent that trigger on ticket events. Each agent can have multiple automation rules.

### Get Automations for an Agent

```http
GET /automations/:agent_id
```

---

### Upsert Automation Rule

```http
PUT /automations/:agent_id
```

The body is a `oneOf` union — pick one `type` per call.

#### Type: `ai_response` — Auto-reply using AI

```typescript
{
  type: "ai_response";
  is_enabled?: boolean;
  config: {
    prompt: string;              // reply prompt (max 1024 chars, required)
    reply_mode?: "first" | "always"; // "first" = reply only to first message
    delay?: { from: number; to: number }; // random delay in seconds (0–43200)
    user_id?: number;            // send reply as this user
  };
}
```

#### Type: `set_priority` — AI auto-sets ticket priority

```typescript
{
  type: "set_priority";
  is_enabled?: boolean;
  config: {
    prompt?: string;             // classification prompt
    priority?: Array<"low" | "medium" | "high" | "urgent">;
  };
}
```

#### Type: `set_tags` — AI auto-tags tickets

```typescript
{
  type: "set_tags";
  is_enabled?: boolean;
  config: {
    prompt?: string;
    tags: string[];              // list of possible tags (1–32, required)
  };
}
```

#### Type: `close_ticket` — Auto-close after inactivity

```typescript
{
  type: "close_ticket";
  is_enabled?: boolean;
  config: {
    inactivity_timeout: number;  // seconds of inactivity before closing (required)
  };
}
```

#### Type: `ai_followup` — AI sends follow-up after inactivity

```typescript
{
  type: "ai_followup";
  is_enabled?: boolean;
  config: {
    prompt: string;              // follow-up message prompt (required)
    inactivity_timeout: number;  // seconds (required)
  };
}
```

#### Type: `assign_to` — Auto-assign ticket to a user

```typescript
{
  type: "assign_to";
  is_enabled?: boolean;
  config: {
    user_id: number | null;      // (required)
  };
}
```

#### Type: `block_list` — Block emails, domains, or keywords

```typescript
{
  type: "block_list";
  is_enabled?: boolean;
  config: {
    emails?: string[];
    domains?: string[];
    keywords?: string[];
    action?: "skip_automation" | "close_ticket";  // default: "close_ticket"
  };
}
```

**Example — set up AI auto-response:**

```typescript
await fetch(`${BASE_URL}/automations/1`, {
  // 1 = agent_id
  method: 'PUT',
  headers: HEADERS,
  body: JSON.stringify({
    type: 'ai_response',
    config: {
      prompt:
        'Reply to this support ticket helpfully and concisely based on the knowledge base. If you cannot resolve the issue, let the customer know a human agent will follow up.',
      reply_mode: 'always',
      delay: { from: 5, to: 30 },
    },
  }),
});
```

---

### Enable / Disable Automation

```http
PATCH /automations/:id
```

```typescript
{
  is_enabled: boolean;
}
```

---

### Trigger Automation Manually on a Ticket

```http
POST /automations
```

```typescript
{
  ticket_id: number;
}
```

---

### Get Automation Logs

```http
GET /logs?type={automation_type}&id={agent_id}
```

---

## Tags

### List Tags

```http
GET /tags
```

Query params: `offset`, `limit`, `search`

---

### Create Tag

```http
POST /tags
```

**Required:** `name`, `color`

```typescript
{
  name: string; // 1–60 chars (required)
  color: string; // hex color, 3–10 chars (required), e.g. "#3B82F6"
}
```

**Example:**

```typescript
await fetch(`${BASE_URL}/tags`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({ name: 'billing', color: '#F59E0B' }),
});
```

---

### Get Tag

```http
GET /tags/:id
```

---

### Update Tag

```http
PUT /tags/:id
```

Same body as `POST /tags`.

---

### Delete Tag

```http
DELETE /tags/:id
```

---

## Canned Responses

Pre-built reply templates that support merge variables for personalization.

### List Canned Responses

```http
GET /canned
```

---

### Create Canned Response

```http
POST /canned
```

**Required:** `name`, `content`

```typescript
{
  name: string; // template name (required)
  content: string; // message body, supports merge variables (required)
}
```

**Example:**

```typescript
await fetch(`${BASE_URL}/canned`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    name: 'Greeting',
    content:
      "Hi {{requester.name}}, thanks for reaching out! We'll get back to you within 24 hours.",
  }),
});
```

---

### Get / Update / Delete Canned Response

```http
GET  /canned/:id
PUT  /canned/:id   (same body as POST)
DELETE /canned/:id
```

---

## Users (Team Members)

### List Users

```http
GET /users
```

---

### Invite Users

```http
POST /users
```

```typescript
{
  emails: string[];              // 1–1000 email addresses (required)
  role: "Owner" | "Admin" | "Staff" | "Custom";  // (required)
  permissions?: string;          // required when role is "Custom"
  team_access?: boolean;         // can see all team tickets (default: false)
}
```

---

### Get / Update / Delete User

```http
GET    /users/:id
PUT    /users/:id
DELETE /users/:id
```

---

## Senders (Custom SMTP / Email Accounts)

Custom sender accounts let you send ticket emails from your own domain.

### List / Create / Get / Update / Delete

```http
GET    /senders
POST   /senders
GET    /senders/:id
PUT    /senders/:id
DELETE /senders/:id
```

### Test Sender (sends a test email)

```http
POST /senders/test
```

---

## Chat Sessions

### List Sessions

```http
GET /sessions?site_id={site_id}
```

---

### Get Session (with messages)

```http
GET /sessions/:id
```

---

### Update Session

```http
PATCH /sessions/:id
```

---

### Delete Session

```http
DELETE /sessions/:id
```

---

### Send Transcript

```http
POST /sessions/:id/transcript
```

```typescript
{
  to: 'operator' | 'user' | 'all'; // (required)
}
```

---

## Reports

```http
GET /reports
```

**Query Parameters:**

| Parameter | Type   | Values                                                                                                              | Default       |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------- | ------------- |
| start     | date   | `YYYY-MM-DD`                                                                                                        | —             |
| end       | date   | `YYYY-MM-DD`                                                                                                        | —             |
| type      | string | `new-tickets`, `source-breakdown`, `tickets-by-status`, `resolution-time`, `first-response-time`, `tickets-handled` | `new-tickets` |
| interval  | number | number of days per interval                                                                                         | 7             |

**Example:**

```typescript
const res = await fetch(
  `${BASE_URL}/reports?start=2025-04-01&end=2025-04-30&type=tickets-by-status&interval=7`,
  { headers: HEADERS },
);
```

---

## Complete Setup Workflow

This section shows the recommended sequence to go from zero to a fully working AI-powered inbox.

### Step 1 — Create an AI Agent

```typescript
const agentRes = await fetch(`${BASE_URL}/agents`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    name: 'Support Bot',
    prompt:
      'You are a helpful customer support agent. Answer questions based on your knowledge base. Be polite, concise, and professional. If you cannot resolve an issue, let the customer know a human agent will follow up shortly.',
    temperature: 0.5,
    confidence: 0.5,
  }),
});
const { agent_id } = await agentRes.json();
```

### Step 2 — Add Training Sources

```typescript
await fetch(`${BASE_URL}/sources`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    agent_id,
    sources: [
      { type: 'url', url: 'https://yourcompany.com/faq', name: 'FAQ' },
      {
        type: 'sitemap',
        url: 'https://yourcompany.com/sitemap.xml',
        name: 'Docs Sitemap',
      },
      {
        type: 'text',
        name: 'Refund Policy',
        text: 'We offer a 30-day money-back guarantee. Contact support to initiate a refund.',
      },
    ],
  }),
});
```

### Step 3 — Set Up AI Auto-Response Automation

```typescript
await fetch(`${BASE_URL}/automations/${agent_id}`, {
  method: 'PUT',
  headers: HEADERS,
  body: JSON.stringify({
    type: 'ai_response',
    config: {
      prompt:
        'Reply to the customer ticket helpfully using your knowledge base. If unresolved, inform them a human agent will follow up.',
      reply_mode: 'always',
      delay: { from: 5, to: 30 },
    },
  }),
});
```

### Step 4 — Create the Inbox and Connect the Agent

```typescript
const inboxRes = await fetch(`${BASE_URL}/inboxes`, {
  method: 'POST',
  headers: HEADERS,
  body: JSON.stringify({
    name: 'Your Company Support',
    email: 'yourcompany', // → yourcompany@ticketdesk.ai
    agent_id, // connects the AI agent for auto-responses
  }),
});
const inbox = await inboxRes.json();
```

### Step 5 — Create Default Tags (optional but recommended)

```typescript
const tags = [
  { name: 'billing', color: '#F59E0B' },
  { name: 'bug', color: '#EF4444' },
  { name: 'feature', color: '#8B5CF6' },
  { name: 'question', color: '#3B82F6' },
  { name: 'urgent', color: '#DC2626' },
];

for (const tag of tags) {
  await fetch(`${BASE_URL}/tags`, {
    method: 'POST',
    headers: HEADERS,
    body: JSON.stringify(tag),
  });
}
```

---

## Error Handling

All endpoints return standard HTTP status codes. Always check for non-2xx responses:

```typescript
async function apiCall(url: string, options?: RequestInit) {
  const res = await fetch(`${BASE_URL}${url}`, {
    ...options,
    headers: { ...HEADERS, ...options?.headers },
  });

  if (!res.ok) {
    const error = await res.json().catch(() => ({ message: res.statusText }));
    throw new Error(
      `Ticketdesk API error ${res.status}: ${JSON.stringify(error)}`,
    );
  }

  return res.json();
}

// Usage
const ticket = await apiCall('/tickets/42');
const newTicket = await apiCall('/tickets', {
  method: 'POST',
  body: JSON.stringify({
    subject: 'Help!',
    requester: { email: 'user@example.com' },
  }),
});
```

---

## Quick Reference

| Resource    | List                         | Create                       | Get                 | Update                   | Delete                   |
| ----------- | ---------------------------- | ---------------------------- | ------------------- | ------------------------ | ------------------------ |
| Tickets     | `GET /tickets`               | `POST /tickets`              | `GET /tickets/:id`  | `PUT/PATCH /tickets/:id` | `DELETE /tickets/:id`    |
| Activities  | `GET /activities/:id`        | `POST /activities`           | —                   | `PUT /activities/:id`    | `DELETE /activities/:id` |
| Inboxes     | `GET /inboxes`               | `POST /inboxes`              | `GET /inboxes/:id`  | `PUT /inboxes/:id`       | `DELETE /inboxes/:id`    |
| AI Agents   | `GET /agents`                | `POST /agents`               | `GET /agents/:id`   | `PUT /agents/:id`        | `DELETE /agents/:id`     |
| Sources     | `GET /sources`               | `POST /sources`              | `GET /sources/:id`  | `PATCH /sources/:id`     | `DELETE /sources/:id`    |
| Automations | `GET /automations/:agent_id` | `PUT /automations/:agent_id` | —                   | `PATCH /automations/:id` | —                        |
| Tags        | `GET /tags`                  | `POST /tags`                 | `GET /tags/:id`     | `PUT /tags/:id`          | `DELETE /tags/:id`       |
| Canned      | `GET /canned`                | `POST /canned`               | `GET /canned/:id`   | `PUT /canned/:id`        | `DELETE /canned/:id`     |
| Users       | `GET /users`                 | `POST /users`                | `GET /users/:id`    | `PUT /users/:id`         | `DELETE /users/:id`      |
| Senders     | `GET /senders`               | `POST /senders`              | `GET /senders/:id`  | `PUT /senders/:id`       | `DELETE /senders/:id`    |
| Sessions    | `GET /sessions`              | —                            | `GET /sessions/:id` | `PATCH /sessions/:id`    | `DELETE /sessions/:id`   |
| Reports     | `GET /reports`               | —                            | —                   | —                        | —                        |
