3 · Être prévenu
Plutôt que de demander « quoi de neuf ? » toutes les minutes, cette application se fait prévenir : OKLÉON lui envoie un webhook dès que des fiches changent, et elle relit alors ce qui a bougé. Le message ne contient que des identifiants — jamais de coordonnées.
La bonne façon de se tenir à jour
| Façon de faire | Appels par jour quand rien ne change | Délai |
|---|---|---|
Interroger /changements toutes les minutes | 1 440 | ≤ 1 min |
| Webhook + relecture ciblée + filet horaire ce que fait cette démo | 24 | 1 à 2 min |
Un webhook arrive → on relit seulement les fiches annoncées (un appel chacune).
Une fois par heure, un passage sur /changements rattrape un webhook qui se serait perdu.
Plus de 20 fiches d'un coup (un import) → une synchronisation par pages, moins coûteuse.
L'adresse de réception
- Adresse
https://integration.okleon.fr/webhook.php- Déclarée dans
- demo → Administration → API → Webhooks
- Ensuite
- relit les fiches par le miroir, avec la clé
Les trois règles
- Vérifier la signature sur le corps brut, avant de le lire.
- Écarter un doublon : un envoi peut arriver deux fois.
- Répondre 2xx vite, puis travailler.
Derniers envois reçus
| Reçu | Envoi | Événement | Fiches | Verdict | Ensuite |
|---|---|---|---|---|---|
| 23/10 14:45:23 | whe_23 |
changements | 51 | accepté | incrémentale : 0 fiche(s) relue(s), 0 suppression(s), 7 appel(s) |
Les requêtes refusées sont notées sans leur contenu : rien ne prouve qu'elles viennent d'OKLÉON.
Vérifier la signature — src/Recepteur.php
/**
* Vérifie la signature. Rend null si elle est bonne, sinon la raison du refus.
* ⚠️ $corps est le corps BRUT : un JSON décodé puis réencodé ne donne pas les
* mêmes octets, et la signature ne correspondrait plus.
*/
public static function verifier(string $secret, string $corps, string $entete, int $maintenant): ?string
{
if ($secret === '') return 'secret_absent';
if (!preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', trim($entete), $m)) return 'signature_absente';
if (abs($maintenant - (int)$m[1]) > self::TOLERANCE_S) return 'horodatage_perime';
$attendu = hash_hmac('sha256', $m[1] . '.' . $corps, $secret);
// hash_equals et non === : une comparaison qui s'arrête au premier octet
// différent laisse deviner la signature, octet par octet, au chronomètre.
return hash_equals($attendu, $m[2]) ? null : 'signature_fausse';
}
Recevoir un envoi — src/Recepteur.php
/**
* Traite une requête reçue. Rend [code HTTP, envoi accepté ou null].
* Le travail (relire les fiches) est fait par l'appelant, APRÈS la réponse.
*/
public function recevoir(string $secret, string $corps, string $entete, int $maintenant): array
{
$refus = self::verifier($secret, $corps, $entete, $maintenant);
if ($refus !== null) {
// On note le refus, jamais le corps : il n'a pas été prouvé qu'il vient d'OKLÉON.
$this->noter(null, null, 0, $refus);
return [400, null];
}
$envoi = json_decode($corps, true);
if (!is_array($envoi) || empty($envoi['id'])) {
$this->noter(null, null, 0, 'corps_illisible');
return [400, null];
}
// ⭐ Déjà reçu (un nouvel essai alors qu'on avait bien répondu) : on répond
// 200 — sinon OKLÉON réessaierait encore — et on ne refait rien.
if (!$this->noter((string)$envoi['id'], (string)($envoi['evenement'] ?? ''),
count((array)($envoi['data'] ?? [])), 'accepte')) {
$this->noter((string)$envoi['id'], (string)($envoi['evenement'] ?? ''), 0, 'doublon');
return [200, null];
}
return [200, $envoi];
}
Relire seulement les fiches annoncées — src/Miroir.php
/**
* Applique un webhook : relit SEULEMENT les fiches qu'il annonce.
* Une fiche = un appel. Rien de plus, rien d'autre.
* ⚠️ Ne touche PAS au point de reprise : il appartient au filet de sécurité
* (synchroniser), qui repassera de toute façon sur ces fiches — sans danger,
* l'enregistrement est idempotent.
*/
public function appliquerEnvoi(ClientOkleon $api, array $changements): array
{
// Un gros lot (un import chez l'agence) : une synchronisation lit par
// pages de 100, c'est bien moins d'appels que 100 lectures une à une.
if (count($changements) > self::RELECTURE_MAX) return $this->synchroniser($api);
$t0 = microtime(true);
$bilan = ['mode' => 'webhook', 'recus' => 0, 'supprimes' => 0, 'ignorees' => [], 'erreur' => null];
try {
foreach ($changements as $ch) {
$c = (string)($ch['collection'] ?? '');
$id = (string)($ch['id'] ?? '');
// Une collection que cette démo ne recopie pas : on l'ignore.
if (!in_array($c, self::COLLECTIONS, true) || $id === '') continue;
if (($ch['action'] ?? '') === 'supprime') {
$bilan['supprimes'] += $this->supprimer($c, $id);
continue;
}
try {
// Toujours relire : le webhook dit QUE la fiche a bougé, pas CE qu'elle contient.
$this->enregistrer($c, $api->get('/' . $c . '/' . rawurlencode($id)));
$bilan['recus']++;
} catch (ErreurOkleon $e) {
// Effacée entre l'envoi et notre lecture : la suppression suivra dans
// le flux ; on l'applique dès maintenant.
if ($e->http !== 404) throw $e;
$bilan['supprimes'] += $this->supprimer($c, $id);
}
}
} catch (Throwable $e) {
// Rien de grave : le filet de sécurité rattrapera à son prochain passage.
$bilan['erreur'] = $e instanceof ErreurOkleon ? $e->codeApi . ' — ' . $e->getMessage() : $e->getMessage();
}
return $this->clore($bilan, $api, $t0);
}