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.

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_…
Een API-sleutel hoort op je server thuis. Zet hem nooit in code die in een browser of app bij je bezoekers terechtkomt. Voor je website gebruik je de gewone widget.

Sleutel controleren

GET /key

Geeft 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

GET /bots

Alle 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

GET /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

POST /bots

Maakt 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.

VeldTypeBetekenis
nametekstNaam van de bot, max. 100 tekens. Verplicht.
site_urltekstWebsite die de bot leest, bijvoorbeeld www.jouwbedrijf.be. Verplicht.
default_localetekstHoofdtaal: nl, en, fr, de of es. Standaard de taal van je account.
localeslijstExtra talen van de bot. Talen die de bot op de website vindt, komen er vanzelf bij.
plantekstlite, plus, pro of business. Zonder plan kiezen we het kleinste plan dat bij de website past.
support_emailtekstAdres voor vragen die de bot doorstuurt; standaard het supportadres van je account.
auto_approveja/neeNieuwe en gewijzigde pagina's meteen gebruiken (standaard ja) of eerst laten goedkeuren in het portaal.
for_customerja/neeAlleen voor partners: een bot voor een klant, die niet onder je eigen abonnement valt.
confirm_priceja/neeBevestigt 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

POST /bots/{id}/messages

Stuurt 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.

VeldTypeBetekenis
messagetekstDe vraag of het antwoord van de gebruiker, max. 4.000 tekens.
conversation optioneeltekstId van een gesprek dat met deze sleutel gestart werd.
locale optioneeltekstTaal 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 optioneelgetalVestiging waar een nieuw gesprek over gaat.
user optioneeltekstJe eigen kenmerk voor de gebruiker, max. 40 tekens. Zo vind je zijn gesprekken terug in het portaal. Geen namen of e-mailadressen.
stream optioneelbooleantrue 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
  }
}

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

GET /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.

EventWanneer
lead.createdEen bezoeker vroeg om gecontacteerd te worden.
lead.updatedDe opvolging van een lead veranderde (new, contacted, closed), in het portaal of via de API. Met previous_status.
report.createdDe 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"
    }
  }
}

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

GET /events

Dezelfde 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.

ParameterBetekenis
after optioneelEnkel events met een hogere id.
type optioneelEen of meer events, gescheiden door een komma, bijvoorbeeld lead.created.
bot optioneelEnkel events van deze bot.
limit optioneel1 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

GET /leads

Leads 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.

GET /leads/{id}

Eén lead, met dezelfde velden als in lead.created, plus bot.

PATCH /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.

POST /webhooks
VeldTypeBetekenis
urltekstPubliek https-adres. Verplicht.
events optioneellijstStandaard alle events.
bots optioneellijstIds van bots. Standaard de bots van de sleutel.
name optioneeltekstNaam 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.

StatusBetekenis
401Geen, ongeldige of ingetrokken sleutel.
402Het account is niet actief (bijvoorbeeld proefperiode afgelopen), of bij POST /bots: de prijs moet bevestigd worden ("code": "price_confirmation_required").
403Deze sleutel mag geen bots aanmaken, of heeft geen toegang tot leads.
404De bot, het gesprek, de lead of de webhook bestaat niet, of deze sleutel heeft er geen toegang toe.
422Een veld ontbreekt of is ongeldig.
429Te 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