Ticketing API
The Ticketing API is the REST surface behind Ticketing: one API over every queue, whether the queue runs on the Ema Ticketing Desk or passes through to a connected ServiceNow instance. Use it to raise tickets from your own systems, sync a queue into a report, or drive the same operations the app performs.
All requests require a JWT or API key, sent as Authorization: Bearer <token> or the X-API-Key header. Examples use https://your-tenant.ema.ai as the host, and every path below is relative to the service prefix:
https://your-tenant.ema.ai/api/v1/hcm-ticketing
Enabled per Workspace. Every route answers 404 when ticketing is not enabled for the Workspace, for every role, so a not-found on a path you expect to exist means the feature is off rather than the URL being wrong.
Tickets are addressed by group
Every ticket route carries the group's UUID, and the ticket id is the backend's own handle (a ServiceNow sys_id, for example). There is no /tickets/{id} route: a queue decides which system a ticket lives in, and no copy is kept in Ema, so the group is what tells the service where to look. Start from GET /queues to resolve the group ids you can use.
Authorization follows the app: effective membership of the queue's group is required for every operation (nesting included), System Admins reach every queue, and user groups have to be enabled for the Workspace. A group that auth cannot resolve returns 403 rather than falling back to a Workspace-wide list.
Queues
| Endpoint | What it does |
|---|---|
GET /queues | The queues the caller may see. Add ?routable=true to drop queues whose group has been deleted. |
POST /groups/{groupID}/queue | Bind a group to a queue. Group admin or System Admin. |
PATCH /groups/{groupID}/queue/routing | Replace the queue's routing guidance, leaving its system and connection alone. |
GET https://your-tenant.ema.ai/api/v1/hcm-ticketing/queues
X-API-Key: ema_sk_live_2f9c...redacted
{
"queues": [
{
"group_id": "8f14e45f-ea0e-4c7f-9a1a-6f2f0a3b1c22",
"group_tag": "emahcm-human-resources",
"name": "Human Resources",
"backend": "builtin",
"connection_id": null,
"routing_description": "Payroll, benefits, and leave questions. High priority for a missed payment.",
"group_archived": false,
"request_types": []
}
]
}
backend is builtin for the Ema Ticketing Desk or servicenow for a pass-through queue. request_types lists the values the queue accepts as a ticket's type, in the backend's own order; an absent or empty list means the queue does not publish a closed set, so do not validate against it. Binding a queue takes a name (the group's name) and, for servicenow, the connection_id of a ticketing connection; builtin must omit it. The queue's label is derived from the name server-side and is immutable.
Tickets
| Endpoint | What it does |
|---|---|
POST /groups/{groupID}/tickets | Raise a ticket. |
GET /groups/{groupID}/tickets | Page the queue, with filters and sort. |
GET /groups/{groupID}/tickets/{ticketID} | Read one ticket. |
PATCH /groups/{groupID}/tickets/{ticketID} | Update title, description, priority, type, or labels. |
POST /groups/{groupID}/tickets/{ticketID}/status | Set OPEN, IN_PROGRESS, or HOLD. |
POST /groups/{groupID}/tickets/{ticketID}/close | Close it, with an optional resolution. |
POST /groups/{groupID}/tickets/{ticketID}/assign | Assign, or clear the assignee with null. |
POST /groups/{groupID}/tickets/{ticketID}/move | Move to another queue on the same connection. Clears the assignee. |
DELETE /groups/{groupID}/tickets/{ticketID} | Delete. Group admins and System Admins. |
POST /groups/{groupID}/tickets/{ticketID}/subtickets | Raise a sub-ticket under this one. |
GET /groups/{groupID}/tickets/{ticketID}/children | List its sub-tickets. |
GET /me/tickets | The caller's own tickets across every group they belong to. |
POST https://your-tenant.ema.ai/api/v1/hcm-ticketing/groups/8f14e45f-ea0e-4c7f-9a1a-6f2f0a3b1c22/tickets
X-API-Key: ema_sk_live_2f9c...redacted
Content-Type: application/json
{
"client_request_id": "payroll-2026-08-24-4471",
"title": "August paycheck missing overtime",
"description": "Overtime hours from the week of Aug 11 are not on the August payslip.",
"priority": "HIGH",
"type": "Payroll",
"tags": ["overtime"]
}
client_request_id is the idempotency key: a retry carrying the same value does not create a second ticket. requester_email defaults to the caller, and naming someone else requires a System Admin or a service principal. assignee_email may pre-assign anyone, and any effective member of the group may set it, but the address has to resolve to a real person: a member of the Workspace on the Ema desk, or a user of the instance on a connected queue.
Ticket status is OPEN, IN_PROGRESS, HOLD, or CLOSED, and CLOSED is reachable only through the close endpoint: sending it to the status endpoint is a 422. Priority is CRITICAL, HIGH, MEDIUM, or LOW, matched case-insensitively on input.
The emahcm- label namespace is reserved. It carries queue membership, so a tags value inside it is rejected (the queue's own tag is the one exception, accepted and dropped). Read a ticket's queue from group_tag, and change it with the move endpoint.
Paging, filtering, and sorting a queue
GET https://your-tenant.ema.ai/api/v1/hcm-ticketing/groups/{groupID}/tickets?status=OPEN&sort=priority&order=asc&page_size=50
| Parameter | Notes |
|---|---|
status, assignee_email, requester_email, label | Exact-match filters. |
q | Case-insensitive substring over title and description. Not ranked. |
created_after, created_before | UTC RFC3339. after is inclusive, before exclusive. |
sort | number, priority, status, or updated_at. Omitted means newest first. |
order | asc or desc over Ema's canonical rank, so sort=priority&order=asc is Critical to Low whatever the backend stores. Ignored without sort. |
cursor | Opaque. Pass back verbatim; a cursor from another list, group, or sort is rejected. |
page_size | Hint. The default is 25 and values above 100 are clamped. |
Sorting happens server-side over the whole result set before the page is sliced. Not every backend can honor every key: the response's sortable_by advertises the ones this queue can serve, and asking for another is a 422 naming sort, rather than an answer in a different order.
GET /me/tickets takes scope (created, assigned, or commented, defaulting to created), the same status, priority, text, and date filters, and an optional group to narrow to one group. It carries no sortable_by, since it is not a single queue.
Comments and activity
| Endpoint | What it does |
|---|---|
POST /groups/{groupID}/tickets/{ticketID}/comments | Add a comment. visibility is public (default, the requester sees it) or internal. |
GET /groups/{groupID}/tickets/{ticketID}/comments | List comments and notes, oldest first. |
GET /groups/{groupID}/tickets/{ticketID}/events | The activity timeline: create, comment, work_note, and field_change entries, oldest first. |
A field_change entry names the field with its old_value and new_value, canonicalized (status and priority as Ema's values, assignee as an email or name). actor_type attributes it: user for a person, ema for an AI Employee, service for an account the system of record itself treats as a machine user. For a connected queue this history is read live from that system rather than from a copy in Ema.
Errors
| Status | Meaning |
|---|---|
| 401 | Missing or invalid credentials. |
| 403 | Not an effective member of the queue's group, user groups not enabled, a requester named by a caller without the privilege to name one, or the queue's connection refusing the request (CONNECTION_FORBIDDEN). |
| 404 | Ticketing not enabled for the Workspace, no queue configured for the group, or an unknown group or ticket. |
| 409 | State-dependent rejection: a write against a closed ticket, or a double close. |
| 413 | Body over 1 MiB. |
| 422 | A value outside the accepted domain: CLOSED on the status endpoint, a sort key this queue cannot serve, a reserved label, an unresolvable assignee, a request type the queue does not accept, or a replayed cursor. |
| 424 | The queue's connection has been deleted, disconnected, or is misconfigured. Fix or repoint the connection; retrying will not help. |
| 503 | The backend is transiently unavailable, or its connection could not be resolved. Retry. |
What's next
- Ticketing for what queues are and how they are set up.
- Ticket Tools for the tools that wrap this API for AI Employees.
- Authentication for tokens and API keys.