Aller au contenu principal

Développeurs

API publique et webhooks Control

Control reste autonome : toute intégration externe est facultative et passe par des appels signés. Aucune donnée voyageur n'est échangée.

1. Authentification des appels

Chaque requête entrante et chaque webhook sortant est signé en HMAC-SHA256 avec la clé partagée de l'intégration. La signature porte sur la concaténationtimestamp + "." + corps brut et le corps doit être transmis tel quel, sans reformatage.

x-control-timestamp: 1756838400000        # millisecondes, tolérance ±5 minutes
x-control-signature: sha256=<hex minuscule>  # HMAC-SHA256(clé, timestamp + "." + body)
x-control-event-id: <uuid>                   # webhooks sortants uniquement (idempotence)
content-type: application/json               # corps limité à 64 Ko

Un horodatage hors tolérance ou une signature invalide renvoie 401, un rejeu déjà traité renvoie 409, un corps non conforme 400.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, timestamp, rawBody, header) {
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const received = header.replace(/^sha256=/, "");
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  return a.length === b.length && timingSafeEqual(a, b);
}
TS=$(date +%s000)
BODY='{"connection_id":"00000000-0000-0000-0000-000000000000"}'
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$CONTROL_SHARED_SECRET" -hex | awk '{print $2}')

curl -X POST https://control.marrerosterenn.fr/api/public/heating/status \
  -H "content-type: application/json" \
  -H "x-control-timestamp: $TS" \
  -H "x-control-signature: sha256=$SIG" \
  -d "$BODY"

2. Points d'entrée

POST
/api/public/link/request

Ouvre une demande de liaison et renvoie l'URL de consentement à présenter au propriétaire.

Requête

{
  "external_account_id": "ms-owner-42",
  "external_account_email": "proprietaire@example.com",
  "external_property_id": "ms-property-001",
  "scopes": ["heating:read", "heating:write"],
  "redirect_uri": "https://exemple.fr/retour-control",
  "state": "a1b2c3d4e5f6a7b8c9d0"
}

Réponse

{ "request_id": "…", "consent_url": "https://…/liaison?…", "expires_at": "…" }
POST
/api/public/link/exchange

Échange le code temporaire (durée de vie 2 minutes) contre la connexion active.

Requête

{ "code": "…", "state": "a1b2c3d4e5f6a7b8c9d0" }

Réponse

{ "connection_id": "…", "organization_id": "…", "scopes": ["heating:read"] }
POST
/api/public/link/status

Retourne l'état de la liaison et du forfait Control pour un compte externe.

Requête

{ "external_account_id": "ms-owner-42" }

Réponse

{ "status": "authorized", "subscription_status": "active" }
POST
/api/public/heating/status

Lecture du chauffage, des consignes et des alertes du logement relié.

Requête

{ "connection_id": "…" }

Réponse

{ "mode": "eco", "target_temperature": 17, "devices": [], "alerts": [] }
POST
/api/public/heating/command

Applique un mode ou une consigne. Nécessite le droit heating:write ; bornée par les seuils du logement.

Requête

{ "connection_id": "…", "mode": "comfort", "target_temperature": 20 }

Réponse

{ "applied": 2, "failed": 0, "target_temperature": 20 }
POST
/api/public/properties/devices/status

Vue conciergerie / propriétaire : liste en direct les appareils réels du logement (ballon, chauffage, prises, capteurs) avec pièce, état et actions possibles.

Requête

{ "external_property_id": "ms-property-001" }

Réponse

{
  "property_name": "La Maison de Marylou",
  "controllable": true,
  "emergency_stop": false,
  "devices": [{ "device_id": "…", "name": "Ballon", "kind": "water_heater", "room": "Garage",
    "online": true, "state": "off", "actions": ["turn_on", "turn_off"] }]
}
POST
/api/public/properties/devices/command

Pilote un appareil (turn_on, turn_off, set_temperature). Refusé si arrêt d'urgence, installation non validée ou abonnement inactif. Chaque commande est journalisée avec sa source (concierge ou owner).

Requête

{
  "external_property_id": "ms-property-001",
  "device_id": "…",
  "action": "turn_on",
  "source": "concierge"
}

