Aller au contenu
Livstick Développeurs

Garanties d’envoi

Lisez ces règles avant d’écrire un récepteur.

  • Au moins une fois. Un envoi peut se répéter, par exemple après un timeout de votre côté. Dédoublonnez sur l’en-tête Livstick-Event-Id.
  • Aucun ordre garanti. Un message.ready peut arriver avant le message.held du même message, et les événements de galerie peuvent arriver dans n’importe quel ordre. Fiez-vous à l’état indiqué dans le payload, pas à l’ordre d’arrivée.
  • Nouvelles tentatives pendant environ 24 heures. Un envoi qui ne reçoit pas de réponse 2xx est retenté selon le calendrier ci-dessous, puis mis de côté.
  • Aucune donnée personnelle. Le payload ne transporte que des identifiants et des états : ni nom, ni texte, ni e-mail, ni adresse, ni URL de média, ni clé d’édition, ni jeton.

Catalogue des événements

Sept événements. Abonnez un endpoint à tous, ou à une partie d’entre eux.

Événements webhook
Événement Quand il est envoyé
message.held Un message enregistré avec hold est encodé. Il n’est pas encore visible.
message.ready Un message est encodé et publié, ou un message retenu est libéré. Pour le message principal d’une galerie, c’est le seul événement.
gallery.message_added Une contribution à une galerie devient définitivement visible.
gallery.viewed Le destinataire ouvre une galerie pour la première fois.
message.viewed Le destinataire voit un message pour la première fois.
reaction.created Une réaction est enregistrée : quick, video ou thanks. Le champ reaction précise laquelle.
message.deleted Un message est supprimé par l’API, le portail, un modérateur ou la purge des messages retenus. Le champ reason précise l’origine.

Payload

Le corps est en JSON, 16 Kio au plus. Le même corps est envoyé quel que soit le mode du conteneur, et l’ensemble des clés est fixe pour chaque événement.

json
{
  "id": "evt_3f9c0a1b2c3d4e5f60718293a4b5c6d7",
  "type": "message.held",
  "createdAt": "2026-10-20T09:15:41Z",
  "platformId": "<platform id>",
  "data": {
    "crid": "CAG-8F2K",
    "urid": "4hc8wq2x1m",
    "mrid": "DON-51873",
    "messageId": "Qm4Zt8LpXa",
    "role": "contributor",
    "state": "held",
    "revision": 1,
    "durationMs": 41000
  }
}
  • id est stable pour l’événement. Un même événement a toujours le même id, d’une tentative à l’autre.
  • mrid vaut null quand le message n’en a pas.
  • reason est présent sur message.deleted : author, client, portal ou hold_expired.
  • reaction est présent sur reaction.created : {"kind": "quick" | "video" | "thanks", "replyMessageId": "…"}. Ses messageId et mrid sont ceux du message visé par la réaction.
  • messageId, role, state, revision et durationMs sont absents ou null sur gallery.viewed.

En-têtes

Chaque envoi porte ces en-têtes.

En-têtes d’un envoi
En-tête Valeur
Content-Type application/json
User-Agent Livstick-Webhooks/1
Livstick-Event-Id L’identifiant de l’événement, evt_…. Servez-vous-en pour dédoublonner.
Livstick-Event-Type Le type d’événement, par exemple message.ready.
Livstick-Delivery-Id L’identifiant de l’envoi, dlv_…. Un par événement et par endpoint.
Livstick-Delivery-Attempt Le numéro de la tentative, de 1 à 8.
Livstick-Signature t=<unix seconds>,v1=<hex>. Deux valeurs v1 apparaissent tant qu’un secret remplacé reste valide.

Vérifier la signature

La signature est un HMAC-SHA256 calculé sur l’horodatage, un point et le corps brut, avec pour clé la chaîne complète du secret (elle commence par whsec_). Vérifiez les octets reçus, avant tout parsing JSON, et rejetez un horodatage qui s’écarte de plus de 5 minutes de votre horloge. Comparez en temps constant. Acceptez l’événement si l’une des valeurs v1 correspond.

javascript
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 300;

