Guide d'intégration des webhooks
Intégrez les événements de réservation Mekago dans votre DMS.
Vue d'ensemble
Mekago envoie des requêtes HTTPS POST à l'endpoint que vous avez enregistré dès qu'un événement du cycle de vie d'une réservation se produit.
reservation.created, Une nouvelle réservation a été confirmée.reservation.updated, Une réservation existante a été modifiée (créneau, mécanicien, etc.).reservation.cancelled, Une réservation a été annulée par un client ou par le garage.reservation.status_changed, Le statut d'une réservation a changé (par exemple IN_PROGRESS, COMPLETED).reservation.vehicle_ready, Un véhicule a été marqué comme prêt à être récupéré par le client.
En-têtes HTTP
X-Mekago-Event, Le type d'événement (par exemple reservation.created).X-Mekago-Timestamp, Secondes Unix au moment où Mekago a signé la requête.X-Mekago-Signature, sha256=HEX, HMAC-SHA256 de `TIMESTAMP.BODY` avec votre secret de webhook.X-Mekago-Delivery, Identifiant unique de cette tentative de livraison, à utiliser comme clé d'idempotence.
Payload
Chaque événement partage la même enveloppe. La forme de l'objet data est stable et versionnée.
{
"event": "reservation.created",
"version": 1,
"timestamp": "2026-04-18T10:00:00.000Z",
"garageId": "grg_abc",
"data": {
"reservationId": "res_123",
"status": "CONFIRMED",
"startTime": "2026-04-18T09:00:00.000Z",
"endTime": "2026-04-18T10:30:00.000Z",
"estimatedCost": 8900,
"finalCost": null,
"mechanicId": "mec_xyz",
"mechanicExternalId": "dms_jean_123",
"customer": {
"firstName": "Alice",
"lastName": "Doe",
"phone": "+33600000000",
"email": "alice@example.com"
},
"vehicle": {
"licensePlate": "AB-123-CD",
"make": "Renault",
"model": "Clio"
},
"offerings": [
{ "name": "Vidange", "duration": 60 }
]
}
}Vérifier la signature
Pour confirmer qu'un payload provient bien de Mekago et n'a pas été altéré :
- Stockez le secret renvoyé par integration-webhook-register (il n'est affiché qu'une seule fois).
- Rejetez les requêtes datant de plus de 5 minutes pour limiter les attaques par rejeu.
- Calculez HMAC-SHA256(secret, `TIMESTAMP.RAW_BODY`) et comparez en temps constant.
- Persistez X-Mekago-Delivery pour dédupliquer les tentatives retransmises.
Node.js / TypeScript (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const WEBHOOK_SECRET = process.env.MEKAGO_WEBHOOK_SECRET ?? '';
app.post(
'/mekago/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const timestamp = req.header('X-Mekago-Timestamp');
const signature = req.header('X-Mekago-Signature');
if (!timestamp || !signature) return res.status(400).end();
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (ageSeconds > 300) return res.status(408).end();
const rawBody = req.body.toString('utf8');
const expected =
'sha256=' +
crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end();
}
const event = JSON.parse(rawBody);
// TODO: upsert into your DMS using event.data.reservationId as idempotency key
return res.status(200).json({ externalReservationId: 'dms_res_456' });
},
);
app.listen(3000);Python (Flask)
import hmac, hashlib, time, json
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = b"<your-webhook-secret>"
@app.post("/mekago/webhook")
def mekago_webhook():
timestamp = request.headers.get("X-Mekago-Timestamp", "")
signature = request.headers.get("X-Mekago-Signature", "")
if not timestamp or not signature:
abort(400)
if abs(time.time() - int(timestamp)) > 300:
abort(408)
raw_body = request.get_data(as_text=True)
mac = hmac.new(
WEBHOOK_SECRET,
f"{timestamp}.{raw_body}".encode("utf-8"),
hashlib.sha256,
)
expected = "sha256=" + mac.hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
event = json.loads(raw_body)
# TODO: upsert into your DMS using event["data"]["reservationId"]
return jsonify(externalReservationId="dms_res_456"), 200PHP
<?php
$secret = getenv('MEKAGO_WEBHOOK_SECRET') ?: '';
$timestamp = $_SERVER['HTTP_X_MEKAGO_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_MEKAGO_SIGNATURE'] ?? '';
if ($timestamp === '' || $signature === '') {
http_response_code(400);
exit;
}
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(408);
exit;
}
$rawBody = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true);
// TODO: upsert into your DMS using $event['data']['reservationId']
header('Content-Type: application/json');
echo json_encode(['externalReservationId' => 'dms_res_456']);C# (ASP.NET Core)
using System.Security.Cryptography;
using System.Text;
app.MapPost("/mekago/webhook", async (HttpContext ctx) =>
{
var secret = Environment.GetEnvironmentVariable("MEKAGO_WEBHOOK_SECRET") ?? "";
var timestamp = ctx.Request.Headers["X-Mekago-Timestamp"].ToString();
var signature = ctx.Request.Headers["X-Mekago-Signature"].ToString();
if (string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(signature))
return Results.BadRequest();
var nowSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(nowSeconds - long.Parse(timestamp)) > 300)
return Results.StatusCode(408);
using var reader = new StreamReader(ctx.Request.Body);
var rawBody = await reader.ReadToEndAsync();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var computed = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}"));
var expected = "sha256=" + Convert.ToHexString(computed).ToLowerInvariant();
var expectedBytes = Encoding.UTF8.GetBytes(expected);
var signatureBytes = Encoding.UTF8.GetBytes(signature);
if (!CryptographicOperations.FixedTimeEquals(expectedBytes, signatureBytes))
return Results.Unauthorized();
// TODO: parse rawBody and upsert into your DMS
return Results.Ok(new { externalReservationId = "dms_res_456" });
});Java (Spring Boot)
@PostMapping(value = "/mekago/webhook", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, String>> handle(
@RequestHeader("X-Mekago-Timestamp") String timestamp,
@RequestHeader("X-Mekago-Signature") String signature,
@RequestBody String rawBody
) throws Exception {
long now = Instant.now().getEpochSecond();
if (Math.abs(now - Long.parseLong(timestamp)) > 300) {
return ResponseEntity.status(408).build();
}
String secret = System.getenv("MEKAGO_WEBHOOK_SECRET");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal((timestamp + "." + rawBody).getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : digest) hex.append(String.format("%02x", b));
String expected = "sha256=" + hex;
if (!MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8))) {
return ResponseEntity.status(401).build();
}
// TODO: parse rawBody and upsert into your DMS
return ResponseEntity.ok(Map.of("externalReservationId", "dms_res_456"));
}Idempotence
Comme Mekago réessaie les livraisons échouées, le même événement peut arriver plusieurs fois. Dédupliquez par X-Mekago-Delivery ou par (event, reservationId, timestamp) dans votre DMS.
Nouvelles tentatives
Les réponses non-2xx (et les erreurs réseau) déclenchent de nouvelles tentatives avec un délai croissant :
- Deuxième tentative après 1 minute
- Troisième tentative après 5 minutes
- Quatrième tentative après 30 minutes
- Cinquième tentative après 2 heures, puis la livraison est marquée comme échouée.
Corps de réponse optionnel
Si votre DMS attribue son propre identifiant, renvoyez-le dans le JSON de réponse sous la clé `externalReservationId`. Mekago le stocke et l'inclura dans les payloads reservation.updated / reservation.cancelled suivants pour la même réservation.