Public API
The helpdesk exposes one public API: a GraphQL endpoint. Every operation in the schema is reachable from outside the browser with a Personal Access Token (PAT).
This page is the contract — what's stable, how to authenticate, what scopes grant, and how errors are shaped. The hdmx CLI is built on top of it, and API versioning covers how the surface evolves.
The endpoint
POST https://<your-host>/backend/<subdomain>/api/<version>/graphql
<subdomain>identifies your account (the same handle you use to sign in).<version>is a dated API version such as2026-07— see API versioning. You can also omit the version segment and call…/backend/<subdomain>/graphql; the request is then served by the oldest supported version and every response tells you which version answered it.
Authentication
Send a Personal Access Token as a Bearer credential:
Authorization: Bearer hdmx_<random>
A PAT is long-lived (with a mandatory expiry), scoped, IP-restricted, and revocable. It is bound to one account: presenting a token to a different account's subdomain is rejected with HTTP 401.
Creating a token
Create and manage tokens yourself in the agent app under Settings → API tokens:
- Click New token.
- Give it a name you'll recognize later.
- Choose an expiry (up to 365 days — there is no "never").
- Set an access policy — switch on exactly the resources this token needs, at read or read-&-write. Everything defaults to no access.
- Add at least one IP address or CIDR to the allowlist (required — see IP allowlist). The form can add your current IP for you.
- Review and create. The full token is shown exactly once — copy it immediately. Only a hash is stored, so it can never be shown again; if you lose it, rotate the token to get a new one.
You can later edit a token's access policy and allowlist, rotate it (issue a new secret while the old one keeps working for a short grace period so you can cut over without downtime), and revoke it. Each token shows when and from where it was last used, plus a recent-activity log.
Scopes
Access is coarse-grained and resource-family shaped. A token carries a fixed set of scopes; every top-level operation checks one.
| Scope | Grants |
|---|---|
tickets:read | Read tickets, messages, drafts, history, attachments, reports |
tickets:write | Create / update / delete tickets, post messages, upload attachments. Implies tickets:read |
chat:read | Read chats, conversations, messages, sessions |
chat:write | Send agent messages, join / leave / close / reopen conversations. Implies chat:read |
customers:read | Read customer records |
customers:write | Create / update / merge customer records. Implies customers:read |
settings:read | Read account settings (departments, statuses, priorities, mailboxes — without secrets) |
Mailbox credential fields (IMAP/SMTP usernames, passwords, and hosts) are never returned to a token, regardless of scope — those secrets belong to your email provider and stay out of reach of the public API.
Error taxonomy
GraphQL responses use errors[].extensions.code:
| Code | Meaning | HTTP |
|---|---|---|
SCOPE_DENIED | The token lacks the required scope. extensions.valid_values lists the scope(s) that would satisfy the operation | 200 |
RATE_LIMITED | The per-token request budget is exhausted. extensions.retryAfter is seconds until reset, and the Retry-After header carries the same number | 429 |
INVALID_API_TOKEN | Token not found, malformed, or otherwise unusable | 200 |
Revoked tokens, expired tokens, cross-account mismatches, and IP-allowlist denials are rejected before the query runs and surface as HTTP 401 with no GraphQL body. Treat a 401 on a token request as "this token is no longer valid for this request — check its expiry, scopes, and IP allowlist."
Rate limits
Every token request carries headers:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 96
X-RateLimit-Reset: 1715692800
The default budget is 120 requests per minute per token. Every request costs 1, whether it reads or writes. Going over returns HTTP 429 with a Retry-After header and extensions.code = "RATE_LIMITED"; a well-behaved client backs off until the reset epoch.
IP allowlist
Every self-serve token must carry at least one IP or CIDR — there are no "any IP" tokens. Each request's source IP is matched against the token's list; a miss is rejected with HTTP 401 and extensions.sub_code = "ip_not_allowed".
- IPv4, IPv6, and CIDR ranges are all accepted. Use
/32(IPv4) or/128(IPv6) for a single host. - Ranges are validated and normalized when you save them.
- The allowlist is editable after creation — editing it does not change the secret.
Use a token from a stable egress: a CI runner, a scheduled job, a fixed-office NAT, or your own servers on a known IP range. A leaked token is useless from anywhere outside its allowlist.
Token activity
Every authenticated request a token makes is recorded against it: the time, the operation name, the source IP, the outcome (success, error, rate-limited, scope-denied, revoked, or expired), and the round-trip latency. This is the trail to check if a token behaves unexpectedly.
Versioning & deprecation
The API is versioned with dated YYYY-MM versions. Pin one in the URL and it holds stable for at least 12 months; deprecations are announced in-band before anything you use goes away. See API versioning for the full policy.
Schema discovery
The schema is self-describing: use standard GraphQL introspection against the versioned endpoint to generate types and explore the surface. The interactive GraphQL Playground is available at …/backend/<subdomain>/playground.
curl quickstart
These assume a host of helpdesk.example.com, subdomain acme, version 2026-07, and a token in $TOKEN.
Read the current user (any scope)
curl -sS https://helpdesk.example.com/backend/acme/api/2026-07/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ currentUser { id email firstname lastname } }"}'
List recent tickets (tickets:read)
curl -sS https://helpdesk.example.com/backend/acme/api/2026-07/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query ($first: Int!) {
ticketList(first: $first) {
edges { node { code subject status { name } updatedAt } }
}
}",
"variables": { "first": 10 }
}'
Post a public reply (tickets:write)
curl -sS https://helpdesk.example.com/backend/acme/api/2026-07/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation ($input: SaveTicketInput!) {
saveTicket(input: $input) {
ticket { code }
error { code message }
}
}",
"variables": {
"input": {
"id": "TICKET-UUID-HERE",
"message": "Thanks — fixing this now.",
"messageType": "PUBLIC"
}
}
}'
For day-to-day use, the hdmx CLI wraps this whole surface behind ergonomic commands.