Spaces:
Sleeping
Ticket Comments API
Team collaboration system for tickets with threading, mentions, and attachments.
Overview
The ticket comments system enables team members to collaborate on tickets through:
- Internal comments (team only) and external comments (client-visible)
- Threading (replies to comments)
- Mentions (tag team members)
- Attachments (link documents and images)
- Edit tracking (audit trail)
Comment Attachments vs Ticket Images
Comment Attachments (Informal/Conversational):
- Quick photo sharing during discussions
- Reference diagrams, PDFs, screenshots
- Part of team collaboration flow
- Can be edited/deleted with comment
Ticket Images (Formal/Evidence):
- Official before/after photos
- Installation proof for completion
- Linked to progress/incident reports
- Part of formal ticket record
- PM verification workflow
Use comment attachments for: "Hey team, look at this issue I found"
Use ticket images for: "Official proof of installation completion"
API Endpoints
1. Create Comment
POST /api/v1/tickets/{ticket_id}/comments
Request Body:
{
"comment_text": "Customer prefers morning visits between 9-11am",
"is_internal": true,
"comment_type": "note",
"parent_comment_id": null,
"mentioned_user_ids": [],
"attachment_document_ids": []
}
Response: 201 Created
{
"id": "uuid",
"ticket_id": "uuid",
"user_id": "uuid",
"user_name": "John Doe",
"comment_text": "Customer prefers morning visits between 9-11am",
"is_internal": true,
"comment_type": "note",
"parent_comment_id": null,
"mentioned_user_ids": [],
"attachment_document_ids": [],
"is_edited": false,
"edited_at": null,
"edited_by_user_id": null,
"edited_by_user_name": null,
"additional_metadata": {},
"created_at": "2025-11-30T12:00:00Z",
"updated_at": "2025-11-30T12:00:00Z",
"reply_count": 0
}
2. Update Comment
PUT /api/v1/comments/{comment_id}
Authorization: Comment author only
Request Body:
{
"comment_text": "Updated: Customer now prefers afternoon visits"
}
Response: 200 OK
{
"id": "uuid",
"comment_text": "Updated: Customer now prefers afternoon visits",
"is_edited": true,
"edited_at": "2025-11-30T12:05:00Z",
"edited_by_user_id": "uuid",
"edited_by_user_name": "John Doe",
...
}
3. Delete Comment
DELETE /api/v1/comments/{comment_id}
Authorization: Comment author or Project Manager
Response: 200 OK
{
"success": true,
"message": "Comment deleted successfully",
"comment_id": "uuid"
}
4. List Comments
GET /api/v1/tickets/{ticket_id}/comments?page=1&page_size=50&is_internal=true&parent_only=false
Query Parameters:
page(int): Page number (default: 1)page_size(int): Items per page (default: 50, max: 100)is_internal(bool): Filter by internal/externalcomment_type(string): Filter by type (note, issue, resolution, question, update)parent_only(bool): Show only top-level comments (default: false)
Response: 200 OK
{
"comments": [...],
"total": 25,
"page": 1,
"page_size": 50,
"pages": 1
}
5. Get Comment Replies
GET /api/v1/comments/{comment_id}/replies
Response: 200 OK
[
{
"id": "uuid",
"parent_comment_id": "parent-uuid",
"comment_text": "I'll handle this",
"user_name": "Jane Smith",
...
}
]
Comment Types
| Type | Description | Use Case |
|---|---|---|
note |
General note | "Customer prefers morning visits" |
issue |
Problem/blocker | "Customer location is incorrect" |
resolution |
Solution/fix | "Updated customer address" |
question |
Question for team | "Should we reschedule?" |
update |
Status update | "Work 50% complete" |
Threading (Replies)
Create a Reply
{
"comment_text": "I'll handle the rescheduling",
"parent_comment_id": "parent-comment-uuid",
"is_internal": true,
"comment_type": "note"
}
Load Thread
- List top-level comments:
GET /tickets/{id}/comments?parent_only=true - Load replies:
GET /comments/{comment_id}/replies
Mentions
Tag Team Members
{
"comment_text": "@john Can you check the customer location?",
"mentioned_user_ids": ["john-uuid"],
"is_internal": true,
"comment_type": "question"
}
Future: Mentioned users will receive notifications
Attachments
Link Documents & Images
{
"comment_text": "See attached site survey photos",
"attachment_document_ids": ["doc-uuid-1", "doc-uuid-2"],
"is_internal": true,
"comment_type": "note"
}
Workflow:
- Upload image:
POST /api/v1/documents/uploadwithentity_type=ticketandentity_id=ticket-uuid - Get document ID from response
- Create comment with document ID in
attachment_document_idsarray
Response includes enriched attachment info:
{
"id": "comment-uuid",
"comment_text": "Found this wiring issue",
"attachment_document_ids": ["doc-uuid-1"],
"attachments": [
{
"id": "doc-uuid-1",
"file_name": "wiring_issue.jpg",
"file_type": "image/jpeg",
"file_url": "https://storage.example.com/...",
"file_size": 245678,
"is_image": true
}
]
}
Internal vs External Comments
Internal Comments (Team Only)
{
"comment_text": "Customer is difficult, be patient",
"is_internal": true
}
- Visible to: Team members only
- Use for: Internal notes, issues, coordination
External Comments (Client-Visible)
{
"comment_text": "Installation scheduled for tomorrow 10am",
"is_internal": false
}
- Visible to: Team + Client
- Use for: Customer updates, status changes
Edit Tracking
When a comment is edited:
is_edited→trueedited_at→ timestampedited_by_user_id→ editor's IDedited_by_user_name→ editor's name
Original text is NOT preserved (consider adding version history if needed)
Authorization
| Action | Who Can Do It |
|---|---|
| Create comment | All authenticated users |
| Update comment | Comment author only |
| Delete comment | Comment author OR Project Manager |
| View comments | All authenticated users |
Error Handling
404 Not Found
{
"detail": "Ticket not found"
}
403 Forbidden
{
"detail": "You can only edit your own comments"
}
400 Bad Request
{
"detail": "One or more mentioned users not found"
}
Complete Workflow Example
Field Agent Posts Issue with Photo
// 1. Agent takes photo of problem
const photo = capturePhoto();
// 2. Upload photo
const formData = new FormData();
formData.append('file', photo);
formData.append('entity_type', 'ticket');
formData.append('entity_id', ticketId);
formData.append('document_type', 'comment_attachment');
const doc = await api.post('/documents/upload', formData);
// 3. Create comment with photo
await api.post(`/tickets/${ticketId}/comments`, {
comment_text: "Found damaged cable at junction box",
is_internal: true,
comment_type: "issue",
attachment_document_ids: [doc.data.id],
mentioned_user_ids: [dispatcherId]
});
Dispatcher Responds with Reference Image
// 1. Upload reference diagram
const diagram = selectFile();
const formData = new FormData();
formData.append('file', diagram);
formData.append('entity_type', 'ticket');
formData.append('entity_id', ticketId);
formData.append('document_type', 'comment_attachment');
const doc = await api.post('/documents/upload', formData);
// 2. Reply with solution
await api.post(`/tickets/${ticketId}/comments`, {
comment_text: "Replace with this cable type. See diagram.",
parent_comment_id: agentCommentId,
is_internal: true,
comment_type: "resolution",
attachment_document_ids: [doc.data.id]
});
Best Practices
For Field Agents
- Use
notetype for general observations - Use
issuetype for problems encountered - Attach photos when reporting issues - visual evidence is crucial
- Keep comments concise and actionable
- Always mark as
is_internal: true
For Dispatchers
- Use
questiontype when asking agents - Use
updatetype for status changes - Tag relevant team members with mentions
- Share reference images/diagrams when providing solutions
- Use external comments for customer updates
For Project Managers
- Review external comments before posting
- Use
resolutiontype for solutions - Delete inappropriate comments if needed
- Monitor comment activity for team collaboration
- Verify photo attachments in issue reports
Frontend Integration
Mobile App (Field Agents)
// List comments
const comments = await api.get(`/tickets/${ticketId}/comments`, {
params: { is_internal: true, page_size: 20 }
});
// Create comment
await api.post(`/tickets/${ticketId}/comments`, {
comment_text: "Customer not home, will retry tomorrow",
is_internal: true,
comment_type: "update"
});
// Create comment with image attachment
// Step 1: Upload image
const formData = new FormData();
formData.append('file', imageFile);
formData.append('entity_type', 'ticket');
formData.append('entity_id', ticketId);
formData.append('document_type', 'comment_attachment');
formData.append('document_category', 'operational');
const uploadResponse = await api.post('/documents/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
// Step 2: Create comment with attachment
await api.post(`/tickets/${ticketId}/comments`, {
comment_text: "Found this issue with the installation",
is_internal: true,
comment_type: "issue",
attachment_document_ids: [uploadResponse.data.id]
});
// Reply to comment
await api.post(`/tickets/${ticketId}/comments`, {
comment_text: "Okay, I'll reschedule",
parent_comment_id: parentCommentId,
is_internal: true,
comment_type: "note"
});
Web Dashboard (Dispatchers/PMs)
// Load threaded comments
const topLevel = await api.get(`/tickets/${ticketId}/comments`, {
params: { parent_only: true }
});
// Load replies for each comment
for (const comment of topLevel.comments) {
if (comment.reply_count > 0) {
const replies = await api.get(`/comments/${comment.id}/replies`);
comment.replies = replies;
}
}
// Display images inline
function renderComment(comment) {
const imageAttachments = comment.attachments.filter(a => a.is_image);
const otherAttachments = comment.attachments.filter(a => !a.is_image);
return (
<div>
<p>{comment.comment_text}</p>
{/* Display images inline */}
{imageAttachments.length > 0 && (
<div className="image-gallery">
{imageAttachments.map(img => (
<img key={img.id} src={img.file_url} alt={img.file_name} />
))}
</div>
)}
{/* Display other attachments as links */}
{otherAttachments.length > 0 && (
<div className="attachments">
{otherAttachments.map(doc => (
<a key={doc.id} href={doc.file_url} download={doc.file_name}>
{doc.file_name}
</a>
))}
</div>
)}
</div>
);
}
Database Schema
CREATE TABLE ticket_comments (
id UUID PRIMARY KEY,
ticket_id UUID NOT NULL REFERENCES tickets(id),
user_id UUID REFERENCES users(id),
comment_text TEXT NOT NULL,
is_internal BOOLEAN DEFAULT TRUE,
comment_type VARCHAR(50) DEFAULT 'note',
parent_comment_id UUID REFERENCES ticket_comments(id),
mentioned_user_ids UUID[],
attachment_document_ids UUID[],
is_edited BOOLEAN DEFAULT FALSE,
edited_at TIMESTAMP WITH TIME ZONE,
edited_by_user_id UUID REFERENCES users(id),
additional_metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
Future Enhancements
Notifications
- Notify mentioned users
- Notify ticket assignees on external comments
- Real-time updates via SSE
Rich Text
- Markdown support
- Code blocks
- Formatting
Reactions
- Like/emoji reactions
- Quick acknowledgments
Version History
- Track all edits
- View previous versions
- Restore old versions
Search
- Full-text search across comments
- Filter by author
- Date range filtering