{
  "openapi": "3.1.0",
  "info": {
    "title": "Helmdesk API",
    "version": "1.2.0",
    "description": "REST API for Helmdesk — helpdesk, email gateway, knowledge base, logs, feedback, and customers.\n\n**Authentication.** Every request needs an API key as a Bearer token:\n`Authorization: Bearer sk_live_...`. Keys belong to the **account** and carry a\nProjects field: all projects, or a chosen subset (Admin Settings → API keys).\n\n**Which project.** Project-scoped endpoints act on one project. Name it with the\n`Helmdesk-Project` header or `?project=<slug-or-id>`; a key that can reach\nexactly one project needs neither. A key that can reach several projects and\nnames none is refused with `400 project.required` (the response lists the\nchoices) — the API never guesses. Naming a project the key cannot reach is\n`403 project.forbidden`. `GET /projects` lists what a key can reach.\n\n**Environments.** The key prefix bounds the data plane: `sk_sandbox_` reads and\nwrites sandbox data and never sends real email; `sk_live_` reads and writes live\ndata unless the request asks for sandbox with the `Helmdesk-Environment: sandbox`\nheader or `?environment=sandbox` — the way to try a flow with a real key without\nreaching a real inbox. The downgrade is the only direction: a sandbox key asked\nfor live is `403 environment.forbidden`.\n\n**Scopes.** Keys carry scopes (`tickets:read`, `emails:send`, …). A key with an\nempty scope list has full access. A request missing the scope its endpoint\nrequires returns `403 auth.forbidden` naming the scope.\n\n**Rate limit.** 100 requests per minute per API key; over that returns\n`429 rate_limit.exceeded`.\n\n**Errors.** Every failure returns `{ \"error\": { \"code\", \"message\", \"details\"? } }`.\nSee https://helmdesk.dev/docs/errors.",
    "contact": {
      "name": "Helmdesk Support",
      "url": "https://helmdesk.dev"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://helmdesk.dev/terms"
    }
  },
  "servers": [
    {
      "url": "https://helmdesk.dev/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/tickets": {
      "get": {
        "operationId": "listTickets",
        "summary": "List tickets",
        "description": "Returns a paginated list of tickets for the authenticated project. Every row carries lastSenderType and lastMessageAt (the latest message on the thread, computed in the same query) so a caller can see who is waiting on whom without opening each ticket. Requires the tickets:read scope.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "new",
                "open",
                "pending",
                "resolved",
                "closed"
              ]
            }
          },
          {
            "name": "priority",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "high",
                "urgent"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search by subject or customer email"
          },
          {
            "name": "assignee",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by assignee: a user id, a comma-separated list of ids, or \"unassigned\""
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "bug",
                "feature_request",
                "billing",
                "howto",
                "other"
              ]
            }
          },
          {
            "name": "number",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Exact ticket number within the project (the human-facing \"#42\")"
          },
          {
            "name": "awaitingReply",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "true (or 1) → only tickets waiting on you: status new, open, or pending AND the latest message came from the customer. The same definition the Agents Activity inbox uses. Combine with status to narrow it further."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated ticket list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Ticket"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "tickets": [
                    {
                      "id": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                      "projectId": "3f2a1c44-7d21-4a5e-9f0b-2c8d6e1a9b77",
                      "projectSlug": "ledgerly",
                      "number": 1042,
                      "subject": "Webhook retries are firing twice",
                      "status": "open",
                      "priority": "high",
                      "category": "bug",
                      "assignedTo": null,
                      "customerEmail": "dana@acme.io",
                      "customerName": "Dana Okafor",
                      "lastSenderType": "customer",
                      "lastMessageAt": "2026-07-21T11:02:47.980Z",
                      "createdAt": "2026-07-21T09:14:03.221Z",
                      "updatedAt": "2026-07-21T11:02:47.980Z"
                    }
                  ],
                  "total": 137,
                  "page": 1,
                  "limit": 50
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "createTicket",
        "summary": "Create a ticket",
        "description": "Creates a new support ticket. Auto-creates or matches the customer by email.",
        "tags": [
          "Tickets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTicketInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedTicket"
                },
                "example": {
                  "id": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                  "number": 1042,
                  "subject": "Webhook retries are firing twice",
                  "status": "new",
                  "priority": "high",
                  "customer": {
                    "email": "dana@acme.io",
                    "name": "Dana Okafor"
                  },
                  "createdAt": "2026-07-21T09:14:03.221Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "403": {
            "description": "Forbidden. `auth.forbidden` for scope/IP; `plan.ticket_limit` when ticket creation is paused for the account — the Free allowance is used up, or a paid plan reached its overage ceiling (3× the allowance). `details` carries `current`, `allowance`, and `reason` (`free_allowance` | `ceiling`). Paid plans below the ceiling never get this: tickets past the allowance are accepted and billed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/tickets/{ticketId}": {
      "get": {
        "operationId": "getTicket",
        "summary": "Get ticket details",
        "description": "Returns a ticket with its customer info, messages, and tags.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Ticket detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TicketDetail"
                },
                "example": {
                  "id": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                  "number": 1042,
                  "subject": "Webhook retries are firing twice",
                  "status": "open",
                  "priority": "high",
                  "category": "bug",
                  "customerEmail": "dana@acme.io",
                  "customerName": "Dana Okafor",
                  "createdAt": "2026-07-21T09:14:03.221Z",
                  "updatedAt": "2026-07-21T11:02:47.980Z",
                  "closedAt": null,
                  "customer": {
                    "email": "dana@acme.io",
                    "name": "Dana Okafor"
                  },
                  "messages": [
                    {
                      "id": "2c8a5d31-7b64-4f0e-9a12-6d3e5f0b7c48",
                      "ticketId": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                      "senderType": "customer",
                      "body": "Our endpoint received the same delivery twice, about 30s apart.",
                      "createdAt": "2026-07-21T09:14:03.221Z",
                      "attachments": [
                        {
                          "id": "0d5c8b71-4e29-4a13-8f60-2b7d9e1c3a58",
                          "fileName": "delivery-log.txt",
                          "fileSize": 2148,
                          "mimeType": "text/plain",
                          "url": "https://cdn.helmdesk.dev/ticket-attachments/0d5c8b71.txt",
                          "createdAt": "2026-07-21T09:14:03.221Z"
                        }
                      ]
                    }
                  ],
                  "tags": [
                    "webhooks",
                    "needs-repro"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateTicket",
        "summary": "Update a ticket",
        "description": "Updates ticket status, priority, category, or assignment.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTicketInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated ticket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TicketDetail"
                },
                "example": {
                  "id": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                  "number": 1042,
                  "subject": "Webhook retries are firing twice",
                  "status": "resolved",
                  "priority": "high",
                  "category": "bug",
                  "customerEmail": "dana@acme.io",
                  "customerName": "Dana Okafor",
                  "createdAt": "2026-07-21T09:14:03.221Z",
                  "updatedAt": "2026-07-21T11:02:47.980Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "operationId": "deleteTicket",
        "summary": "Delete a ticket",
        "description": "Permanently deletes a ticket with its messages, tags, and attachments (storage objects are removed and the storage counter released). The customer is NOT emailed. Scoped to the project and the request's environment — a live id given to a sandbox request is a 404. To finish a ticket without losing its history, set its status to closed instead. Requires the tickets:write scope.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "attachmentsRemoved": {
                      "type": "integer",
                      "description": "Storage objects removed with the ticket."
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The project this belongs to."
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "id": "3b8e1c2d-9f4a-4e6b-8c7d-1a2b3c4d5e6f",
                  "attachmentsRemoved": 2,
                  "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                  "projectSlug": "abc-salon"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/tickets/{ticketId}/messages": {
      "post": {
        "operationId": "addMessage",
        "summary": "Reply to a ticket or add an internal note",
        "description": "Appends a staff message to the ticket thread. With visibility \"public\" (the default) the reply is emailed to the customer and a ticket in status 'new' transitions to 'open'. With visibility \"internal\" the message is a staff-only note: it is stored on the thread and returned by GET /tickets/{ticketId}, but never shown on the customer portal, never emailed, and the ticket's status does not change. Sandbox tickets never email anyone either way. Requires the tickets:write scope.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 10000
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "internal"
                    ],
                    "default": "public",
                    "description": "\"internal\" writes a staff-only note (not emailed, not on the portal, no status change)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created message (visibility says whether the customer saw it)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TicketMessage"
                },
                "example": {
                  "id": "5f3b9e07-1a2c-4d68-8e90-7c1d4a6b2f35",
                  "ticketId": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                  "senderType": "staff",
                  "visibility": "public",
                  "body": "Thanks Dana — checking the delivery log now.",
                  "createdAt": "2026-07-21T11:02:47.980Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/emails/send": {
      "post": {
        "operationId": "sendEmail",
        "summary": "Send an email — from a template, or a one-off custom message",
        "description": "Sends a transactional email from a registered template (`templateKey` + `variables`), or a one-off custom email (`subject` + Markdown `body`, optional `layout`) when there is no template because there will never be a second send. Both take the same path: quota, suppression, sending modes, scheduling, sandbox capture, and the Email Logs row (custom sends are marked as such). Supports idempotency via the Idempotency-Key header.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "Prevents duplicate sends for the same key within 24h"
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Send result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendEmailResult"
                },
                "example": {
                  "projectId": "7c1e2c1a-9d3f-4e1b-9c0e-2d4f0a6b8e51",
                  "projectSlug": "ledgerly",
                  "id": "d41f9a6b-88c5-4e2a-b7d0-1e5c3f9a2b64",
                  "kind": "template",
                  "status": "queued",
                  "to": "dana@acme.io",
                  "subject": "Welcome to Acme",
                  "deduplicated": false,
                  "blocked": false,
                  "blockReason": null,
                  "scheduledAt": null,
                  "createdAt": "2026-07-21T09:15:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/emails/batch": {
      "post": {
        "operationId": "sendEmailBatch",
        "summary": "Send a batch of transactional emails",
        "description": "Sends up to 100 transactional emails in one request. Each item is delivered independently — one failed item does not fail the batch; inspect each result's error/blocked field. Idempotency is a per-item field (idempotencyKey) rather than the Idempotency-Key header used by single send. Requires the emails:send scope and the Starter plan or higher; the Free plan returns 403. One batch counts as a single request against the rate limit.",
        "tags": [
          "Emails"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailBatchInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch send result with per-item outcomes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendEmailBatchResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/emails/preview": {
      "post": {
        "operationId": "previewEmail",
        "summary": "Preview an email — a template, or a custom message",
        "description": "Renders a template with the given variables, or a custom email (subject + Markdown body wrapped in its layout), and returns the subject and HTML. Pass format: \"text\" to get a readable plain-text conversion instead of the HTML. Nothing is sent or stored.",
        "tags": [
          "Emails"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "templateKey"
                    ],
                    "description": "Preview a template.",
                    "properties": {
                      "templateKey": {
                        "type": "string"
                      },
                      "variables": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "format": {
                        "type": "string",
                        "enum": [
                          "html",
                          "text"
                        ],
                        "default": "html",
                        "description": "`text` returns a plain-text conversion of the rendering (tags stripped, links as `text (url)`, one line per block) instead of the HTML — the compact way to check a render."
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "subject",
                      "body"
                    ],
                    "description": "Preview a custom one-off email.",
                    "properties": {
                      "subject": {
                        "type": "string",
                        "maxLength": 200
                      },
                      "body": {
                        "type": "string",
                        "maxLength": 50000,
                        "description": "Markdown"
                      },
                      "layout": {
                        "type": "string",
                        "description": "Project or @account/ layout key; omit for the built-in shell."
                      },
                      "format": {
                        "type": "string",
                        "enum": [
                          "html",
                          "text"
                        ],
                        "default": "html",
                        "description": "`text` returns a plain-text conversion of the rendering (tags stripped, links as `text (url)`, one line per block) instead of the HTML — the compact way to check a render."
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rendered preview",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subject": {
                      "type": "string"
                    },
                    "html": {
                      "type": "string",
                      "description": "The rendering. Absent when format is text."
                    },
                    "text": {
                      "type": "string",
                      "description": "Plain-text conversion of the rendering. Present only when format is text."
                    },
                    "detectedVariables": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Variables the template references (empty for a custom email)."
                    }
                  }
                },
                "example": {
                  "subject": "Welcome, Dana",
                  "html": "<html>…</html>",
                  "detectedVariables": [
                    "name",
                    "activationUrl"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/email-templates/{key}/schema": {
      "get": {
        "operationId": "getTemplateSchema",
        "summary": "Get template variable schema",
        "description": "Returns the template metadata and its detected variables.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Template schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateSchema"
                },
                "example": {
                  "key": "welcome",
                  "name": "Welcome",
                  "subject": "Welcome to {{brandName}}",
                  "variables": [
                    "name",
                    "activationUrl"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/email-templates/import": {
      "post": {
        "operationId": "importEmailTemplate",
        "summary": "Import a .hbs file",
        "description": "Upload a Handlebars file as a template, layout, or partial. Re-importing an existing key updates it in place — templates get a new version.\n\nTemplates are **linked** to their layout at import time, from the `layout` field or a `{{!-- layout: key --}}` comment. Check `layoutLinked` in the response: if it is `false` the layout did not resolve (see `warnings`) and sends will render the body unwrapped.\n\nProject templates can only use project layouts. To use a shared layout, import it into the project, or put the template in the account scope instead.\n\nPartial references are validated and reported in `warnings`; they never fail the import.",
        "tags": [
          "Emails"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The .hbs file to upload"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "template",
                      "layout",
                      "partial"
                    ],
                    "default": "template"
                  },
                  "key": {
                    "type": "string",
                    "description": "Override the key (defaults to filename without .hbs)"
                  },
                  "name": {
                    "type": "string",
                    "description": "Override the display name"
                  },
                  "subject": {
                    "type": "string",
                    "description": "Subject line for templates (auto-detected from {{!-- subject: ... --}} if not set)"
                  },
                  "layout": {
                    "type": "string",
                    "description": "Key of an existing layout to wrap this template in (templates only). Overrides a `{{!-- layout: key --}}` comment in the file. The layout must already exist in the same scope and environment — import it first with `type=layout`. Omit the field to keep the template's current layout; send it empty (or `none`) to remove the layout."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import result. Always inspect `warnings` — the import succeeds even when a layout or partial is unresolved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                },
                "examples": {
                  "linked": {
                    "summary": "Template linked to its layout",
                    "value": {
                      "id": "7d2e4c19-3f5a-4b8e-9c01-2a6d8f4b7e35",
                      "key": "welcome",
                      "name": "Welcome",
                      "type": "template",
                      "subject": "Welcome to {{brandName}}",
                      "variables": [
                        "name",
                        "activationUrl"
                      ],
                      "referencedPartials": [
                        "footer"
                      ],
                      "layoutKey": "branded",
                      "layoutId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "layoutLinked": true,
                      "warnings": []
                    }
                  },
                  "unresolvedLayout": {
                    "summary": "Layout not found — imported unwrapped",
                    "value": {
                      "id": "7d2e4c19-3f5a-4b8e-9c01-2a6d8f4b7e35",
                      "key": "welcome",
                      "name": "Welcome",
                      "type": "template",
                      "subject": "Welcome to {{brandName}}",
                      "variables": [
                        "name"
                      ],
                      "referencedPartials": [],
                      "layoutKey": null,
                      "layoutId": null,
                      "layoutLinked": false,
                      "warnings": [
                        "Layout \"branded\" does not exist in this project, so the template was imported without a layout. Import the layout first (type=layout), then re-import this template."
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/account/email-templates/import": {
      "post": {
        "operationId": "importAccountEmailTemplate",
        "summary": "Import a .hbs file to account scope",
        "description": "Upload a Handlebars file as an account-level shared template, layout, or partial. Shared items are available to every project in the account via the `@account/` prefix and are environment-less (there is no sandbox copy).\n\nShared templates reference their layout by key, resolved at send time against the account's layouts. As with project imports, omitting `layout` keeps the current one.",
        "tags": [
          "Emails"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The .hbs file to upload"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "template",
                      "layout",
                      "partial"
                    ],
                    "default": "template"
                  },
                  "key": {
                    "type": "string",
                    "description": "Override the key (defaults to filename without .hbs)"
                  },
                  "name": {
                    "type": "string",
                    "description": "Override the display name"
                  },
                  "subject": {
                    "type": "string",
                    "description": "Subject line for templates (auto-detected from {{!-- subject: ... --}} if not set)"
                  },
                  "layout": {
                    "type": "string",
                    "description": "Key of an existing layout to wrap this template in (templates only). Overrides a `{{!-- layout: key --}}` comment in the file. The layout must already exist in the same scope and environment — import it first with `type=layout`. Omit the field to keep the template's current layout; send it empty (or `none`) to remove the layout."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                },
                "example": {
                  "id": "c9e1a740-62b3-4d5f-8a17-90b2c4d6e8f1",
                  "key": "invoice-paid",
                  "name": "Invoice Paid",
                  "type": "template",
                  "scope": "account",
                  "subject": "Your invoice is paid",
                  "referencedPartials": [
                    "@account/footer"
                  ],
                  "layoutKey": "house-style",
                  "layoutLinked": true,
                  "variables": [
                    "invoiceNumber",
                    "amount"
                  ],
                  "warnings": []
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/articles/search": {
      "get": {
        "operationId": "searchArticles",
        "summary": "Search published articles",
        "description": "Full-text keyword search across published articles.",
        "tags": [
          "Articles"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Search query"
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by category slug"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ArticleSearchResult"
                      }
                    },
                    "query": {
                      "type": "string",
                      "description": "The query that was run, echoed back."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of results returned. Search is not paginated."
                    }
                  }
                },
                "example": {
                  "results": [
                    {
                      "id": "3a7f1b25-9c48-4e60-bd13-5f2a8c7e4901",
                      "title": "Verifying webhook signatures",
                      "slug": "verifying-webhook-signatures",
                      "excerpt": "Every delivery is signed with your endpoint secret…",
                      "categoryId": "6b0d2e88-4f13-4a95-9c72-8e5b1d3f7a24",
                      "publishedAt": "2026-05-11T12:00:00.000Z"
                    }
                  ],
                  "query": "webhook signature",
                  "count": 1
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/tickets/{ticketId}/tags": {
      "post": {
        "operationId": "addTicketTag",
        "summary": "Add a tag to a ticket",
        "description": "Adds a tag (label) to a ticket. Idempotent. Requires the tickets:write scope.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tag"
                ],
                "properties": {
                  "tag": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 50
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ticket id and its full tag list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "removeTicketTag",
        "summary": "Remove a tag from a ticket",
        "description": "Removes a tag from a ticket. Requires the tickets:write scope.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The tag to remove"
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The ticket id and its full tag list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/articles": {
      "post": {
        "operationId": "createArticle",
        "summary": "Create a knowledge-base article",
        "description": "Creates a knowledge-base article. The slug is auto-generated from the title when omitted. Defaults to draft; published articles are indexed for semantic search. Requires the articles:write scope.",
        "tags": [
          "Articles"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateArticleInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created article",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Article"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      },
      "get": {
        "operationId": "listArticles",
        "summary": "List articles (drafts included)",
        "description": "Lists the project's articles in the request's environment, newest-updated first — drafts included, unlike /articles/search, which only sees published articles. Bodies are not returned; GET /articles/{articleId} has them. Requires the articles:read scope.",
        "tags": [
          "Articles"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "published"
              ]
            },
            "description": "Omit for both."
          },
          {
            "name": "categoryId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Substring match on title or slug."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Article listing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "articles": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "projectId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The project this belongs to."
                          },
                          "projectSlug": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                          },
                          "title": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "draft",
                              "published"
                            ]
                          },
                          "excerpt": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "categoryId": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uuid"
                          },
                          "helpfulYes": {
                            "type": "integer"
                          },
                          "helpfulNo": {
                            "type": "integer"
                          },
                          "publishedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updatedAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "articles": [
                    {
                      "id": "9c1f2e3d-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
                      "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                      "projectSlug": "abc-salon",
                      "title": "How to reset your password",
                      "slug": "how-to-reset-your-password",
                      "status": "draft",
                      "excerpt": null,
                      "categoryId": null,
                      "helpfulYes": 0,
                      "helpfulNo": 0,
                      "publishedAt": null,
                      "createdAt": "2026-09-09T10:00:00.000Z",
                      "updatedAt": "2026-09-09T10:00:00.000Z"
                    }
                  ],
                  "total": 1,
                  "page": 1,
                  "limit": 25
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/logs": {
      "post": {
        "operationId": "ingestLogs",
        "summary": "Ingest log events",
        "description": "Ingest a batch of up to 100 application log events. Events are fingerprinted server-side and grouped into log issues that the Log Watch agent monitors. Requires the logs:write scope. Live events count against the monthly plan quota; sandbox keys write quota-exempt sandbox events.",
        "tags": [
          "Logs"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "events"
                ],
                "properties": {
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "$ref": "#/components/schemas/LogEventInput"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Events accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accepted": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "accepted": 2
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      },
      "get": {
        "operationId": "listLogs",
        "summary": "List log events",
        "description": "List raw log events, newest first. Requires the logs:read scope.",
        "tags": [
          "Logs"
        ],
        "parameters": [
          {
            "name": "level",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated level list, e.g. 'error,fatal'."
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Matches message or scope."
          },
          {
            "name": "fingerprint",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Log events",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "logs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LogEvent"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/log-issues": {
      "get": {
        "operationId": "listLogIssues",
        "summary": "List log issues",
        "description": "List grouped error issues (one per fingerprint), most recently seen first. Requires the logs:read scope.",
        "tags": [
          "Logs"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated status list, e.g. 'open,acknowledged'."
          },
          {
            "name": "level",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated level list."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Matches sample message or scope."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Log issues",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LogIssue"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "issues": [
                    {
                      "id": "f0a3c5d7-2b18-4e94-a6c3-7d1f5b8e2094",
                      "fingerprint": "8f14e45fceea167a",
                      "level": "error",
                      "scope": "checkout",
                      "sampleMessage": "Stripe webhook signature verification failed",
                      "eventCount": 47,
                      "status": "open",
                      "firstSeenAt": "2026-07-19T02:11:44.000Z",
                      "lastSeenAt": "2026-07-21T08:52:10.000Z"
                    }
                  ],
                  "total": 3,
                  "page": 1,
                  "limit": 50
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/email-templates": {
      "get": {
        "operationId": "listEmailTemplates",
        "summary": "List email templates",
        "description": "Lists the project's email templates for the key's environment (with current-version subject and detected variables), plus the account's shared templates (keys prefixed with @account/). Use this to discover valid templateKey values for send and preview. Requires the emails:send scope.",
        "tags": [
          "Emails"
        ],
        "responses": {
          "200": {
            "description": "Template listing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "subject": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "variables": {
                            "type": [
                              "array",
                              "null"
                            ],
                            "items": {
                              "type": "string"
                            }
                          },
                          "updatedAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "accountTemplates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string",
                            "example": "@account/welcome"
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "subject": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "updatedAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "templates": [
                    {
                      "key": "welcome",
                      "name": "Welcome",
                      "description": "Subject: Welcome to {{brandName}}",
                      "subject": "Welcome to {{brandName}}",
                      "variables": [
                        "name",
                        "activationUrl"
                      ],
                      "updatedAt": "2026-07-20T16:31:12.004Z"
                    }
                  ],
                  "accountTemplates": [
                    {
                      "key": "@account/invoice-paid",
                      "name": "Invoice Paid",
                      "description": null,
                      "subject": "Your invoice is paid",
                      "updatedAt": "2026-06-02T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/members": {
      "get": {
        "operationId": "listTeamMembers",
        "summary": "List team members",
        "description": "Lists the active members of the project's account. Use the userId values as valid assignedTo targets when updating tickets. Requires the tickets:read scope.",
        "tags": [
          "Team"
        ],
        "responses": {
          "200": {
            "description": "Member listing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "userId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "email": {
                            "type": "string",
                            "format": "email"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "owner",
                              "admin",
                              "member"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "members": [
                    {
                      "userId": "e7c4a190-5b62-4d38-9f01-6a2e8c5b3d47",
                      "name": "Priya Raman",
                      "email": "priya@acme.io",
                      "role": "admin"
                    },
                    {
                      "userId": "3d9f5b28-7a41-4c06-8b52-1e4a7d0c9f63",
                      "name": "Sam Iyer",
                      "email": "sam@acme.io",
                      "role": "member"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/articles/{articleId}": {
      "get": {
        "operationId": "getArticle",
        "summary": "Get an article",
        "description": "Returns a single article, including its full markdown body. Requires the articles:read scope.",
        "tags": [
          "Articles"
        ],
        "parameters": [
          {
            "name": "articleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The article",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ArticleDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateArticle",
        "summary": "Update an article",
        "description": "Updates an article's content, category, or status. Publishing for the first time stamps publishedAt and indexes the article for search. Requires the articles:write scope.",
        "tags": [
          "Articles"
        ],
        "parameters": [
          {
            "name": "articleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "slug": {
                    "type": "string",
                    "maxLength": 300,
                    "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 200000
                  },
                  "excerpt": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "categoryId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "draft",
                      "published"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated article",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Article"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Slug already exists in this environment"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "operationId": "deleteArticle",
        "summary": "Delete an article",
        "description": "Permanently deletes an article. It disappears from the public help centre and from search at once; images it referenced from the article-images bucket are removed and their bytes released from the storage quota. To unpublish without losing it, PATCH status to draft instead. Requires the articles:write scope.",
        "tags": [
          "Articles"
        ],
        "parameters": [
          {
            "name": "articleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "imagesRemoved": {
                      "type": "integer",
                      "description": "Images removed from the article-images bucket."
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The project this belongs to."
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "id": "9c1f2e3d-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
                  "imagesRemoved": 0,
                  "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                  "projectSlug": "abc-salon"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/article-categories": {
      "get": {
        "operationId": "listArticleCategories",
        "summary": "List article categories",
        "description": "Lists the project's article categories for the key's environment — the valid categoryId values. Requires the articles:read scope.",
        "tags": [
          "Articles"
        ],
        "responses": {
          "200": {
            "description": "Category listing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "description": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sortOrder": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      },
      "post": {
        "operationId": "createArticleCategory",
        "summary": "Create an article category",
        "description": "Creates a category in the request's environment. The slug is derived from the name when omitted; a slug already used in the project + environment is a 409. Requires the articles:write scope.",
        "tags": [
          "Articles"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
                    "description": "Lowercase, hyphenated. Derived from the name when omitted."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "sortOrder": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                  }
                }
              },
              "example": {
                "name": "Getting started",
                "description": "First steps in the app"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created category",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "sortOrder": {
                      "type": "integer"
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The project this belongs to."
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                    }
                  }
                },
                "example": {
                  "id": "5d6e7f80-9a1b-4c2d-8e3f-4a5b6c7d8e9f",
                  "name": "Getting started",
                  "slug": "getting-started",
                  "description": "First steps in the app",
                  "sortOrder": 0,
                  "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                  "projectSlug": "abc-salon"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "A category with that slug already exists in this environment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "List webhooks",
        "description": "Lists the project's webhook endpoints for the key's environment. Signing secrets are never included. Requires the webhooks:manage scope.",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Webhook listing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  }
                },
                "example": {
                  "webhooks": [
                    {
                      "id": "1f9b7c34-8d25-4a06-b1e8-5c2d9f4a7031",
                      "url": "https://acme.io/hooks/helmdesk",
                      "events": [
                        "ticket.created",
                        "message.received"
                      ],
                      "active": true,
                      "slackFormat": false,
                      "createdAt": "2026-04-18T08:00:00.000Z",
                      "updatedAt": "2026-04-18T08:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      },
      "post": {
        "operationId": "createWebhook",
        "summary": "Create a webhook",
        "description": "Creates a webhook endpoint. The whsec_ signing secret is returned only in this response — store it immediately. Requires the webhooks:manage scope.",
        "tags": [
          "Webhooks"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "ticket.created",
                        "ticket.resolved",
                        "message.received",
                        "feedback.submitted",
                        "email.bounced",
                        "customer.created",
                        "customer.updated"
                      ]
                    }
                  },
                  "slackFormat": {
                    "type": "boolean",
                    "default": false
                  },
                  "active": {
                    "type": "boolean",
                    "default": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created webhook (includes the one-time signing secret)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "secret": {
                          "type": "string",
                          "example": "whsec_..."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/webhooks/{webhookId}": {
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Delete a webhook",
        "description": "Deletes a webhook endpoint. Deliveries stop immediately. Requires the webhooks:manage scope.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "summary": "Update a webhook",
        "description": "Changes an endpoint's URL, subscribed events, active flag, or Slack formatting. At least one field is required. The signing secret cannot be changed — delete and recreate the endpoint for a new one. Requires the `webhooks:manage` scope.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "ticket.created",
                        "ticket.resolved",
                        "message.received",
                        "feedback.submitted",
                        "email.bounced",
                        "customer.created",
                        "customer.updated"
                      ]
                    }
                  },
                  "active": {
                    "type": "boolean"
                  },
                  "slackFormat": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "active": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated endpoint (never the secret)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "active": {
                      "type": "boolean"
                    },
                    "slackFormat": {
                      "type": "boolean"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updatedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "id": "3f2b1c9a-8d7e-4f60-9b21-6c5d4e3f2a10",
                  "projectId": "5c1e7d0a-2b34-4f68-9a1d-3e8c7b6a5f40",
                  "projectSlug": "ledgerly",
                  "url": "https://example.com/hooks/helmdesk",
                  "events": [
                    "ticket.created",
                    "message.received"
                  ],
                  "active": false,
                  "slackFormat": false,
                  "createdAt": "2026-08-30T09:00:00.000Z",
                  "updatedAt": "2026-09-09T11:02:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/feedback/conversations": {
      "get": {
        "operationId": "listFeedbackConversations",
        "summary": "List feedback conversations",
        "description": "List feedback conversations, newest first. Filter by customer, status, rating, source, or date range. Requires the feedback:read scope and the Starter plan or higher.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated list: open,closed."
          },
          {
            "name": "rating",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated ratings 1-5."
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated list: request,api,widget,dashboard."
          },
          {
            "name": "customerEmail",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Substring match on the customer email."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Matches title or customer email."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ISO date lower bound (createdAt)."
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ISO date upper bound (createdAt)."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FeedbackConversation"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "conversations": [
                    {
                      "id": "8c2f6a41-9e05-4b73-a1d6-4f8b0c3e7952",
                      "projectId": "2d5e8f10-3a4b-4c6d-8e9f-0a1b2c3d4e5f",
                      "environment": "live",
                      "title": "Thoughts on the new dashboard",
                      "customerEmail": "dana@acme.io",
                      "customerName": "Dana Okafor",
                      "status": "open",
                      "rating": 4,
                      "ratingComment": "Much faster, but the filters are hard to find.",
                      "source": "request",
                      "requestId": "4b7d1e93-6c28-40f5-b9a1-7e3c5d8f2064",
                      "createdAt": "2026-07-15T10:04:00.000Z",
                      "updatedAt": "2026-07-15T10:04:00.000Z",
                      "closedAt": null
                    }
                  ],
                  "total": 28,
                  "page": 1,
                  "limit": 50
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "createFeedbackConversation",
        "summary": "Create a feedback conversation",
        "description": "Record customer feedback (e.g. collected inside your app). The body becomes the first message. Requires the feedback:write scope and the Starter plan or higher.",
        "tags": [
          "Feedback"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "body",
                  "customerEmail"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "customerEmail": {
                    "type": "string",
                    "format": "email"
                  },
                  "customerName": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "rating": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5
                  },
                  "ratingComment": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created conversation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackConversation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/feedback/conversations/{conversationId}": {
      "get": {
        "operationId": "getFeedbackConversation",
        "summary": "Get a feedback conversation",
        "description": "Fetch one conversation with its full message thread. Requires the feedback:read scope.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "conversationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Conversation with messages",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/FeedbackConversation"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "messages": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/FeedbackMessage"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "operationId": "updateFeedbackConversation",
        "summary": "Update a feedback conversation",
        "description": "Close or reopen a conversation, or set/clear the rating. Requires the feedback:write scope.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "conversationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "closed"
                    ]
                  },
                  "rating": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 5
                  },
                  "ratingComment": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated conversation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackConversation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/feedback/conversations/{conversationId}/messages": {
      "post": {
        "operationId": "replyToFeedbackConversation",
        "summary": "Reply to a feedback conversation",
        "description": "Post a team reply. The customer is emailed a link back to the thread. Requires the feedback:write scope.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "conversationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 10000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created message",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackMessage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/feedback/requests": {
      "get": {
        "operationId": "listFeedbackRequests",
        "summary": "List feedback requests",
        "description": "List outbound feedback requests with their funnel status (created, sent, opened, responded). Link-channel requests include their respondUrl. Requires the feedback:read scope.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated list: created,sent,opened,responded."
          },
          {
            "name": "customerEmail",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Substring match on the customer email."
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Matches title or customer email."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ISO date lower bound (createdAt)."
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ISO date upper bound (createdAt)."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FeedbackRequest"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "createFeedbackRequest",
        "summary": "Request feedback from a customer",
        "description": "Create a feedback request. With the default channel 'email', Helmdesk sends the built-in branded email with a one-click respond link (sandbox requests are captured, not delivered). With channel 'link', no email is sent — the response includes respondUrl for you to deliver yourself (e.g. inside your own coupon email); the funnel still tracks opened/responded. Requires the feedback:write scope and the Starter plan or higher.",
        "tags": [
          "Feedback"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "message",
                  "customerEmail"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 300,
                    "description": "Also used as the email subject."
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "customerEmail": {
                    "type": "string",
                    "format": "email"
                  },
                  "customerName": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "email",
                      "link"
                    ],
                    "default": "email",
                    "description": "'email' sends the built-in branded email; 'link' skips the email and returns respondUrl."
                  },
                  "internalNote": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Staff-only note, never shown to the customer."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackRequest"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/feedback/requests/{requestId}": {
      "get": {
        "operationId": "getFeedbackRequest",
        "summary": "Get a feedback request",
        "description": "Fetch one feedback request including its funnel status. Requires the feedback:read scope.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Feedback request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackRequest"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "delete": {
        "operationId": "deleteFeedbackRequest",
        "summary": "Delete a feedback request",
        "description": "Deletes a feedback request. Only the request goes: a conversation the customer started from it is kept. The respond link stops working; nobody is emailed. Requires the feedback:write scope and a plan with feedback conversations.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The project this belongs to."
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "id": "2e3f4a5b-6c7d-4e8f-9a0b-1c2d3e4f5a6b",
                  "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                  "projectSlug": "abc-salon"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/customers": {
      "get": {
        "operationId": "listCustomers",
        "summary": "List customers",
        "description": "List the customer directory for the project, most recently seen first. The directory builds itself: every ticket, feedback conversation, and feedback request upserts its customer by email. Requires the customers:read scope.",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Substring match on email, name, or company."
          },
          {
            "name": "externalId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact match on the customer's id in your own app."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter to customers carrying this tag. Repeat the parameter or comma-separate for several; multiple tags are ANDed (`?tag=billing:paying&tag=plan:gold` returns paying gold customers, not the union)."
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Customers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "customers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Customer"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "customers": [
                    {
                      "id": "b4d8e2f6-1a39-4c57-8e0b-3f7a9d2c6154",
                      "email": "dana@acme.io",
                      "name": "Dana Okafor",
                      "company": "Acme",
                      "externalId": "usr_8812",
                      "internalNote": null,
                      "metadata": {
                        "plan": "growth"
                      },
                      "environment": "live",
                      "lastSeenAt": "2026-07-21T09:14:03.221Z",
                      "createdAt": "2026-03-02T14:20:00.000Z",
                      "updatedAt": "2026-07-21T09:14:03.221Z"
                    }
                  ],
                  "total": 412,
                  "page": 1,
                  "limit": 50
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "createCustomer",
        "summary": "Create a customer",
        "description": "Explicitly create a customer record (e.g. syncing your own user base). Emails are stored lowercase and are unique per project and environment — creating a duplicate returns 409. Requires the customers:write scope.",
        "tags": [
          "Customers"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "company": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "externalId": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "The customer's id in your own app."
                  },
                  "internalNote": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Staff-only note — never shown to the customer."
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Arbitrary JSON, at most 8 KB serialized."
                  },
                  "tags": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string",
                      "maxLength": 64
                    },
                    "description": "Initial tags."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created customer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "A customer with this email already exists in this environment"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/customers/{customerId}": {
      "get": {
        "operationId": "getCustomer",
        "summary": "Get a customer by id or email",
        "description": "Fetch one customer including their interaction history — their tickets and feedback conversations (joined by email, most recent 50 of each). The path segment is either the customer's id or their email address (percent-encoded, e.g. /customers/jane%40example.com); the email is matched case-insensitively within the project and environment. Requires the customers:read scope.",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Customer id (UUID), or the customer's email address (percent-encode the @). Email lookups are GET-only."
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Customer with history",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Customer"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "history": {
                          "type": "object",
                          "properties": {
                            "tickets": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "format": "uuid"
                                  },
                                  "number": {
                                    "type": "integer"
                                  },
                                  "subject": {
                                    "type": "string"
                                  },
                                  "status": {
                                    "type": "string"
                                  },
                                  "createdAt": {
                                    "type": "string",
                                    "format": "date-time"
                                  }
                                }
                              }
                            },
                            "feedbackConversations": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "format": "uuid"
                                  },
                                  "title": {
                                    "type": "string"
                                  },
                                  "rating": {
                                    "type": [
                                      "integer",
                                      "null"
                                    ]
                                  },
                                  "status": {
                                    "type": "string"
                                  },
                                  "createdAt": {
                                    "type": "string",
                                    "format": "date-time"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateCustomer",
        "summary": "Update a customer",
        "description": "Partially update a customer. Email is immutable through the API — it is the join key back to tickets and feedback. Pass null to clear a field. Requires the customers:write scope. Addresses the customer by id only; to find the id from an email, GET /customers/{email} first.",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Customer id (UUID). Unlike GET, this route does not accept an email — an email here is a 422."
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "company": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "externalId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200
                  },
                  "internalNote": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000
                  },
                  "metadata": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "additionalProperties": true
                  },
                  "tags": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string",
                      "maxLength": 64
                    },
                    "description": "Replaces the customer's ENTIRE tag set. Omit to leave tags untouched; `[]` clears them. Use PUT /customers/{customerId}/tags to change one namespace."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated customer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "deleteCustomer",
        "summary": "Delete a customer",
        "description": "Hard-delete a customer record. Their tickets and feedback conversations are untouched (linkage is by email, not foreign key) — a new interaction from the same email re-creates the record. Requires the customers:write scope. Addresses the customer by id only; to find the id from an email, GET /customers/{email} first.",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Customer id (UUID). Unlike GET, this route does not accept an email — an email here is a 422."
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/customers/{customerId}/tags": {
      "post": {
        "operationId": "addCustomerTag",
        "summary": "Add a tag to a customer",
        "description": "Adds one tag to a customer. Tags are normalized (trimmed, whitespace-collapsed, lowercased) before storage, so `Plan:Gold` and `plan:gold` are the same tag. Idempotent. A tag may carry an optional `namespace:value` shape and may contain at most one `:`. Requires the customers:write scope.",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tag"
                ],
                "properties": {
                  "tag": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64,
                    "description": "e.g. `vip` or `plan:gold`"
                  }
                }
              },
              "example": {
                "tag": "plan:gold"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The customer id and its full tag list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "id": "b2c3d4e5-0000-4000-8000-000000000000",
                  "tags": [
                    "plan:gold",
                    "vip"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "operationId": "setCustomerTags",
        "summary": "Replace a customer's tags",
        "description": "Replaces tags in one call — the endpoint to use when syncing state from your own system. Without `namespace` the customer's ENTIRE tag set is replaced. With `namespace`, only tags in that namespace are replaced and every other tag is left alone, and bare values are qualified for you: `{\"namespace\":\"plan\",\"tags\":[\"premium\"]}` turns `plan:gold` into `plan:premium` and does not touch an unrelated `vip` tag.\n\nPrefer this over POST/DELETE whenever the thing you are setting has exactly one value at a time. Because it converges rather than mutates, a billing webhook can call it on every event without ever having to know the customer's previous plan in order to un-set it. Requires the customers:write scope.",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tags"
                ],
                "properties": {
                  "tags": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string",
                      "maxLength": 64
                    },
                    "description": "The tags the customer should end up with. Empty clears (the namespace, or everything)."
                  },
                  "namespace": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Limit the replacement to this namespace, e.g. `plan`. Omit to replace every tag."
                  }
                }
              },
              "example": {
                "namespace": "plan",
                "tags": [
                  "premium"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The customer id and its full tag list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "id": "b2c3d4e5-0000-4000-8000-000000000000",
                  "tags": [
                    "plan:gold",
                    "vip"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "removeCustomerTag",
        "summary": "Remove a tag from a customer",
        "description": "Removes one tag from a customer. Requires the customers:write scope.",
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The tag to remove"
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The customer id and its full tag list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "id": "b2c3d4e5-0000-4000-8000-000000000000",
                  "tags": [
                    "plan:gold",
                    "vip"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/log-issues/{issueId}": {
      "delete": {
        "operationId": "deleteLogIssue",
        "summary": "Delete a log issue and its events",
        "description": "Deletes a grouped log issue **and the raw log events behind it** (same fingerprint, project, and environment), in one transaction. Deleting only the rollup would leave the event stream full of events for an error the issues list says no longer exists.\n\nTwo things this deliberately does not do: it does not refund quota (`log_usage` counts what was ingested that month), and it does not alter other issues' counters. If the same error occurs again it comes back as a new issue.\n\nRequires the `logs:delete` scope — deliberately separate from `logs:write`, so a log-shipper key living in your production app cannot erase your error history.",
        "tags": [
          "Logs"
        ],
        "parameters": [
          {
            "name": "issueId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The deleted issue and how many events went with it",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "fingerprint": {
                      "type": "string"
                    },
                    "eventsDeleted": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "id": "b0a07054-d9c7-4d61-8d6e-5f84a2f49322",
                  "fingerprint": "61b154d62305e0d2e971e443876b0c08",
                  "eventsDeleted": 2
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "setLogIssueStatus",
        "summary": "Acknowledge, resolve, or reopen a log issue",
        "description": "Sets the status of a grouped log issue. `acknowledged` means \"seen it\"; `resolved` means \"fixed\" — a resolved issue reopens by itself if the same error recurs; `open` reopens it by hand. This is the reversible alternative to `DELETE`.\n\nRequires the `logs:write` scope (marking an error handled is not erasing it, so the shipper's scope suffices). Scoped to the project and environment of the request: an id from elsewhere is a 404.",
        "tags": [
          "Logs"
        ],
        "parameters": [
          {
            "name": "issueId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "acknowledged",
                      "resolved"
                    ]
                  }
                }
              },
              "example": {
                "status": "acknowledged"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated issue",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LogIssue"
                },
                "example": {
                  "id": "b0a07054-d9c7-4d61-8d6e-5f84a2f49322",
                  "projectId": "5c1e7d0a-2b34-4f68-9a1d-3e8c7b6a5f40",
                  "projectSlug": "ledgerly",
                  "fingerprint": "61b154d62305e0d2e971e443876b0c08",
                  "level": "error",
                  "scope": "checkout",
                  "sampleMessage": "Payment failed: card declined",
                  "eventCount": 7,
                  "status": "acknowledged",
                  "firstSeenAt": "2026-09-01T08:12:00.000Z",
                  "lastSeenAt": "2026-09-09T10:41:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/logs/delete": {
      "post": {
        "operationId": "deleteLogEvents",
        "summary": "Delete raw log events by id",
        "description": "Deletes up to 200 raw log events. Ids are scoped to the authenticated key's project and environment inside the query, so an id from elsewhere matches nothing rather than erroring — compare `deleted` against `requested` to detect that.\n\nGrouped issue counters are **not** recomputed: `eventCount` on a log issue is a lifetime total of what arrived, not a live count of what is still stored. To remove an error entirely, delete the issue.\n\nA POST rather than `DELETE /logs?ids=…` because 200 uuids exceed what some proxies will forward in a query string, and DELETE bodies are stripped by enough clients to be an interop risk. Requires the `logs:delete` scope.",
        "tags": [
          "Logs"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 200,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              },
              "example": {
                "ids": [
                  "3f2b1c4d-0000-4000-8000-000000000001"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many events were deleted, against how many were requested",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "integer"
                    },
                    "requested": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "deleted": 2,
                  "requested": 3
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "Validation failed"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ]
      }
    },
    "/projects": {
      "get": {
        "operationId": "listProjects",
        "summary": "List the projects this key can reach",
        "description": "Every project the key may act on, and the project a request without a Helmdesk-Project header would act on (null when the key reaches more than one). Requires the projects:read scope.",
        "tags": [
          "Projects"
        ],
        "responses": {
          "200": {
            "description": "Reachable projects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "allProjects": {
                      "type": "boolean",
                      "description": "True when the key reaches every project in the account, including ones created later."
                    },
                    "defaultProjectId": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    }
                  }
                },
                "example": {
                  "projects": [
                    {
                      "id": "5c1e7d0a-2b34-4f68-9a1d-3e8c7b6a5f40",
                      "slug": "ledgerly",
                      "name": "Ledgerly"
                    },
                    {
                      "id": "9b2f4c8e-1d7a-4e03-8c5b-2a6f9d3e1c74",
                      "slug": "nightjar",
                      "name": "Nightjar"
                    }
                  ],
                  "allProjects": true,
                  "defaultProjectId": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createProject",
        "summary": "Create a project",
        "description": "Creates a project in the account. Needs a key with access to all projects and the projects:manage scope. The slug is derived from the name when omitted. The response carries the env line to write into the app's .env so later sessions pin this project.",
        "tags": [
          "Projects"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 100
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
                    "minLength": 3,
                    "maxLength": 50
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "example": {
                "name": "ABC Salon",
                "description": "Booking app for ABC Salon"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "env": {
                      "type": "object",
                      "properties": {
                        "HELMDESK_PROJECT": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "project": {
                    "id": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                    "slug": "abc-salon",
                    "name": "ABC Salon",
                    "description": "Booking app for ABC Salon",
                    "createdAt": "2026-09-07T14:02:11.000Z"
                  },
                  "env": {
                    "HELMDESK_PROJECT": "abc-salon"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Slug already taken",
            "content": {
              "application/json": {
                "example": {
                  "error": {
                    "code": "conflict",
                    "message": "The slug \"abc-salon\" is already taken"
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/api-keys": {
      "post": {
        "operationId": "createApiKey",
        "summary": "Create an API key",
        "description": "Mints a key for an app you are building. Requires the api_keys:manage scope. The new key can only reach projects the calling key reaches, can only hold scopes the calling key holds (a full-access key may mint anything), and can only step down in environment — it defaults to the caller's plane, a live caller may pass environment: \"sandbox\" to mint a sandbox key, and a sandbox request can never mint a live key. The raw key is returned exactly once.",
        "tags": [
          "API keys"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "projects"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "ai:use",
                        "api_keys:manage",
                        "articles:read",
                        "articles:write",
                        "customers:read",
                        "customers:write",
                        "emails:manage",
                        "emails:send",
                        "feedback:read",
                        "feedback:write",
                        "logs:delete",
                        "logs:read",
                        "logs:write",
                        "projects:manage",
                        "projects:read",
                        "tickets:read",
                        "tickets:write",
                        "webhooks:manage"
                      ]
                    },
                    "description": "Empty = full access (only a full-access caller may mint one)."
                  },
                  "projects": {
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "all"
                        ],
                        "description": "Every project — needs an all-projects caller."
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "minItems": 1,
                        "maxItems": 50,
                        "description": "Project slugs or ids."
                      }
                    ]
                  },
                  "allowedIps": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 20
                  },
                  "environment": {
                    "type": "string",
                    "enum": [
                      "live",
                      "sandbox"
                    ],
                    "description": "Which plane the new key lives on. Defaults to the request's environment. A live caller may mint either — a sandbox key for an app's dev config is the point; a sandbox request (a sandbox key, or a live key acting as sandbox) may only mint sandbox (403 otherwise)."
                  }
                }
              },
              "example": {
                "name": "ABC Salon app",
                "scopes": [
                  "emails:send",
                  "tickets:write",
                  "logs:write",
                  "customers:write"
                ],
                "projects": [
                  "abc-salon"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key created — the raw key is shown once",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string",
                      "description": "The full secret. Store it now; it is never shown again."
                    },
                    "keyPrefix": {
                      "type": "string"
                    },
                    "lastFour": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "live",
                        "sandbox"
                      ]
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "allProjects": {
                      "type": "boolean"
                    },
                    "projects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "slug": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "id": "c4d1e2f3-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
                  "name": "ABC Salon app",
                  "key": "sk_live_3f9a…e2c1",
                  "keyPrefix": "sk_live_3f9a",
                  "lastFour": "e2c1",
                  "environment": "live",
                  "scopes": [
                    "emails:send",
                    "tickets:write",
                    "logs:write",
                    "customers:write"
                  ],
                  "allProjects": false,
                  "projects": [
                    {
                      "id": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                      "slug": "abc-salon"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/agent-activity": {
      "get": {
        "operationId": "listAgentActivity",
        "summary": "List agent activity",
        "description": "The review queue for a project: what its agents did that needs a human, newest first. By default only items with `reviewState: pending`; pass `state=all` to include reviewed and informational rows (weekly digests, email-health findings).\n\nEach item's `details.kind` says what approving or reverting will do:\n\n- `auto-reply` — the Auto-Responder drafted a reply. **Approve posts it to the ticket as a staff message and emails the customer**; revert discards the draft (or removes it from the thread if it was already posted).\n- `drafted-article` — KB Gap Analysis drafted an article. Approve publishes it; revert deletes the draft.\n- `closed-ticket` — Auto-Close closed a stale ticket (already done). Approve keeps it closed; revert reopens it to its previous status.\n- `log-issue` — Log Watch alerted on an error. Approve acknowledges the underlying log issue; revert just dismisses the alert.\n- `email-health` — findings only; either action just marks it reviewed.\n\nAgents act on the live plane, so agent activity is not environment-scoped: a sandbox request sees the same list. Requires the `agents:review` scope.",
        "tags": [
          "Agent activity"
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "reverted",
                "all"
              ],
              "default": "pending"
            },
            "description": "Which review state to list. `all` includes informational rows whose `reviewState` is null."
          },
          {
            "name": "workflowType",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only one agent: auto_close, auto_responder, kb_gap_analysis, weekly_digest, email_health, or log_watch."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Agent activity, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentActivityItem"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "c4f2a8e1-6b3d-4f0a-9e17-2d8b5c7a1f34",
                      "projectId": "0f4e7a2c-9b1d-4c6e-8a3f-5d2b7e9c1a40",
                      "projectSlug": "ledgerly",
                      "workflowType": "auto_responder",
                      "status": "success",
                      "summary": "Drafted a reply to #42 — Login link expired",
                      "details": {
                        "kind": "auto-reply",
                        "ticket": {
                          "id": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                          "number": 42,
                          "subject": "Login link expired"
                        },
                        "reply": "Hi Dana — magic links expire after 15 minutes. Request a new one from the sign-in page and open it on the same device.",
                        "sources": [
                          {
                            "id": "6a1e3b7c-2f4d-4e8a-b9c0-1d5f7a3e9b22",
                            "title": "Signing in with a magic link",
                            "slug": "magic-link-sign-in"
                          }
                        ]
                      },
                      "reviewState": "pending",
                      "executedAt": "2026-09-09T08:14:03.512Z"
                    }
                  ],
                  "total": 1,
                  "page": 1,
                  "limit": 25
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/agent-activity/{logId}/approve": {
      "post": {
        "operationId": "approveAgentActivity",
        "summary": "Approve an agent activity item",
        "description": "Keep what the agent did and mark the item `approved`. What that means depends on `details.kind`:\n\n- `auto-reply` — **the drafted reply is posted to the ticket as a staff message attributed to this API key, the ticket moves to `open`, and the customer is emailed.** For a sandbox ticket nothing is sent. Read the draft with `GET /agent-activity` first; this cannot be undone once the email is out.\n- `drafted-article` — the draft is published.\n- `log-issue` — the underlying log issue is acknowledged, so Log Watch stops re-alerting on it unless it regresses.\n- `closed-ticket`, `email-health` — only the review state changes.\n\nRecorded in the audit trail as `agent_activity.approved`. Returns `409` if the item was already approved or reverted. Requires the `agents:review` scope.",
        "tags": [
          "Agent activity"
        ],
        "parameters": [
          {
            "name": "logId",
            "in": "path",
            "required": true,
            "description": "The agent-activity item id, from `GET /agent-activity`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The item after approval",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentActivityItem"
                },
                "example": {
                  "id": "c4f2a8e1-6b3d-4f0a-9e17-2d8b5c7a1f34",
                  "projectId": "0f4e7a2c-9b1d-4c6e-8a3f-5d2b7e9c1a40",
                  "projectSlug": "ledgerly",
                  "workflowType": "auto_responder",
                  "status": "success",
                  "summary": "Drafted a reply to #42 — Login link expired",
                  "details": {
                    "kind": "auto-reply",
                    "ticket": {
                      "id": "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                      "number": 42,
                      "subject": "Login link expired"
                    },
                    "reply": "Hi Dana — magic links expire after 15 minutes. Request a new one from the sign-in page and open it on the same device.",
                    "sources": [
                      {
                        "id": "6a1e3b7c-2f4d-4e8a-b9c0-1d5f7a3e9b22",
                        "title": "Signing in with a magic link",
                        "slug": "magic-link-sign-in"
                      }
                    ]
                  },
                  "reviewState": "approved",
                  "executedAt": "2026-09-09T08:14:03.512Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The item was already reviewed (`reviewState` is not `pending`). Nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/agent-activity/{logId}/revert": {
      "post": {
        "operationId": "revertAgentActivity",
        "summary": "Revert an agent activity item",
        "description": "Undo what the agent did and mark the item `reverted`. Nothing is emailed. By `details.kind`:\n\n- `closed-ticket` — the ticket reopens to the status it held before Auto-Close, with `closedAt` / `closedBy` cleared.\n- `drafted-article` — the draft article is deleted (only while still unpublished).\n- `auto-reply` — a reply that was already posted is removed from the thread; an unposted draft is simply discarded.\n- `log-issue`, `email-health` — only the review state changes.\n\nRecorded in the audit trail as `agent_activity.reverted`. Returns `409` if the item was already approved or reverted. Requires the `agents:review` scope.",
        "tags": [
          "Agent activity"
        ],
        "parameters": [
          {
            "name": "logId",
            "in": "path",
            "required": true,
            "description": "The agent-activity item id, from `GET /agent-activity`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The item after reverting",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentActivityItem"
                },
                "example": {
                  "id": "e7a1c3d5-8f2b-4a6e-9c0d-3b5f7e1a9d28",
                  "projectId": "0f4e7a2c-9b1d-4c6e-8a3f-5d2b7e9c1a40",
                  "projectSlug": "ledgerly",
                  "workflowType": "auto_close",
                  "status": "success",
                  "summary": "Auto-closed #37 after 14 days without a customer reply",
                  "details": {
                    "kind": "closed-ticket",
                    "ticket": {
                      "id": "2c8e4a6f-1d3b-4e5a-8f7c-9a0b1c2d3e4f",
                      "number": 37,
                      "subject": "Invoice PDF is blank",
                      "previousStatus": "pending"
                    }
                  },
                  "reviewState": "reverted",
                  "executedAt": "2026-09-08T07:00:12.004Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The item was already reviewed (`reviewState` is not `pending`). Nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/tickets/bulk": {
      "post": {
        "operationId": "bulkUpdateTickets",
        "summary": "Bulk-update tickets",
        "description": "Apply one change — status, priority, and/or assignee — to up to 100 tickets at once, the same operation as the dashboard's bulk action bar. Each ticket is updated individually, so a resolved ticket still fires its ticket.resolved webhook and emails the customer the satisfaction survey (never for sandbox tickets); staff notification emails are deliberately NOT sent per ticket. An assignee that is not an active member fails that ticket (reported in failures) rather than the whole batch. Ids that do not belong to this project and environment are counted as skipped and never touched. One ticket.bulk_updated audit row is recorded. Requires the tickets:write scope.",
        "tags": [
          "Tickets"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkUpdateTicketsInput"
              },
              "example": {
                "ticketIds": [
                  "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                  "c4e2a8b1-6f3d-4e7a-9b21-0d5f8a7c3e19"
                ],
                "status": "closed",
                "assignedTo": null
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-batch counts and the ids that changed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkUpdateTicketsResult"
                },
                "example": {
                  "projectId": "3f2a1c44-7d21-4a5e-9f0b-2c8d6e1a9b77",
                  "projectSlug": "ledgerly",
                  "updated": 2,
                  "failed": 0,
                  "skipped": 0,
                  "ids": [
                    "9b1c7f2e-40d3-4a1b-9c2f-2f6b1e4a8d10",
                    "c4e2a8b1-6f3d-4e7a-9b21-0d5f8a7c3e19"
                  ],
                  "failures": []
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/emails": {
      "get": {
        "operationId": "listEmailSends",
        "summary": "List email sends (the email log)",
        "description": "The project's email log in the key's environment, newest first — the \"did the customer get it?\" endpoint. Each row carries the status, block reason and provider message id; the rendered HTML is never included (see GET /emails/{sendId}). Requires the emails:read scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring match on the recipient address."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated: queued, scheduled, sent, delivered, bounced, failed, blocked, sandboxed, held, canceled."
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "template",
                "custom"
              ]
            }
          },
          {
            "name": "templateKey",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact template key."
          },
          {
            "name": "dateFrom",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "ISO date or timestamp; sends created at or after it."
          },
          {
            "name": "dateTo",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "ISO date (through the end of that day) or timestamp."
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Email sends",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sends": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EmailSend"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "sends": [
                    {
                      "id": "d41f9a6b-88c5-4e2a-b7d0-1e5c3f9a2b64",
                      "projectId": "11111111-1111-4111-8111-111111111111",
                      "projectSlug": "ledgerly",
                      "kind": "template",
                      "status": "delivered",
                      "toEmail": "dana@acme.io",
                      "toName": "Dana Okafor",
                      "fromEmail": "hello@ledgerly.app",
                      "fromName": "Ledgerly",
                      "subject": "Welcome to Ledgerly",
                      "templateKey": "welcome",
                      "environment": "live",
                      "sendingMode": "live",
                      "scheduledAt": null,
                      "blocked": false,
                      "blockReason": null,
                      "providerMessageId": "<20260909101502.1.abc@brevo>",
                      "idempotencyKey": "signup-dana-2026",
                      "createdAt": "2026-09-09T10:15:00.000Z"
                    }
                  ],
                  "total": 1,
                  "page": 1,
                  "limit": 25
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/emails/{sendId}": {
      "get": {
        "operationId": "getEmailSend",
        "summary": "Get an email send with its event timeline",
        "description": "One send with its events in order (queued → sent → delivered / bounced …). Add ?includeHtml=true for the stored rendering — it is large, so it is off by default. 404 when the id is not in this project and environment. Requires the emails:read scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "sendId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "includeHtml",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Include the rendered HTML as `html`."
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Email send",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailSendDetail"
                },
                "example": {
                  "id": "d41f9a6b-88c5-4e2a-b7d0-1e5c3f9a2b64",
                  "projectId": "11111111-1111-4111-8111-111111111111",
                  "projectSlug": "ledgerly",
                  "kind": "template",
                  "status": "delivered",
                  "toEmail": "dana@acme.io",
                  "toName": "Dana Okafor",
                  "fromEmail": "hello@ledgerly.app",
                  "fromName": "Ledgerly",
                  "subject": "Welcome to Ledgerly",
                  "templateKey": "welcome",
                  "environment": "live",
                  "sendingMode": "live",
                  "scheduledAt": null,
                  "blocked": false,
                  "blockReason": null,
                  "providerMessageId": "<20260909101502.1.abc@brevo>",
                  "idempotencyKey": "signup-dana-2026",
                  "createdAt": "2026-09-09T10:15:00.000Z",
                  "variables": {
                    "name": "Dana",
                    "activationUrl": "https://app.ledgerly.app/activate?t=abc"
                  },
                  "events": [
                    {
                      "id": "5c1a0e2f-0b7d-4f0e-9c2a-1d3e4f5a6b7c",
                      "event": "queued",
                      "data": null,
                      "occurredAt": "2026-09-09T10:15:00.000Z",
                      "createdAt": "2026-09-09T10:15:00.000Z"
                    },
                    {
                      "id": "6d2b1f30-1c8e-4a1f-8d3b-2e4f5a6b7c8d",
                      "event": "sent",
                      "data": {
                        "messageId": "<20260909101502.1.abc@brevo>"
                      },
                      "occurredAt": "2026-09-09T10:15:02.000Z",
                      "createdAt": "2026-09-09T10:15:02.000Z"
                    },
                    {
                      "id": "7e3c2041-2d9f-4b20-9e4c-3f5a6b7c8d9e",
                      "event": "delivered",
                      "data": {
                        "messageId": "<20260909101502.1.abc@brevo>"
                      },
                      "occurredAt": "2026-09-09T10:15:09.000Z",
                      "createdAt": "2026-09-09T10:15:10.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/emails/{sendId}/resend": {
      "post": {
        "operationId": "resendEmail",
        "summary": "Resend an email to its original recipient",
        "description": "Sends an existing email again, as a new send: a template send re-renders with the template's current version, a custom send replays its stored subject and body. It takes the normal gateway path, so quota, suppression, sending mode, sandbox capture and idempotency all apply — a suppressed recipient comes back blocked, and in sandbox the resend is captured, not delivered. This reaches a real inbox on a live key. 404 outside the project/environment; 409 when the original cannot be replayed (its template was deleted, or a custom body was never stored). Requires the emails:send scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "sendId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The new send",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/SendEmailResult"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "originalId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "The send that was replayed."
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "id": "e52a0b7c-99d6-4f3b-c8e1-2f6d4a0b3c75",
                  "originalId": "d41f9a6b-88c5-4e2a-b7d0-1e5c3f9a2b64",
                  "projectId": "11111111-1111-4111-8111-111111111111",
                  "projectSlug": "ledgerly",
                  "kind": "template",
                  "status": "queued",
                  "to": "dana@acme.io",
                  "subject": "Welcome to Ledgerly",
                  "deduplicated": false,
                  "blocked": false,
                  "blockReason": null,
                  "scheduledAt": null,
                  "createdAt": "2026-09-09T11:02:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "conflict — the original cannot be replayed (template deleted, or custom body not stored)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/emails/{sendId}/cancel": {
      "post": {
        "operationId": "cancelScheduledEmail",
        "summary": "Cancel a scheduled email",
        "description": "Cancels a send that is still waiting in status `scheduled` (one created with sendAt). The row stays in the log as `canceled`. 404 outside the project/environment; 409 when it is no longer scheduled — it already fired, or it was never scheduled. Requires the emails:send scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "sendId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Canceled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "canceled"
                      ]
                    },
                    "to": {
                      "type": "string"
                    },
                    "subject": {
                      "type": "string"
                    },
                    "scheduledAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "blockReason": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "id": "d41f9a6b-88c5-4e2a-b7d0-1e5c3f9a2b64",
                  "projectId": "11111111-1111-4111-8111-111111111111",
                  "projectSlug": "ledgerly",
                  "status": "canceled",
                  "to": "dana@acme.io",
                  "subject": "Your trial ends in 3 days",
                  "scheduledAt": "2026-09-12T09:00:00.000Z",
                  "blockReason": "Canceled before scheduled send time",
                  "createdAt": "2026-09-09T10:15:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "conflict — the email is no longer scheduled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/suppressions": {
      "get": {
        "operationId": "listSuppressions",
        "summary": "List suppressed addresses",
        "description": "The addresses this project will not email in the key's environment: hard bounces and complaints recorded from the provider, unsubscribes, and manual blocks. A send to a suppressed address is recorded with status `blocked` instead of being delivered — this list is the first place to look when a customer is not getting your emails. Requires the emails:read scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring match."
          },
          {
            "name": "reason",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "bounce",
                "complaint",
                "unsubscribe",
                "manual"
              ]
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 25,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Suppressions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suppressions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Suppression"
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "suppressions": [
                    {
                      "id": "b7d2c4e6-3f19-4a8b-9c0d-5e6f7a8b9c0d",
                      "projectId": "11111111-1111-4111-8111-111111111111",
                      "projectSlug": "ledgerly",
                      "email": "bounced@example.com",
                      "reason": "bounce",
                      "environment": "live",
                      "createdAt": "2026-09-02T08:11:44.000Z"
                    }
                  ],
                  "total": 1,
                  "page": 1,
                  "limit": 25
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "addSuppression",
        "summary": "Suppress an address",
        "description": "Blocks an address: every later send to it (in this environment) is recorded as `blocked` instead of delivered. The address is lowercased. Idempotent — an address that is already suppressed comes back with `created: false` and its original reason, never a conflict. Requires the emails:manage scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "bounce",
                      "complaint",
                      "unsubscribe",
                      "manual"
                    ],
                    "default": "manual"
                  }
                }
              },
              "example": {
                "email": "dana@acme.io",
                "reason": "unsubscribe"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The suppression (existing or new)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Suppression"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "created": {
                          "type": "boolean",
                          "description": "false when the address was already suppressed."
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "id": "b7d2c4e6-3f19-4a8b-9c0d-5e6f7a8b9c0d",
                  "projectId": "11111111-1111-4111-8111-111111111111",
                  "projectSlug": "ledgerly",
                  "email": "dana@acme.io",
                  "reason": "unsubscribe",
                  "environment": "live",
                  "createdAt": "2026-09-02T08:11:44.000Z",
                  "created": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/suppressions/{suppressionId}": {
      "delete": {
        "operationId": "removeSuppression",
        "summary": "Remove a suppression",
        "description": "Lets mail flow to the address again. 404 when the id is not in this project and environment. Note that a provider hard bounce will suppress the address again on the next failed delivery. Requires the emails:manage scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "suppressionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "email": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "id": "b7d2c4e6-3f19-4a8b-9c0d-5e6f7a8b9c0d",
                  "email": "bounced@example.com"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/email-templates/{key}": {
      "delete": {
        "operationId": "deleteEmailTemplate",
        "summary": "Delete an email template",
        "description": "Deletes the project template with this key in the request's environment; its versions go with it. Email Logs rows rendered from it are kept, but sends that name the key afterwards fail. `@account/` keys are refused with 422 — shared templates are account-level and cannot be deleted through a project route. Requires the emails:manage scope.",
        "tags": [
          "Emails"
        ],
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The template key, e.g. welcome."
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Deletion result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "key": {
                      "type": "string"
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The project this belongs to."
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                    }
                  }
                },
                "example": {
                  "deleted": true,
                  "id": "7d2e4c19-3f5a-4b8e-9c01-2a6d8f4b7e35",
                  "key": "welcome",
                  "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                  "projectSlug": "abc-salon"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/api-keys/{keyId}": {
      "delete": {
        "operationId": "revokeApiKey",
        "summary": "Revoke an API key",
        "description": "Revokes a key in the caller's account. It stops authenticating on its next request and every app using it stops working immediately; there is no un-revoke — mint a new key instead. Only keys in the caller's account are visible (anything else is a 404), and a key cannot revoke itself (409) — use another key or the dashboard. Requires the api_keys:manage scope.",
        "tags": [
          "API keys"
        ],
        "parameters": [
          {
            "name": "keyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revocation result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "revoked": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "revoked": true,
                  "id": "c4d1e2f3-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
                  "name": "ABC Salon app"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The key tried to revoke itself",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sandbox/reset": {
      "post": {
        "operationId": "resetSandbox",
        "summary": "Reset the project's sandbox",
        "description": "Deletes every transactional sandbox row of the project — tickets (with messages, tags, attachments), feedback conversations and requests, email sends, customers, and log events/issues — and removes the storage objects behind the attachments. Sandbox templates, layouts, and articles survive (they are what gets promoted). This is the same purge as the dashboard's Reset sandbox button: it only ever touches sandbox rows, whatever the key's environment, so live data is never in scope. There is no undo. Requires the projects:manage scope.",
        "tags": [
          "Sandbox"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "What was deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The project this belongs to."
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "sandbox"
                      ]
                    },
                    "tickets": {
                      "type": "integer"
                    },
                    "feedbackConversations": {
                      "type": "integer"
                    },
                    "feedbackRequests": {
                      "type": "integer"
                    },
                    "emailSends": {
                      "type": "integer"
                    },
                    "customers": {
                      "type": "integer"
                    },
                    "logEvents": {
                      "type": "integer"
                    },
                    "logIssues": {
                      "type": "integer"
                    },
                    "attachments": {
                      "type": "integer"
                    },
                    "freedBytes": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
                  "projectSlug": "abc-salon",
                  "environment": "sandbox",
                  "tickets": 14,
                  "feedbackConversations": 2,
                  "feedbackRequests": 3,
                  "emailSends": 40,
                  "customers": 9,
                  "logEvents": 120,
                  "logIssues": 4,
                  "attachments": 1,
                  "freedBytes": 204800
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/webhooks/{webhookId}/test": {
      "post": {
        "operationId": "testWebhook",
        "summary": "Send a test delivery to a webhook",
        "description": "Delivers a synthetic `ticket.created` payload (ticket number 0, all-zero id, `data.test: true`) to this one endpoint — regardless of its active flag or subscriptions — and waits for the answer. The same delivery the dashboard's Test button makes; it is recorded, so it also appears in the deliveries list. Reaches the developer's own endpoint, never a customer. Requires the `webhooks:manage` scope.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "What the endpoint answered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhookId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "event": {
                      "type": "string",
                      "enum": [
                        "ticket.created"
                      ]
                    },
                    "deliveryId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "success": {
                      "type": "boolean",
                      "description": "True for a 2xx response."
                    },
                    "responseStatus": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Null when the request never got a response (timeout, DNS, refused)."
                    },
                    "responseBody": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The endpoint's response body, or the transport error, truncated to 1 KB."
                    },
                    "durationMs": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "webhookId": "3f2b1c9a-8d7e-4f60-9b21-6c5d4e3f2a10",
                  "projectId": "5c1e7d0a-2b34-4f68-9a1d-3e8c7b6a5f40",
                  "projectSlug": "ledgerly",
                  "url": "https://example.com/hooks/helmdesk",
                  "event": "ticket.created",
                  "deliveryId": "0d8c5b3a-2e1f-4a67-9c40-7b6a5d4e3f21",
                  "success": true,
                  "responseStatus": 200,
                  "responseBody": "ok",
                  "durationMs": 184
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks/{webhookId}/deliveries": {
      "get": {
        "operationId": "listWebhookDeliveries",
        "summary": "List a webhook's recent deliveries",
        "description": "The endpoint's most recent deliveries, newest first — the \"is my endpoint receiving?\" view. Every attempt is listed, successes and failures, including test deliveries. Requires the `webhooks:manage` scope.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhookId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "success",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Only successful (true) or only failed (false) deliveries."
          },
          {
            "name": "includePayload",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Include the delivered payload on each row."
          },
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Recent deliveries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhookId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "event": {
                            "type": "string"
                          },
                          "success": {
                            "type": "boolean"
                          },
                          "responseStatus": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "responseBody": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "attemptCount": {
                            "type": "integer"
                          },
                          "deliveredAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "payload": {
                            "type": "object",
                            "description": "Only with includePayload=true."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "webhookId": "3f2b1c9a-8d7e-4f60-9b21-6c5d4e3f2a10",
                  "projectId": "5c1e7d0a-2b34-4f68-9a1d-3e8c7b6a5f40",
                  "projectSlug": "ledgerly",
                  "url": "https://example.com/hooks/helmdesk",
                  "limit": 20,
                  "deliveries": [
                    {
                      "id": "0d8c5b3a-2e1f-4a67-9c40-7b6a5d4e3f21",
                      "event": "ticket.created",
                      "success": true,
                      "responseStatus": 200,
                      "responseBody": "ok",
                      "attemptCount": 1,
                      "deliveredAt": "2026-09-09T11:05:12.000Z"
                    },
                    {
                      "id": "6a1f4e2d-9c3b-4d58-8e07-2f1a0b9c8d76",
                      "event": "message.received",
                      "success": false,
                      "responseStatus": 502,
                      "responseBody": "Bad Gateway",
                      "attemptCount": 1,
                      "deliveredAt": "2026-09-09T09:47:30.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/projects/{projectId}": {
      "get": {
        "operationId": "getProject",
        "summary": "Get a project's setup view",
        "description": "Branding, the public support and knowledge-base URLs, whether email sending is configured (the from address, mode and provider name — never credentials), and the widget install snippet. The path parameter is a slug or an id, resolved among the projects this key can reach (`403 project.forbidden` otherwise).\n\n`widget.identitySecret` — the HMAC secret for identified widget users — is included only when the key holds `projects:manage`, and it is the secret for the request's environment (live or sandbox). Requires the `projects:read` scope.",
        "tags": [
          "Projects"
        ],
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Project slug or id."
          }
        ],
        "responses": {
          "200": {
            "description": "The setup view",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "branding": {
                      "type": "object",
                      "properties": {
                        "brandName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "logoUrl": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "iconUrl": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "primaryColor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "websiteUrl": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "urls": {
                      "type": "object",
                      "properties": {
                        "support": {
                          "type": "string",
                          "format": "uri"
                        },
                        "articles": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    },
                    "email": {
                      "type": "object",
                      "properties": {
                        "configured": {
                          "type": "boolean"
                        },
                        "fromEmail": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "fromName": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "replyTo": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sendingMode": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "live",
                            "paused",
                            "sandbox",
                            "allowlist",
                            null
                          ]
                        },
                        "provider": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "brevo",
                            "resend",
                            "sendgrid",
                            "ses",
                            "smtp",
                            null
                          ]
                        },
                        "supportEmail": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "widget": {
                      "type": "object",
                      "properties": {
                        "installSnippet": {
                          "type": "string"
                        },
                        "identitySecret": {
                          "type": "string",
                          "description": "Only with projects:manage."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "id": "5c1e7d0a-2b34-4f68-9a1d-3e8c7b6a5f40",
                  "slug": "ledgerly",
                  "name": "Ledgerly",
                  "description": "Invoicing for freelancers",
                  "createdAt": "2026-06-14T09:30:00.000Z",
                  "branding": {
                    "brandName": "Ledgerly",
                    "logoUrl": "https://cdn.ledgerly.app/logo.png",
                    "iconUrl": null,
                    "primaryColor": "#6366f1",
                    "websiteUrl": "https://ledgerly.app"
                  },
                  "urls": {
                    "support": "https://helmdesk.dev/p/ledgerly/support",
                    "articles": "https://helmdesk.dev/p/ledgerly/articles"
                  },
                  "email": {
                    "configured": true,
                    "fromEmail": "hello@ledgerly.app",
                    "fromName": "Ledgerly",
                    "replyTo": null,
                    "sendingMode": "live",
                    "provider": "resend",
                    "supportEmail": "support@ledgerly.app"
                  },
                  "widget": {
                    "installSnippet": "<script src=\"https://helmdesk.dev/widget.js\" data-project=\"ledgerly\"></script>"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/overview": {
      "get": {
        "operationId": "getOverview",
        "summary": "One snapshot of how the project is doing",
        "description": "Ticket queue (by status, awaiting a reply, stale, resolved this week), open error issues and last-24h event volume, email delivery this week, feedback, and agent actions awaiting review — for the project and environment of the request, in one call. Ticket definitions are the dashboard's own (\"stale\" = awaiting a reply for 5+ days). Sandbox reports 0 pending agent actions (agents run on the live plane). Requires the `projects:read` scope.",
        "tags": [
          "Overview"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProjectHeader"
          },
          {
            "$ref": "#/components/parameters/ProjectQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projectId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "projectSlug": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "live",
                        "sandbox"
                      ]
                    },
                    "tickets": {
                      "type": "object",
                      "properties": {
                        "new": {
                          "type": "integer"
                        },
                        "open": {
                          "type": "integer"
                        },
                        "pending": {
                          "type": "integer"
                        },
                        "awaitingReply": {
                          "type": "integer"
                        },
                        "resolvedLast7d": {
                          "type": "integer"
                        },
                        "staleAwaitingReply": {
                          "type": "integer"
                        }
                      }
                    },
                    "logs": {
                      "type": "object",
                      "properties": {
                        "openErrorIssues": {
                          "type": "integer"
                        },
                        "eventsLast24h": {
                          "type": "integer"
                        }
                      }
                    },
                    "emails": {
                      "type": "object",
                      "properties": {
                        "sentLast7d": {
                          "type": "integer"
                        },
                        "failedLast7d": {
                          "type": "integer"
                        },
                        "blockedLast7d": {
                          "type": "integer"
                        },
                        "scheduled": {
                          "type": "integer"
                        }
                      }
                    },
                    "feedback": {
                      "type": "object",
                      "properties": {
                        "openConversations": {
                          "type": "integer"
                        },
                        "responsesLast30d": {
                          "type": "integer"
                        },
                        "averageRating30d": {
                          "type": [
                            "number",
                            "null"
                          ]
                        }
                      }
                    },
                    "agents": {
                      "type": "object",
                      "properties": {
                        "pendingReview": {
                          "type": "integer"
                        }
                      }
                    },
                    "generatedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "projectId": "5c1e7d0a-2b34-4f68-9a1d-3e8c7b6a5f40",
                  "projectSlug": "ledgerly",
                  "environment": "live",
                  "tickets": {
                    "new": 3,
                    "open": 7,
                    "pending": 2,
                    "awaitingReply": 5,
                    "resolvedLast7d": 14,
                    "staleAwaitingReply": 1
                  },
                  "logs": {
                    "openErrorIssues": 2,
                    "eventsLast24h": 418
                  },
                  "emails": {
                    "sentLast7d": 1250,
                    "failedLast7d": 4,
                    "blockedLast7d": 1,
                    "scheduled": 12
                  },
                  "feedback": {
                    "openConversations": 3,
                    "responsesLast30d": 21,
                    "averageRating30d": 4.3
                  },
                  "agents": {
                    "pendingReview": 2
                  },
                  "generatedAt": "2026-09-09T11:10:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Plan and quota usage for the account",
        "description": "Where the account stands against its plan — the \"why did I hit a cap\" answer. Every meter is reported next to its limit; **`-1` means unlimited**, the same convention as the plan table. Monthly meters (tickets, emails, logs, agent runs) reset on the 1st of the UTC month given in `period`. Tickets count live tickets opened this month; `overage: true` means the allowance is used up — paid tiers keep accepting and bill the excess, Free pauses intake. Account-scoped: no project is needed. Requires the `projects:read` scope.",
        "tags": [
          "Overview"
        ],
        "responses": {
          "200": {
            "description": "Plan and meters",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "object",
                      "properties": {
                        "tier": {
                          "type": "string",
                          "enum": [
                            "free",
                            "starter",
                            "growth",
                            "scale",
                            "enterprise"
                          ]
                        },
                        "status": {
                          "type": "string"
                        }
                      }
                    },
                    "period": {
                      "type": "object",
                      "properties": {
                        "start": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "tickets": {
                      "type": "object",
                      "properties": {
                        "used": {
                          "type": "integer"
                        },
                        "allowance": {
                          "type": "integer",
                          "description": "-1 = unlimited."
                        },
                        "overage": {
                          "type": "boolean"
                        }
                      }
                    },
                    "emails": {
                      "type": "object",
                      "properties": {
                        "used": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "-1 = unlimited."
                        }
                      }
                    },
                    "logs": {
                      "type": "object",
                      "properties": {
                        "used": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "-1 = unlimited."
                        },
                        "retentionDays": {
                          "type": "integer"
                        }
                      }
                    },
                    "storage": {
                      "type": "object",
                      "properties": {
                        "usedBytes": {
                          "type": "integer"
                        },
                        "limitBytes": {
                          "type": "integer",
                          "description": "-1 = unlimited."
                        }
                      }
                    },
                    "agentRuns": {
                      "type": "object",
                      "properties": {
                        "used": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "-1 = unlimited."
                        }
                      }
                    },
                    "projects": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "-1 = unlimited."
                        }
                      }
                    },
                    "teamMembers": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "-1 = unlimited."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "plan": {
                    "tier": "starter",
                    "status": "active"
                  },
                  "period": {
                    "start": "2026-09-01T00:00:00.000Z",
                    "end": "2026-10-01T00:00:00.000Z"
                  },
                  "tickets": {
                    "used": 412,
                    "allowance": 1000,
                    "overage": false
                  },
                  "emails": {
                    "used": 9800,
                    "limit": 25000
                  },
                  "logs": {
                    "used": 61200,
                    "limit": 100000,
                    "retentionDays": 7
                  },
                  "storage": {
                    "usedBytes": 184549376,
                    "limitBytes": 5368709120
                  },
                  "agentRuns": {
                    "used": 120,
                    "limit": 500
                  },
                  "projects": {
                    "count": 2,
                    "limit": 3
                  },
                  "teamMembers": {
                    "count": 3,
                    "limit": -1
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Account API key (sk_live_... or sk_sandbox_...) from Admin Settings → API keys."
      }
    },
    "schemas": {
      "Ticket": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "number": {
            "type": "integer"
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "open",
              "pending",
              "resolved",
              "closed"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "urgent"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "bug",
              "feature_request",
              "billing",
              "howto",
              "other"
            ]
          },
          "assignedTo": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Account member the ticket is assigned to, or null."
          },
          "customerEmail": {
            "type": "string"
          },
          "customerName": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastSenderType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "customer",
              "staff",
              null
            ],
            "description": "Who wrote the latest message on the thread (internal notes included). \"customer\" means they are waiting on you. Null on a ticket with no messages."
          },
          "lastMessageAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the latest message on the thread was written. Null on a ticket with no messages."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TicketDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "number": {
            "type": "integer"
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "open",
              "pending",
              "resolved",
              "closed"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "urgent"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "closedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "customer": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "description": "Convenience copy of customerEmail/customerName."
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TicketMessage"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "customerEmail": {
            "type": "string",
            "format": "email"
          },
          "customerName": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "TicketMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "ticketId": {
            "type": "string",
            "format": "uuid"
          },
          "senderType": {
            "type": "string",
            "enum": [
              "customer",
              "staff"
            ]
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "internal"
            ],
            "description": "\"internal\" is a staff-only note: never shown on the customer portal, never emailed. Only staff messages can be internal."
          },
          "body": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Attachment"
            },
            "description": "Files attached to this message. Empty when there are none."
          }
        }
      },
      "CreateTicketInput": {
        "type": "object",
        "required": [
          "subject",
          "body",
          "customerEmail"
        ],
        "properties": {
          "subject": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000
          },
          "customerEmail": {
            "type": "string",
            "format": "email"
          },
          "customerName": {
            "type": "string",
            "maxLength": 200
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "urgent"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "bug",
              "feature_request",
              "billing",
              "howto",
              "other"
            ]
          }
        }
      },
      "UpdateTicketInput": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "new",
              "open",
              "pending",
              "resolved",
              "closed"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "urgent"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "bug",
              "feature_request",
              "billing",
              "howto",
              "other"
            ]
          },
          "assignedTo": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "SendEmailInput": {
        "description": "Exactly one shape: a template send (`templateKey`), or a custom one-off send (`subject` + `body`). Mixing them is a validation error.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/SendTemplateEmailInput"
          },
          {
            "$ref": "#/components/schemas/SendCustomEmailInput"
          }
        ]
      },
      "SendEmailResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "kind": {
            "type": "string",
            "enum": [
              "template",
              "custom"
            ],
            "description": "'template' for a template send, 'custom' for a one-off subject + body."
          },
          "status": {
            "type": "string",
            "description": "queued, sent, scheduled, sandboxed, held, or failed."
          },
          "to": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "deduplicated": {
            "type": "boolean"
          },
          "blocked": {
            "type": "boolean"
          },
          "blockReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "scheduledAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When a scheduled send will fire; null for immediate sends."
          }
        }
      },
      "SendEmailBatchItem": {
        "type": "object",
        "required": [
          "templateKey",
          "to"
        ],
        "properties": {
          "templateKey": {
            "type": "string"
          },
          "to": {
            "type": "object",
            "required": [
              "email"
            ],
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "variables": {
            "type": "object",
            "additionalProperties": true
          },
          "environment": {
            "type": "string",
            "enum": [
              "production",
              "staging",
              "development"
            ]
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Per-item idempotency key. Prevents duplicate sends for the same key within 24h."
          }
        }
      },
      "SendEmailBatchInput": {
        "type": "object",
        "required": [
          "emails"
        ],
        "properties": {
          "emails": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/SendEmailBatchItem"
            }
          }
        }
      },
      "SendEmailBatchResult": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "total": {
            "type": "integer",
            "description": "Number of items in the batch"
          },
          "sent": {
            "type": "integer",
            "description": "Items queued successfully"
          },
          "blocked": {
            "type": "integer",
            "description": "Items blocked (e.g. suppressed or sandbox-redirected)"
          },
          "failed": {
            "type": "integer",
            "description": "Items that errored"
          },
          "results": {
            "type": "array",
            "description": "Per-item outcome, in request order. Each slot is a success or an error object.",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/BatchItemSuccess"
                },
                {
                  "$ref": "#/components/schemas/BatchItemError"
                }
              ]
            }
          }
        }
      },
      "BatchItemSuccess": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Zero-based position in the request emails array"
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "deduplicated": {
            "type": "boolean"
          },
          "blocked": {
            "type": "boolean"
          },
          "blockReason": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BatchItemError": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Zero-based position in the request emails array"
          },
          "to": {
            "type": "string"
          },
          "error": {
            "type": "string",
            "description": "Failure message for this item"
          }
        }
      },
      "TemplateSchema": {
        "type": "object",
        "description": "The variables a template expects, detected from its current version. Built-in branding variables (brandName, brandLogoUrl, supportEmail, …) are excluded — Helmdesk fills those in for you.",
        "properties": {
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "subject": {
            "type": "string",
            "description": "Subject line of the current version, with variables unresolved."
          },
          "variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of the variables referenced by the template body."
          }
        }
      },
      "ImportResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "template",
              "layout",
              "partial"
            ]
          },
          "scope": {
            "type": "string",
            "enum": [
              "account"
            ],
            "description": "Only present for account-level imports"
          },
          "subject": {
            "type": "string",
            "description": "Subject line (templates only)"
          },
          "variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Detected template variables"
          },
          "referencedPartials": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Partial references found in the template"
          },
          "layoutKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "The layout key the template is now stored with, or null if it has none. Omitting the `layout` field on re-import leaves an existing layout in place, and this reports it."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Non-blocking problems: missing partial references, unresolved layouts. The import still succeeded."
          },
          "layoutId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Id of the linked layout. Project templates only; null when nothing was linked."
          },
          "layoutLinked": {
            "type": "boolean",
            "description": "Whether the template will actually render inside a layout."
          }
        },
        "description": "The stored result of an import. For templates, `layoutKey` is the layout the template will render with and `layoutLinked` says whether that layout currently resolves. If you requested a layout and `layoutLinked` is false, read `warnings`."
      },
      "ApiError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object"
              }
            }
          }
        }
      },
      "Article": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published"
            ]
          },
          "excerpt": {
            "type": [
              "string",
              "null"
            ]
          },
          "categoryId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ArticleDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Article"
          },
          {
            "type": "object",
            "properties": {
              "body": {
                "type": "string",
                "description": "Markdown"
              },
              "helpfulYes": {
                "type": "integer"
              },
              "helpfulNo": {
                "type": "integer"
              },
              "publishedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "updatedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        ]
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "ticket.created",
                "ticket.resolved",
                "message.received",
                "feedback.submitted",
                "email.bounced",
                "customer.created",
                "customer.updated"
              ]
            }
          },
          "active": {
            "type": "boolean"
          },
          "slackFormat": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreateArticleInput": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "slug": {
            "type": "string",
            "description": "Lowercase, hyphenated. Auto-generated from the title when omitted.",
            "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
          },
          "body": {
            "type": "string",
            "maxLength": 200000,
            "description": "Markdown"
          },
          "excerpt": {
            "type": "string",
            "maxLength": 500
          },
          "categoryId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published"
            ],
            "default": "draft"
          }
        }
      },
      "LogEventInput": {
        "type": "object",
        "required": [
          "level",
          "message"
        ],
        "properties": {
          "level": {
            "type": "string",
            "enum": [
              "debug",
              "info",
              "warn",
              "error",
              "fatal"
            ]
          },
          "message": {
            "type": "string",
            "maxLength": 4000
          },
          "scope": {
            "type": "string",
            "maxLength": 120,
            "description": "Logger scope/source, e.g. 'email' or 'api.checkout'."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Structured context. Must serialize to at most 8192 bytes."
          }
        }
      },
      "LogEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "level": {
            "type": "string",
            "enum": [
              "debug",
              "info",
              "warn",
              "error",
              "fatal"
            ]
          },
          "scope": {
            "type": [
              "string",
              "null"
            ]
          },
          "message": {
            "type": "string"
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "fingerprint": {
            "type": "string",
            "description": "Hash of level + scope + normalized message; groups events into issues."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LogIssue": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "fingerprint": {
            "type": "string"
          },
          "level": {
            "type": "string",
            "enum": [
              "debug",
              "info",
              "warn",
              "error",
              "fatal"
            ]
          },
          "scope": {
            "type": [
              "string",
              "null"
            ]
          },
          "sampleMessage": {
            "type": "string",
            "description": "Most recent raw message for this fingerprint."
          },
          "eventCount": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "acknowledged",
              "resolved"
            ]
          },
          "firstSeenAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastSeenAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FeedbackConversation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ]
          },
          "title": {
            "type": "string"
          },
          "customerEmail": {
            "type": "string",
            "format": "email"
          },
          "customerName": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "closed"
            ]
          },
          "rating": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 5
          },
          "ratingComment": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "request",
              "api",
              "widget",
              "dashboard"
            ]
          },
          "requestId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "closedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "FeedbackMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "senderType": {
            "type": "string",
            "enum": [
              "customer",
              "staff",
              "api"
            ]
          },
          "body": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "attachments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "fileName": {
                  "type": "string"
                },
                "fileSize": {
                  "type": "integer"
                },
                "mimeType": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "FeedbackRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ]
          },
          "title": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "customerEmail": {
            "type": "string",
            "format": "email"
          },
          "customerName": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "sent",
              "opened",
              "responded"
            ]
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "link"
            ],
            "description": "'email': Helmdesk sent the built-in branded email. 'link': the owner delivers the respond URL themselves."
          },
          "internalNote": {
            "type": [
              "string",
              "null"
            ],
            "description": "Staff-only note, never shown to the customer."
          },
          "respondUrl": {
            "type": "string",
            "description": "Public respond URL — present on link-channel requests only. Embed it in your own email or message."
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "openedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "respondedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Customer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Always lowercase; unique per project and environment."
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "externalId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The customer's id in your own app."
          },
          "internalNote": {
            "type": [
              "string",
              "null"
            ],
            "description": "Staff-only note — never shown to the customer."
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Free-text tags, alphabetical. May carry an optional `namespace:value` shape, e.g. `plan:gold`."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ]
          },
          "lastSeenAt": {
            "type": "string",
            "format": "date-time",
            "description": "Bumped on every interaction (ticket, feedback conversation, feedback request)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CreatedTicket": {
        "type": "object",
        "description": "The confirmation returned when a ticket is created. Narrower than the Ticket you get back from a list or fetch — call GET /tickets/{ticketId} for the full record.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "number": {
            "type": "integer",
            "description": "Human-facing ticket number, unique per project."
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "open",
              "pending",
              "resolved",
              "closed"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "urgent"
            ]
          },
          "customer": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ArticleSearchResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "excerpt": {
            "type": [
              "string",
              "null"
            ]
          },
          "categoryId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Attachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fileName": {
            "type": "string"
          },
          "fileSize": {
            "type": "integer",
            "description": "Bytes."
          },
          "mimeType": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public URL of the stored file."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SendTemplateEmailInput": {
        "type": "object",
        "required": [
          "templateKey",
          "to"
        ],
        "description": "Send a registered template. `variables` fill the template; the subject comes from the template.",
        "properties": {
          "templateKey": {
            "type": "string",
            "description": "Template key. Use the @account/ prefix for a shared template."
          },
          "to": {
            "type": "object",
            "required": [
              "email"
            ],
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "variables": {
            "type": "object",
            "additionalProperties": true
          },
          "environment": {
            "type": "string",
            "enum": [
              "production",
              "staging",
              "development"
            ]
          },
          "sendAt": {
            "type": "string",
            "format": "date-time",
            "description": "Schedule the send for later (up to 30 days ahead). A past timestamp sends immediately."
          }
        }
      },
      "SendCustomEmailInput": {
        "type": "object",
        "required": [
          "subject",
          "body",
          "to"
        ],
        "description": "Send a one-off custom email: a subject and a Markdown body, no template. Same delivery rules as a template send; shows in Email Logs as a custom send. For anything sent more than once, use a template.",
        "properties": {
          "to": {
            "type": "object",
            "required": [
              "email"
            ],
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "subject": {
            "type": "string",
            "maxLength": 200,
            "description": "Plain text. Not a template: {{…}} is sent literally."
          },
          "body": {
            "type": "string",
            "maxLength": 50000,
            "description": "Markdown (headings, bold, lists, links, images, code). Raw HTML is escaped. Rendered to email-safe HTML and wrapped in the layout."
          },
          "layout": {
            "type": "string",
            "description": "Key of a project layout (or @account/<key>) to wrap the body in. Omit for the built-in Helm Basic shell with the project branding. An unknown key fails the send."
          },
          "environment": {
            "type": "string",
            "enum": [
              "production",
              "staging",
              "development"
            ]
          },
          "sendAt": {
            "type": "string",
            "format": "date-time",
            "description": "Schedule the send for later (up to 30 days ahead). A past timestamp sends immediately."
          }
        }
      },
      "AgentActivityDetails": {
        "description": "What the agent touched. Discriminated by `kind`; informational rows (weekly digests) carry `null`.",
        "oneOf": [
          {
            "title": "closed-ticket",
            "type": "object",
            "description": "Auto-Close closed a stale ticket. Revert reopens it to `previousStatus`.",
            "required": [
              "kind",
              "ticket"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "closed-ticket"
                ]
              },
              "ticket": {
                "type": "object",
                "required": [
                  "id",
                  "number",
                  "subject",
                  "previousStatus"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "number": {
                    "type": "integer"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "previousStatus": {
                    "type": "string",
                    "enum": [
                      "new",
                      "open",
                      "pending",
                      "resolved",
                      "closed"
                    ]
                  }
                }
              }
            }
          },
          {
            "title": "drafted-article",
            "type": "object",
            "description": "KB Gap Analysis drafted an article (status `draft`). Approve publishes it; revert deletes the draft.",
            "required": [
              "kind",
              "article"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "drafted-article"
                ]
              },
              "article": {
                "type": "object",
                "required": [
                  "id",
                  "title"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "title": {
                    "type": "string"
                  }
                }
              }
            }
          },
          {
            "title": "auto-reply",
            "type": "object",
            "description": "The Auto-Responder drafted a reply. While `messageId` is absent the reply is a draft: approve posts it as a staff message and emails the customer. When `messageId` is present the reply is already on the thread and revert removes it.",
            "required": [
              "kind",
              "ticket"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "auto-reply"
                ]
              },
              "ticket": {
                "type": "object",
                "required": [
                  "id",
                  "number",
                  "subject"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "number": {
                    "type": "integer"
                  },
                  "subject": {
                    "type": "string"
                  }
                }
              },
              "reply": {
                "type": "string",
                "description": "The drafted reply body (Markdown/plain text)."
              },
              "messageId": {
                "type": "string",
                "format": "uuid",
                "description": "Set once the reply is on the ticket thread."
              },
              "sources": {
                "type": "array",
                "description": "Knowledge-base articles the draft drew on.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "title": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          {
            "title": "email-health",
            "type": "object",
            "description": "Email Health findings. Informational — approve and revert only change the review state.",
            "required": [
              "kind",
              "findings"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "email-health"
                ]
              },
              "findings": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "severity",
                    "title",
                    "detail"
                  ],
                  "properties": {
                    "severity": {
                      "type": "string",
                      "enum": [
                        "high",
                        "medium",
                        "low"
                      ]
                    },
                    "title": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          {
            "title": "log-issue",
            "type": "object",
            "description": "Log Watch alerted on a new or recurring error. Approve (\"Got it\") also acknowledges the underlying log issue.",
            "required": [
              "kind",
              "issue"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "log-issue"
                ]
              },
              "issue": {
                "type": "object",
                "required": [
                  "id",
                  "fingerprint",
                  "level",
                  "sampleMessage",
                  "trigger"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "fingerprint": {
                    "type": "string"
                  },
                  "level": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string",
                    "nullable": true
                  },
                  "sampleMessage": {
                    "type": "string"
                  },
                  "totalCount": {
                    "type": "integer"
                  },
                  "occurrencesInWindow": {
                    "type": "integer"
                  },
                  "windowMinutes": {
                    "type": "integer"
                  },
                  "firstSeenAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "trigger": {
                    "type": "string",
                    "enum": [
                      "new",
                      "recurring"
                    ]
                  }
                }
              }
            }
          }
        ]
      },
      "AgentActivityItem": {
        "type": "object",
        "description": "One thing an agent did. `reviewState` is `pending` while it waits for you, `approved` / `reverted` once reviewed, and `null` for informational rows.",
        "required": [
          "id",
          "projectId",
          "projectSlug",
          "workflowType",
          "status",
          "summary",
          "details",
          "reviewState",
          "executedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid"
          },
          "projectSlug": {
            "type": "string",
            "nullable": true
          },
          "workflowType": {
            "type": "string",
            "description": "Which agent: auto_close, auto_responder, kb_gap_analysis, weekly_digest, email_health, log_watch."
          },
          "status": {
            "type": "string",
            "description": "How the run ended, e.g. success, skipped, failed."
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "details": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AgentActivityDetails"
              },
              {
                "type": "null"
              }
            ]
          },
          "reviewState": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "reverted"
            ],
            "nullable": true
          },
          "executedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BulkUpdateTicketsInput": {
        "type": "object",
        "required": [
          "ticketIds"
        ],
        "properties": {
          "ticketIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "minItems": 1,
            "maxItems": 100,
            "description": "Up to 100 ticket ids. Ids outside the project or the request environment are skipped, never touched."
          },
          "status": {
            "type": "string",
            "enum": [
              "new",
              "open",
              "pending",
              "resolved",
              "closed"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "urgent"
            ]
          },
          "assignedTo": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "An active account member to assign every ticket to, or null to unassign. Omit to leave assignment untouched."
          }
        },
        "description": "At least one of status, priority, or assignedTo is required. Category is deliberately not a bulk field."
      },
      "BulkUpdateTicketsResult": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string",
            "format": "uuid"
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated": {
            "type": "integer",
            "description": "Tickets the patch was applied to."
          },
          "failed": {
            "type": "integer",
            "description": "Tickets that matched but could not be updated (see failures)."
          },
          "skipped": {
            "type": "integer",
            "description": "Requested ids that are not in this project + environment. Never touched."
          },
          "ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The ids that were actually updated."
          },
          "failures": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "message": {
                  "type": "string"
                }
              }
            },
            "description": "One entry per failed ticket, with the reason (for example an assignee who is not an active member)."
          }
        }
      },
      "EmailSend": {
        "type": "object",
        "description": "One row of the email log. Never carries the rendered HTML — ask GET /emails/{sendId}?includeHtml=true for that.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "kind": {
            "type": "string",
            "enum": [
              "template",
              "custom"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "scheduled",
              "sent",
              "delivered",
              "bounced",
              "failed",
              "blocked",
              "sandboxed",
              "held",
              "canceled"
            ],
            "description": "queued (accepted, handing to the provider), scheduled (waiting for sendAt), sent (provider accepted it), delivered / bounced (provider webhook), failed (provider refused), blocked (suppressed recipient or sending mode), sandboxed (captured, never delivered), held (sending paused), canceled."
          },
          "toEmail": {
            "type": "string"
          },
          "toName": {
            "type": [
              "string",
              "null"
            ]
          },
          "fromEmail": {
            "type": "string"
          },
          "fromName": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "templateKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "The template it was rendered from; null for a custom send or a deleted template."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ]
          },
          "sendingMode": {
            "type": "string",
            "enum": [
              "live",
              "paused",
              "sandbox",
              "allowlist"
            ],
            "description": "The project's sending mode at the time of the send."
          },
          "scheduledAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "blocked": {
            "type": "boolean"
          },
          "blockReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it was blocked or held — 'Recipient is suppressed', 'Sending paused', a domain outside the allowlist, or the cancel note."
          },
          "providerMessageId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The provider's id for the message, set once the provider accepted it."
          },
          "idempotencyKey": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmailEvent": {
        "type": "object",
        "description": "One step in a send's timeline. `queued` and `sent`/`bounced` are written by the gateway; `delivered`, `opened`, `clicked`, `bounced`, `complained`, `unsubscribed` arrive from the provider's webhook.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "event": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "opened",
              "clicked",
              "bounced",
              "complained",
              "unsubscribed"
            ]
          },
          "data": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Provider details: messageId on sent, error on bounced, reason on a blocked/held queue event, canceled: true on a cancel."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EmailSendDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/EmailSend"
          },
          {
            "type": "object",
            "properties": {
              "variables": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": true,
                "description": "The inputs: the template variables, or { subject, body, layout } for a custom send."
              },
              "events": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/EmailEvent"
                },
                "description": "Chronological."
              },
              "html": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The stored rendering. Only present with ?includeHtml=true."
              }
            }
          }
        ]
      },
      "Suppression": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "projectId": {
            "type": "string",
            "format": "uuid",
            "description": "The project this belongs to. Every project-scoped object carries it, so a key that reaches several projects can always tell what it is looking at."
          },
          "projectSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project's slug — the value you pass in Helmdesk-Project or ?project=."
          },
          "email": {
            "type": "string",
            "description": "Lowercased."
          },
          "reason": {
            "type": "string",
            "enum": [
              "bounce",
              "complaint",
              "unsubscribe",
              "manual"
            ]
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "sandbox"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Insufficient scope or plan (e.g. batch sending requires the Starter plan or higher)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Request body validation failed",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      }
    },
    "parameters": {
      "ProjectHeader": {
        "name": "Helmdesk-Project",
        "in": "header",
        "required": false,
        "description": "Project slug or id to act on. Required when the key can reach more than one project; ignored when the key reaches exactly one (which must match if given).",
        "schema": {
          "type": "string"
        }
      },
      "ProjectQuery": {
        "name": "project",
        "in": "query",
        "required": false,
        "description": "Same as the Helmdesk-Project header, as a query parameter. The header wins when both are present.",
        "schema": {
          "type": "string"
        }
      }
    }
  },
  "tags": [
    {
      "name": "Tickets",
      "description": "Support tickets, staff replies, and tags. The core helpdesk resource."
    },
    {
      "name": "Customers",
      "description": "The per-project customer directory. Built automatically from every ticket and feedback interaction; `externalId` links a customer to their record in your own app."
    },
    {
      "name": "Emails",
      "description": "Transactional sending plus the Handlebars template system (templates, layouts, partials)."
    },
    {
      "name": "Articles",
      "description": "Knowledge-base articles and categories, including search."
    },
    {
      "name": "Feedback",
      "description": "Feedback requests and conversations — ratings and sentiment, kept separate from tickets. Requires the Starter plan or higher."
    },
    {
      "name": "Logs",
      "description": "Application log ingestion and the grouped error issues the Log Watch agent monitors."
    },
    {
      "name": "Webhooks",
      "description": "Endpoint registration for event delivery."
    },
    {
      "name": "Team",
      "description": "Account members — the valid targets for ticket assignment."
    },
    {
      "name": "Projects",
      "description": "The projects an API key can reach, and creating new ones."
    },
    {
      "name": "API keys",
      "description": "Minting keys for the apps you build (needs api_keys:manage)."
    },
    {
      "name": "Agent activity",
      "description": "The review queue: what the project's agents did that needs a human — drafted replies, drafted articles, auto-closed tickets, Log Watch alerts, email-health findings — with approve and revert. Requires the agents:review scope."
    },
    {
      "name": "Sandbox",
      "description": "The sandbox data plane — reset it between test runs. Never touches live data."
    },
    {
      "name": "Overview",
      "description": "One-call snapshots: how a project is doing right now (`/overview`) and where the account stands against its plan (`/usage`)."
    }
  ],
  "externalDocs": {
    "description": "Helmdesk developer documentation",
    "url": "https://helmdesk.dev/docs"
  }
}
