Skip to content

Tickets

Create, list, update, and manage support tickets programmatically. Tickets are the core of the Helmdesk helpdesk — each belongs to a project and is linked to a customer.

Which project?

Every ticket belongs to one project. A key limited to one project implies it — send nothing extra. A key that reaches several projects (the kind you give a coding agent) names the project on each request with the Helmdesk-Project header (slug or id) or ?project=; without it the API answers 400 project.required with the projects it could have meant. Every object in a response carries projectId and projectSlug, so you can always tell what you are looking at.

curl
curl https://helmdesk.dev/api/v1/tickets \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: ledgerly"
SDK
// Pin the project once — or set HELMDESK_PROJECT and omit it.
const helmdesk = new HelmdeskClient({ apiKey, project: 'ledgerly' })
await helmdesk.tickets.list()

See Authentication for how keys and projects fit together.

POST/api/v1/tickets

Create a support ticket. A customer record is automatically created or matched by email.

Request

const ticket = await helmdesk.tickets.create({
  subject: 'Cannot login',
  body: 'I keep getting a 403 error when...',
  customerEmail: 'user@example.com',
  customerName: 'Jane Smith',
  priority: 'high',
})
GET/api/v1/tickets

List tickets with pagination. Filter by status, priority, category, assignee, exact ticket number, awaitingReply, or search by subject/email. The assignee filter accepts a user id, a comma-separated list, or 'unassigned'. Every row carries lastSenderType and lastMessageAt — the latest message on the thread, computed in the same query.

Request

const { tickets, total } = await helmdesk.tickets.list({
  status: 'open',
  priority: 'high',
  assignee: 'unassigned',
  category: 'billing',
  number: 42, // exact "#42" lookup
  awaitingReply: true, // only tickets waiting on you
  page: 1,
  limit: 25,
})
// tickets[0].lastSenderType -> 'customer' | 'staff' | null
// tickets[0].lastMessageAt  -> ISO timestamp | null

Tickets awaiting a reply

The question a support tool gets asked most is "what needs me?". Rather than reading every thread, pass awaitingReply=true: it returns tickets in status new, open, or pending whose latest message came from the customer — the same definition the Agents Activity inbox uses. Each row also says who wrote last (lastSenderType) and when (lastMessageAt), so a list is enough to triage.

curl
curl "https://helmdesk.dev/api/v1/tickets?awaitingReply=true&limit=50" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: ledgerly"
SDK
const { tickets } = await helmdesk.tickets.list({ awaitingReply: true })
for (const t of tickets) {
  console.log('#' + t.number + ' ' + t.subject + ' — customer wrote at ' + t.lastMessageAt)
}
GET/api/v1/tickets/:id

Get full ticket details including messages, tags, and customer info.

Request

const ticket = await helmdesk.tickets.get('ticket-uuid')
// ticket.messages, ticket.tags, ticket.customer
PATCH/api/v1/tickets/:id

Update ticket status, priority, category, or assignee. The assignee must be an active member of your account — discover valid ids with the members endpoint below.

Request

await helmdesk.tickets.update('ticket-uuid', {
  status: 'resolved',
  priority: 'low',
  assignedTo: 'user-uuid', // or null to unassign
})
POST/api/v1/tickets/:id/messages

Reply to a ticket as staff. With visibility 'public' (the default) the reply is appended to the thread, emailed to the customer, and a 'new' ticket transitions to 'open'. With visibility 'internal' it is a staff-only note — see below.

Request

await helmdesk.tickets.addMessage('ticket-uuid', {
  body: 'Thanks for reaching out! Here\'s how to fix it...',
})

Internal notes

A message with visibility: "internal" is for the team only: it sits on the thread and comes back from GET /tickets/:id marked internal, but it is never shown on the customer portal, never emailed, and it does not move the ticket status. Use it for context and handoffs — what you found, what you tried, why the ticket is waiting.

curl
curl -X POST https://helmdesk.dev/api/v1/tickets/TICKET_ID/messages \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: ledgerly" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Escalated to eng — ETA Friday.", "visibility": "internal" }'
SDK
await helmdesk.tickets.addNote('ticket-uuid', 'Escalated to eng — ETA Friday.')

