API
Tickets from your own systems
A ticket opened through the API is a real ticket: it creates a Discord channel, your team answers it as usual, and the answer goes straight back to the visitor. They never need a Discord account.
curl https://api.ticketanizer.com/v1/tickets \
-H "X-API-Key: tk_dein_schluessel" \
-H "Content-Type: application/json" \
-d '{
"panel_id": 3,
"visitor_name": "Anna Berger",
"first_message": "Meine Bestellung ist nicht angekommen.",
"metadata": { "Bestellnummer": "2026-8891", "Tarif": "Pro" }
}'
{
"ticket_request_id": 4102,
"session_token": "3Qk1r…",
"status": "pending"
}
The channel appears seconds later. Keep the session_token — it is the only thing that lets that one visitor read and answer their ticket.
Two keys, two jobs
Which one you send decides what the call is allowed to do.
X-API-Key
Your key from the dashboard. It covers the whole server: open tickets, read them, answer as the team.
Belongs on your server and nowhere else. In a browser, it lets any visitor read every ticket.
X-Session-Token
Comes back when you open a ticket and covers exactly that one ticket — not the server.
This one is safe to hand to the visitor whose ticket it is.
Permissions per key
You tick what the key may do when you create it. It can never do more.
ticket:createOpen ticketsticket:readRead tickets, messages and panelsticket:replyWrite into a ticket as the teamstats:readRead aggregate numbers
A missing permission returns 403, not 401. The key is fine — it simply lacks that tick.
Only send fields documented here. An unknown field name is not silently ignored but rejected with 422 — a typo should show up straight away, not as an empty ticket.
The endpoints
Six calls cover the whole flow. The full reference with every field is linked at the bottom.
| POST | /v1/tickets | Open a ticket and receive a session token |
| GET | /v1/tickets | List tickets, paged through a cursor |
| GET | /v1/tickets/{id} | One ticket including its conversation |
| POST | /v1/tickets/{id}/messages | Answer as the team — the route for e-mail, CRM or a helpdesk |
| GET | /v1/panels | List panels to find the panel_id |
| GET | /v1/stats | Numbers for your own reporting |
Limits
Counted per key, not per IP — so all of your customers can share one backend. Every response tells you how much you have left.
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 47
Retry-After: 47
Retries
When a request times out, you cannot tell whether the ticket was created. Send a key you choose yourself and the retry returns the same answer instead of a second ticket.
Idempotency-Key: 7f3a1c9e-4b21-4f0a-9d55-1e8c2b7a6d40
We remember the key for 24 hours. Reusing one key for a genuinely different request returns 422 — better an error than the wrong ticket number.
Webhooks: when something should arrive at your end
The other direction: instead of you asking, Ticketanizer calls you. Every event goes out as a signed POST to a URL of your choice — for an e-mail bridge, a CRM, or simply a notification in your own system.
{
"event": "message.created",
"sent_at": "2026-08-01T12:34:56Z",
"guild_id": "1470847667327471738",
"ticket": {
"id": 42, "number": 17,
"panel_id": 3, "panel_name": "Support",
"status": "open", "priority": 2,
"opener_id": "234...", "metadata": null
},
"message": {
"id": 8891,
"discord_message_id": "13...",
"origin": "discord",
"is_staff": true,
"is_ai": false,
"author_id": "234...",
"author_bot": false,
"author_name": "Lisa",
"content": "Ich schaue mir das an.",
"created_at": "2026-08-01T12:34:56Z"
}
}
X-Ticketanizer-Event: message.created
X-Ticketanizer-Delivery: 90183
X-Ticketanizer-Timestamp: 1785600896
X-Ticketanizer-Signature: t=1785600896,v1=9f2c…
X-Ticketanizer-Delivery is unique per delivery — remember it and you can spot a repeat and avoid processing it twice.
sent_at and created_at in webhooks are UTC. The read API still returns times in server time without an offset — keep that in mind when you merge webhook pushes with polling.
Verifying the signature
Compute HMAC-SHA256 over <timestamp>.<rawBody> with your secret and compare the result against one of the v1 values. Two things that are easy to miss: the header can carry several v1 values (during a secret rotation), and the comparison should run in constant time.
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const parts = header.split(",").map(p => p.split("="));
const ts = parts.find(([k]) => k === "t")?.[1];
// v1 kann MEHRFACH vorkommen — waehrend einer Secret-Rotation
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(ts + "." + rawBody)
.digest("hex");
return sigs.some(s => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}
We sign the raw body — exactly the bytes that arrive. Verifying after parsing and re-serialising the JSON produces a different signature and nothing but failures.
The events
You tick what you want when you create the endpoint. With no selection you get every event — with one exception, see below.
created | Ticket opened |
claimed · unclaimed | Ticket claimed or released again |
closed · reopened | Ticket closed or reopened |
escalated | Ticket handed over to another panel |
priority · member_added · member_removed | Priority changed, someone added to or removed from the ticket |
feedback | Rating given after closing |
message.created | Every message in the ticket, full text. Only when explicitly ticked — conversation content should not flow unasked to an endpoint someone set up for status changes. |
deleted · transcript · snoozed | Ticket deleted, transcript stored, ticket snoozed. If your system mirrors tickets, tick deleted as well — otherwise a deleted ticket stays open on your side forever. |
What to know about delivery
RetriesAnswer with a 2xx status. Anything else counts as a failure and we try again — after 1, 5, 15 and 60 minutes, then no more.Loop guardEvery message carries anorigin(discord,web,inbox,relay). If you answer in response to an event, filter out your own — otherwise your reply triggers the next event.DuplicatesAnswer too slowly and the same delivery arrives again. That is deliberate: twice beats not at all. The delivery ID is what protects you from processing it twice.Dead endpointsAfter 20 deliveries in a row that fail for good, we switch the endpoint off and write the reason into the dashboard. A brief outage will not do it — counting starts only once all retries are exhausted.Rotating the secretWhen you rotate, the old secret stays valid for 7 days and we sign with both. That lets you switch over calmly without losing a delivery.
Deliveries are visible in the dashboard — including the payload sent and your server's response. From there you can also replay a delivery by hand, as long as it is still in the log.
The full reference
Every endpoint, field and error code — generated from the running service and testable right in the browser.