API

Des tickets depuis tes propres systèmes

Un ticket ouvert via l'API est un vrai ticket : il crée un canal Discord, ton équipe le traite comme d'habitude, et la réponse revient directement au visiteur. Il n'a jamais besoin d'un compte Discord.

POST /v1/tickets
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" }
      }'
Réponse
{
  "ticket_request_id": 4102,
  "session_token": "3Qk1r…",
  "status": "pending"
}

Le canal apparaît quelques secondes plus tard. Garde le session_token — c'est la seule chose qui permet au visiteur de lire et de répondre à son ticket.

Deux clés, deux rôles

Celle que tu envoies décide ce que l'appel est autorisé à faire.

X-API-Key

Ta clé depuis le tableau de bord. Elle couvre tout le serveur : ouvre des tickets, les lis, réponds en tant qu'équipe.

Reste sur ton serveur et nulle part ailleurs. Dans un navigateur, elle laisse n'importe quel visiteur lire tous les tickets.

X-Session-Token

Revient quand tu ouvres un ticket et couvre exactement ce ticket — pas le serveur.

Celle-ci est sûre à donner au visiteur dont c'est le ticket.

Permissions par clé

Tu coches ce que la clé peut faire quand tu la crées. Elle ne peut jamais en faire plus.

  • ticket:createOuvre des tickets
  • ticket:readLis les tickets, messages et panneaux
  • ticket:replyÉcris dans un ticket en tant qu'équipe
  • stats:readLis les chiffres agrégés

Une permission manquante retourne 403, pas 401. La clé va bien — elle n'a simplement pas cette case cochée.

Envoie uniquement les champs documentés ici. Un nom de champ inconnu n'est pas ignoré silencieusement mais rejeté avec 422 — une typo devrait apparaître tout de suite, pas comme un ticket vide.

Les endpoints

Six appels couvrent tout le flux. La référence complète avec chaque champ est liée en bas.

POST/v1/ticketsOuvre un ticket et reçois un token de session
GET/v1/ticketsListe les tickets, paginés via un curseur
GET/v1/tickets/{id}Un ticket avec sa conversation
POST/v1/tickets/{id}/messagesRéponds en tant qu'équipe — la route pour l'e-mail, le CRM ou un helpdesk
GET/v1/panelsListe les panneaux pour trouver le panel_id
GET/v1/statsChiffres pour tes propres rapports

Limites

Comptées par clé, pas par IP — donc tous tes clients peuvent partager un seul backend. Chaque réponse te dit combien il t'en reste.

Sur chaque réponse
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 47
Retry-After: 47

Tentatives

Quand une demande expire, tu ne peux pas dire si le ticket a été créé. Envoie une clé que tu choisis toi-même et la tentative retourne la même réponse au lieu d'un deuxième ticket.

Lors de l'ouverture d'un ticket
Idempotency-Key: 7f3a1c9e-4b21-4f0a-9d55-1e8c2b7a6d40

On se souvient de la clé pendant 24 heures. Réutiliser une clé pour une demande vraiment différente retourne 422 — mieux une erreur que le mauvais numéro de ticket.

Webhooks : quand quelque chose doit t'arriver

L'autre direction : au lieu que tu demandes, Ticketanizer t'appelle. Chaque événement est envoyé en POST signé vers une URL de ton choix — pour un pont e-mail, un CRM, ou simplement une notification dans ton propre système.

POST ton URL
{
  "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"
  }
}
En-têtes à chaque livraison
X-Ticketanizer-Event: message.created
X-Ticketanizer-Delivery: 90183
X-Ticketanizer-Timestamp: 1785600896
X-Ticketanizer-Signature: t=1785600896,v1=9f2c…

X-Ticketanizer-Delivery est unique pour chaque livraison — retiens-le et tu pourras repérer un doublon et éviter de le traiter deux fois.

sent_at et created_at dans les webhooks sont en UTC. L'API de lecture retourne encore les temps en heure serveur sans décalage — garde ça en tête quand tu fusionnes les webhooks avec le polling.

Vérifier la signature

Calcule HMAC-SHA256 sur <timestamp>.<rawBody> avec ton secret et compare le résultat à l'une des valeurs v1. Deux choses faciles à oublier : l'en-tête peut porter plusieurs valeurs v1 (pendant une rotation de secret), et la comparaison doit s'effectuer en temps constant.

Node.js
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)));
}

Nous signons le corps brut — exactement les octets qui arrivent. Vérifier après avoir analysé et ré-sérialisé le JSON produit une signature différente et rien que des échecs.

Les événements

Tu coches ce que tu veux quand tu crées le point de terminaison. Sans sélection tu obtiens chaque événement — sauf une exception, voir ci-dessous.

createdTicket ouvert
claimed · unclaimedTicket attribué ou libéré à nouveau
closed · reopenedTicket fermé ou réouvert
escalatedTicket transféré à un autre panel
priority · member_added · member_removedPriorité modifiée, quelqu'un ajouté ou supprimé du ticket
feedbackNote donnée après fermeture
message.createdChaque message dans le ticket, texte complet. Seulement quand explicitement coché — le contenu de la conversation ne doit pas s'écouler sans demande vers un point de terminaison que quelqu'un a configuré pour les changements de statut.
deleted · transcript · snoozedTicket supprimé, transcript stockée, ticket mis en pause. Si ton système reflète les tickets, coche aussi deleted — sinon un ticket supprimé reste ouvert chez toi pour toujours.

Ce qu'il faut savoir sur la livraison

  • TentativesRéponds avec un statut 2xx. Tout le reste compte comme un échec et nous réessayons — après 1, 5, 15 et 60 minutes, puis c'est fini.
  • Protection contre les bouclesChaque message porte une origin (discord, web, inbox, relay). Si tu réponds à un événement, filtre les tiens — sinon ta réponse déclenche l'événement suivant.
  • DoublonsTu réponds trop lentement et la même livraison arrive à nouveau. C'est intentionnel : deux fois vaut mieux que pas du tout. L'ID de livraison est ce qui te protège de le traiter deux fois.
  • Points de terminaison mortsAprès 20 livraisons d'affilée qui échouent définitivement, nous désactivons le point de terminaison et écrivons la raison dans le tableau de bord. Une brève interruption ne suffira pas — le comptage commence seulement une fois que tous les essais sont épuisés.
  • Rotation du secretQuand tu effectues une rotation, l'ancien secret reste valide pendant 7 jours et nous signons avec les deux. Cela te permet de basculer tranquillement sans perdre une livraison.

Les livraisons sont visibles dans le tableau de bord — y compris la charge utile envoyée et la réponse de ton serveur. De là tu peux aussi rejouer une livraison à la main, tant qu'elle est encore dans le journal.

La référence complète

Chaque endpoint, champ et code d'erreur — généré depuis le service en cours d'exécution et testable directement dans le navigateur.