Réponse

{ "ok": true, "device_id": "…", "action": "turn_on", "state": "on" }
POST
/api/public/occupancy/upsert

Transmet une période d'occupation minimale. Aucune donnée voyageur n'est acceptée.

Requête

{
  "connection_id": "…",
  "external_property_id": "ms-property-001",
  "occupied_from": "2026-09-05T15:00:00.000Z",
  "occupied_until": "2026-09-08T10:00:00.000Z"
}

Réponse

{ "ok": true }
POST
/api/public/properties/sync

Synchronise la liste des logements autorisés côté application externe.

Requête

{
  "organization_id": "…",
  "properties": [{ "external_property_id": "ms-property-001", "display_name": "Studio Port" }]
}

Réponse

{ "linked": 1, "unlinked": 0 }
POST
/api/public/connections/revoke

Révoque une connexion. Control cesse immédiatement d'accepter ses commandes.

Requête

{ "connection_id": "…", "reason": "compte fermé" }

Réponse

{ "ok": true, "status": "revoked" }

3. Webhooks sortants signés

Control envoie chaque événement à l'URL webhook configurée, avec les mêmes en-têtes de signature. La livraison est réessayée avec un report exponentiel (jusqu'à 8 tentatives, plafonné à 6 heures). Répondez 2xx pour acquitter, ou 409 si l'événement a déjà été traité : Control considère alors la livraison comme réussie. Chaque tentative est consignée dans le journal d'événements signés de l'administration Control.

{
  "event_id": "8f0f9a4c-…",            // identifiant unique, sert d'idempotence
  "event": "heating.status_changed",
  "version": 1,
  "occurred_at": "2026-09-02T18:12:04.220Z",
  "sent_at": "2026-09-02T18:12:05.010Z",
  "organization_id": "…",
  "connection_id": "…",                 // null si aucune liaison externe active
  "control_property_id": "…",
  "data": { "mode": "comfort", "target_temperature": 20 }
}
reservation.created

Nouvelle réservation importée d'un flux iCal ou saisie manuellement.

"data": {
  "reservation_id": "…",
  "calendar_id": "…",
  "source": "airbnb",
  "external_uid": "…",
  "starts_at": "2026-09-05T14:00:00.000Z",
  "ends_at": "2026-09-08T11:00:00.000Z",
  "status": "upcoming"
}
reservation.updated

Dates, statut ou résumé modifiés lors d'une synchronisation.

"data": { "reservation_id": "…", "starts_at": "…", "ends_at": "…", "status": "in_progress" }
reservation.cancelled

Réservation annulée manuellement ou disparue du flux iCal.

"data": { "reservation_id": "…", "starts_at": "…", "ends_at": "…" }
heating.settings_updated

Consignes ou bornes de température du logement modifiées.

"data": { "min_temperature": 7, "max_temperature": 24, "comfort_temperature": 20, "eco_temperature": 17, "frost_temperature": 8 }
heating.status_changed

Mode ou consigne appliqués (action utilisateur, API ou automatisation).

"data": { "mode": "comfort", "target_temperature": 20, "devices_applied": 2, "devices_failed": 0, "origin": "automation", "changed_at": "…" }
water_heater.settings_updated

Réglages du ballon modifiés (mode, consigne, heures creuses, cycle sanitaire).

"data": { "mode": "auto", "target_temperature": 55, "offpeak_enabled": true, "sanitary_cycle_temperature": 60 }
water_heater.status_changed

Marche, arrêt ou cycle sanitaire du ballon d'eau chaude.

"data": { "action": "on", "temperature": 55, "measured": false, "changed_at": "…" }

4. Règles et limites

  • Aucune donnée voyageur (nom, contact, montant) n'est émise ni acceptée.
  • Les commandes de chauffage restent bornées par les seuils du logement et refusées en cas d'arrêt d'urgence.
  • La révocation d'une liaison est immédiate et bilatérale : Control continue de fonctionner sans l'intégration.
  • Corps limité à 64 Ko, tolérance d'horodatage de ±5 minutes, anti-rejeu actif.