// rawBody: the exact bytes received (Buffer or string), not re-serialised JSON.
export function verifyLivstickSignature(rawBody, header, secret, now = Date.now()) {
  let timestamp = null;
  const signatures = [];
  for (const item of header.split(",")) {
    const i = item.trim().indexOf("=");
    if (i < 0) return false;
    const key = item.trim().slice(0, i);
    const value = item.trim().slice(i + 1);
    if (key === "t") timestamp = value;
    else if (key === "v1") signatures.push(value);
  }
  if (!timestamp || !/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Math.floor(now / 1000) - Number(timestamp)) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();

  return signatures.some((hex) => {
    if (!/^[0-9a-f]{64}$/i.test(hex)) return false;
    return crypto.timingSafeEqual(Buffer.from(hex, "hex"), expected);
  });
}
go
package webhooks

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"strconv"
	"strings"
	"time"
)

const tolerance = 5 * time.Minute

var errSignature = errors.New("invalid Livstick-Signature")

// Verify checks the Livstick-Signature header against the raw request body.
func Verify(header, secret string, body []byte, now time.Time) error {
	var ts string
	var candidates [][]byte
	for _, item := range strings.Split(header, ",") {
		key, value, ok := strings.Cut(strings.TrimSpace(item), "=")
		if !ok {
			return errSignature
		}
		switch key {
		case "t":
			ts = value
		case "v1":
			if sig, err := hex.DecodeString(value); err == nil && len(sig) == sha256.Size {
				candidates = append(candidates, sig)
			}
		}
	}
	sent, err := strconv.ParseInt(ts, 10, 64)
	if err != nil {
		return errSignature
	}
	if d := now.Sub(time.Unix(sent, 0)); d > tolerance || d < -tolerance {
		return errSignature
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	expected := mac.Sum(nil)

	for _, candidate := range candidates {
		if hmac.Equal(candidate, expected) {
			return nil
		}
	}
	return errSignature
}

Nouvelles tentatives et échecs

Votre endpoint doit répondre par un 2xx en moins de 10 secondes. Toute autre réponse, un timeout ou une erreur réseau compte comme une tentative échouée. Livstick réessaie selon ce calendrier, compté à partir de la tentative échouée.

Calendrier des nouvelles tentatives
Tentative Délai avant la tentative suivante
1 1 minute
2 5 minutes
3 30 minutes
4 1 heure
5 3 heures
6 6 heures
7 13 heures
8 Dernière tentative, sans délai supplémentaire. Si elle échoue, l’envoi est mis de côté comme échoué
  • L’ensemble du calendrier dure environ 23 heures et 36 minutes.
  • Une réponse 4xx, y compris 410, est elle aussi retentée : vous pouvez corriger votre serveur pendant cette fenêtre.
  • Un envoi échoué est conservé dans le journal des envois et peut être rejoué depuis le portail client Livstick.
  • Un endpoint en pause ne reçoit rien : ses envois en attente sont ignorés et ne sont pas retentés.

Endpoints et secrets

Les endpoints se créent et se gèrent dans le portail client Livstick, par un utilisateur autorisé à gérer les webhooks. Une plateforme peut avoir jusqu’à cinq endpoints.

  • L’URL doit utiliser https, sans identifiants ni fragment. Les adresses de réseaux privés ou de loopback sont refusées.
  • Un secret commence par whsec_. Il n’est affiché en entier qu’une fois, à sa création ou à sa rotation. Stockez-le dans votre gestionnaire de secrets.
  • La rotation d’un secret laisse le précédent valide pendant 24 heures. Pendant ce temps, l’en-tête de signature porte deux valeurs v1, et les deux sont valides.
  • Un appel de test envoie à l’endpoint un événement webhook.test signé : vous pouvez ainsi vérifier votre récepteur sans attendre un vrai événement.

Journal des envois

GET /webhooks/deliveries liste les envois récents de votre plateforme, du plus récent au plus ancien, avec le type d’événement, le conteneur, le mrid, l’état et le nombre de tentatives. Il ne renvoie jamais le corps ni le texte de l’erreur. Servez-vous-en pour retrouver ce que votre récepteur a manqué, puis rejouez-le depuis le portail. Les filtres sont eventType, state (pending, done, failed, skipped), crid, mrid, limit (1 à 100) et skip.