Developer guide
Create and update tickets from your own systems, and get told when something happens. Everything here works with a plain HTTP client.
Quickstart
Make a token in the desk under Manage, API (owners and admins only). Copy it when it appears: it is shown once. Then create your first ticket:
curl https://app.effdesk.com/api/v1/tickets \ -H "Authorization: Bearer effk.YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"subject":"Hello from the API","body":"It works.","requesterEmail":"tina@example.com"}'
The ticket appears in your desk with the history line API: your token name. It starts the same automations and webhooks as a ticket made by hand.
Authentication
Send the token on every request as a bearer token. The base address is https://app.effdesk.com/api/v1 and bodies are JSON.
Authorization: Bearer effk.<organization id>.<secret>
Tokens belong to your organization, not a person. Delete a token under Manage, API and it stops working at once. Keep tokens out of browsers and public code.
Errors
Errors are JSON with a message you can show to a person: { "error": "…" }.
| Status | Meaning |
|---|---|
400 | The input is wrong, for example a missing subject or an unknown field value. |
401 | No token, or a wrong or deleted one. |
402 | The feature is part of a higher plan. The body has "upgrade": true. |
404 | No such ticket or path. |
405 | That method is not allowed on this path. |
413 | The body is too large (the limit is 100 KB). |
List tickets
Newest first, with the count in each status.
| Query | Meaning |
|---|---|
status | open, pending, solved or closed |
q | Search text |
tag | Only tickets with this tag |
limit, offset | Paging |
curl "https://app.effdesk.com/api/v1/tickets?status=open&tag=vip&limit=20" \ -H "Authorization: Bearer effk.YOUR_TOKEN"
Create a ticket
| Field | Meaning |
|---|---|
subject | Required. Up to 200 characters. |
body | Required. The first message. It supports formatting. |
requesterEmail | Required. The customer. |
requesterName | Optional. |
priority | Optional: low, normal, high, urgent. |
custom | Optional. Values for your custom fields. |
The reply is 201 with the ticket:
{ "ticket": { "number": 1043, "subject": "Payment failed", "status": "open",
"priority": "normal", "channel": "api", "requester_email": "tina@example.com",
"custom": { "order_number": "A-1042" }, … } }
Formatting in message bodies
Message text is plain text with a few shortcuts: a line of --- becomes a rule, text between lines of three backticks becomes a code block, `like this` is inline code, **bold** is bold, web addresses become links, and tab-separated rows (pasted from a spreadsheet) or | pipe | tables | become tables. Everything else is shown as typed, and nothing is run as HTML.
Get a ticket
The ticket and its public messages, oldest first. Internal notes are left out.
curl https://app.effdesk.com/api/v1/tickets/1043 -H "Authorization: Bearer effk.YOUR_TOKEN" { "ticket": { … }, "messages": [ { "id": 9, "kind": "requester", "author": "Tina", "body": "…", "createdAt": "2026-11-10T16:02:11Z" } ] }
Update a ticket
Send only what changes: status, priority, type, tags (the whole list), assigneeId, queueId, cc or custom. Automations and ticket.updated webhooks run as usual.
curl -X PATCH https://app.effdesk.com/api/v1/tickets/1043 \ -H "Authorization: Bearer effk.YOUR_TOKEN" -H "Content-Type: application/json" \ -d '{"status":"pending","tags":["billing","vip"]}'
Reply or add a note
| Field | Meaning |
|---|---|
body | Required. The message. |
public | true (the default) emails the customer. false adds a private note only your team sees. |
status | Optional, on a public reply: set the ticket to pending or solved. Solving asks the customer for a rating. |
curl https://app.effdesk.com/api/v1/tickets/1043/comments \ -H "Authorization: Bearer effk.YOUR_TOKEN" -H "Content-Type: application/json" \ -d '{"body":"We have refunded the charge. Anything else?","status":"solved"}'
Custom fields
Admins add fields under Manage, Fields: text, number, dropdown, yes or no, and date. Each has a key made from its name (for example Order number becomes order_number). Send values by key in custom when you create or update a ticket. An empty value clears a field.
{ "custom": { "order_number": "A-1042", "units": 3, "urgent_call": true, "product": "Pro" } }
Dropdown values must be one of the listed choices, numbers must be numbers and dates look like 2026-11-30. Otherwise you get a 400 that names the field.
Webhooks: events and payload
Add a webhook under Manage, Integrations. The address must be public and use https. Pick the events you want:
| Event | When |
|---|---|
ticket.created | A ticket is made, by email, the desk or the API. |
ticket.updated | Status, priority, assignee, tags or fields change. |
message.agent | An agent replies to the customer. |
message.customer | The customer writes back. |
ticket.mentioned | Someone is @mentioned in an internal note. |
Each delivery is a POST of JSON with a five second limit:
{ "event": "ticket.created", "organization": "org_123", "occurredAt": "2026-11-10T16:02:11.000Z",
"ticket": { "number": 1043, "subject": "Payment failed", "status": "open", "priority": "normal",
"channel": "api", "requester": { "name": "Tina", "email": "tina@example.com" },
"assigneeId": null, "tags": [], "custom": {} } }
Headers: X-EffDesk-Event, X-EffDesk-Timestamp (seconds since 1970) and X-EffDesk-Signature. Reply with any 2xx status. The last result of each webhook is shown in the desk.
Verify a signature
The secret is shown once when you add the webhook. The signature is sha256= followed by the hex HMAC-SHA256, with that secret, of the timestamp, a dot and the raw request body. Compare in constant time, and reject a timestamp more than a few minutes old so a captured request cannot be replayed.
// Node.js import crypto from 'node:crypto'; export function verify(rawBody, headers, secret) { const ts = headers['x-effdesk-timestamp'], sig = headers['x-effdesk-signature'] || ''; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const want = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex'); return sig.length === want.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want)); }
# Python import hmac, hashlib, time def verify(raw_body: bytes, headers, secret: str) -> bool: ts = headers.get("X-EffDesk-Timestamp", ""); sig = headers.get("X-EffDesk-Signature", "") if abs(time.time() - float(ts or 0)) > 300: return False want = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(sig, want)
Slack and Teams
For chat you do not need code. Choose Slack or Microsoft Teams when you add a webhook and paste the channel's incoming webhook address (Slack: https://hooks.slack.com/services/…; Teams: the address from a Workflows or connector webhook). EffDesk posts a short message for each event you pick. Teams receives an Adaptive Card.