EffDesk

Developer guide

Create and update tickets from your own systems, and get told when something happens. Everything here works with a plain HTTP client.

Plans. The REST API and custom fields need the Suite plan or above. Webhooks, Slack and Teams need Team or above.

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": "…" }.

StatusMeaning
400The input is wrong, for example a missing subject or an unknown field value.
401No token, or a wrong or deleted one.
402The feature is part of a higher plan. The body has "upgrade": true.
404No such ticket or path.
405That method is not allowed on this path.
413The body is too large (the limit is 100 KB).

List tickets

GET/tickets

Newest first, with the count in each status.

QueryMeaning
statusopen, pending, solved or closed
qSearch text
tagOnly tickets with this tag
limit, offsetPaging
curl "https://app.effdesk.com/api/v1/tickets?status=open&tag=vip&limit=20" \
  -H "Authorization: Bearer effk.YOUR_TOKEN"

Create a ticket

POST/tickets
FieldMeaning
subjectRequired. Up to 200 characters.
bodyRequired. The first message. It supports formatting.
requesterEmailRequired. The customer.
requesterNameOptional.
priorityOptional: low, normal, high, urgent.
customOptional. 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

GET/tickets/{number}

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

PATCH/tickets/{number}

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

POST/tickets/{number}/comments
FieldMeaning
bodyRequired. The message.
publictrue (the default) emails the customer. false adds a private note only your team sees.
statusOptional, 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:

EventWhen
ticket.createdA ticket is made, by email, the desk or the API.
ticket.updatedStatus, priority, assignee, tags or fields change.
message.agentAn agent replies to the customer.
message.customerThe customer writes back.
ticket.mentionedSomeone 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)
Use the raw body exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the signature.

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.

Start free   See plans