Spark AI in je eigen app
Zet het chatvenster van Spark AI in je eigen website of app. Het werkt zoals de chat in de Suite: antwoorden die je ziet ontstaan, de stappen die de assistent zet, tabellen met export, grafieken, formules, vervolgvragen, bijlagen en feedback. Je hebt één sleutel nodig, en die blijft op je eigen server.
Zo werkt het
Het pakket @floraspark/spark-chat heeft twee delen. In de browser staat het chatvenster, <spark-chat>. Op je eigen server staat één route, standaard /api/spark. Het venster praat alleen met die route, en de route zet je sleutel erbij en praat met api.floraspark.nl.
browser <spark-chat endpoint="/api/spark"> geen sleutel
│
jouw server /api/spark SPARK_API_KEY staat hier
│
FloraSpark https://api.floraspark.nl/v1/chat/...Waarom zo? Een sleutel leest alle data van je organisatie en gebruikt jullie AI-tegoed. In browsercode kan iedereen hem uitlezen. Op je server blijft hij van jou.
In drie stappen
1. Maak een sleutel aan
Ga in de Suite naar Ontwikkelaars, tabblad Sleutels, en maak een sleutel aan (zie Ontwikkelaars). Geef hem een herkenbaar label, zoals "Klantportaal": dat label zie je straks terug in de Chatbot Monitor. Wil je dat het venster minder ziet dan alles, geef de sleutel dan een rolbeperking, bijvoorbeeld Alleen inkoop.
Zet de sleutel op je server als omgevingsvariabele:
SPARK_API_KEY=fsk_...2. Voeg de route toe
npm install @floraspark/spark-chatNext.js (App Router), in app/api/spark/route.js:
export { GET, POST } from '@floraspark/spark-chat/server';
export const dynamic = 'force-dynamic';Express:
import { sparkExpress } from '@floraspark/spark-chat/server/express';
app.all('/api/spark', sparkExpress());Node zonder framework:
import http from 'node:http';
import { sparkNode } from '@floraspark/spark-chat/server/express';
const spark = sparkNode();
http.createServer((req, res) => {
if (req.url.startsWith('/api/spark')) return spark(req, res);
// je eigen routes
}).listen(3000);Remix, Hono, Bun, Deno, Cloudflare Workers en alles wat met Request en Response werkt:
import { createSparkHandler } from '@floraspark/spark-chat/server';
const spark = createSparkHandler({ apiKey: env.SPARK_API_KEY });
// Hono: app.all('/api/spark', (c) => spark(c.req.raw));Python (FastAPI): het pakket bevat een kant-en-klare router in examples/python/spark_proxy.py. Voeg hem toe met app.include_router(router). Een andere taal? De route is klein; zie De route zelf bouwen.
3. Zet het venster op je pagina
Het venster vult het element waar het in staat, dus geef het een hoogte.
<script type="module">import '@floraspark/spark-chat';</script>
<spark-chat endpoint="/api/spark" style="height: 600px"></spark-chat>In React:
import { SparkChat } from '@floraspark/spark-chat/react';
<SparkChat endpoint="/api/spark" style={{ height: 600 }} />Zonder bundler, met één bestand:
<script src="https://cdn.jsdelivr.net/npm/@floraspark/spark-chat@0.1.0/dist/spark-chat.js" defer></script>
<spark-chat endpoint="/api/spark" style="height: 100vh"></spark-chat>Open je pagina en stel een vraag.
Eerst proberen zonder sleutel
Het pakket bevat een nagebootste FloraSpark met dezelfde routes en verzonnen antwoorden: een tabel, een grafiek, een formule, vervolgvragen en een paar fouten om je foutafhandeling te testen.
node node_modules/@floraspark/spark-chat/examples/mock-upstream.mjs
SPARK_API_BASE_URL=http://localhost:8787 SPARK_API_KEY=fsk_test npm run devEen vraag met "fout" geeft een foutmelding, met "tegoed" een leeg AI-tegoed en met "lang" een traag antwoord om de stopknop te proberen. Haal SPARK_API_BASE_URL weg zodra je met je echte sleutel werkt.
Instellingen van het venster
| Attribuut | Standaard | Wat het doet |
|---|---|---|
endpoint | /api/spark | De route op je eigen server |
language | taal van de pagina, anders nl | nl of en: de teksten van het venster en de taal van de antwoorden |
theme | auto | auto volgt het systeem, of light of dark |
persist | local | local bewaart het gesprek in de browser, none niet |
storage-key | de endpoint | Eigen naam voor die opslag, bijvoorbeeld per ingelogde gebruiker |
heading | Spark AI | De naam in de kopbalk. Gebruik heading en niet title |
header | none verbergt de kopbalk | |
placeholder | Stel een vraag… | De tekst in het lege invoerveld |
welcome-title, welcome-subtitle | De tekst op het welkomstscherm | |
suggestions | vier standaardvragen | Startvragen als JSON: '["Omzet deze maand?"]' |
math | aan | off laadt het formulepakket nooit. Anders alleen als een antwoord een formule bevat |
fonts | aan | none laadt geen eigen lettertypen; dan geldt jouw lettertype |
Vanuit je eigen code kun je ook een vraag stellen (chat.send('...')), een nieuw gesprek beginnen (chat.reset()) en luisteren naar spark-answer en spark-error.
In je eigen stijl
Het venster heeft standaard de FloraSpark-stijl. Zet je het in de stijl van je eigen site, dan blijven alle functies hetzelfde. Er zijn drie niveaus.
Kleuren, lettertype en afronding
Zet CSS-variabelen op het element. Jouw waarden winnen altijd, ook in de donkere modus.
spark-chat {
--spark-primary: #0a58ca;
--spark-on-primary: #ffffff;
--spark-bg: #f8f9fa;
--spark-surface: #ffffff;
--spark-text: #212529;
--spark-muted: #6c757d;
--spark-border: #dee2e6;
--spark-font: Inter, system-ui, sans-serif;
--spark-font-display: Inter, system-ui, sans-serif;
--spark-radius: 6px;
}Heeft je site een donkere modus, zet de kleuren dan ook voor spark-chat[theme="dark"] (en voor prefers-color-scheme: dark als je theme="auto" gebruikt). Anders blijven je lichte kleuren in de donkere modus staan.
| Variabele | Waarvoor |
|---|---|
--spark-primary, --spark-primary-hover, --spark-on-primary | Knoppen, vet in antwoorden, accenten, en de tekst daarop |
--spark-accent, --spark-accent-mid, --spark-tint | Lichte accenten en de achtergrond van een gekozen knop |
--spark-bg, --spark-surface | De achtergrond van het venster; invoerveld, tabellen en grafieken |
--spark-text, --spark-muted, --spark-faint | Tekst, secundaire tekst, de lichtste tekst |
--spark-border, --spark-divider | Randen en scheidingslijnen |
--spark-danger, --spark-danger-strong, --spark-danger-bg | Foutmeldingen |
--spark-code-bg, --spark-code-text | Codeblokken |
--spark-chart-1 tot en met --spark-chart-4 | De reeksen in een grafiek |
--spark-font, --spark-font-mono, --spark-font-display | Gewone tekst, getallen en code, koppen |
--spark-radius, --spark-max-width | De afronding van het invoerveld, de breedte van de tekstkolom |
Delen van het venster
Met ::part() stijl je de grote vlakken zelf: root, header, body en composer. Met header="none" en de tekstattributen hierboven maak je het venster verder je eigen.
spark-chat::part(composer) { border-top: 1px solid var(--spark-border); }Een volledig eigen interface
Wil je ook tabellen, grafieken en de opbouw van een antwoord zelf tekenen, gebruik dan de client zonder weergave uit @floraspark/spark-chat/client. Je krijgt dezelfde stream, bijlagen en feedback, en dezelfde logica waarmee de Suite een antwoord opbouwt.
import { SparkChatClient, buildPayload, createTurn, applyTurnEvent } from '@floraspark/spark-chat/client';
const client = new SparkChatClient({ endpoint: '/api/spark' });
const { payload } = buildPayload(history, { role: 'user', content: 'Omzet deze week?' }, { sessionId });
let turn = createTurn('t1');
await client.stream(payload, {
onEvent: (name, data) => { turn = applyTurnEvent(turn, name, data); render(turn); },
});Wat er in een antwoord kan staan
Hetzelfde als in de Suite:
- tekst met koppen, vet, lijsten en code;
- tabellen: sorteren per kolom en downloaden als CSV;
- lijn- en staafgrafieken met tot vier reeksen, op te slaan als afbeelding;
- formules;
- vervolgvragen als knoppen;
- de stappen van de assistent, live en daarna ingeklapt;
- een duim omhoog of omlaag. Een duim omlaag gaat met je toelichting naar FloraSpark, net als in de Suite.
Wat niet kan: kaartjes die iets in de Suite openen (een taak, een rapport, een dashboardtegel, churn of een product), en acties in apps die onder Koppelingen gekoppeld zijn. Die lopen via de koppeling van één medewerker, en horen niet bij iedereen die je app gebruikt.
Veiligheid en privacy
- De sleutel staat alleen op je server. De route stuurt nooit de cookies of de eigen inloggegevens van de browser door naar FloraSpark.
- Iedereen die bij de route kan, gebruikt jouw sleutel. Staat het venster achter je eigen login, laat de route dat dan ook controleren:
createSparkHandler({ authorize: async (request) => Boolean(await getSession(request)) });- Alle gebruikers delen de rol en het AI-tegoed van de sleutel. Een sleutel met Alleen inkoop laat in het venster ook alleen inkoop zien.
- Gesprekken staan in de Chatbot Monitor. Beheerders van je organisatie zien ze in de Suite onder Toegang en toezicht, met het label App-sleutel en de naam van de sleutel. Als gebruiker staat er het e-mailadres van de sleutel, niet dat van de persoon in je app. Ze verschijnen niet in iemands eigen gespreksgeschiedenis, en worden na 90 dagen verwijderd. Zet in je privacyverklaring dat vragen worden bewaard en door beheerders kunnen worden ingezien.
- In de browser staat het gesprek in de lokale opslag van de gebruiker. Delen meerdere mensen één browser, geef dan per gebruiker een eigen
storage-key, of zetpersist="none". - Intrekken werkt direct. Trek de sleutel in onder Ontwikkelaars en het venster stopt.
Grenzen en kosten
| Wat | Grens |
|---|---|
| Vragen | 20 per minuut per sleutel |
| Instellingen, bijlagen en feedback | 60 verzoeken per minuut per sleutel |
| Lengte van een vraag | 8.000 tekens; het venster stuurt de laatste 40 berichten als geschiedenis mee |
| Bijlagen | 4 per vraag en 8 per gesprek, elk tot 10 MB: afbeeldingen, pdf, Word, Excel, csv en tekst |
| Een verzoek | 256 KB, een upload 12 MB |
Elke vraag gaat van het AI-tegoed van je organisatie af, net als in Spark AI. Een bijlage blijft ongeveer anderhalf uur bruikbaar; daarna vraagt het venster om hem opnieuw toe te voegen.
Foutmeldingen
Het venster toont bij elke fout een begrijpelijke melding met Opnieuw proberen. Bouw je zelf, dan zijn dit de codes:
| Status | Code | Betekenis |
|---|---|---|
| 500 | missing_api_key | SPARK_API_KEY staat niet op je server |
| 401 | De sleutel is onbekend of ingetrokken, of je authorize weigerde (unauthorized) | |
| 402 | ai_budget_exhausted | Het AI-tegoed van deze maand is op |
| 404 | chat_disabled | Het chatvenster staat voor jullie organisatie nog niet aan. Neem contact met ons op |
| 413 | payload_too_large | Het verzoek of de bijlage is te groot |
| 422 | attachment_expired | Een bijlage is verlopen; voeg hem opnieuw toe. Zonder code: de vraag is te lang of er zijn te veel bijlagen |
| 429 | Te veel vragen per minuut; wacht even | |
| 502 | upstream_unreachable | Je server kan FloraSpark niet bereiken, of een storing aan onze kant |
De route zelf bouwen
Werk je niet met JavaScript of Python, dan bouw je de route zelf. Hij doet vier dingen:
| Het venster vraagt | Jouw route vraagt aan https://api.floraspark.nl |
|---|---|
GET /api/spark?op=config | GET /v1/chat/config |
POST /api/spark?op=stream (JSON) | POST /v1/chat/stream. Het antwoord is text/event-stream: geef het ongebufferd door |
POST /api/spark?op=upload (multipart, veld file) | POST /v1/chat/upload, met dezelfde Content-Type |
POST /api/spark?op=feedback (JSON) | POST /v1/chat/feedback |
- Zet
Authorization: Bearer <SPARK_API_KEY>erbij, en stuur geen cookies ofAuthorizationvan de browser door. - Geef de statuscode en de JSON-fout ongewijzigd terug. Weiger andere waarden van
op. - Zet geen compressie of buffering op de stream, anders komt het antwoord pas binnen als het af is.
De stream
POST /v1/chat/stream neemt {"messages": [...], "session_id": "...", "level": "snel", "language": "nl"}. messages is het gesprek tot nu toe, met als laatste de nieuwe vraag; een bericht is {"role": "user" | "assistant", "content": "...", "attachments": ["<file_id>"]}. Stuur de session_id uit het vorige antwoord mee, dan komt de vervolgvraag in hetzelfde gesprek in de Chatbot Monitor.
curl -N https://api.floraspark.nl/v1/chat/stream \
-H "Authorization: Bearer $SPARK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Wat was de omzet vorige week?"}]}'Het antwoord is een reeks events, elk als event: <naam> en data: <json>:
| Event | Wat erin staat |
|---|---|
thinking | Een nieuwe denkronde begint: {"round": 1} |
reasoning | Een stukje van de gedachtegang, in het Engels: {"text": "..."} |
step_start | De assistent begint een stap, bijvoorbeeld een databasevraag: {"tool": "execute_sql", "title": "..."} |
step_end | De stap is klaar: uitkomst, aantal rijen en duur |
delta | Een stukje van het antwoord: {"text": "..."} |
answer | Het volledige antwoord, met answer, status, steps, session_id en message_id. Dit is altijd het laatste event |
error | Er ging onderweg iets mis: {"error": "...", "code": "..."} |
Het antwoord bevat gewone tekst en blokken tussen drie backticks: csv voor een tabel, chart voor een grafiek (JSON met type, title, labels en datasets) en suggestions voor vervolgvragen, één per regel. Andere blokken kun je weglaten.
Instellingen, bijlagen en feedback
GET /v1/chat/configgeeft de niveaus die je mag kiezen (levels,default_level), de grenzen voor bijlagen enmax_message_chars.POST /v1/chat/uploadneemt één bestand in het veldfileen geeft{"file_id": "...", "filename": "...", "kind": "image" | "file" | "text"}terug. Zet hetfile_idbij de vraag inattachments.POST /v1/chat/feedbackneemt{"rating": "up" | "down", "category": "begrip" | "berekening" | "anders", "comment": "...", "session_id": "...", "message_id": 123, "question": "...", "answer": "..."}. Bij een duim omlaag zijncategoryencommentverplicht.