Tenant API v1 — Help Desk
Base path: /api/tenant/v1
Middleware: auth:tenant-api, tenant.user, not.suspended, verified, module:help-desk, plus permission middleware / policies.
No hard module_dependencies — Help Desk installs standalone. contact_id and company_id are optional; supplying either requires the corresponding module (contacts / companies) to be entitled on the workspace, enforced by LinkableContact / LinkableCompany.
Assignee scoping: without help-desk.assign (and not superadmin), list/board/stats/view/update/close/reopen only include tickets where assigned_to is the current user.
Stats & board
GET /help-desk/stats
Same filters as list (minus pagination/sort). Response:
{
"total_tickets": 0,
"my_tickets": 0,
"open": 0,
"in_progress": 0,
"waiting": 0,
"resolved": 0,
"closed": 0,
"overdue": 0,
"sla_breached": 0,
"sla_at_risk": 0,
"scope": "org | mine"
}overdue counts tickets in open, in_progress, or waiting with due_at before now (UTC instant comparison). sla_breached / sla_at_risk use SLA clock columns (UtcInstant); at-risk means due within the next hour and not yet breached.
GET /help-desk/board
One column per status (open, in_progress, waiting, resolved, closed): status, label, ticket_count, tickets[]. Honors the same filters as list (including assignee scoping). Optional per_column (1–100, default 50). Fixed enum columns — same pattern as Tasks, not Opportunities stages.
Tickets CRUD
GET /help-desk
Query: search (matches subject, number, or description), status (open|in_progress|waiting|resolved|closed), priority (low|medium|high|urgent), category_id, contact_id, company_id, assigned_to (unassigned or user id), my_tickets, overdue, sla_breached (1 / true / response / resolve), sla_at_risk, trashed (true|only), sort, direction, page, per_page.
List items include status, priority, source (manual|email), SLA clock fields (sla_policy_id, first_responded_at, sla_response_due_at, sla_resolve_due_at, sla_response_breached_at, sla_resolve_breached_at), embedded sla_policy / category, due_at, contact/company refs (when linked), knowledge_base_articles summary, assignee/creator refs, and latest_note.
POST /help-desk
Body: subject (required), description (optional), priority (optional, default medium), category_id (optional — tenant help_desk_categories id, must be active; defaults to seeded Other), contact_id (optional — Contacts module must be entitled), company_id (optional — Companies module must be entitled), knowledge_base_article_ids (optional int[] — Knowledge Base module must be entitled; each id validated via LinkableKnowledgeBaseArticle: published for view-only actors, or draft/archived when actor has knowledge-base.update), due_at (optional ISO datetime), assigned_to (optional — requires actor to hold help-desk.assign; otherwise defaults to creator).
Status always starts at open; number is auto-generated (HD-00001, configurable via help_desk_number_prefix tenant setting).
GET /help-desk/{id}
Includes category, contact, company, assignee, creator, knowledge_base_articles (when KB entitled), notes, and timeline activities. Contact refs include email / phone when present (for soft-gated Communication Template WhatsApp). Embedded notes and timeline/domain activities are newest-first (created_at DESC, then id DESC).
PUT /help-desk/{id}
Partial update of non-closed tickets only. Closed tickets return 422 (Closed tickets cannot be edited.). Accepts knowledge_base_article_ids (same rules as create). Assignment after create uses POST /help-desk/{id}/assign.
DELETE /help-desk/{id}
Soft delete. Permission: help-desk.delete.
POST /help-desk/{id}/restore
Permission: help-desk.restore.
DELETE /help-desk/{id}/force
Permanently delete a soft-deleted ticket. Permission: help-desk.force.delete (owner/superadmin only by default).
Actions
POST /help-desk/{id}/assign
{ "assigned_to": number|null }
Permission: help-desk.assign.
POST /help-desk/{id}/close
Transitions to closed from open, in_progress, waiting, or resolved. Permission: help-desk.close (assignee-scoped unless the actor has help-desk.assign or is superadmin).
POST /help-desk/{id}/reopen
Transitions to open from resolved or closed. Permission: help-desk.reopen (assignee-scoped unless the actor has help-desk.assign or is superadmin).
POST /help-desk/{id}/status
{ "status": "open"|"in_progress"|"waiting"|"resolved"|"closed" }
Permission: help-desk.update. Rejects disallowed transitions with a 422 validation error on status. Records a status_changed timeline entry. Prefer explicit close / reopen when those semantics apply (they use dedicated permissions).
POST /help-desk/{id}/notes
{ "body": string }
Permission: help-desk.update.
GET /help-desk/{id}/timeline
Domain timeline entries (created, updated, assigned, status_changed, note_added, articles_synced, deleted, restored).
PUT /help-desk/{id}/articles
{ "article_ids": number[] } — replace linked Knowledge Base articles (same validation as knowledge_base_article_ids). Permission: help-desk.update. Records articles_synced when the set changes.
Help desk categories
Categories use the same module:help-desk gate and help-desk.* permissions (no separate permission family). Listing lazy-seeds General / Technical / Billing / Account / Other when missing. Starter slugs (general|technical|billing|account|other) are not changed on rename. Listing does not restore a soft-deleted starter except Other.
GET /help-desk-categories— list (help-desk.view)POST /help-desk-categories— create (help-desk.create). Body:name(required); optionalslug,sort_order,is_activeGET|PUT|DELETE /help-desk-categories/{helpDeskCategory}— view, update, soft-delete (view/update/delete)POST /help-desk-categories/{helpDeskCategory}/restore— restore (help-desk.restore)DELETE /help-desk-categories/{helpDeskCategory}/force— permanently delete a soft-deleted category (help-desk.force.delete)
Delete and force-delete return 422 if the category slug is other, or if any tickets (including trashed, for force) still reference the category.
SLA policies (1.3.0)
Same module:help-desk gate and help-desk.* permissions (no separate family).
GET /help-desk-sla-policies— list (help-desk.view)POST /help-desk-sla-policies— create (help-desk.create). Body:name(required);first_response_minutes,resolve_minutes(required integers ≥ 1); optionalcategory_id,priority(null= any),is_activeGET|PUT|DELETE /help-desk-sla-policies/{helpDeskSlaPolicy}— view / update / soft-deletePOST …/restore,DELETE …/force— restore / force-delete
Policy matching on ticket create (and open priority/category change): category+priority > priority-only > category-only > default (both null). Breach scan: php artisan help-desk:scan-sla-breaches.
Shared mailboxes / email intake (1.4.0)
Dedicated IMAP support mailboxes (not personal Email module accounts). Passwords are encrypted at rest and never returned in resources (has_password boolean only).
GET /help-desk/mailboxes— list (help-desk.view)POST /help-desk/mailboxes— create (help-desk.create). Body:name,address,imap_host,imap_port,imap_encryption(ssl|tls|none),imap_username,imap_password, optionalis_activeGET|PUT|DELETE /help-desk/mailboxes/{helpDeskMailbox}— view / update / delete (view/update/delete)POST /help-desk/mailboxes/{helpDeskMailbox}/test— test IMAP (help-desk.update)POST /help-desk/mailboxes/{helpDeskMailbox}/sync— dispatchSyncHelpDeskMailboxJobon queuehelp-desk-ingest(help-desk.update)
Scheduler: help-desk:sync-mailboxes every minute. Inbound without a ticket number creates source=email tickets; subject/body matching {prefix}\d{5,} appends an unauthored note.