sondahub

APIs / Helpdesk

Helpdesk

A support desk: tickets, messages, agents, customers, SLAs — the state-machine one.

Two thousand tickets with their message threads, twenty-five agents in four teams and three hundred customer companies. A ticket moves open → pending → resolved → closed; PATCH its status and the hub checks the transition and stamps the timestamps. 7,398 records in all.

Connect

Base URL
https://api.sondahub.com/v1/helpdesk
OpenAPI 3
https://api.sondahub.com/v1/helpdesk/openapi.json
GraphQL
https://api.sondahub.com/v1/helpdesk/graphql
WebSocket
wss://api.sondahub.com/v1/helpdesk/ws
SSE
https://api.sondahub.com/v1/helpdesk/events
The data
teams.json, agents.json, customers.json, tickets.json, messages.json

In Sonda: Import → From a URL with the OpenAPI address and the whole API lands as a project, one request per operation with example bodies. No keys, no headers to add. More on each protocol.

Writes here are simulated: POST, PUT, PATCH and DELETE are validated, run through the real logic and answered as a real server would — then forgotten. The answer carries _note and X-Sondahub-Write: simulated. A GET afterwards will not find what you wrote.
The API describes itself
curl https://api.sondahub.com/v1/helpdesk

Lists and filters

Every list answers { "data": [...], "meta": { "page", "limit", "total", "pages" } } with X-Total-Count and Link headers (next, prev, first, last). These options work on every collection and every nested route:

OptionMeaningExample
page, limitPaging, 1-based; limit 1–200, default 20. offset works too.?page=3&limit=50
sortComma list of fields, - for descending. Default id here.?sort=-id,id
field=valueEquals. Booleans as true/false, null for missing.?id=1
_ne _gt _gte _lt _lteNot equal and comparisons, on numbers, dates and strings.?id_gt=10
_likeContains, case-insensitive.?name_like=an
_inAny of a comma list.?id_in=1,2,3
_nulltrue: missing; false: present.?slug_null=true
a.b=valueInside a JSON field, dotted.?attachments.name=…
qSearch across the text fields.?q=alpine
fieldsOnly these fields back.?fields=id,name
expandEmbed related records.?expand=agents

A name that is not a field answers 400 and lists the fields. Writes answer 422 with one line per problem, 404 for a missing id, 405 with an Allow header for a verb a route does not take.

teams

Support teams. 4 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
namerequiredstring
slugstring
timezonestring
hoursstring

Relations: agents → the agents whose team_id is this team. Use ?expand=agents to embed them, or the routes below.

Endpoints

GET/v1/helpdesk/teamsA page, with every filter, sort, search, field and expand option below.
POST/v1/helpdesk/teamsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/helpdesk/teams/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/helpdesk/teams/{id}Change the fields you send.
PUT/v1/helpdesk/teams/{id}Replace the record; required fields must all be there.
DELETE/v1/helpdesk/teams/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/helpdesk/teams/{id}/agentsIts agents, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/helpdesk/teams?limit=3"
One record
curl https://api.sondahub.com/v1/helpdesk/teams/1
Its agents
curl "https://api.sondahub.com/v1/helpdesk/teams/1/agents?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/helpdesk/teams \
  -H "Content-Type: application/json" \
  -d '{"name":"Tier 1","hours":"24/7"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/helpdesk/teams/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Changed name"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/helpdesk/teams/1

agents

The people answering tickets. 25 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
namerequiredstring
emailrequiredstring
team_idrequiredint → teams
roleenumagent senior lead admin
statusenumavailable busy away offline
skillsjson string[]
open_ticketsread-onlyint
ratingread-onlyfloat

Relations: team → one team through team_id; tickets → the tickets whose assignee_id is this agent. Use ?expand=team,tickets to embed them, or the routes below.

Endpoints

GET/v1/helpdesk/agentsA page, with every filter, sort, search, field and expand option below.
POST/v1/helpdesk/agentsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/helpdesk/agents/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/helpdesk/agents/{id}Change the fields you send.
PUT/v1/helpdesk/agents/{id}Replace the record; required fields must all be there.
DELETE/v1/helpdesk/agents/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/helpdesk/agents/{id}/teamThe team this record points at.
GET/v1/helpdesk/agents/{id}/ticketsIts tickets, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/helpdesk/agents?role=senior&expand=team&limit=3"
One record
curl https://api.sondahub.com/v1/helpdesk/agents/1?expand=team
Its tickets
curl "https://api.sondahub.com/v1/helpdesk/agents/1/tickets?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/helpdesk/agents \
  -H "Content-Type: application/json" \
  -d '{"name":"A name","email":"A email","team_id":1,"role":"agent","status":"available"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/helpdesk/agents/1 \
  -H "Content-Type: application/json" \
  -d '{"role":"senior"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/helpdesk/agents/1

