Ontwikkelaars
API
Met de API gebruik je je bots vanuit je eigen software, zoals een intranet, een app of een CRM. De bot antwoordt net als in de chat op je website: met je eigen inhoud, vestigingen, openingsuren, regels en keuzeflows.
- Basis-URL:
https://chatonpage.com/api/v1 - Verzoeken en antwoorden in JSON (UTF-8). Stuur
Accept: application/jsonmee. - Werk je met een AI-assistent zoals ChatGPT of Claude? Gebruik dan de MCP-server.
- Nieuwe leads meteen in je CRM, Zapier of Make? Gebruik webhooks.
Authenticatie
Maak een sleutel aan in het portaal onder Koppelingen → API-sleutels. Geef hem een naam en kies of hij toegang heeft tot alle bots van je account (ook bots die je later toevoegt) of tot één of meer bots, en of hij bots mag aanmaken. Je ziet de sleutel maar één keer. Je kunt er zoveel aanmaken als je wilt en ze elk apart intrekken.
Stuur de sleutel mee in de header van elk verzoek:
Authorization: Bearer cop_…
Sleutel controleren
/keyGeeft de naam van de sleutel, het account en de bots waar hij bij kan. Handig om te testen of alles werkt.
{
"name": "Intranet",
"account": "Kindercentrum",
"all_bots": false,
"bots": [{ "id": 24, "name": "Kindercentrum" }]
}
Bots opvragen
/botsAlle bots waar deze sleutel toegang toe heeft. Gepauzeerde bots staan er niet bij. Met de id kies je de bot in de volgende calls.
{
"data": [
{
"id": 24,
"name": "Kindercentrum",
"site_url": "https://www.kindercentrum.be",
"status": "active",
"default_locale": "nl",
"locales": ["nl", "fr"]
}
]
}
Eén bot
/bots/{id}Dezelfde gegevens, plus de begroeting, de startknoppen en de vestigingen van de bot.
{
"data": {
"id": 24,
"name": "Kindercentrum",
…
"greeting": "Hallo! Ik ben de assistent van Kindercentrum. Waarmee kan ik u helpen?",
"start_choices": ["Inschrijven", "Openingsuren"],
"offices": [{ "id": 7, "name": "Biezebos", "city": "Gent" }],
"progress": { "percent": 100, "status": "Klaar", "ready": true, "pages": 84 }
}
}
Bot aanmaken
/botsMaakt een nieuwe bot voor een website, zoals Nieuwe bot in het portaal: de website wordt meteen gelezen, kleuren en icoon worden overgenomen, en wie de sleutel aanmaakte krijgt een mail zodra de bot klaar is om uit te proberen. Volg de voortgang met GET /bots/{id} (progress).
Alleen met een sleutel waarbij Mag bots aanmaken aan staat. Elke bot heeft een eigen plan: kost de nieuwe bot extra, dan krijg je eerst 402 met de prijs, en stuur je het verzoek opnieuw met "confirm_price": true. Hoogstens 10 nieuwe bots per uur per sleutel.
| Veld | Type | Betekenis |
|---|---|---|
name | tekst | Naam van de bot, max. 100 tekens. Verplicht. |
site_url | tekst | Website die de bot leest, bijvoorbeeld www.jouwbedrijf.be. Verplicht. |
default_locale | tekst | Hoofdtaal: nl, en, fr, de of es. Standaard de taal van je account. |
locales | lijst | Extra talen van de bot. Talen die de bot op de website vindt, komen er vanzelf bij. |
plan | tekst | lite, plus, pro of business. Zonder plan kiezen we het kleinste plan dat bij de website past. |
support_email | tekst | Adres voor vragen die de bot doorstuurt; standaard het supportadres van je account. |
auto_approve | ja/nee | Nieuwe en gewijzigde pagina's meteen gebruiken (standaard ja) of eerst laten goedkeuren in het portaal. |
for_customer | ja/nee | Alleen voor partners: een bot voor een klant, die niet onder je eigen abonnement valt. |
confirm_price | ja/nee | Bevestigt de prijs als de bot extra kost. |
POST /bots
{ "name": "Webshop", "site_url": "www.webshop.be", "plan": "pro", "confirm_price": true }
201 Created
{
"data": {
"id": 31,
"name": "Webshop",
"site_url": "https://www.webshop.be/",
"status": "active",
…
"progress": { "percent": 10, "status": "Website wordt gelezen", "ready": false, "pages": 0 },
"embed_code": "<script src="https://chatonpage.com/widget.js" data-key="pk_…" async></script>",
"portal_url": "https://chatonpage.com/app/…/bots/31/edit"
}
}
Vraag stellen
/bots/{id}/messagesStuurt een bericht naar de bot en geeft het antwoord terug. Zonder conversation start er een nieuw gesprek. Stuur de conversation uit het antwoord mee in de volgende vraag, dan onthoudt de bot waar het gesprek over gaat.
| Veld | Type | Betekenis |
|---|---|---|
message | tekst | De vraag of het antwoord van de gebruiker, max. 4.000 tekens. |
conversation optioneel | tekst | Id van een gesprek dat met deze sleutel gestart werd. |
locale optioneel | tekst | Taal van een nieuw gesprek (nl, fr, en, …). Standaard de taal van de bot. Antwoordt de gebruiker in een andere taal, dan volgt de bot die. |
office_id optioneel | getal | Vestiging waar een nieuw gesprek over gaat. |
user optioneel | tekst | Je eigen kenmerk voor de gebruiker, max. 40 tekens. Zo vind je zijn gesprekken terug in het portaal. Geen namen of e-mailadressen. |
stream optioneel | boolean | true om het antwoord woord per woord te ontvangen. Zie Streaming. |
{
"conversation": "9f0c2a4e-6b1d-4c3e-9a57-2f1e8d7c6b5a",
"reply": {
"message_id": 18342,
"content": "Voor welk type opvang wil je inschrijven?",
"sources": [{ "title": "Inschrijven", "url": "https://www.kindercentrum.be/inschrijven" }],
"choices": ["Kinderdagverblijf", "Buitenschoolse opvang (BKO)", "Gezinsopvang"],
"office_options": [],
"escalated": false
}
}
content: het antwoord in Markdown (vet, lijstjes, links).sources: de pagina's waarop het antwoord gebaseerd is.choices: de bot vraagt de gebruiker een keuze te maken. Zie Keuzes.office_options: de bot vraagt welke vestiging bedoeld wordt (id,name,city).escalated: de vraag werd doorgegeven aan een medewerker, zoals ingesteld bij de bot.
Streaming
Met "stream": true krijg je een text/event-stream (Server-Sent Events), zodat je het antwoord al toont terwijl de bot schrijft:
event: start
data: {"conversation":"9f0c2a4e-…"}
event: delta
data: {"text":"Voor welk type "}
event: delta
data: {"text":"opvang wil je inschrijven?"}
event: done
data: {"conversation":"9f0c2a4e-…","reply":{ …zoals hierboven… }}
Gaat er onderweg iets mis, dan volgt event: error met een message.
Gesprek ophalen
/bots/{id}/conversations/{conversation}Alle berichten van een gesprek dat met deze sleutel gestart werd.
{
"data": {
"id": "9f0c2a4e-…",
"bot_id": 24,
"locale": "nl",
"office_id": null,
"started_at": "2026-10-17T10:12:03+02:00",
"messages": [
{ "id": 18341, "role": "user", "content": "Ik wil mijn kind inschrijven", "choices": [], "at": "…" },
{ "id": 18342, "role": "assistant", "content": "Voor welk type opvang …", "choices": ["Kinderdagverblijf", …], "at": "…" }
]
}
}
Leads doorsturen
Vraagt een bezoeker om gecontacteerd te worden, dan maakt de bot een lead met naam, e-mailadres, telefoonnummer, vraag en samenvatting. Met webhooks gaat elke nieuwe lead meteen naar je CRM, Zapier, Make of eigen software. Je kunt leads en events ook zelf ophalen.
Leads bevatten contactgegevens van bezoekers. Daarom werken de calls hieronder enkel met een sleutel waarbij Toegang tot leads aan staat, en enkel voor de bots van die sleutel. Zonder die toegang krijg je 403.
Webhooks
Maak een webhook in het portaal onder Verbindingen → Webhooks, of via de API. Kies de URL, de events en de bots. Bij elk event sturen we een POST met JSON naar die URL.
| Event | Wanneer |
|---|---|
lead.created | Een bezoeker vroeg om gecontacteerd te worden. |
lead.updated | De opvolging van een lead veranderde (new, contacted, closed), in het portaal of via de API. Met previous_status. |
report.created | De bot noteerde een klacht of bugmelding. Vijf minuten later, zodat aanvullingen van de bezoeker erin zitten. |
POST https://jouw-crm.example.com/hooks/chatonpage
Content-Type: application/json
X-ChatOnPage-Event: lead.created
X-ChatOnPage-Event-Id: 5821
X-ChatOnPage-Delivery: 9310
X-ChatOnPage-Signature: t=1792923600,v1=5f2b…
{
"id": 5821,
"type": "lead.created",
"created_at": "2026-10-25T14:02:11+02:00",
"account": { "id": 3, "name": "Kindercentrum" },
"bot": { "id": 24, "name": "Kindercentrum" },
"data": {
"lead": {
"id": 1288,
"status": "new",
"name": "An Peeters",
"email": "an@example.com",
"phone": "+32 470 12 34 56",
"question": "Is er nog plaats in de BKO vanaf januari?",
"summary": "Ouder zoekt opvang voor twee kinderen vanaf januari.",
"language": "nl",
"office": { "id": 7, "name": "Biezebos" },
"conversation_id": "9f0c2a4e-6b1d-4c3e-9a57-2f1e8d7c6b5a",
"portal_url": "https://chatonpage.com/app/kindercentrum/leads",
"created_at": "2026-10-25T14:02:09+02:00",
"updated_at": "2026-10-25T14:02:09+02:00"
}
}
}
- Antwoord met een
2xx-status binnen 10 seconden. Verwerk het event liefst in de achtergrond. - Lukt het niet, dan proberen we opnieuw na 1 minuut, 5 minuten, 30 minuten, 2 uur, 6 uur en 12 uur. Gebruik
idom een event dat twee keer aankomt maar één keer te verwerken. - Antwoord je met
410 Gone, dan schakelen we de webhook uit. Lukt het bij 5 events na elkaar niet, dan ook; de beheerders krijgen dan een mail. Daarna zet je hem terug aan en stuur je de mislukte events opnieuw in het portaal. - We volgen geen redirects en sturen enkel naar publieke
https-adressen. - Met Testen in het portaal stuur je een event webhook.test, om je koppeling uit te proberen.
Handtekening controleren
Elke webhook heeft een geheim (whsec_…), te zien in het portaal onder Geheim. De header X-ChatOnPage-Signature bevat t (tijdstip, Unix-seconden) en v1: de HMAC-SHA256 van t, een punt en de ruwe body, met het geheim als sleutel. Vergelijk ze, en weiger berichten waarvan t meer dan 5 minuten afwijkt.
// Node.js (Express): app.post('/hooks/chatonpage', express.raw({ type: 'application/json' }), …)
const crypto = require('crypto');
const { t, v1 } = Object.fromEntries(req.get('X-ChatOnPage-Signature').split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.CHATONPAGE_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) < 300;
# PHP
[$t, $v1] = sscanf($_SERVER['HTTP_X_CHATONPAGE_SIGNATURE'], 't=%d,v1=%s');
$body = file_get_contents('php://input');
$valid = hash_equals(hash_hmac('sha256', $t.'.'.$body, getenv('CHATONPAGE_WEBHOOK_SECRET')), $v1) && abs(time() - $t) < 300;
Events ophalen
/eventsDezelfde events als bij webhooks, oudste eerst, voor wie liever zelf ophaalt (bijvoorbeeld elke 5 minuten). Bewaar next_after en stuur het mee als after: dan krijg je enkel wat er nieuw is. Events blijven 30 dagen bewaard.
| Parameter | Betekenis |
|---|---|
after optioneel | Enkel events met een hogere id. |
type optioneel | Een of meer events, gescheiden door een komma, bijvoorbeeld lead.created. |
bot optioneel | Enkel events van deze bot. |
limit optioneel | 1 tot 100, standaard 50. |
GET /events?type=lead.created&after=5820
{
"data": [{ "id": 5821, "type": "lead.created", "created_at": "…", "account": { … }, "bot": { … }, "data": { "lead": { … } } }],
"next_after": 5821
}
Leads
/leadsLeads van de bots van de sleutel, nieuwste eerst. Filter met status (new, contacted, closed), bot en since (datum of tijdstip, ISO 8601). Is er een volgende pagina, dan stuur je next_before mee als before.
/leads/{id}Eén lead, met dezelfde velden als in lead.created, plus bot.
/leads/{id}Past de opvolging aan, bijvoorbeeld {"status": "contacted"} zodra de lead in je CRM staat. Dat zie je ook in het portaal, en het geeft een event lead.updated.
Webhooks via de API
Zo koppelen automatiseringsplatformen zoals Zapier en Make zich aan en weer af. Een sleutel ziet enkel de webhooks die hij zelf aanmaakte. Wordt de sleutel ingetrokken, dan verdwijnen ook zijn webhooks.
/webhooks| Veld | Type | Betekenis |
|---|---|---|
url | tekst | Publiek https-adres. Verplicht. |
events optioneel | lijst | Standaard alle events. |
bots optioneel | lijst | Ids van bots. Standaard de bots van de sleutel. |
name optioneel | tekst | Naam in het portaal. Standaard de naam van de sleutel. |
POST /webhooks
{ "url": "https://hooks.zapier.com/hooks/standard/123/abc", "events": ["lead.created"] }
201 Created
{
"data": {
"id": 12,
"name": "Zapier",
"url": "https://hooks.zapier.com/hooks/standard/123/abc",
"events": ["lead.created"],
"bots": "all",
"active": true,
"disabled_reason": null,
"created_at": "…",
"secret": "whsec_…"
}
}
Het geheim staat enkel in dit antwoord. Verder: GET /webhooks (lijst), DELETE /webhooks/{id} (afmelden) en POST /webhooks/{id}/test (testevent). Hoogstens 25 webhooks per account.
Keuzes
Bij een keuzeflow (bijvoorbeeld inschrijven) geeft de bot choices mee. Toon ze als knoppen en stuur de gekozen tekst letterlijk terug als volgende message, met dezelfde conversation. De gebruiker mag ook iets anders typen: de bot begrijpt dan zelf of het een keuze is of een nieuwe vraag.
Fouten en limieten
Bij een fout krijg je een HTTP-status met {"error": "…"}. Bij ongeldige velden (422) staan de details in errors.
| Status | Betekenis |
|---|---|
| 401 | Geen, ongeldige of ingetrokken sleutel. |
| 402 | Het account is niet actief (bijvoorbeeld proefperiode afgelopen), of bij POST /bots: de prijs moet bevestigd worden ("code": "price_confirmation_required"). |
| 403 | Deze sleutel mag geen bots aanmaken, of heeft geen toegang tot leads. |
| 404 | De bot, het gesprek, de lead of de webhook bestaat niet, of deze sleutel heeft er geen toegang toe. |
| 422 | Een veld ontbreekt of is ongeldig. |
| 429 | Te veel vragen: maximaal 60 per minuut per sleutel, plus de daglimiet per bot. |
Elk antwoord telt mee voor de antwoorden in je abonnement, net als in de chat op je website. Gesprekken via de API staan in het portaal bij Gesprekken.
Voorbeelden
curl
curl https://chatonpage.com/api/v1/bots \
-H "Authorization: Bearer $CHATONPAGE_KEY"
curl https://chatonpage.com/api/v1/bots/24/messages \
-H "Authorization: Bearer $CHATONPAGE_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "Wat zijn de openingsuren in Gent?"}'
JavaScript (Node.js 18+)
const res = await fetch('https://chatonpage.com/api/v1/bots/24/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CHATONPAGE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: 'Wat zijn de openingsuren in Gent?', conversation }),
});
const { conversation: id, reply } = await res.json();
console.log(reply.content);
Python
import os, requests
r = requests.post(
"https://chatonpage.com/api/v1/bots/24/messages",
headers={"Authorization": f"Bearer {os.environ['CHATONPAGE_KEY']}"},
json={"message": "Wat zijn de openingsuren in Gent?"},
timeout=60,
)
data = r.json()
print(data["reply"]["content"])
conversation = data["conversation"] # meesturen bij de volgende vraag