Webhooks
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
- Dans l’écran Intégrations, va à « Déclarer un point de livraison ».
- Dans « Adresse à appeler », entre une adresse
https. Rally ne poste jamais en clair. - Coche les événements voulus. Ne rien cocher revient à tout recevoir.
- Clique sur « Déclarer le point ».
- 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.objectetdata.id: l’objet concerné et son identifiant.data.revision: la révision de l’objet, ounull.data.previous: ce qui vient de changer, pour quelques types seulement.data.scheduled: la nouvelle place, pourmatch.rescheduledseulement.
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 :
- Refuse la livraison si
webhook-timestamps’écarte de ton heure de plus de 300 secondes. - Retire le préfixe
whsec_du secret, puis décode le reste en base64 : c’est la clé. - Calcule un HMAC-SHA256 de
webhook-id, un point,webhook-timestamp, un point, puis du corps brut. - Encode le résultat en base64 et compare-le à chaque signature
v1,…dewebhook-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);
});
}import base64
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_webhook(secret, headers, raw_body, now=None):
"""secret: the endpoint's whsec_ signing secret.
headers: the request headers (a dict, or your framework's headers object).
raw_body: the body exactly as received, as bytes, before any json.loads.
"""
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not msg_id or not timestamp or not signatures:
return False
try:
sent_at = int(timestamp)
except ValueError:
return False
now = int(time.time()) if now is None else now
if abs(now - sent_at) > TOLERANCE_SECONDS:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
for entry in signatures.split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature.encode(), expected):
return True
return FalseRé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
- Dans « Points de livraison », choisis la grâce de signature : coupure immédiate, 24 heures, 3 jours ou 7 jours au plus.
- Clique sur « Faire tourner le secret de », suivi de l’adresse du point.
- 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"const response = await fetch('https://gorally.games/v1/events', {
headers: {
Authorization: `Bearer ${process.env.RALLY_API_KEY}`,
},
});
const data = await response.json();import os
import requests
response = requests.get(
"https://gorally.games/v1/events",
headers={"Authorization": f"Bearer {os.environ['RALLY_API_KEY']}"},
timeout=10,
)
data = response.json()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 |