Skip to main content
WordPress est l’un des cas les plus courants : un thème ou un plugin maison qui affiche les menus et les horaires d’un restaurant. Le bon réflexe est d’appeler l’API depuis le serveur (PHP), de mettre en cache la réponse, et de purger ce cache quand un webhook signale un changement.
La clé d’API est une clé serveur-à-serveur. Ne la mettez jamais dans du JavaScript de front, un attribut data-* ou un champ caché : elle serait exposée à n’importe quel visiteur. Tous les appels wp_remote_get ci-dessous tournent côté PHP, la clé ne quitte jamais le serveur.

Stocker la clé d’API

Définissez la clé dans wp-config.php (hors du dépôt versionné si possible) ou dans une option chiffrée. Une constante reste la plus simple.
wp-config.php
define( 'LECOMMIS_API_KEY', 'votre_cle_serveur_a_serveur' );
define( 'LECOMMIS_SLUG', 'au-bistrot' );

Appeler l’API et mettre en cache

L’API expose un entier content_revision sur chaque réponse : il s’incrémente à chaque changement de contenu de l’établissement, indépendamment de la langue. On l’utilise comme clé de cache. Tant que la révision ne bouge pas, le contenu servi est identique. Ici on mémorise la dernière révision connue dans une option, et on stocke la charge utile dans un transient. La clé de cache inclut la locale car le contenu de menu diffère par langue (le content_revision, lui, est commun à fr et en).
functions.php
<?php

const LECOMMIS_API_BASE = 'https://app.lecommis.fr/api/v1';

/**
 * Récupère un menu (avec cache transient indexé sur content_revision + locale).
 */
function lecommis_get_menu( string $menu_type_slug, string $locale = 'fr' ) {
	$revision      = (int) get_option( 'lecommis_content_revision', 0 );
	$transient_key = "lecommis_menu_{$menu_type_slug}_{$locale}_{$revision}";

	$cached = get_transient( $transient_key );
	if ( false !== $cached ) {
		return $cached;
	}

	$url = sprintf(
		'%s/establishments/%s/menus/%s?locale=%s',
		LECOMMIS_API_BASE,
		rawurlencode( LECOMMIS_SLUG ),
		rawurlencode( $menu_type_slug ),
		rawurlencode( $locale )
	);

	$response = wp_remote_get(
		$url,
		array(
			'timeout' => 8,
			'headers' => array(
				'X-Api-Key' => LECOMMIS_API_KEY,
				'Accept'    => 'application/json',
			),
		)
	);

	if ( is_wp_error( $response ) ) {
		return null;
	}

	$code = wp_remote_retrieve_response_code( $response );

	// 404 : menu inexistant ou API désactivée. 401 : clé invalide. 429 : quota de requêtes dépassé.
	if ( 200 !== $code ) {
		return null;
	}

	$body = json_decode( wp_remote_retrieve_body( $response ), true );
	if ( ! is_array( $body ) ) {
		return null;
	}

	// On suit la révision côté serveur pour pouvoir invalider l'ancien cache.
	if ( isset( $body['content_revision'] ) ) {
		update_option( 'lecommis_content_revision', (int) $body['content_revision'] );
	}

	// 12 h de TTL de sécurité : le webhook reste le déclencheur principal de purge.
	set_transient( $transient_key, $body, 12 * HOUR_IN_SECONDS );

	return $body;
}
Mettez la locale dans la clé de cache, jamais la révision seule. Une même content_revision couvre fr et en : sans la locale, vous risqueriez de servir la mauvaise langue depuis le cache.
Le cache n’est pas qu’un confort : il vous protège des quotas de requêtes. Tant que la révision ne bouge pas, vous servez la réponse mémorisée sans rappeler l’API, ce qui évite tout 429.

Afficher le menu

Côté template, on lit la structure renvoyée. Le prix est sérialisé en string ("12.50") et peut être null hors des items à la carte.
template-part.php
<?php
$menu = lecommis_get_menu( 'menu-du-midi', 'fr' );

if ( $menu && ! empty( $menu['sections'] ) ) :
	foreach ( $menu['sections'] as $section ) : ?>
		<section>
			<h3><?php echo esc_html( $section['name'] ); ?></h3>
			<ul>
				<?php foreach ( $section['items'] as $item ) : ?>
					<li>
						<strong><?php echo esc_html( $item['name'] ); ?></strong>
						<?php if ( ! empty( $item['description'] ) ) : ?>
							<p><?php echo esc_html( $item['description'] ); ?></p>
						<?php endif; ?>
						<?php if ( ! is_null( $item['price'] ) ) : ?>
							<span><?php echo esc_html( $item['price'] ); ?></span>
						<?php endif; ?>
					</li>
				<?php endforeach; ?>
			</ul>
		</section>
	<?php endforeach;
