Skip to main content

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 as 2026-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:

  1. Click New token.
  2. Give it a name you'll recognize later.
  3. Choose an expiry (up to 365 days — there is no "never").
  4. Set an access policy — switch on exactly the resources this token needs, at read or read-&-write. Everything defaults to no access.
  5. Add at least one IP address or CIDR to the allowlist (required — see IP allowlist). The form can add your current IP for you.
  6. 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.

ScopeGrants
tickets:readRead tickets, messages, drafts, history, attachments, reports
tickets:writeCreate / update / delete tickets, post messages, upload attachments. Implies tickets:read
chat:readRead chats, conversations, messages, sessions
chat:writeSend agent messages, join / leave / close / reopen conversations. Implies chat:read
customers:readRead customer records
customers:writeCreate / update / merge customer records. Implies customers:read
settings:readRead 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:

CodeMeaningHTTP
SCOPE_DENIEDThe token lacks the required scope. extensions.valid_values lists the scope(s) that would satisfy the operation200
RATE_LIMITEDThe per-token request budget is exhausted. extensions.retryAfter is seconds until reset, and the Retry-After header carries the same number429
INVALID_API_TOKENToken not found, malformed, or otherwise unusable200

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.