Une balise Blade pour le widget, un appel HTTP pour les demandes ouvertes depuis votre application métier. Rien d'autre à installer.
Prérequis : le site est créé dans CONTROL et ses domaines sont déclarés. Voir le README.
// config/services.php 'inodesk' => [ // Clé PUBLIQUE : elle vit dans le HTML, c'est sa raison d'être. 'public_key' => env('INODESK_PUBLIC_KEY'), // Clé SECRÈTE : serveur uniquement. Ne jamais l'exposer à une vue Blade. 'secret' => env('INODESK_SECRET_KEY'), 'endpoint' => env('INODESK_ENDPOINT', 39;https://messagerie.inovertix.com/api/inodesk'), 'widget' => env('INODESK_WIDGET_URL', 39;https://messagerie.inovertix.com/inodesk.js'), ],
# .env INODESK_PUBLIC_KEY=INODSK_PUB_votre_cle_publique INODESK_SECRET_KEY=INODSK_SEC_votre_cle_secrete INODESK_ENDPOINT=https://messagerie.inovertix.com/api/inodesk INODESK_WIDGET_URL=https://messagerie.inovertix.com/inodesk.js
{{-- resources/views/layouts/app.blade.php, juste avant </body> --}} @if (config('services.inodesk.public_key')) <script src="{{ config('services.inodesk.widget') }}" data-site="{{ config('services.inodesk.public_key') }}" data-endpoint="{{ config('services.inodesk.endpoint') }}" defer></script> @endif
Le @if n'est pas de la coquetterie : en préproduction ou en local, la clé est souvent absente. Sans lui, la balise serait rendue avec data-site="" et le widget enverrait une requête vouée à échouer à chaque page.
@if
data-site=""
<button type="button" onclick="window.inodesk && window.inodesk('open', 'support')"> Nous contacter </button>
<div id="contact-form" data-inodesk-form="contact"></div>
Le widget remplit lui-même le conteneur avec le formulaire déclaré dans CONTROL, dans son propre shadow DOM. Aucune définition de formulaire à maintenir de votre côté.
Cas typique : un paiement échoue, une livraison est en retard, un utilisateur signale un problème depuis son espace connecté. Vous avez déjà le contexte — ne le faites pas ressaisir.
<?php namespace App\Services; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; /** * Ouverture d'une demande INODESK depuis l'application. * * DEUX RÈGLES qui expliquent tout le code ci-dessous : * * 1. TIMEOUT COURT. Ouvrir une demande accompagne une action de l'utilisateur ; ça * ne doit jamais la faire attendre. Quatre secondes au total, une pour établir * la connexion. * * 2. ÉCHEC NON BLOQUANT. Si CONTROL est indisponible, l'utilisateur ne doit pas en * subir les conséquences : on journalise et on rend la main. Une demande perdue * est un incident ; une commande perdue parce que le service client était * injoignable en est un autre, bien plus cher. */ class InoDeskClient { /** * @param array{name?: string, email?: string, phone?: string, city?: string} $contact * @return array{reference: string, track_url: string}|null */ public function ouvrirDemande( string $sujet, string $message, array $contact, string $priorite = 'normale', ): ?array { $endpoint = config('services.inodesk.endpoint'); $secret = config('services.inodesk.secret'); if (blank($endpoint) || blank($secret)) { Log::warning('INODESK non configuré : demande non transmise.'); return null; } try { $response = Http::withToken($secret) ->connectTimeout(1) ->timeout(4) ->acceptJson() ->asJson() ->post("{$endpoint}/tickets", [ 'subject' => $sujet, 'message' => $message, 'contact' => $contact, 'priority' => $priorite, ]); } catch (\Throwable $e) { Log::warning('INODESK injoignable : '.$e->getMessage()); return null; } if ($response->failed()) { // Le corps porte {statusCode, name, message} : `name` suffit au diagnostic // et n'expose aucune donnée personnelle dans les journaux. Log::warning('INODESK a refusé la demande : '.$response->json('name')); return null; } return [ 'reference' => (string) $response->json('reference'), // `track_url` n'est renvoyé QU'À la clé secrète. Vous pouvez l'afficher à // l'utilisateur CONCERNÉ ; ne le stockez pas dans un endroit partagé. 'track_url' => (string) $response->json('track_url'), ]; } }
$demande = app(InoDeskClient::class)->ouvrirDemande( sujet: "Commande {$commande->reference} non reçue", message: "Le client signale un retard de livraison depuis son espace.", contact: [ 'name' => $commande->client_nom, 'email' => $commande->client_email, 'phone' => $commande->client_telephone, 'city' => $commande->ville, ], priorite: 'haute', ); if ($demande !== null) { session()->flash('info', "Votre demande {$demande['reference']} est enregistrée."); }
// app/Jobs/OuvrirDemandeInoDesk.php public function handle(InoDeskClient $client): void { $client->ouvrirDemande($this->sujet, $this->message, $this->contact); }
La file rend l'échec rejouable : un CONTROL indisponible cinq minutes ne fait perdre aucune demande, elle repart au prochain essai.
Si vous avez déjà un formulaire Laravel et voulez que son contenu arrive dans INODESK comme n'importe quelle soumission — champ par champ, avec le formulaire déclaré dans CONTROL :
Http::withToken(config('services.inodesk.secret')) ->timeout(4) ->post(config('services.inodesk.endpoint').'/submissions', [ 'form_slug' => 'contact', 'data' => [ 'name' => $request->input('nom'), 'email' => $request->input('email'), 'phone' => $request->input('telephone'), 'message' => $request->input('message'), ], 'source_url' => $request->url(), ]);
La réponse est 202 dans tous les cas exploitables, y compris quand la soumission est classée en spam. Les suspectes sont rangées dans le dossier « Spam » de l'inbox et restaurables d'un clic — jamais refusées.
Le plus simple, une balise dans une vue :
<div data-inodesk-reviews data-site="{{ config('services.inodesk.public_key') }}" data-endpoint="{{ config('services.inodesk.endpoint') }}" data-mode="wall"></div> <script src="{{ str_replace('inodesk.js', 'inodesk-reviews.js', config('services.inodesk.widget')) }}" defer></script>
Fichier séparé du widget (~4 Ko) : une page « Ils nous ont fait confiance » n'a aucune raison de télécharger le panneau de contact complet. data-mode="badge" n'affiche que la note, data-limit="6" borne le nombre d'avis. Zéro avis publié → le bloc n'affiche rien.
data-mode="badge"
data-limit="6"
Seuls les avis publiés dans CONTROL apparaissent. La modération est obligatoire : rien ne se publie sans une action humaine, quelle que soit la note.
Alors le mur doit être rendu côté serveur, JSON-LD compris. Un balisage injecté en JavaScript est lu de façon inégale par les moteurs — c'est précisément pourquoi inodesk-reviews.js n'en pose aucun.
inodesk-reviews.js
// Clé PUBLIQUE : cet endpoint ne rend que du contenu déjà public. $data = Cache::remember('inodesk.reviews', 300, function () { $res = Http::timeout(5)->get(config('services.inodesk.endpoint').'/reviews', [ 'k' => config('services.inodesk.public_key'), ]); // null en cas d'échec : la page s'affiche sans balisage plutôt que de planter. return $res->successful() ? $res->json() : null; });
Le service complet (classe, JSON-LD, vue Blade) est dans avis-seo.md. Respectez le Cache-Control: max-age=300 de l'API : un mur d'avis n'a pas besoin d'être frais à la seconde.
Cache-Control: max-age=300
La solution sans travail : lier /care/{votre-site}/avis, déjà brandée, balisée et indexable.
/care/{votre-site}/avis
$res = Http::timeout(5)->post( config('services.inodesk.endpoint').'/order-lookup?k='.config('services.inodesk.public_key'), ['order_number' => $request->input('numero'), 'phone' => $request->input('telephone')], ); if (! $res->successful()) { // ⚠️ Le message du serveur, TEL QUEL. Il est identique pour toutes les causes // d'échec — numéro inconnu, téléphone faux, IP verrouillée. Le préciser // transformerait cette route en confirmateur de commandes. return back()->withErrors(['numero' => $res->json('message')]); } $order = $res->json('order'); // number, status, status_label, placed_at, total, currency, city
Deux réflexes :
Ne pas ajouter de latence conditionnelle. Le serveur applique déjà un plancher de temps de réponse ; un sleep() côté application le réduirait à néant.
sleep()
Rien à intégrer : le pipeline vit dans CONTROL. Une soumission d'un formulaire de type devis crée le devis, l'équipe l'édite et l'envoie, et le client décide sur /q/{token}. Votre site n'a qu'à laisser passer la demande — par le widget, par le portail, ou par POST /submissions comme ci-dessus.
/q/{token}
POST /submissions
curl -sS "$INODESK_ENDPOINT/ping?k=$INODESK_PUBLIC_KEY"
{"ok":true,…}
grep -r "INODSK_SEC_" public/
Test d'indisponibilité : arrêter CONTROL, refaire le parcours métier — il doit se dérouler normalement, avec seulement une ligne dans les journaux.
php artisan config:cache
data-inodesk-reviews
Si le SEO est visé : le HTML source contient le application/ld+json, et la note balisée est celle affichée.
application/ld+json
Le cache d'avis (Cache::remember, 300 s) expire bien, et la page reste lisible quand CONTROL est arrêté.
Cache::remember
Suivi de commande : un couple faux renvoie exactement le même message et le même délai qu'un numéro inconnu.
INODESK_PUBLIC_KEY
/config
origin_not_allowed
www
401 secret_key_in_url
?k=
Http::withToken()
403 secret_key_required
422 unknown_form
form_slug
php artisan config:clear
Les codes d'erreur complets sont dans le tableau du README.