S
supero.docs
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.
FieldTypeNotes
statusstring (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).
subjectstringRequired. The case title.
descriptionstringFull case description.
prioritystringSet via set_priority: low, normal, high, or urgent.
assignee_uuidstringuuid of whoever the ticket is assigned to. Set by assign_ticket.
queuestringFree-text routing label, e.g. billing, tier2.
due_atstringAn SLA timestamp you set yourself — the platform does not enforce SLA policy.
first_response_atstringStamped automatically the first time assign_ticket moves a ticket out of open.
resolved_at / closed_at / reopened_atstringStamped by resolve_ticket / close_ticket / reopen_ticket respectively.
resolution_notesstringFree text you pass to resolve_ticket.
tagsstringFree-text tags.
metadatajsonFreeform structured data.
display_name / summarystringTitle 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

StateMeaning
openDefault state on creation. Not yet assigned.
in_progressBeing worked. Reached by assign_ticket.
on_holdWaiting on the customer or a third party. See the callout below — nothing assigns this automatically.
resolvedThe work is done. Not yet closed.
closedTerminal. Only reachable from resolved.
reopenedWork resumed after resolved or closed.
OperationFromToNotes
assign_ticketopen, in_progress, on_hold, resolved, reopenedin_progress (from open/reopened); unchanged otherwiseRejected from closed. Reassigning a ticket that is already in_progress, on_hold, or resolved only updates assignee_uuid — the status stays put.
resolve_ticketopen, in_progress, on_hold, reopenedresolvedIdempotent — resolving an already-resolved ticket returns the same result instead of erroring.
reopen_ticketresolved, closedreopenedIdempotent. Clears resolved_at and closed_at.
close_ticketresolvedclosed (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.
OperationPurposeKey inputIdempotency
assign_ticketAssign or reassign a ticket to a user.ticket_uuid, assignee_uuidNaturally idempotent — reassigning to the same user is a harmless repeat.
resolve_ticketMark the ticket resolved.ticket_uuid, resolution_notes (optional)State-guarded — retrying on an already-resolved ticket returns the same end state.
reopen_ticketReopen a resolved or closed ticket.ticket_uuidState-guarded.
close_ticketClose a resolved ticket (terminal).ticket_uuidState-guarded.
set_priorityUpdate priority to low, normal, high, or urgent.ticket_uuid, priorityNaturally 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
    pass

From 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.
ServiceAdds to your helpdeskBase schema(s)
commentThreaded replies between agent and requester on a ticket.comment:base_comment
attachmentFile uploads on a ticket — screenshots, logs, receipts.attachment:base_attachment
notificationIn-app alerts, e.g. "a ticket was assigned to you."notification:base_notification
approvalAn 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.
taskticket
Use it forInternal TODOs, work queues, project management.Support cases: helpdesk requests, bug reports, incidents.
Statespending → in_progress → completed / cancelled (+ blocked)open → in_progress → on_hold → resolved → closed (+ reopened)
Resolution conceptNone — a task is done or not.resolved is distinct from closed, with a resolution_notes field.
ReopenNo reopen operation — completed/cancelled are terminal.reopen_ticket restarts the lifecycle from resolved or closed.
Response trackingNone.first_response_at auto-stamped by assign_ticket.
Creationcreate_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.