01.Démarrer
Chaque tournoi a ses propres clés d'API. Crée-en une dans le dashboard : Tournois → ton tournoi → onglet API → Nouvelle clé. L'adresse de l'API est :
https://hurix.xyz/api/v1Toutes les réponses sont en JSON. Les écritures passent par le bot Hurix : elles suivent exactement les mêmes règles que le dashboard (statut du tournoi, format BO, tableau) et mettent à jour Discord (fil du match, tableau, annonces, rôles de récompense).
Les résultats envoyés par l'API sont validés directement, sans confirmation des joueurs : la clé agit comme un arbitre. L'historique indique « API · nom de la clé », et le staff peut toujours corriger, faire rejouer ou trancher un signalement.
// Fin de partie sur ton serveur de jeu : envoi du score à Hurix.
const res = await fetch("https://hurix.xyz/api/v1/tournaments/42/results", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HURIX_KEY}`,
"Content-Type": "application/json",
// Même valeur si tu renvoies la requête : le score ne compte qu'une fois.
"Idempotency-Key": `partie-${gameId}`,
},
body: JSON.stringify({
players: ["123456789012345678", "234567890123456789"], // comptes Discord
scores: [2, 1],
note: "Partie #" + gameId,
}),
});
if (!res.ok) {
const err = await res.json(); // { error: "bad_score", message: "…" }
console.error(res.status, err.error, err.message);
}02.Clés et permissions
Envoie la clé dans l'en-tête Authorization :
Authorization: Bearer hxt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx- read — tournoi, participants, matchs, classement.
- register — inscrire ou retirer des participants.
- results — scores et forfaits, validés directement.
- manage — ouvrir ou fermer les inscriptions, lancer, terminer.
Une clé ne fonctionne que pour son tournoi (une autre adresse répond 404), est affichée une seule fois et seule son empreinte est conservée. Révoque-la à tout moment depuis le dashboard ; elle s'arrête d'elle-même 30 jours après la fin du tournoi. 10 clés actives au plus par tournoi.
Utilise l'API depuis un serveur (plugin, back-end, script) : jamais depuis le code d'une page web, la clé y serait visible de tous. GET /api/v1/me renvoie la clé utilisée, son tournoi et ses permissions.
03.Erreurs, limites et nouvelles tentatives
En cas d'erreur, la réponse contient un code stable et un message lisible :
HTTP/1.1 409 Conflict
{ "error": "downstream_played", "message": "La suite du tableau a déjà été jouée." }- 400 invalid, bad_score, draw_not_allowed, match_closed… · 401 unauthorized · 403 missing_scope · 404 not_found, match_not_found, not_registered · 409 bad_status, full, already_registered, downstream_played, idempotency_in_progress · 429 rate_limited · 503 bot_unavailable
Limite : 120 requêtes par minute et par clé. Au-delà, la réponse est 429 avec l'en-tête Retry-After (en secondes). En cas de 503, le bot redémarre : réessaie quelques secondes plus tard.
Idempotency-Key (conseillé sur toutes les écritures) : une requête renvoyée avec la même valeur, par exemple après une coupure réseau, reçoit la première réponse (en-tête Idempotent-Replayed: true) sans rien refaire. Les réponses sont gardées 24 h.
04.Lecture
/tournaments/{id}readLe tournoi : nom, jeu, format, statut, dates, réglages, nombre de participants.
/tournaments/{id}/participantsreadInscrits avec leur identifiant (entryId), compte Discord (userId), statut, tête de série, bilan et membres d'équipe.
/tournaments/{id}/matches?status=open|completed|allreadMatchs du tableau. status=open : matchs ouverts, à jouer maintenant.
/tournaments/{id}/matches/{matchId}readUn match avec ses déclarations et son litige éventuel.
/tournaments/{id}/standingsreadClassement (championnat) ou places finales.
05.Résultats
/tournaments/{id}/resultsresultsLe plus simple pour un serveur de jeu : donne les deux comptes Discord et leurs scores, Hurix retrouve le match en cours entre eux et remet les scores dans le bon ordre.
{ "players": ["123456789012345678", "234567890123456789"], "scores": [2, 1], "note": "optionnel" }/tournaments/{id}/matches/{matchId}/resultresultsScore d'un match précis (scoreA pour le participant a, scoreB pour b). Renvoie le match à jour.
{ "scoreA": 2, "scoreB": 0 }/tournaments/{id}/matches/{matchId}/forfeitresultsForfait du perdant, désigné par discordId ou entryId. Sans l'un ni l'autre : double forfait.
{ "discordId": "234567890123456789", "note": "Absent" }Un score impossible pour le format (3–0 en BO3), un match pas encore ouvert ou un match dont la suite est déjà jouée sont refusés (400 ou 409). En championnat, l'égalité est acceptée si le tournoi l'autorise.
06.Inscriptions
/tournaments/{id}/participantsregisterInscrit un membre du serveur Discord par son identifiant. Pour les tournois par équipes, teamName crée l'équipe dont il est capitaine. Si le tournoi est complet, il passe en liste d'attente.
{ "discordId": "123456789012345678", "teamName": "Les Renards" }/tournaments/{id}/participants/{entryId}registerRetire un participant (exclusion). replaceWithWaitlist (vrai par défaut) fait monter le premier de la liste d'attente avant le début.
{ "reason": "Désinscrit sur le site", "replaceWithWaitlist": true }07.Gestion du tournoi
/tournaments/{id}/registration/openmanageOuvre les inscriptions (le tournoi doit déjà être publié sur Discord).
/tournaments/{id}/registration/closemanageFerme les inscriptions.
/tournaments/{id}/startmanageLance le tournoi : tirage, tableau, ouverture des premiers matchs.
/tournaments/{id}/finishmanageTermine le tournoi quand tous les matchs sont joués (places finales et récompenses).
08.Webhooks
Ajoute jusqu'à 5 adresses https dans l'onglet API du tournoi. À chaque événement choisi, Hurix envoie une requête POST en JSON :
{
"id": "5b0f1c6e-8f0a-4c55-9a77-3f2b9f1d2c10",
"event": "match.completed",
"createdAt": 1762876800000,
"tournamentId": 42,
"data": {
"match": {
"matchId": 318,
"code": "Q2",
"side": "winners",
"round": 2,
"position": 2,
"bestOf": 3,
"status": "completed",
"a": { "entryId": 77, "name": "Léa", "discordId": "123456789012345678" },
"b": { "entryId": 81, "name": "Maxime", "discordId": "234567890123456789" },
"scoreA": 2,
"scoreB": 1,
"winnerEntryId": 77,
"forfeit": false,
"deadlineAt": null
}
}
}Événements disponibles :
match.openedmatch.completedmatch.disputedtournament.registration_openedtournament.registration_closedtournament.startedtournament.completedtournament.cancelledentry.registeredentry.withdrawnentry.excludedentry.replacedping
En-têtes : X-Hurix-Event, X-Hurix-Delivery (identifiant unique, utile pour ignorer un doublon), X-Hurix-Timestamp (secondes) et X-Hurix-Signature = sha256=HMAC-SHA256(secret, « horodatage.corps »). Vérifie toujours la signature avec le corps brut, et refuse un horodatage de plus de 5 minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
// express.raw({ type: "application/json" }) : il faut le corps brut.
app.post("/hurix", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Hurix-Timestamp");
const signature = req.get("X-Hurix-Signature") ?? "";
const expected = "sha256=" + createHmac("sha256", process.env.HURIX_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
if (!fresh || signature.length !== expected.length ||
!timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
if (event.event === "match.opened") startMatch(event.data.match);
res.sendStatus(200); // réponds vite (moins de 8 s), traite ensuite
});<?php
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_HURIX_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_HURIX_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, getenv('HURIX_WEBHOOK_SECRET'));
if (abs(time() - (int) $timestamp) > 300 || !hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(200);Réponds par un code 2xx en moins de 8 secondes. Sinon, Hurix réessaie après 1 min, 5 min, 30 min, 2 h, 6 h puis 24 h, et abandonne. Après 20 échecs d'affilée, le webhook est coupé ; réactive-le depuis le dashboard. Les redirections ne sont pas suivies et seules les adresses publiques sont acceptées. Les envois partent au plus tard 20 secondes après l'événement.