endif;

Recevoir un webhook et purger le cache

Le webhook establishment.content_updated est un signal : il ne transporte pas le contenu, juste de quoi savoir qu’il faut re-fetcher. On enregistre une route REST custom, on vérifie la signature HMAC, puis on bascule la révision connue (ce qui invalide automatiquement tous les transients dont la clé contient l’ancienne révision).
<?php

const LECOMMIS_WEBHOOK_SECRET = 'votre_signing_secret';

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'lecommis/v1',
			'/webhook',
			array(
				'methods'             => 'POST',
				'permission_callback' => '__return_true',
				'callback'            => 'lecommis_handle_webhook',
			)
		);
	}
);

function lecommis_handle_webhook( WP_REST_Request $request ) {
	$raw       = $request->get_body();
	$signature = $request->get_header( 'x_lecommis_signature' ); // sha256=<hex>
	$timestamp = $request->get_header( 'x_lecommis_timestamp' ); // epoch seconds

	if ( ! lecommis_verify_signature( $raw, $signature, $timestamp ) ) {
		return new WP_REST_Response( array( 'error' => 'invalid signature' ), 401 );
	}

	$payload = json_decode( $raw, true );

	if ( 'establishment.content_updated' === ( $payload['type'] ?? '' ) ) {
		$revision = (int) ( $payload['data']['content_revision'] ?? 0 );
		// Avancer la révision suffit à périmer tous les anciens transients.
		update_option( 'lecommis_content_revision', $revision );
	}

	// 2xx = succès, le webhook ne sera pas rejoué.
	return new WP_REST_Response( array( 'ok' => true ), 200 );
}

function lecommis_verify_signature( string $raw, ?string $signature, ?string $timestamp ): bool {
	if ( empty( $signature ) || empty( $timestamp ) ) {
		return false;
	}

	// Tolérance d'horloge : on rejette les livraisons trop anciennes (anti-rejeu).
	if ( abs( time() - (int) $timestamp ) > 300 ) {
		return false;
	}

	$signed_payload = $timestamp . '.' . $raw;
	$expected       = 'sha256=' . hash_hmac( 'sha256', $signed_payload, LECOMMIS_WEBHOOK_SECRET );

	// Comparaison à temps constant.
	return hash_equals( $expected, $signature );
}
La signature porte sur "{X-LeCommis-Timestamp}.{corps brut JSON}". Vous devez disposer du corps brut non re-sérialisé : $request->get_body() le fournit. Ne reconstruisez jamais le JSON depuis le tableau décodé, l’octet à octet ne serait plus identique.
L’en-tête X-LeCommis-Delivery fournit un identifiant unique de livraison : stockez-le si vous voulez ignorer un éventuel doublon (idempotence), par exemple après un retry.

Option sans code : les redirections de fichiers du menu

Si vous voulez seulement afficher le PDF ou l’image d’un menu sans toucher à PHP, utilisez les URLs de redirection plug-and-play. Le navigateur suit le 302 jusqu’au fichier final (image ou PDF hébergé par Le Commis). Aucune clé n’est nécessaire, l’URL est publique et conçue pour être collée telle quelle.
Menu courant (image ou PDF)
<!-- Fichier web du menu courant, en français -->
<img
  src="https://app.lecommis.fr/r/menus?establishment=au-bistrot&menu_type=menu-du-midi&locale=fr"
  alt="Menu du midi" />

<!-- Carte unifiée combinée (toujours servie, aucune activation requise) -->
<a href="https://app.lecommis.fr/r/master_menu?establishment=au-bistrot">
  Voir la carte complète (PDF)
</a>
La redirection /r/menus exige que les redirections soient activées sur l’établissement (redirect_enabled, désactivé par défaut, à activer dans les réglages menus). La redirection /r/master_menu, qui sert la Carte unifiée, est toujours disponible. Il n’y a aucun fallback inter-langue : si aucun fichier (image ou PDF) ne correspond à la locale demandée, vous obtenez un 404 (texte « Menu non disponible »), jamais l’autre langue.
La Carte unifiée est le PDF qui regroupe toutes vos cartes en un seul document. Elle reste accessible via /r/master_menu sans aucune activation, contrairement aux fichiers de menu individuels.