Documentation
JavaScript et TypeScript
Le SDK npm de Gaspard pour Node.js, Next.js, les navigateurs, Deno et Bun : sans dépendance, ESM et CommonJS.
SDK JavaScript / TypeScript officiel du hub HubMix (API publique /v1/public) :
Gaspard, l'assistant IA, la recherche dans ses connaissances et, depuis la 1.1.0, le tuteur,
la confiance (signaux d'arnaque des annonces), les embeddings et le vérificateur des
webhooks de l'ancien portail développeurs (retiré le 28 octobre 2026). Aucune dépendance : il s'appuie sur fetch, ReadableStream, TextDecoder et
AbortController, et fonctionne avec Node 18+, les navigateurs, Deno et Bun. Paquet ESM + CommonJS,
types inclus.
Installation #
npm install @valais-connect/gaspardimport { Gaspard, GaspardError } from '@valais-connect/gaspard'; // ESM / TypeScript
const { Gaspard } = require('@valais-connect/gaspard'); // CommonJSDeux sortes de clés #
| Clé | Où | Option | En-tête envoyé |
|---|---|---|---|
Secrète vck_live_… |
Serveur uniquement | apiKey |
Authorization: Bearer … |
Publiable vcp_live_… |
Page web, application | publishableKey |
X-Gaspard-Key: … (+ jeton visiteur) |
Il faut exactement l'une des deux. Une clé secrète utilisée dans un navigateur lève une TypeError :
gardez-la sur votre serveur.
| Option | Défaut | Rôle |
|---|---|---|
baseUrl |
https://hubmix.dealmood-solutions.com |
Hôte du service (le SDK ajoute /v1/public) |
origin |
— | Clé publiable hors navigateur : envoyée comme Origin (ignorée dans un navigateur) |
endUser |
— | Clé secrète : identifiant de votre utilisateur (X-End-User, 1–128 caractères ASCII imprimables) |
timeout |
60000 |
Délai par requête, en millisecondes (flux : jusqu'au début de la réponse) |
maxRetries |
2 |
Nouvelles tentatives (voir Erreurs et nouvelles tentatives), 0 pour les désactiver |
userAgent |
gaspard-js/1.1.0 |
User-Agent (jamais envoyé depuis un navigateur) |
fetch |
globalThis.fetch |
Une autre implémentation de fetch (tests, environnements particuliers) |
Côté serveur #
Express #
import express from 'express';
import { Gaspard, GaspardError } from '@valais-connect/gaspard';
const gaspard = new Gaspard({ apiKey: process.env.GASPARD_API_KEY! });
const app = express();
app.use(express.json());
app.post('/api/gaspard/conversations', async (req, res) => {
const conversation = await gaspard.forEndUser(req.user.id).startConversation('fr', req.body.page);
res.status(201).json(conversation);
});
app.post('/api/gaspard/conversations/:id/messages', async (req, res) => {
try {
const answer = await gaspard.forEndUser(req.user.id).ask(req.params.id, req.body.text);
res.json(answer);
} catch (error) {
if (error instanceof GaspardError) {
res.status(error.status || 502).json({ code: error.code, retryAfter: error.retryAfter });
} else throw error;
}
});Next.js (route handler, réponse en flux) #
// app/api/gaspard/[id]/route.ts
import { Gaspard } from '@valais-connect/gaspard';
const gaspard = new Gaspard({ apiKey: process.env.GASPARD_API_KEY! });
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const { text } = await request.json();
const encoder = new TextEncoder();
const body = new ReadableStream({
async start(controller) {
try {
for await (const event of gaspard.askStream(id, text, undefined, { signal: request.signal })) {
controller.enqueue(encoder.encode(JSON.stringify(event) + '\n'));
}
controller.close();
} catch (error) {
controller.error(error);
}
},
});
return new Response(body, { headers: { 'Content-Type': 'application/x-ndjson' } });
}Vos utilisateurs (X-End-User) #
Avec une clé secrète, indiquez qui pose la question pour que chaque personne ne voie que ses conversations et puisse exercer ses droits (seul un hachage à clé de l'identifiant est conservé) :
const forLea = gaspard.forEndUser('user-42'); // client dérivé, même configuration
await forLea.giveConsent(notice.version); // après avoir montré la notice
await forLea.exportData(); // droit d'accès
await forLea.eraseData(); // droit à l'effacement
await gaspard.consent({ endUser: 'user-42' }); // ou appel par appelDans le navigateur (clé publiable et consentement) #
La clé publiable ne fonctionne que depuis les origines déclarées pour votre client. Le visiteur doit
d'abord accepter la notice de confidentialité ; openSession ouvre alors une session visiteur et
garde son jeton (vst_…, valable 30 jours) pour tous les appels suivants.
import { Gaspard } from '@valais-connect/gaspard';
const gaspard = new Gaspard({ publishableKey: 'vcp_live_…' });
const saved = localStorage.getItem('gaspard-token');
if (saved) {
gaspard.setVisitorToken(saved);
} else {
const notice = await gaspard.privacyNotice('fr');
if (!(await showNoticeAndAsk(notice.text))) throw new Error('Pas de consentement');
const session = await gaspard.openSession({ consent: true, locale: 'fr', allowTraining: false });
localStorage.setItem('gaspard-token', session.token);
}
const conversation = await gaspard.startConversation('fr', location.pathname.slice(0, 80));
const answer = await gaspard.ask(conversation.id, 'Que faire à Lyon ce week-end ?');openSession({ consent: false }) est refusé localement (TypeError). Le visiteur peut retirer son
consentement (withdrawConsent()), consulter ses données (exportData()) ou les effacer
(eraseData()) ; setVisitorToken(null) oublie le jeton.
Hors navigateur (application de bureau, React Native…), passez aussi origin : il doit faire partie
des origines déclarées.
Réponses en flux #
askStream renvoie un itérable asynchrone : des événements delta (morceaux de texte), puis une
seule réponse answer complète. La requête part au début de l'itération ; sortir de la boucle
(break) ferme le flux.
let text = '';
for await (const event of gaspard.askStream(conversation.id, 'Comment trouver un logement étudiant à Montréal ?')) {
if (event.type === 'delta') {
text += event.text;
render(text);
} else {
showSources(event.answer.sources, event.answer.listings);
}
}Un flux qui se termine par une erreur lève une GaspardError (assistant_busy → 503,
conversation_full → 409…) ; un flux coupé avant la réponse lève stream_incomplete. Un flux déjà
commencé n'est jamais relancé automatiquement.
Méthodes #
| Méthode | Appel | Renvoie |
|---|---|---|
privacyNotice(locale = 'fr') |
GET /privacy-notice |
PrivacyNotice |
openSession({ consent: true, locale?, allowTraining? }) |
POST /sessions |
Session |
setVisitorToken(token | null), visitorToken |
— | — |
startConversation(locale = 'fr', page?) |
POST /gaspard/conversations |
Conversation |
ask(conversationId, text, firstName?) |
POST /gaspard/conversations/{id}/messages |
Answer |
askStream(conversationId, text, firstName?) |
idem, en flux | AsyncIterable<StreamEvent> |
conversation(conversationId) |
GET /gaspard/conversations/{id} |
ConversationThread |
feedback(messageId, 'up' | 'down', comment?) |
POST /gaspard/messages/{id}/feedback |
Feedback |
searchKnowledge(query, limit = 5, topics?) |
POST /knowledge/search |
{ passages } |
consent() / giveConsent(version, allowTraining = false) / withdrawConsent() |
/visitors/me/consent |
ConsentStatus |
exportData() |
GET /visitors/me/data |
VisitorData |
eraseData() |
DELETE /visitors/me |
Erasure |
forEndUser(id) |
— | client dérivé |
products() |
GET /products |
Product[] |
tutor.explain({ lesson, messages, mode?, learner?, locale? }) |
POST /tutor/explanations |
TutorReply |
tutor.quiz({ lesson, count?, difficulty?, kinds?, learner?, locale? }) |
POST /tutor/quizzes |
Quiz |
tutor.flashcards({ lesson, count?, learner?, locale? }) |
POST /tutor/flashcards |
Flashcards |
tutor.grade({ lesson, question, answer, countryCode?, locale? }) |
POST /tutor/grades |
Grade |
trust.assess(listings) |
POST /trust/assessments |
TrustResult |
embeddings.create(texts) |
POST /embeddings |
Embeddings |
Chaque méthode accepte en dernier argument { signal?, requestId?, endUser? } :
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await gaspard.ask(id, 'Bonjour', undefined, { signal: controller.signal, requestId: 'commande-1234' });Les champs sont en camelCase (remainingMessages, sourceName…), les dates restent des chaînes
ISO 8601, et les champs inconnus renvoyés par le serveur sont ignorés.
Produits du hub (clé serveur) #
Le tuteur, la confiance et les embeddings sont activés par application par l'équipe HubMix
(sur demande) ; sans le produit, l'API répond 403 insufficient_scope. Ils demandent une clé
serveur : avec une clé publiable, l'appel est refusé (TypeError) avant toute requête.
const gaspard = new Gaspard({ apiKey: process.env.VC_API_KEY! });
// Confiance : une annonce suspecte avant de la publier
const { assessments } = await gaspard.trust.assess([
{ kind: 'housing', title: 'Studio centre-ville', description: 'Paiement par Western Union avant visite',
priceAmount: 250, currency: 'EUR', countryCode: 'FR', city: 'Lyon', housingType: 'studio' },
]);
if (assessments[0]!.withheld) {
// à faire vérifier par un modérateur ; assessments[0].findings dit pourquoi
}
// Tuteur : des cartes de révision
const { cards } = await gaspard.tutor.flashcards({
lesson: { title: 'La photosynthèse', content: texteDuCours },
count: 8,
learner: { level: 'bachelor', countryCode: 'SN' },
locale: 'fr',
});
// Embeddings : un vecteur par texte, dans le même ordre
const { embeddings, dimensions } = await gaspard.embeddings.create(['chambre à louer', 'room for rent']);Erreurs et nouvelles tentatives #
Toute erreur d'appel est une GaspardError :
| Propriété | Contenu |
|---|---|
status |
Statut HTTP, 0 sans réponse |
code |
error.code de l'API (not_found, rate_limited, token_quota_exceeded, assistant_busy…), sinon http_<statut>, network_error, timeout, invalid_response, stream_incomplete |
message |
Message lisible |
requestId |
Identifiant de la requête (à communiquer au support), ou null |
retryAfter |
Secondes à attendre (Retry-After), ou null |
try {
await gaspard.ask(id, question);
} catch (error) {
if (error instanceof GaspardError && error.code === 'token_quota_exceeded') {
console.warn(`Quota atteint, réessayer dans ${error.retryAfter} s (requête ${error.requestId})`);
} else throw error;
}Les erreurs de configuration (clé manquante, clé secrète dans un navigateur, consent: false,
requestId invalide…) sont des TypeError, levées avant toute requête. Une annulation par
AbortSignal rejette avec la raison du signal (AbortError), pas une GaspardError.
Le SDK réessaie seul, jusqu'à maxRetries fois : 503 assistant_busy, 429 rate_limited et, pour
GET et DELETE seulement, les erreurs réseau et les délais dépassés. Il attend Retry-After
quand il est présent (au-delà de 30 s, il abandonne et lève l'erreur), sinon 0,5 s × 2^tentative.
token_quota_exceeded et les autres erreurs 4xx ne sont jamais réessayées. Réessayer une question est
sans risque : le serveur n'enregistre un échange qu'avec sa réponse.