customers

Companies with a support contract. 300 records — the file.

FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
namerequiredstring
domainstring
planenumfree starter business enterprise
contact_namestring
contact_emailstring
sla_hoursintFirst response target, hours.
open_ticketsread-onlyint
satisfactionread-onlyfloatAverage CSAT, 1–5.

Relations: tickets → the tickets whose customer_id is this customer. Use ?expand=tickets to embed them, or the routes below.

Endpoints

GET/v1/helpdesk/customersA page, with every filter, sort, search, field and expand option below.
POST/v1/helpdesk/customersCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/helpdesk/customers/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/helpdesk/customers/{id}Change the fields you send.
PUT/v1/helpdesk/customers/{id}Replace the record; required fields must all be there.
DELETE/v1/helpdesk/customers/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/helpdesk/customers/{id}/ticketsIts tickets, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/helpdesk/customers?plan=starter&sla_hours_gte=1&limit=3"
One record
curl https://api.sondahub.com/v1/helpdesk/customers/1
Its tickets
curl "https://api.sondahub.com/v1/helpdesk/customers/1/tickets?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/helpdesk/customers \
  -H "Content-Type: application/json" \
  -d '{"name":"Blue Systems Inc.","domain":"bluesystems.example","plan":"free"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/helpdesk/customers/1 \
  -H "Content-Type: application/json" \
  -d '{"plan":"starter"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/helpdesk/customers/1

tickets

A support request. Allowed status moves: open → pending | resolved; pending → open | resolved; resolved → closed | open; closed → open (reopen). Anything else answers 422. 2,000 records — the file.

PATCH checks the status move (open → pending | resolved; pending → open | resolved; resolved → closed | open; closed → open) and answers 422 invalid_transition otherwise; resolving and closing stamp their timestamps, assigning stamps first_response_at. Counters on the customer and agent follow.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
numberread-onlystring
subjectrequiredstring
descriptiontext
statusenumopen pending resolved closed
priorityenumlow normal high urgent
categoryenumbilling account bug feature howto
channelenumemail chat phone web api
customer_idrequiredint → customers
requester_emailstring
assignee_idint → agents
team_idint → teams
tagsjson string[]
first_response_atread-onlydatetime
resolved_atread-onlydatetime
closed_atread-onlydatetime
due_atdatetime
sla_breachedread-onlybool
satisfactionintCSAT given at close.
min 1, max 5
message_countread-onlyint

Relations: customer → one customer through customer_id; assignee → one agent through assignee_id; team → one team through team_id; messages → the messages whose ticket_id is this ticket. Use ?expand=customer,assignee,team,messages to embed them, or the routes below.

Endpoints

GET/v1/helpdesk/ticketsA page, with every filter, sort, search, field and expand option below.
POST/v1/helpdesk/ticketsCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/helpdesk/tickets/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/helpdesk/tickets/{id}Change the fields you send.
PUT/v1/helpdesk/tickets/{id}Replace the record; required fields must all be there.
DELETE/v1/helpdesk/tickets/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/helpdesk/tickets/{id}/customerThe customer this record points at.
GET/v1/helpdesk/tickets/{id}/assigneeThe agent this record points at.
GET/v1/helpdesk/tickets/{id}/teamThe team this record points at.
GET/v1/helpdesk/tickets/{id}/messagesIts messages, as a page with all the list options.

Try it