// The long form, if you are already building the input:
await helmdesk.tickets.addMessage('ticket-uuid', {
  body: 'Escalated to eng — ETA Friday.',
  visibility: 'internal',
})

Only staff can write notes

Internal notes are always staff messages. The customer-facing surfaces (the portal, the widget, email replies) cannot create one, and the portal thread filters them out.

Bulk update

Apply one change — status, priority, and/or assignee — to up to 100 tickets in a call, the same operation as the dashboard 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 in sandbox); staff notification emails are deliberately not sent per ticket. Ids outside the project or environment are counted as skipped and never touched; a ticket that cannot take the patch — say an assignee who is not an active member — lands in failures instead of failing the batch.

POST/api/v1/tickets/bulktickets:write

Body: ticketIds (1–100 UUIDs) plus at least one of status, priority, assignedTo (null unassigns). Category is deliberately not a bulk field. Responds with updated, failed, skipped, the ids that changed, and one failures entry per ticket that could not be updated.

Request

const result = await helmdesk.tickets.bulkUpdate({
  ticketIds: ['ticket-1', 'ticket-2', 'ticket-3'],
  status: 'closed',
  assignedTo: null, // unassign; omit to leave assignment alone
})
// { updated: 3, failed: 0, skipped: 0, ids: [...], failures: [] }

Response

{
  "projectId": "3f2a1c44-7d21-4a5e-9f0b-2c8d6e1a9b77",
  "projectSlug": "ledgerly",
  "updated": 2,
  "failed": 1,
  "skipped": 0,
  "ids": ["9b1c7f2e-…", "c4e2a8b1-…"],
  "failures": [{ "id": "7d3e9a10-…", "message": "Validation failed" }]
}
curl
curl -X POST https://helmdesk.dev/api/v1/tickets/bulk \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: ledgerly" \
  -H "Content-Type: application/json" \
  -d '{ "ticketIds": ["9b1c7f2e-…", "c4e2a8b1-…"], "status": "closed", "assignedTo": null }'
GET/api/v1/members

List the active members of your account (id, name, email, role) — the valid assignedTo values for ticket assignment. Requires the tickets:read scope.

Request

const { members } = await helmdesk.team.list()
// members[0].userId → use as assignedTo
POST/api/v1/tickets/:id/tags

Add a tag (label) to a ticket for triage or categorization. Idempotent — adding an existing tag is a no-op. Returns the ticket's full tag list.

Request

const { tags } = await helmdesk.tickets.addTag('ticket-uuid', 'needs-engineering')
DELETE/api/v1/tickets/:id/tags

Remove a tag from a ticket. Returns the ticket's remaining tags.

Request

const { tags } = await helmdesk.tickets.removeTag('ticket-uuid', 'needs-engineering')

Deleting a ticket

Everything the API can create it can delete, through the same code path the dashboard uses. Deleting a ticket removes its messages, tags, and attachments (the storage objects are removed and the bytes released from your quota). The customer is not emailed. There is no undo — to finish a ticket without losing its history, set its status to closed instead.

DELETE/api/v1/tickets/:idtickets:write

Permanently delete a ticket. Scoped to the project and the request's environment — a live id given to a sandbox request is a 404, never a cross-plane delete.

Request

// SDK
const result = await helmdesk.tickets.delete('ticket-uuid')

// curl
curl -X DELETE https://helmdesk.dev/api/v1/tickets/ticket-uuid \
  -H "Authorization: Bearer sk_live_..." \
  -H "Helmdesk-Project: abc-salon"

Response

{
  "deleted": true,
  "id": "3b8e1c2d-9f4a-4e6b-8c7d-1a2b3c4d5e6f",
  "attachmentsRemoved": 2,
  "projectId": "7f3a9c2e-4b1d-4e8f-a6c5-2d9b8e7f1a30",
  "projectSlug": "abc-salon"
}

Ticket statuses

newopenpendingresolvedclosed

Priority levels

lowmediumhighurgent