OK Intégrer OKLÉON

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 faireAppels par jour quand rien ne changeDélai
Interroger /changements toutes les minutes1 440≤ 1 min
Webhook + relecture ciblée + filet horaire ce que fait cette démo241 à 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

  1. Vérifier la signature sur le corps brut, avant de le lire.
  2. Écarter un doublon : un envoi peut arriver deux fois.
  3. Répondre 2xx vite, puis travailler.

Derniers envois reçus

ReçuEnvoiÉvénementFichesVerdictEnsuite
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);
}