List with a filter
curl "https://api.sondahub.com/v1/helpdesk/tickets?status=pending&satisfaction_gte=1&expand=customer&limit=3"
One record
curl https://api.sondahub.com/v1/helpdesk/tickets/1?expand=customer
Its messages
curl "https://api.sondahub.com/v1/helpdesk/tickets/1/messages?limit=5"
Create (simulated)
curl -X POST https://api.sondahub.com/v1/helpdesk/tickets \
  -H "Content-Type: application/json" \
  -d '{"subject":"A subject","status":"open","priority":"low","category":"billing","channel":"email","customer_id":1}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/helpdesk/tickets/1 \
  -H "Content-Type: application/json" \
  -d '{"status":"pending"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/helpdesk/tickets/1

messages

The thread on a ticket, in order. author_type says who wrote it. 5,069 records — the file.

POST bumps the ticket’s message_count; an agent’s first message stamps first_response_at; a customer message reopens a pending ticket.
FieldTypeNotes
idread-onlyintAssigned by the server. Seed records keep their ids across restarts; records you create continue after the seed.
created_atread-onlydatetimeWhen the record was created (ISO 8601, UTC).
updated_atread-onlydatetimeWhen the record last changed.
ticket_idrequiredint → tickets
author_typerequiredenumcustomer agent system
author_idint → agentsThe agent, when author_type is agent.
author_namestring
bodyrequiredtext
internalboolA private note the customer does not see.
attachmentsjson { name, size, content_type }[]

Relations: ticket → one ticket through ticket_id; author → one agent through author_id. Use ?expand=ticket,author to embed them, or the routes below.

Endpoints

GET/v1/helpdesk/messagesA page, with every filter, sort, search, field and expand option below.
POST/v1/helpdesk/messagesCreate one. Answers 201 with the record, its id and a Location header — simulated, nothing stored; 422 names each field that is wrong.
GET/v1/helpdesk/messages/{id}One record, with an ETag; If-None-Match earns a 304.
PATCH/v1/helpdesk/messages/{id}Change the fields you send.
PUT/v1/helpdesk/messages/{id}Replace the record; required fields must all be there.
DELETE/v1/helpdesk/messages/{id}Answers 200 with what would have been removed and the note (a real server’s 204 lives at /v1/utils/status/204).
GET/v1/helpdesk/messages/{id}/ticketThe ticket this record points at.
GET/v1/helpdesk/messages/{id}/authorThe agent this record points at.

Try it

List with a filter
curl "https://api.sondahub.com/v1/helpdesk/messages?author_type=agent&expand=ticket&limit=3"
One record
curl https://api.sondahub.com/v1/helpdesk/messages/1?expand=ticket
Create (simulated)
curl -X POST https://api.sondahub.com/v1/helpdesk/messages \
  -H "Content-Type: application/json" \
  -d '{"ticket_id":1,"author_type":"customer","body":"A body"}'
Change one field (simulated)
curl -X PATCH https://api.sondahub.com/v1/helpdesk/messages/1 \
  -H "Content-Type: application/json" \
  -d '{"author_type":"agent"}'
Delete (simulated)
curl -X DELETE https://api.sondahub.com/v1/helpdesk/messages/1

WebSocket and SSE

The same stream two ways: the world's own activity, one tick a second, generated for your connection alone. Both push JSON text messages; SSE names each one with event: and numbers it with id:. ?topics=a,b narrows either.

TopicWhat arrivesHow often
ticketsA ticket opening, being assigned, or changing status.5 s
messagesA new message on a ticket.4 s
WebSocket
wss://api.sondahub.com/v1/helpdesk/ws?topics=tickets

> {"type":"hello","api":"helpdesk","topics":[…],"subscribed":[…]}
> {"type":"event","topic":"tickets","api":"helpdesk","ts":"…","data":{…}}
< {"type":"subscribe","topics":["tickets"]}   # narrow to some topics
< {"type":"ping"}                            # → {"type":"pong"}
< anything else                             # → echoed back as {"type":"echo"}
Server-Sent Events
curl -N "https://api.sondahub.com/v1/helpdesk/events?topics=tickets"

retry: 3000
id: 1
event: tickets
data: {"type":"event","topic":"tickets",…}

GraphQL

One endpoint, https://api.sondahub.com/v1/helpdesk/graphql: POST {"query", "variables"} or GET ?query=. Introspection is on, so Sonda's GraphQL mode loads the schema; the SDL is a click away. Every collection is a paged query with the same filter, sort and q options as REST (operators as suffixes: price_lt), a by-id query, relation fields both ways, and create, update, replace and delete mutations — simulated like every write, with the note in extensions.

A query
curl https://api.sondahub.com/v1/helpdesk/graphql -H "Content-Type: application/json" -d '{"query": "{ agents(limit: 3, sort: \"-id\", filter: { role: agent }) { total data { id name email team_id team { name } tickets(limit: 2) { id } } } }"}'
{
  agents(limit: 3, sort: "-id", filter: { role: agent }) {
    total
    data {
      id name email team_id
      team { name }
      tickets(limit: 2) { id }
    }
  }
}