Logo Mekago

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"), 200

PHP

<?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.