Skip to content

API reference

Read and change tickets from your own code, Zapier, Make or n8n. Every plan includes the API. There are no extra charges for using it.

Authentication

An admin makes a key in Flatdesk under Integrations. Send it in the Authorization header. Requests and responses are JSON. A key can make up to 1,000 requests an hour.

curl https://flatdeskfinal.vercel.app/api/v1/tickets?status=open \
  -H "Authorization: Bearer fd_your_key"

Errors come back as { "error": "..." } with a status code: 400 for a bad field, 401 for a missing or revoked key, 402 when the trial ended without a card (reading still works), 404, and 429 when you go too fast.

Endpoints

GET/api/v1/me

The team and key name. Use it to test a connection.

GET/api/v1/tickets

Tickets, most recently changed first.

status
open, pending or closed
updated_since
only tickets changed after this time (ISO 8601), for polling
customer_email
only this customer's tickets
tag
only tickets with this tag
limit
1 to 100, default 50

POST/api/v1/tickets

A new ticket from a customer, as if they had emailed. Your replies go to them by email. The AI answers it like any new email unless ai_answer is false.

customer_email
required
subject
required
body
required, the customer's message
customer_name
optional
tags
optional array; assignment rules apply
ai_answer
optional, default true
{
  "customer_email": "sam@example.com",
  "customer_name": "Sam Lee",
  "subject": "Order #1042 arrived damaged",
  "body": "The mug was broken when it arrived.",
  "tags": ["damaged"]
}

GET/api/v1/tickets/:number

One ticket with its whole conversation, internal notes included.

PATCH/api/v1/tickets/:number

Change the status, priority, assignee or tags.

status
open, pending or closed
priority
low, normal, high or urgent
assignee_email
someone on the team, or null to unassign
tags
replaces the tags
add_tags
adds to the tags
{ "status": "closed", "add_tags": ["refunded"] }

POST/api/v1/tickets/:number/messages

Reply to the customer (emailed to them), or add an internal note. A reply sets the ticket to pending unless you send a status.

body
required
internal
true for a note only the team sees
author_email
who it's from; defaults to whoever made the key
status
open, pending or closed
{ "body": "Sorry about that, Sam. A replacement is on its way.", "status": "closed" }

GET/api/v1/macros

The team's shared macros: name, reply text, tags and status they set. Personal macros stay private.

GET/api/v1/articles

Every help center article (without the text), with whether it is published and its public link.

GET/api/v1/articles/:slug

One article with its text.

POST/api/v1/articles

A new help center article. It stays a draft unless published is true.

title
required
body
required, plain text with a little markdown
section
optional category name
published
optional, default false
internal
optional, true for team-only articles
{ "title": "Change your plan", "body": "Open Settings, then Billing.", "section": "Billing", "published": true }

PATCH/api/v1/articles/:slug

Change an article's title, text, section, published or internal flag.

{ "published": false }

GET/api/v1/customers?email=

A customer, their fields, and their tickets.

Tickets come back with number, subject, status, channel (email, chat, sms or whatsapp), tags, customer, assignee, answered_by_ai, url and timestamps. A ticket's messages have author_type (customer, agent, ai or system), author_name, body, internal and created_at.

Webhooks

Set a webhook address under Settings, Alerts, and choose which tickets to send: only the ones that need a person, or every new one. Slack, Discord and Google Chat addresses get a message in their own format. Any other address gets JSON like this, with event ticket.created, ticket.handed_back (a customer wrote back to an AI answer) or ticket.overdue. Tick more under Alerts to also get ticket.replied, ticket.customer_replied, ticket.closed and ticket.assigned. A ticket set to “update the customer every…” sends ticket.update_due when the update is late:

{
  "event": "ticket.created",
  "text": "New ticket for the team: #1042 Order #1042 arrived damaged from Sam Lee (sam@example.com) by email.",
  "needsTeam": true,
  "reason": null,
  "ticket": {
    "number": 1042,
    "subject": "Order #1042 arrived damaged",
    "channel": "email",
    "status": "open",
    "url": "https://flatdesk.app/app/tickets/1042",
    "customer": { "name": "Sam Lee", "email": "sam@example.com" },
    "assignee": null
  },
  "sentAt": "2026-10-06T09:14:00.000Z"
}

Check it came from Flatdesk with the X-Flatdesk-Signature header: sha256= followed by the HMAC-SHA256 of the raw body, keyed with the secret shown under Alerts.

Webhook events

Every event uses the JSON above with the event name in event. In Zapier use Webhooks by Zapier, Catch Hook; in Make use Webhooks, Custom webhook. Both read the JSON as is, and a Filter on event picks the ones you want.

EventSentWhen
ticket.createdAlwaysA new ticket needs a person, or any new ticket if you chose to send every one.
ticket.handed_backAlwaysA customer wrote back to an answer the AI gave, so a person should look.
ticket.overdueAlwaysA ticket passed its reply or resolve target. The reason field says which.
ticket.repliedTick under AlertsYour team sent a public reply to the customer.
ticket.customer_repliedTick under AlertsA customer wrote back, on any ticket, including ones the AI answered.
ticket.closedTick under AlertsA person closed a ticket from its page or with a reply.
ticket.assignedTick under AlertsA ticket was assigned to someone, or to nobody.
ticket.update_dueWith an update timerA ticket set to "update the customer every..." is late for its next update.
testTest buttonYou pressed Send test under Settings, Alerts. The ticket field is a sample.

Headers

  • X-Flatdesk-Event: The event name, so a Zap or scenario can route without parsing the body.
  • X-Flatdesk-Signature: sha256= and the hex HMAC-SHA256 of the raw body, keyed with your Alerts secret.
  • User-Agent: Flatdesk-Alerts/1

Checking the signature

import { createHmac, timingSafeEqual } from "node:crypto";

function fromFlatdesk(rawBody, signatureHeader, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Use the raw request body, not re-serialized JSON. Zapier's Catch Hook doesn't expose headers, so keep the address secret there; a code step or your own server can verify.

Claude, Cursor and other AI tools (MCP)

Flatdesk is an MCP server, so an AI assistant can search, read and work your tickets: “what are customers saying about the new pricing?”, “tag every ticket about the outage and mark them urgent”, or “draft a note on #1042 with what we know”. Connect it with the same API key:

URL     https://flatdeskfinal.vercel.app/api/mcp
Header  Authorization: Bearer fd_...

# Claude Code
claude mcp add --transport http flatdesk https://flatdeskfinal.vercel.app/api/mcp \
  --header "Authorization: Bearer fd_..."

Tools: search_tickets, list_tickets, get_ticket, get_customer, update_ticket and reply_to_ticket. Replies are emailed to the customer right away, so the assistant is told to check with you before sending one. A team whose trial has ended can read but not change tickets, as with the API.

Zapier, Make and n8n

  • To start a Zap when a ticket comes in, use Webhooks by Zapier, Catch Hook, and paste its address as your webhook in Flatdesk.
  • To make a ticket from another app, add a Webhooks by Zapier, POST step to /api/v1/tickets with your key in the Authorization header.
  • To catch every change, poll /api/v1/tickets?updated_since= with the newest updated_at you've seen.

All integrations