Dispozition

v1.0 · URL stable : https://dispozition.com/spec/webhook/v1

Spécification webhook Dispozition

Ce document décrit le format des notifications HTTP envoyées par Dispozition lorsqu’un billet de voyage évolue. L’objectif est simple : un même langage pour que les constructeurs et les sites de disposition branchent leurs outils (ERP, balance, portail, scripts) sans ambiguïté — et pour faire évoluer l’industrie vers des intégrations ouvertes.

Introduction

Dispozition envoie des requêtes POST vers une URL HTTPS que vous configurez sur votre portail de site de disposition. Chaque message est un objet JSON avec une enveloppe commune (spec_version, event, event_id, etc.) et, pour les événements liés à un voyage, un objet site et un objet voyage détaillés.

Le champ spec_url dans le JSON pointe toujours vers cette page : vous pouvez ainsi découvrir la documentation à partir d’un payload réel. La version courante est 1.0.

Authentification

Chaque livraison est signée avec HMAC-SHA256 en utilisant le secret partagé associé à votre webhook (affiché une seule fois à la configuration). Le corps signé est exactement les octets UTF-8 du corps HTTP que vous recevez (tel quel), après ajout par Dispozition des champs delivered_at et delivery_attempt juste avant l’envoi.

Message signé

message = timestamp_utf8 + "." + raw_body_bytes

timestamp est la valeur du header X-Dispozition-Timestamp (secondes Unix, chaîne décimale). La signature est sha256= suivie de l’hexadécimal du HMAC (minuscules).

Headers HTTP

HeaderDescription
Content-Typeapplication/json; charset=utf-8
User-AgentDispozition-Webhook/1.0
X-Dispozition-Spec-VersionEx. 1.0
X-Dispozition-EventNom de l’événement (ex. voyage.created)
X-Dispozition-DeliveryMême valeur que event_id dans le JSON
X-Dispozition-TimestampHorodatage utilisé dans le HMAC
X-Dispozition-Signaturesha256=<hex>

Bonne pratique : refusez les requêtes trop anciennes (replay), par exemple si |maintenant − timestamp| > 300 secondes.

Exemple — Python

import hmac
import hashlib
import time

def verify_dispozition_webhook(secret: str, raw_body: bytes, headers: dict, max_skew_s: int = 300) -> bool:
    ts = headers.get("X-Dispozition-Timestamp") or headers.get("X-Dispozition-Timestamp".lower())
    sig = headers.get("X-Dispozition-Signature") or ""
    if not ts or not sig.startswith("sha256="):
        return False
    if abs(int(time.time()) - int(ts)) > max_skew_s:
        return False
    msg = ts.encode("utf-8") + b"." + raw_body
    digest = hmac.new(secret.encode("utf-8"), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, "sha256=" + digest)

Exemple — Node.js

const crypto = require("crypto");

function verifyDispozitionWebhook(secret, rawBodyBuffer, headers, maxSkewS = 300) {
  const ts = headers["x-dispozition-timestamp"];
  let sig = headers["x-dispozition-signature"] || "";
  if (!ts || !sig.startsWith("sha256=")) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(ts, 10)) > maxSkewS) return false;
  const msg = Buffer.concat([Buffer.from(String(ts), "utf8"), Buffer.from("."), rawBodyBuffer]);
  const digest = crypto.createHmac("sha256", secret).update(msg).digest("hex");
  const expected = "sha256=" + digest;
  try {
    return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  } catch {
    return false;
  }
}

Exemple — PHP

 $maxSkewS) return false;
    $msg = $ts . '.' . $rawBody;
    $digest = hash_hmac('sha256', $msg, $secret);
    return hash_equals($sig, 'sha256=' . $digest);
}

Événements

ÉvénementQuand
voyage.createdUn nouveau billet de voyage est créé côté constructeur et cible ce site de disposition.
voyage.receivedLe site accuse réception du voyage sur son portail (reçu).
voyage.weight_updatedLe poids mesuré (ou sa suppression) est enregistré sur le portail.
webhook.testÉvénement de test depuis l’interface ; payload minimal sans site ni voyage.

Vous choisissez quels événements activer sur votre webhook.

Payload voyage — structure et exemple

Ci-dessous un exemple réaliste mais fictif (noms et coordonnées d’exemple). Les champs réservés pour extensions futures peuvent être null.

