Rapidy Développeurs

Référence

Webhooks

Être prévenu à chaque changement, et vérifier que le message vient bien de Rapidy.

Pourquoi les webhooks plutôt que l'interrogation

Interroger getShipment en boucle fonctionne, coûte votre quota d'appels, et vous apprend un changement en retard. Un webhook vous le dit au moment où il se produit.

Recevoir

Déclarez une URL HTTPS publique dans la console, sous Intégrations → Webhooks. Rapidy y poste un JSON à chaque événement.

Recevoir et vérifier un webhook
# Rapidy signe chaque webhook :
#   X-Rapidy-Signature: t=<unix_ts>,v1=<hex_hmac_sha256>
# le HMAC portant sur "<t>.<corps_brut>".
#
# Vérification à la main (à n'utiliser que pour comprendre — préférez le SDK,
# qui compare en temps constant) :
TS=$(echo "$SIG_HEADER" | sed -n 's/.*t=\([0-9]*\).*/\1/p')
V1=$(echo "$SIG_HEADER" | sed -n 's/.*v1=\([a-f0-9]*\).*/\1/p')
EXPECTED=$(printf '%s.%s' "$TS" "$RAW_BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)
[ "$EXPECTED" = "$V1" ] && echo "signature valide"
import { verifyWebhookSignature } from '@rapidy/sdk';

app.post('/webhooks/rapidy', express.raw({ type: 'application/json' }), (req, res) => {
  // Le CORPS BRUT, jamais un objet re-sérialisé : une différence d'espacement
  // ou d'ordre de clés suffit à invalider la signature.
  const ok = verifyWebhookSignature({
    rawBody: req.body,
    signatureHeader: req.header('X-Rapidy-Signature'),
    secret: process.env.RAPIDY_WEBHOOK_SECRET!,
  });
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Répondez 2xx VITE : le traitement long va dans une file.
  res.sendStatus(200);
});
from rapidy_sdk import verify_webhook_signature

@app.post("/webhooks/rapidy")
def rapidy_webhook(request):
    # Le CORPS BRUT, jamais un dict re-sérialisé.
    ok = verify_webhook_signature(
        raw_body=request.body,
        signature_header=request.headers.get("X-Rapidy-Signature"),
        secret=os.environ["RAPIDY_WEBHOOK_SECRET"],
    )
    if not ok:
        return HttpResponse(status=401)

    event = json.loads(request.body)
    # Répondez 2xx VITE : le traitement long va dans une file.
    return HttpResponse(status=200)
use Rapidy\Sdk\Webhooks;

// Le CORPS BRUT, jamais un tableau re-sérialisé.
$raw = file_get_contents('php://input');
$ok = Webhooks::verifySignature(
    rawBody: $raw,
    signatureHeader: $_SERVER['HTTP_X_RAPIDY_SIGNATURE'] ?? null,
    secret: getenv('RAPIDY_WEBHOOK_SECRET'),
);
if (!$ok) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
http_response_code(200); // répondez VITE : le traitement long va dans une file
# Le CORPS BRUT, jamais un Hash re-sérialisé.
ok = Rapidy::Webhooks.verify_signature(
  raw_body: request.raw_post,
  signature_header: request.headers['X-Rapidy-Signature'],
  secret: ENV.fetch('RAPIDY_WEBHOOK_SECRET')
)
return head(:unauthorized) unless ok

event = JSON.parse(request.raw_post)
head :ok # répondez VITE : le traitement long va dans une file
func rapidyWebhook(w http.ResponseWriter, r *http.Request) {
    // Le CORPS BRUT, jamais un JSON ré-encodé.
    raw, _ := io.ReadAll(r.Body)

    if !rapidy.VerifyWebhookSignature(raw, r.Header.Get("X-Rapidy-Signature"), secret) {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    var event map[string]any
    _ = json.Unmarshal(raw, &event)
    w.WriteHeader(http.StatusOK) // répondez VITE : le traitement long va dans une file
}
// Le CORPS BRUT, jamais un objet ré-encodé.
byte[] raw = request.getInputStream().readAllBytes();

boolean ok = Webhooks.verifySignature(
    raw, request.getHeader("X-Rapidy-Signature"), System.getenv("RAPIDY_WEBHOOK_SECRET"));
if (!ok) {
    response.setStatus(401);
    return;
}

// Répondez 2xx VITE : le traitement long va dans une file.
response.setStatus(200);
// Le CORPS BRUT, jamais un objet ré-encodé.
using var reader = new StreamReader(Request.Body);
var raw = Encoding.UTF8.GetBytes(await reader.ReadToEndAsync());

var ok = Webhooks.VerifySignature(
    raw, Request.Headers["X-Rapidy-Signature"], secret);
if (!ok) return Unauthorized();

// Répondez 2xx VITE : le traitement long va dans une file.
return Ok();
// Le CORPS BRUT, jamais un Map ré-encodé.
final raw = await utf8.decodeStream(request);

final ok = verifyWebhookSignature(
  rawBody: raw,
  signatureHeader: request.headers.value('X-Rapidy-Signature'),
  secret: Platform.environment['RAPIDY_WEBHOOK_SECRET']!,
);
if (!ok) {
  request.response.statusCode = 401;
  await request.response.close();
  return;
}

request.response.statusCode = 200; // répondez VITE
await request.response.close();

Vérifier la signature

Chaque message porte l'en-tête :

X-Rapidy-Signature: t=<horodatage_unix>,v1=<hmac_sha256_hex>

Le HMAC-SHA256 est calculé sur la chaîne "<t>.<corps_brut>", avec votre secret de webhook.

Vérifiez sur le corps BRUT reçu. Jamais sur un objet re-sérialisé : une différence d'espacement ou d'ordre de clés suffit à invalider la signature — et le symptôme (« mes webhooks ne passent plus ») ne désigne jamais la cause. En Express, utilisez express.raw() ; en Rails, request.raw_post ; en PHP, php://input.

Comparez en temps constant. Une comparaison naïve laisse fuir la signature attendue, octet par octet, à qui mesure votre temps de réponse. Les SDK le font pour vous.

Fenêtre anti-rejeu

RègleValeur
Âge maximal accepté300 s
Avance maximale tolérée60 s

Un message plus vieux que la fenêtre est un rejeu : refusez-le. Une petite avance est tolérée parce qu'une horloge émettrice peut dériver — mieux vaut cela que de rejeter un message légitime.

Ces valeurs viennent du même manifeste que les SDK : elles sont identiques dans les huit, par construction.

Répondre

  • Répondez 2xx rapidement. Le traitement long va dans une file, pas dans le gestionnaire du webhook.
  • Une signature invalide → 401, et passez au message suivant. Ce n'est pas un incident.
  • Une erreur de votre côté → un statut d'erreur : Rapidy réessaiera.

Idempotence

La livraison est au moins une fois : le même événement peut vous parvenir deux fois, notamment après un temps de réponse trop long de votre côté.

Traitez chaque message de façon idempotente — déduplication sur l'identifiant d'événement, ou opération naturellement rejouable. Un compteur incrémenté sans garde sera faux le jour où le réseau hoquette.

Que faire à la réception

ÉvénementCe que votre système fait typiquement
Envoi affectéAfficher « un livreur arrive » côté client.
Colis enlevéPasser la commande en « expédiée ».
Colis livréClore la commande, déclencher l'après-vente.
Espèce remiseRien côté client — c'est le signal qui ouvre votre règlement.
Litige ouvertSuspendre l'après-vente automatique, alerter un humain.