Tickets
Customer-support tickets with messages, internal notes, attachments, tags, SLA tracking, canned responses, and a full audit trail — separate from internal-engineering issues.
Tickets are ProxifAI’s customer-support workspace. They’re a separate primitive from issues — issues track internal engineering work, tickets track external requests from customers (or anyone outside the team). Both share the same RBAC, audit, and @-mention plumbing, but tickets carry a richer surface around messaging, SLAs, and channel intake.
Anatomy of a ticket
| Field | Notes |
|---|---|
ticketNumber | Per-org auto-incrementing integer (#42); shown in the UI |
subject | Short headline, like an email subject |
description | Long-form description; renders as Markdown |
status | One of 5 values (see Status) |
priority | low / medium / high / urgent |
category | general / bug / feature / billing / account / technical |
source | Where the ticket came from — web / email / portal / api |
teamId | Optional — scope the ticket to one team |
assigneeId | Person responsible for the next response |
creatorId | Internal author, if created from inside (otherwise the contact info applies) |
contactEmail, contactName | External requester — the user the ticket is for (not necessarily a ProxifAI user) |
slaDeadline, firstResponseAt, resolvedAt, closedAt | SLA-tracking timestamps (see SLA) |
tags[] | Many-to-many via TicketTag |
messageCount | Cached count of TicketMessage rows |
Status
Five values from TicketStatus. The flow generally moves left-to-right but can revisit pending or on_hold at any time:
| Status | Meaning |
|---|---|
open | New; no agent has worked it yet |
pending | Awaiting customer reply — firstResponseAt is set, but they need to respond before we can continue |
on_hold | Waiting on something internal (a fix, an external dep, a decision) — pauses SLA accrual |
resolved | Agent considers it done; sets resolvedAt. Customer has a window to push back |
closed | Final state; sets closedAt. Reopening creates a follow-up ticket |
Status changes are logged as TicketActivity entries.
Messages and internal notes
A ticket is a thread. Every reply is a TicketMessage row with one bit that changes everything:
- Public message (
isInternalNote: false) — visible to the contact, included in any email/portal response - Internal note (
isInternalNote: true) — only visible to org members, used for “FYI: this customer also reported X last month” or “@alice can you look at this?”
Internal notes still mention-trigger agents, so you can write @triage-agent please categorize this inside an internal note and the agent runs without the customer ever seeing it. Both message types support @-mentions, file attachments via TicketAttachment, and code blocks.
# Public reply — goes to the customer
pfai api POST /api/v1/tickets/{id}/messages \
-d '{"content": "Hi! We just shipped a fix in v1.4.3 — please try again.", "isInternalNote": false}'
# Internal note — team-only
pfai api POST /api/v1/tickets/{id}/messages \
-d '{"content": "@bob this is the third report this week — file an issue?", "isInternalNote": true}'
Activity timeline
Every change appends a TicketActivity row. Nine activity types tracked (models.go):
created · status_changed · priority_changed · assigned
category_changed · tag_added · tag_removed
message_added · note_added
Activities carry oldValue / newValue so the UI can render diffs (“priority medium → urgent”) without needing to reconstruct from older rows.
Tags
TicketTag is a per-team taxonomy: a name + a color. Useful for slicing the ticket list by ad-hoc dimensions the built-in category enum can’t capture — e.g. regression, enterprise-customer, tracking-1.5-release. Tags are managed independently of tickets; create them once, reuse forever.
pfai api POST /api/v1/ticket-tags -d '{"name": "regression", "color": "#ef4444"}'
SLA policies
SLAPolicy rows define response/resolution targets per priority (and optionally per team):
| Field | Notes |
|---|---|
priority | Which TicketPriority this policy applies to |
firstResponseMinutes | Target time to first response |
resolutionMinutes | Target time to resolved |
teamId | Optional — scope policy to a single team’s tickets |
When a ticket is created (or its priority changes), the matching policy stamps slaDeadline. The first message a non-creator posts sets firstResponseAt; SLA breach badges appear in the UI when now > slaDeadline and the ticket isn’t yet resolved.
Canned responses
CannedResponse is a saved snippet of message content (title + body) for the replies your team writes 50 times a week. Insert one from the message composer; canned responses can be team-scoped (just support’s) or org-wide.
CLI
pfai ticket is a thin wrapper over the REST surface:
pfai ticket list # list tickets in the current org
pfai ticket list --status open --priority high # filter
pfai ticket view <id> # full ticket including thread
pfai ticket create --subject "Login broken" --priority high --category technical
pfai ticket update <id> --status resolved --assignee alice
Subcommands inherit the standard kubectl-style verbs (ls/list, view/get, delete/rm) and the global --output/--json/--query flags.
REST endpoints
| Method · Path | Purpose |
|---|---|
GET /api/v1/tickets | List with filters (status, priority, category, assigneeId, teamId, tag) |
POST /api/v1/tickets | Create — minimum is {subject}, everything else has defaults |
GET /api/v1/tickets/stats | Aggregate counts (per-status, per-priority, SLA-breach count) for dashboards |
GET /api/v1/tickets/{id} | Read with embedded assignee, creator, tags |
PATCH /api/v1/tickets/{id} | Update fields |
DELETE /api/v1/tickets/{id} | Soft-delete |
POST /api/v1/tickets/{id}/assign | Reassign — separate endpoint for proper activity logging |
GET /api/v1/tickets/{id}/messages | Paginated thread |
POST /api/v1/tickets/{id}/messages | Post a message; isInternalNote toggles visibility |
PATCH /api/v1/tickets/{id}/messages/{msgId} | Edit your own message |
DELETE /api/v1/tickets/{id}/messages/{msgId} | Delete your own message |
GET /api/v1/tickets/{id}/activities | Audit log |
* /api/v1/ticket-tags | Tag CRUD |
* /api/v1/canned-responses | Canned-response CRUD |
* /api/v1/sla-policies | SLA-policy CRUD |
How tickets relate to issues
| Question | Answer |
|---|---|
| Is a ticket an issue? | No — separate table, separate model, separate UI route. |
| Can a ticket spawn an issue? | Yes — convert “this is a real bug we need to fix” via the Convert to issue action, which creates an Issue linked back to the ticket via the issue’s metadata. |
| Do they share assignees, mentions, audit? | Yes — same User table, same @-mention parser, same RBAC permission checks. |
| Do they share status enums? | No — IssueStatus (6) and TicketStatus (5) are independent. |
| Where do they show up in the rail? | Issues under Plan; tickets under their own UI tab (or surfaced in the customer-facing portal at /portal). |
The split is intentional: issues are unbounded engineering work, tickets are bounded support requests with response targets. Mixing them dilutes both — you don’t want a backlog grooming session blocked because somebody filed an SLA-driven ticket overnight.
See also
Internal-engineering work tracking — share assignees and @-mentions with tickets.
Scope tickets, SLA policies, and canned responses to a specific team.
Approval, alert, and incident notifications surface in the same Inbox as ticket assignments.
React to `ticket.created` events with workflow YAML or trigger rules — auto-assign, auto-tag, dispatch a triage agent.