for the builders
Run your helpdesk from your own product
A REST API and webhooks on every plan. Create tickets from a contact form, sync customers from your CRM, reply from your app, or react to every change. The HDdesk console runs on the same API.
curl https://app.hddesk.com/v1/tickets \
-H "Authorization: Bearer $HDDESK_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "Order 1182 arrived damaged",
"requester": { "email": "jo@example.com" },
"domain_id": "dom_01JB...",
"tags": ["orders"]
}'
# 201 Created
{ "id": "tkt_01JC...", "number": "ACME-1042", "status": "open" }
The basics
- Base URL
https://app.hddesk.com/v1. JSON in and out,snake_casefields, ISO 8601 times in UTC.- Authentication
- API keys from Settings, API keys in the console, sent as a Bearer token. Tools that only speak Basic auth can use the key as the username. Keys are read-write or read-only.
- Acting as an agent
- Changes are recorded as made by the key, or attributed to an agent with
agent_idor theHDdesk-On-Behalf-Ofheader. - Rate limits
- 20 requests per second per account, bursts up to 40.
RateLimit-*headers on every response,Retry-Afteron 429. - Idempotency
- Send an
Idempotency-Keyheader with POST requests; a retry within 24 hours returns the original response. - Pagination
- Cursor-based. Pages stay stable while new items arrive.
- Errors
- HTTP status codes and one JSON error format with a type, code, message, parameter and request id.
- Versioning
/v1is stable: fields and endpoints are added, never removed or renamed.
Examples
Tickets can be referred to by their tkt_ id or by the number agents see.
Create a ticket from a contact form
curl https://app.hddesk.com/v1/tickets \
-H "Authorization: Bearer $HDDESK_KEY" \
-H "Idempotency-Key: form-7d1c2f" \
-H "Content-Type: application/json" \
-d '{"subject":"Order 1182 arrived damaged","description_text":"The box was crushed.","requester":{"email":"jo@example.com","name":"Jo Lee"},"domain_id":"dom_01JB...","tags":["orders"]}'
Reply and set the ticket to Pending
curl https://app.hddesk.com/v1/tickets/1042/reply \
-H "Authorization: Bearer $HDDESK_KEY" -H "Content-Type: application/json" \
-d '{"body_text":"Sorry about that, a replacement is on its way.","status":"pending"}'
List urgent open tickets in one brand
curl "https://app.hddesk.com/v1/tickets?status=open&priority=urgent&domain_id=dom_01JB..." -H "Authorization: Bearer $HDDESK_KEY"
What the API covers
Tickets and conversations
List with filters, create, update, delete and restore, bulk update, merge, split, watchers, activity. Reply, add notes, add customer messages, forward, upload attachments.
Customers and team
Contacts (including merge and erasure), companies, agents, groups. Search across tickets, contacts and companies.
Brands and settings
Domains and their templates, mailboxes, ticket types, statuses, custom fields, tags, canned responses, business hours, SLA policies, automations, views and reports.
Webhooks
Register an HTTPS endpoint and HDdesk POSTs every matching event to it, optionally only for some brands.
- Ticket events: created, updated, status, priority, assignment, group and tag changes, merged, deleted, restored, spam
- Message events: customer reply, agent reply, note, forward, bounce
- SLA warnings and breaches; contact and company changes
- Signed with HMAC-SHA256; secrets can be rotated with a 24-hour overlap
- Retried with backoff for 3 days; delivery log, manual retry and test pings
GET /v1/eventskeeps 30 days of events as a polling fallback
Every request carries HDdesk-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with your endpoint secret. Reject requests older than 5 minutes.
Verify a delivery in Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, header, rawBody) {
const parts = header.split(",").map((p) => p.split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return parts.filter(([k]) => k === "v1").some(([, v]) => {
const got = Buffer.from(v, "hex");
return got.length === expected.length && timingSafeEqual(got, expected);
});
}
Verify a delivery in Python
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = [p.split("=", 1) for p in header.split(",")]
t = int(next((v for k, v in parts if k == "t"), 0))
if not t or abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1")