{
  "spec_version": "1.0",
  "spec_url": "https://dispozition.com/spec/webhook/v1",
  "event": "voyage.created",
  "event_id": "evt_abc123…",
  "occurred_at": "2026-05-07T18:30:00.000000+00:00",
  "delivered_at": "2026-05-07T18:30:00.123456+00:00",
  "delivery_attempt": 1,
  "platform": {
    "name": "Dispozition",
    "url": "https://dispozition.com"
  },
  "site": {
    "id": "ds_42",
    "nom": "Centre de tri Example",
    "portal_code": "N3T6DVR8",
    "site_type": "centre",
    "site_subtype": null,
    "address": {
      "raw": "100 rue Example, Montréal QC",
      "region": "Québec",
      "region_code": 11,
      "country": "CA",
      "lat": 45.5,
      "lng": -73.5,
      "google_place_id": "ChIJ…"
    }
  },
  "voyage": {
    "id": "v_1001",
    "voyage_code": "550e8400-e29b-41d4-a716-446655440000",
    "external_reference": null,
    "ticket_number": null,
    "status": "en_attente",
    "is_received": false,
    "is_double": false,
    "origin": {
      "type": "construction_site",
      "name": "Chantier tunnel Example",
      "constructor": { "id": "ent_7", "nom": "Excavations Example inc." },
      "address": {
        "raw": "200 av. Chantier",
        "region": null,
        "region_code": null,
        "country": "CA",
        "lat": null,
        "lng": null,
        "google_place_id": null
      }
    },
    "destination": {
      "type": "disposal_site",
      "site_id": "ds_42",
      "address": {
        "raw": "100 rue Example, Montréal QC",
        "region": "Québec",
        "region_code": 11,
        "country": "CA",
        "lat": 45.5,
        "lng": -73.5,
        "google_place_id": "ChIJ…"
      }
    },
    "vehicle": {
      "fleet_number": "T-12",
      "license_plate": "ABC 123",
      "license_plate_region": "QC",
      "transporter": { "nom": "Transport Example" }
    },
    "material": {
      "code": null,
      "name": "Terre excavee propre",
      "category": "clean_soil",
      "is_contaminated": false
    },
    "measurement": {
      "estimated_tonnes": 22.5,
      "measured_tonnes": null,
      "measured_at": null,
      "source": null
    },
    "timeline": {
      "created_at": "2026-05-07T17:00:00+00:00",
      "received_at": null,
      "completed_at": null
    },
    "compliance": {
      "ca_reference": null,
      "tracking_required": null,
      "manifest_id": null
    },
    "links": {
      "portal_view": "https://dispozition.com/destinataire/N3T6DVR8"
    }
  }
}

Référence des champs (enveloppe)

spec_versionVersion du contrat (ex. 1.0).
spec_urlLien vers cette documentation.
eventNom de l’événement livré.
event_idIdentifiant unique de la livraison ; idempotence côté client possible.
occurred_atMoment métier de l’événement (ISO 8601 avec fuseau).
delivered_atAjouté au moment de l’envoi HTTP.
delivery_attemptNuméro de tentative (1 à 4 selon la politique de retry).
platformInfos sur Dispozition (name, url).
siteSite de disposition cible (absent pour webhook.test).
voyageDétails du billet (absent pour webhook.test).

Référence — voyage (extraits)

voyage.idIdentifiant interne préfixé v_.
voyage.voyage_codeUUID public du billet (stable).
voyage.statusVoir enum voyage.status ci-dessous.
voyage.origin / destinationProvenance chantier et arrivée site ; adresses structurées.
voyage.material.categoryCatégorie normalisée (enum material.category).
voyage.material.is_contaminatedtrue si la catégorie implique un suivi renforcé (ex. sol contaminé).
voyage.measurement.sourceOrigine du poids mesuré si présent : portail_manuel, sync_balance, api, ou null.
voyage.links.portal_viewLien vers le portail du site (si disponible).

Enums

material.category

Valeurs stables (anglais, snake_case). Les libellés métier restent dans material.name.

voyage.status

Politique de version

Le header X-Dispozition-Spec-Version et le champ JSON spec_version restent alignés.

Changelog

v1.0 — 7 mai 2026

Première publication publique de la spécification : événements voyage.created, voyage.received, voyage.weight_updated, signature HMAC-SHA256, modèle site + voyage, enums material.category et statuts de voyage documentés.

Samuel Jacques
Auteur de cette version