Documentation/Building Your App/Support & Ticketing
Support & Ticketing
Build a helpdesk on the ticket transactional service: the base_ticket lifecycle, its five operations, and how to compose it with comment, attachment, notification, and approval for a full support flow.
Overview
ticket is the transactional service for support cases: helpdesk requests, bug reports, and incidents. Like every transactional service, you do not hand-roll a status field or a case state machine — you extend the platform's ticket:base_ticket schema with your own fields, then drive every lifecycle change through one of five operations instead of writing a status value directly.
This page assumes you're already familiar with the shared extends + /services/execute model — see Transactional Services for the full mechanics (state machines, idempotency categories, error shape, client access patterns). What's here is specific to ticket: its schema, its lifecycle, and how to compose it with comment, attachment, notification, and approval into a complete helpdesk.
http
POST https://api.supero.dev/api/v1/services/execute
X-API-Key: ak_...
Content-Type: application/json
{
"service_id": "ticket",
"operation": "resolve_ticket",
"input": {
"ticket_uuid": "9c1d...",
"extending_schema": "helpdesk:support_ticket",
"resolution_notes": "Reset the password and confirmed login."
},
"domain": "acme-corp",
"project_uuid": "8f2a1c40-..."
}json
{
"success": true,
"service_id": "ticket",
"operation": "resolve_ticket",
"output": {
"record_uuid": "9c1d...",
"status": "resolved",
"resolved_at": "2026-07-18T09:12:03Z"
},
"record_uuid": "9c1d...",
"error": null
}The Ticket Schema
ticket:base_ticket ships the lifecycle and case fields every helpdesk needs. Your extending schema inherits all of these and adds whatever is specific to your app.
| Field | Type | Notes |
|---|---|---|
| status | string (enum) | open, in_progress, on_hold, resolved, closed, reopened — default open. Mutate it through an operation, not a direct PUT (see Status Lifecycle below for the one exception). |
| subject | string | Required. The case title. |
| description | string | Full case description. |
| priority | string | Set via set_priority: low, normal, high, or urgent. |
| assignee_uuid | string | uuid of whoever the ticket is assigned to. Set by assign_ticket. |
| queue | string | Free-text routing label, e.g. billing, tier2. |
| due_at | string | An SLA timestamp you set yourself — the platform does not enforce SLA policy. |
| first_response_at | string | Stamped automatically the first time assign_ticket moves a ticket out of open. |
| resolved_at / closed_at / reopened_at | string | Stamped by resolve_ticket / close_ticket / reopen_ticket respectively. |
| resolution_notes | string | Free text you pass to resolve_ticket. |
| tags | string | Free-text tags. |
| metadata | json | Freeform structured data. |
| display_name / summary | string | Title and one-line summary rendered by record lists and the admin UI. |
Requester, assignee, and category
assignee_uuid and queue are the only case-routing fields base_ticket ships with — there is no built-in requester field, because who files a ticket varies by app (a Customer record, a plain user account, or just a contact email for anonymous submissions). Add a reference for it on your extending schema, plus whatever category/severity field your routing needs.
json
{
"name": "support_ticket",
"parent_type": "tenant",
"extends": "ticket:base_ticket",
"attributes": [
{ "name": "category", "type": "string", "values": ["billing", "technical", "account", "bug"] },
{ "name": "channel", "type": "string", "values": ["email", "chat", "portal"] }
],
"references": [
{ "name": "requester", "type": "Customer", "cardinality": "one" }
]
}Status Lifecycle
| State | Meaning |
|---|---|
| open | Default state on creation. Not yet assigned. |
| in_progress | Being worked. Reached by assign_ticket. |
| on_hold | Waiting on the customer or a third party. See the callout below — nothing assigns this automatically. |
| resolved | The work is done. Not yet closed. |
| closed | Terminal. Only reachable from resolved. |
| reopened | Work resumed after resolved or closed. |
| Operation | From | To | Notes |
|---|---|---|---|
| assign_ticket | open, in_progress, on_hold, resolved, reopened | in_progress (from open/reopened); unchanged otherwise | Rejected from closed. Reassigning a ticket that is already in_progress, on_hold, or resolved only updates assignee_uuid — the status stays put. |
| resolve_ticket | open, in_progress, on_hold, reopened | resolved | Idempotent — resolving an already-resolved ticket returns the same result instead of erroring. |
| reopen_ticket | resolved, closed | reopened | Idempotent. Clears resolved_at and closed_at. |
| close_ticket | resolved | closed (terminal) | Strict — the only legal from-state is resolved. reopen_ticket is the only way back once closed. |
⚠️ on_hold has no operation of its own
Every other transition above goes through an operation. on_hold is the one exception: nothing in the ticket service moves a ticket into it, so if your workflow needs a hold state, set status: "on_hold" with a direct record update. Both assign_ticket and resolve_ticket accept on_hold as a valid starting state, so a held ticket can still be reassigned or resolved once you're ready to move it forward.
🚨 Never write status directly otherwise
Outside the on_hold case above, do not PUT status on a ticket record. It skips input validation, the state machine's from-state check, and the timestamp stamping (first_response_at, resolved_at, closed_at, reopened_at) that assign_ticket / resolve_ticket / reopen_ticket / close_ticket do for you.
Operations Reference
Every operation's input requires extending_schema — the qualified name of your schema that extends ticket:base_ticket — so the handler knows which record type to read and write. Register it once (see Enabling & Extending below) instead of passing it on every call.
| Operation | Purpose | Key input | Idempotency |
|---|---|---|---|
| assign_ticket | Assign or reassign a ticket to a user. | ticket_uuid, assignee_uuid | Naturally idempotent — reassigning to the same user is a harmless repeat. |
| resolve_ticket | Mark the ticket resolved. | ticket_uuid, resolution_notes (optional) | State-guarded — retrying on an already-resolved ticket returns the same end state. |
| reopen_ticket | Reopen a resolved or closed ticket. | ticket_uuid | State-guarded. |
| close_ticket | Close a resolved ticket (terminal). | ticket_uuid | State-guarded. |
| set_priority | Update priority to low, normal, high, or urgent. | ticket_uuid, priority | Naturally idempotent — setting the same value twice is harmless. |
💡 first_response_at is tracked for you
assign_ticket stamps first_response_at automatically the first time it moves a ticket out of open. Use it to measure time-to-first-touch without adding your own field or timestamp logic.
There is no create_ticket operation — a ticket is created like any other record, with an ordinary CRUD create against your extending schema. Operations only cover lifecycle mutation after that.
Enabling & Extending Ticket
Import the service into your project once. Internal services like ticket don't need provider credentials, so there's no separate config step — unlike a payment gateway or email provider, which you'd also configure with PUT /api/v1/services/{service_id}/config.
bash
curl -X POST https://api.supero.dev/api/v1/services/import \
-H "X-API-Key: ak_..." \
-H "Content-Type: application/json" \
-d '{"service_id": "ticket", "domain": "acme-corp", "project_uuid": "8f2a1c40-..."}'Extend base_ticket with your own schema (see The Ticket Schema above), then create tickets with ordinary CRUD and drive the lifecycle through /services/execute.
From server-side Python
python
import supero
from supero.service_lib import ServiceLib
org = supero.login(domain_name='acme-corp', email='[email protected]', password='...')
# Create — ordinary CRUD, not a service operation
ticket = org.crud.create('helpdesk:support_ticket', name='case-4821',
subject='Cannot reset password', category='account')
sl = ServiceLib(api_key='ak_...', domain='acme-corp', project_uuid='8f2a1c40-...')
sl.transactional.register_extensions({'ticket': 'helpdesk:support_ticket'})
sl.ticket.assign(ticket['uuid'], assignee_uuid=agent_uuid)
sl.ticket.resolve(ticket['uuid'], resolution_notes='Reset the password and confirmed login.')
sl.ticket.close(ticket['uuid'])
try:
sl.ticket.close(other_ticket_uuid)
except StateConflictError as e:
# not resolved yet — close_ticket is strict about its from-state
passFrom your app frontend (JavaScript)
javascript
client.transactional.registerExtensions({ ticket: 'helpdesk:support_ticket' });
await client.transactional.ticket.assign(ticketUuid, agentUuid);
await client.transactional.ticket.resolve(ticketUuid, 'Reset the password and confirmed login.');
await client.transactional.ticket.close(ticketUuid);ℹ️ Discover legal transitions before you render action buttons
GET https://api.supero.dev/api/v1/services/transactional-catalog returns every imported service's operations and full state machine in one call, including which operations are legal from a record's current state — useful for deciding which buttons (Assign / Resolve / Close / Reopen) to show on a ticket detail view.
Composing a Full Helpdesk
A real helpdesk is rarely just tickets. comment, attachment, notification, and approval are separate transactional services that follow the exact same pattern as ticket — extend the base schema with a reference back to your ticket, import it, register the extension, call its operations through /services/execute. Wire together as many as your support flow needs.
| Service | Adds to your helpdesk | Base schema(s) |
|---|---|---|
| comment | Threaded replies between agent and requester on a ticket. | comment:base_comment |
| attachment | File uploads on a ticket — screenshots, logs, receipts. | attachment:base_attachment |
| notification | In-app alerts, e.g. "a ticket was assigned to you." | notification:base_notification |
| approval | An escalation gate before a sensitive action tied to a ticket, like a refund or account deletion. | approval:base_approval, approval:base_approval_step |
email is the managed integration for outbound mail (acknowledgement and resolution notices) — import and configure it from the services catalog like any external provider; see Managed Integrations & Provider Catalog for setup.
python
sl.transactional.register_extensions({
'ticket': 'helpdesk:support_ticket',
'comment': 'helpdesk:ticket_comment',
'attachment': 'helpdesk:ticket_attachment',
'notification': 'helpdesk:agent_notification',
})
# Thread a reply on the ticket
reply = org.crud.create('helpdesk:ticket_comment', name='reply-1', ticket_uuid=ticket['uuid'],
body='Looking into this now.')
# A screenshot the requester sent in — the attachment schema's `file` field
# (system File type) drives the upload widget and captures file_id/url/thumbnail_url
# for you; mark_uploaded records the completed transfer.
shot = org.crud.create('helpdesk:ticket_attachment', name='shot-1', ticket_uuid=ticket['uuid'])
sl.attachment.mark_uploaded(shot['uuid'], size_bytes=482113, checksum='a1b2c3...')
# Notify the assignee in-app
note = org.crud.create('helpdesk:agent_notification', name='note-1', recipient_uuid=agent_uuid,
ticket_uuid=ticket['uuid'], title='case-4821 assigned to you')
sl.notification.send(note['uuid'])
# Email the requester (email service imported + configured separately)
sl.email.send(requester_email, 'We are on it',
body_html='<p>Your ticket case-4821 is now in progress.</p>')Escalation with approval
For actions that need sign-off before you act on a ticket, create an approval record referencing the ticket and route it through the same submit → approve/reject flow used for expense reports or procurement.
python
sl.transactional.register_extensions({
'approval': ('helpdesk:ticket_escalation', 'helpdesk:ticket_escalation_step'),
})
escalation = org.crud.create('helpdesk:ticket_escalation', name='esc-1', ticket_uuid=ticket['uuid'],
subject='Refund approval for case-4821')
sl.approval.submit(escalation['uuid'])
# A manager decides:
sl.approval.approve_step(escalation['uuid'], decision_notes='Approved — under $50 threshold.')ℹ️ All internal-path services, one execution model
ticket, comment, attachment, notification, and approval all use the same internal transactional path: no crud_object_type, no separate tracking record — the operation mutates your extending-schema record directly and returns it. email is different: it's a managed integration that calls an external provider and creates its own message record to track delivery.
ticket vs. task
Both are lightweight work-item services, and it's easy to reach for the wrong one. The distinction: task is a plain TODO with no resolution stages; ticket is a support flow with a requester, an assignee, and a real resolution/close/reopen lifecycle.
| task | ticket | |
|---|---|---|
| Use it for | Internal TODOs, work queues, project management. | Support cases: helpdesk requests, bug reports, incidents. |
| States | pending → in_progress → completed / cancelled (+ blocked) | open → in_progress → on_hold → resolved → closed (+ reopened) |
| Resolution concept | None — a task is done or not. | resolved is distinct from closed, with a resolution_notes field. |
| Reopen | No reopen operation — completed/cancelled are terminal. | reopen_ticket restarts the lifecycle from resolved or closed. |
| Response tracking | None. | first_response_at auto-stamped by assign_ticket. |
| Creation | create_task operation (also spawns the record). | Ordinary CRUD create — there is no create_ticket operation. |
💡 When in doubt
"Track tasks" or "a TODO list" points to task. "Helpdesk," "bug tracker," "support cases," or "incident management" points to ticket. If your app's own service picker or App Studio flow is choosing for you, this is the rule it follows too.
Next steps
- •Transactional Services — the shared extends model, state machines, idempotency categories, and error shape that ticket (and every service on this page) builds on.
- •Schema Reference — the full fields[]/references[]/extends format used to build your extending schemas.
- •Managed Integrations & Provider Catalog — importing and configuring the email service for outbound notices.
- •Workflows & Automation — trigger a notification or email automatically when a ticket changes state instead of calling each operation by hand.
- •Email & Notifications — the outbound email and in-app notification surfaces a ticket flow drives.
On this page