> Brancher l'API GraphQL d'un logiciel métier sur WordPress : cron serveur plutôt que WP-Cron, pages créées sans doublons, cache des réponses, réessais et alerte quand un envoi échoue.

*Source : https://coder-studio.com/tutoriels/synchroniser-logiciel-metier-saas-wordpress/*

---

[// WordPress](https://coder-studio.com/tutoriels/)

# Synchroniser un logiciel métier SaaS avec WordPress : cron, cache et idempotence

Par **Kévin Papot** · publié le 19 octobre 2026

Un organisme de formation tient son catalogue dans un logiciel de gestion en ligne, et son site WordPress doit afficher les mêmes programmes, les mêmes dates, les mêmes prix. Tout saisir deux fois garantit qu’un jour les deux versions divergeront. La réponse classique consiste à faire lire l’API du logiciel par WordPress, à intervalle régulier. Sur le papier, c’est un appel HTTP et une boucle. En production, trois questions décident si le script tient : qui déclenche la synchronisation, comment éviter de créer deux fois la même page, et que faire quand l’API ou le réseau ne répond pas. Ce tutoriel prend l’exemple de **Digiforma**, logiciel de gestion pour organismes de formation qui expose une API GraphQL, mais le raisonnement vaut pour n’importe quel SaaS métier.

## Ce que la synchronisation doit faire

Le flux a deux sens. **Descendant** : le script récupère les programmes, les catégories et les sessions, puis crée ou met à jour une page par programme (ici un type de contenu `formation`). **Montant** : les demandes d’inscription saisies sur le site repartent vers le logiciel.

Vérifiez d’abord l’accès : chez Digiforma, toutes les formules d’abonnement n’incluent pas l’API GraphQL. Rangez ensuite le jeton dans `wp-config.php` sous forme de constante, jamais en base ni dans le dépôt. Avec GraphQL, tout passe par une seule requête POST qui décrit les champs voulus. Les noms de champs ci-dessous sont illustratifs : récupérez le schéma réel par introspection avant d’écrire la vôtre.

```
function cs_fetch_programs(): array {
    $response = wp_remote_post( CS_API_URL, [
        'timeout' => 20,
        'headers' => [
            'Authorization' => 'Bearer ' . CS_API_TOKEN,
            'Content-Type'  => 'application/json',
        ],
        'body' => wp_json_encode( [
            'query' => 'query { programs { id name sessions { id startDate } } }',
        ] ),
    ] );
    if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
        throw new RuntimeException( 'API injoignable' );
    }
    $data = json_decode( wp_remote_retrieve_body( $response ), true );
    if ( ! empty( $data['errors'] ) ) {
        throw new RuntimeException( $data['errors'][0]['message'] ?? 'Erreur GraphQL' );
    }
    return $data['data']['programs'] ?? [];
}
```

Le second test compte autant que le premier : un serveur GraphQL répond souvent `200` même quand la requête échoue, avec un tableau `errors` dans le corps. Si vous venez du monde REST, où le code HTTP porte l’erreur, le rappel des conventions de notre [guide des API REST](https://coder-studio.com/tutoriels/api-rest-guide/) aide à voir la différence.

## WP-Cron ou cron serveur

`wp_schedule_event()` donne l’impression d’une tâche planifiée, mais WP-Cron n’est pas un vrai cron : il se déclenche à partir d’une visite. Un site peu fréquenté la nuit lancera la synchronisation en retard, parfois de plusieurs heures. Désactivez ce déclenchement et confiez-le au système :

```
// wp-config.php
define( 'DISABLE_WP_CRON', true );
```

```
*/15 * * * * cd /var/www/site && wp cron event run --due-now --quiet
```

L’événement reste déclaré côté WordPress (`wp_schedule_event( time(), 'hourly', 'cs_sync_catalogue' )`), mais c’est la crontab qui le fait tourner, à heure fixe. Si la syntaxe des cinq champs vous échappe, notre article sur [cron sous Linux](https://coder-studio.com/outils/cron-linux-taches-planifiees/) la détaille, avec les pièges d’environnement et de chemins.

Reste le risque de deux synchronisations simultanées, par exemple quand l’API est lente et que la suivante démarre. Un verrou suffit, car `add_option()` échoue si l’option existe déjà :

```
add_action( 'cs_sync_catalogue', function () {
    if ( ! add_option( 'cs_sync_lock', time(), '', false ) ) {
        return; // une synchronisation tourne déjà
    }
    try {
        foreach ( cs_fetch_programs() as $program ) {
            cs_upsert_program( $program );
        }
    } finally {
        delete_option( 'cs_sync_lock' );
    }
} );
```

Après une erreur fatale de PHP, le bloc `finally` ne s’exécute pas : comparez l’horodatage stocké à l’heure courante et libérez un verrou vieux de plus d’une heure.

## Idempotence : une formation, une page

Lancer la synchronisation une fois ou dix fois doit produire le même site. L’erreur classique consiste à retrouver la page par son titre : le jour où un programme est renommé dans le logiciel, WordPress crée un doublon. La clé, c’est l’identifiant externe, stocké en métadonnée.

```
function cs_upsert_program( array $p ): int {
    $found = get_posts( [
        'post_type'   => 'formation',
        'post_status' => 'any',
        'meta_key'    => '_cs_external_id',
        'meta_value'  => $p['id'],
        'fields'      => 'ids',
        'numberposts' => 1,
    ] );
    $hash = md5( wp_json_encode( $p ) );
    $args = [ 'post_type' => 'formation', 'post_title' => $p['name'] ];

    if ( $found ) {
        $id = $found[0];
        if ( get_post_meta( $id, '_cs_hash', true ) === $hash ) {
            return $id; // rien n'a changé
        }
        wp_update_post( $args + [ 'ID' => $id ] );
    } else {
        $id = wp_insert_post( $args + [ 'post_status' => 'publish' ] );
        update_post_meta( $id, '_cs_external_id', $p['id'] );
    }
    update_post_meta( $id, '_cs_hash', $hash );
    return $id;
}
```

L’empreinte `_cs_hash` évite de réécrire une page identique, donc de modifier sa date de mise à jour et de vider le cache sans raison. Pour un programme supprimé côté logiciel, passez la page en brouillon plutôt que de l’effacer : vous gardez son contenu et vous décidez ensuite d’une redirection.

Tout ce code, il faudra le maintenir à chaque évolution de l’API, de WordPress ou de PHP. Si vous préférez ne pas porter cette charge, le comparatif de chaque [plugin WordPress pour Digiforma](https://www.newp.fr/actualites/marketing-acquisition/plugin-wordpress-digiforma-prix/) disponible en octobre 2026 met en regard les tarifs et la fréquence de synchronisation : une fois par nuit pour le plugin officiel, jusqu’à toutes les heures pour MP Digiforma.

## Cache : l’API ne doit jamais dépendre du trafic

Les pages de formation sont du contenu WordPress : elles sont servies depuis la base et votre cache de pages, sans appel à l’API. Si un bloc affiche les prochaines dates en direct, mettez la réponse en transient et gardez la dernière copie valide pour le jour où l’API tombe.

```
$sessions = get_transient( 'cs_sessions' );
if ( false === $sessions ) {
    try {
        $sessions = cs_fetch_sessions();
        set_transient( 'cs_sessions', $sessions, HOUR_IN_SECONDS );
        update_option( 'cs_sessions_backup', $sessions, false );
    } catch ( RuntimeException $e ) {
        $sessions = get_option( 'cs_sessions_backup', [] );
    }
}
```

Une date affichée avec une heure de retard gêne moins qu’un bloc vide.

## Quand l’envoi échoue : réessais et alerte

Le sens montant est le plus sensible, car une demande d’inscription perdue est un client perdu. Enregistrez toujours la demande localement avant de l’envoyer, puis réessayez avec un délai croissant :

```
function cs_send_request( int $request_id ): void {
    $attempt = (int) get_post_meta( $request_id, '_cs_attempts', true ) + 1;
    update_post_meta( $request_id, '_cs_attempts', $attempt );
    try {
        cs_push_to_api( $request_id );
        update_post_meta( $request_id, '_cs_status', 'sent' );
    } catch ( RuntimeException $e ) {
        $delays = [ 300, 900, 3600, 14400 ];
        if ( $attempt <= count( $delays ) ) {
            wp_schedule_single_event( time() + $delays[ $attempt - 1 ], 'cs_send_request', [ $request_id ] );
        } else {
            update_post_meta( $request_id, '_cs_status', 'failed' );
            wp_mail( get_option( 'admin_email' ), 'Demande non transmise', "Demande #$request_id : " . $e->getMessage() );
        }
    }
}
add_action( 'cs_send_request', 'cs_send_request' );
```

Deux précisions évitent les mauvaises surprises. Distinguez les erreurs : un jeton expiré ou une donnée refusée ne se corrigera pas en réessayant, alertez tout de suite. Et quand un appel a expiré sans réponse, la demande a peut-être été créée malgré tout : avant de renvoyer, cherchez côté API une demande avec la même adresse et la même session, sinon vous fabriquez le doublon dans l’autre sens.

Un verrou, un identifiant externe, une copie de secours et une file de réessais : ces quatre pièces suffisent à rendre une synchronisation prévisible, quel que soit le logiciel en face.
