Un webhook prévient ton serveur dès qu’un fait se produit dans tes ligues : un résultat noté, un horaire publié, une saison close. Rally envoie un POST signé à l’adresse que tu déclares.

Déclarer un point de livraison

  1. Dans l’écran Intégrations, va à « Déclarer un point de livraison ».
  2. Dans « Adresse à appeler », entre une adresse https. Rally ne poste jamais en clair.
  3. Coche les événements voulus. Ne rien cocher revient à tout recevoir.
  4. Clique sur « Déclarer le point ».
  5. Copie le secret de signature, qui commence par whsec_. Rally ne le montre qu’une fois.

L’adresse doit être publique : une adresse locale ou privée est refusée.

Ce que Rally envoie

Chaque livraison est un POST avec un corps JSON et ces en-têtes :

En-tête Contenu
webhook-id L’identifiant de l’événement, égal au champ id du corps.
webhook-timestamp L’heure d’envoi, en secondes Unix.
webhook-signature Une ou plusieurs signatures v1,…, séparées par une espace.
content-type application/json
user-agent Rally-Webhooks/1.0

Le corps est l’événement. Il reste mince : un objet, son identifiant, sa révision. Aucun nom de personne n’y figure.

{
  "id": "8f6e4d2c-0b9a-4c7e-a5d3-1f9e7c5a3b0d",
  "type": "match.completed",
  "created": "2027-01-13T03:12:45.000Z",
  "organization": "9d3f5a7c-2b1e-4c8d-a6f0-3e2d1c0b9a87",
  "data": {
    "object": "match",
    "id": "f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c",
    "revision": 4
  }
}
  • type : le type d’événement, dans le catalogue plus bas.
  • data.object et data.id : l’objet concerné et son identifiant.
  • data.revision : la révision de l’objet, ou null.
  • data.previous : ce qui vient de changer, pour quelques types seulement.
  • data.scheduled : la nouvelle place, pour match.rescheduled seulement.

Relis l’état courant par l’API plutôt que de te fier au corps : une livraison peut arriver en retard.

Vérifier la signature

Vérifie chaque livraison avant de la traiter. La signature suit la spécification Standard Webhooks :

  1. Refuse la livraison si webhook-timestamp s’écarte de ton heure de plus de 300 secondes.
  2. Retire le préfixe whsec_ du secret, puis décode le reste en base64 : c’est la clé.
  3. Calcule un HMAC-SHA256 de webhook-id, un point, webhook-timestamp, un point, puis du corps brut.
  4. Encode le résultat en base64 et compare-le à chaque signature v1,… de webhook-signature, à temps constant.

Vérifie le corps brut, tel que reçu. Un corps décodé puis réécrit en JSON ne donne plus la même signature.

Le plus simple est une bibliothèque officielle Standard Webhooks : standardwebhooks sur npm comme sur PyPI, et d’autres langages sur standardwebhooks.com. Elles acceptent le secret whsec_ tel quel.

Si tu préfères ne rien installer, voici une vérification complète, sans dépendance. Ces deux fichiers sont testés à chaque changement contre les signatures que Rally produit.

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

const TOLERANCE_SECONDS = 300;

/**
 * @param {string} secret The endpoint's `whsec_…` signing secret.
 * @param {Record<string, string | string[] | undefined>} headers The request headers, lowercase keys.
 * @param {string} rawBody The body exactly as received, before any JSON.parse.
 * @param {number} [now] The current Unix time, in seconds.
 * @returns {boolean}
 */
export function verifyWebhook(secret, headers, rawBody, now = Math.floor(Date.now() / 1000)) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (typeof id !== 'string' || typeof timestamp !== 'string' || typeof signatures !== 'string') {
    return false;
  }

  const sentAt = Number(timestamp);
  if (!Number.isInteger(sentAt) || Math.abs(now - sentAt) > TOLERANCE_SECONDS) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = Buffer.from(
    createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64'),
  );

  return signatures.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const presented = Buffer.from(signature);
    return presented.length === expected.length && timingSafeEqual(presented, expected);
  });
}

Réponds par un code 2xx en moins de 10 secondes, puis traite l’événement à part. Au-delà de 10 secondes, Rally compte un échec et relance.

Faire tourner le secret

  1. Dans « Points de livraison », choisis la grâce de signature : coupure immédiate, 24 heures, 3 jours ou 7 jours au plus.
  2. Clique sur « Faire tourner le secret de », suivi de l’adresse du point.
  3. Copie le nouveau secret et déploie-le.

Pendant la grâce, chaque livraison porte deux signatures, une par secret. Ta vérification accepte donc l’ancien comme le nouveau. Une seule grâce à la fois : une seconde rotation avec grâce est refusée par ROTATION_EN_COURS.

Relances et désactivation

Seule une réponse 2xx compte comme une livraison réussie. Une redirection est un échec, comme un délai dépassé ou une adresse injoignable.

Après un échec, Rally relance au bout de 5 s, 30 s, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, puis 48 h. Cela fait 10 tentatives en un peu moins de quatre jours.

Après la dixième tentative ratée, le point est désactivé. L’écran Intégrations le montre, avec le journal des livraisons pour comprendre la panne. Clique sur « Réactiver », suivi de l’adresse, quand ton serveur répond de nouveau.

Une relance peut livrer deux fois le même événement. Ignore un webhook-id déjà traité.

Rattraper les événements manqués

Un point réactivé reçoit les événements à partir de ce moment : rien n’est rejoué. Pour combler le trou, lis GET /v1/events avec une clé read.

curl https://gorally.games/v1/events \
  -H "Authorization: Bearer $RALLY_API_KEY"

C’est le même flux que les webhooks, du plus ancien au plus récent, avec les mêmes identifiants. Pour lire la page suivante, passe l’id du dernier événement de la page dans after.

Catalogue des événements

Il n’existe pas d’événement de classement : le classement est calculé à chaque lecture. Écoute match.completed, puis relis le classement.

Type Quand Champs de data.previous
league.created Une ligue est créée. —
season.created Une saison est créée. —
season.closed Une saison est close. seasonId, status, closureRevision, obsolete
season.reopened Une saison close est rouverte. seasonId, status, closureRevision, obsolete
season.abandoned Une saison est abandonnée. seasonId, status, closureRevision, obsolete
season.renewed Une saison est reconduite dans la suivante. renewalId, sourceSeasonId, targetSeasonId, categories, teams, skippedTeams, skippedMembers
category.created Une catégorie est créée. —
category.updated Les règles ou le nombre de places d’une catégorie changent. —
registration.submitted Une équipe demande à s’inscrire. —
registration.withdrawn Une inscription est retirée. —
registration.reinstated Une inscription retirée est rétablie. —
schedule.published Un horaire est publié. —
match.rescheduled Une rencontre change de place à la publication de l’horaire. resourceId, startsAt, endsAt, aussi dans data.scheduled
match.completed Un résultat est noté. —
match.awarded Un résultat est attribué, par exemple sur forfait. —
match.corrected Un résultat ou une attribution est corrigé. —
match.cancelled Une rencontre pas encore jouée est annulée. —
match.voided Une rencontre commencée ou jouée est invalidée. Une reprise sera une nouvelle rencontre. —
series.opened Une série éliminatoire commence. —
series.updated Un résultat fait avancer une série. seriesId, nodeKey, winnerTeamId, championTeamId, cancelledMatchIds, releasedMatchIds, scheduleRecomputeRequired

Guide suivant : Erreurs →