Developers

API

With the API you use your bots from your own software, such as an intranet, an app or a CRM. The bot answers just as in the chat on your website: with your own content, locations, opening hours, rules and choice flows.

Authentication

Create a key in the portal under Connections → API keys. Give it a name and choose whether it has access to all bots in your account (including bots you add later) or to one or more bots, and whether it may create bots. You only see the key once. You can create as many as you like and revoke each one separately.

Send the key in the header of every request:

Authorization: Bearer cop_…
An API key belongs on your server. Never put it in code that ends up in a browser or app used by your visitors. For your website, use the regular widget.

Check key

GET /key

Returns the name of the key, the account and the bots it can access. Handy for testing whether everything works.

{
  "name": "Intranet",
  "account": "Kindercentrum",
  "all_bots": false,
  "bots": [{ "id": 24, "name": "Kindercentrum" }]
}

Retrieve bots

GET /bots

All bots this key has access to. Paused bots are not included. Use the id to select the bot in subsequent calls.

{
  "data": [
    {
      "id": 24,
      "name": "Kindercentrum",
      "site_url": "https://www.kindercentrum.be",
      "status": "active",
      "default_locale": "nl",
      "locales": ["nl", "fr"]
    }
  ]
}

One bot

GET /bots/{id}

The same details, plus the greeting, the start buttons and the bot's locations.

{
  "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 }
  }
}

Create bot

POST /bots

Creates a new bot for a website, like New bot in the portal: the website is read right away, colours and icon are taken over, and whoever created the key gets an email as soon as the bot is ready to try. Follow the progress with GET /bots/{id} (progress).

Only with a key that has May create bots switched on. Every bot has its own plan: if the new bot costs extra, you first get 402 with the price, and you send the request again with "confirm_price": true. At most 10 new bots per hour per key.

FieldTypeMeaning
nametextName of the bot, max. 100 characters. Required.
site_urltextWebsite the bot reads, for example www.yourcompany.com. Required.
default_localetextMain language: nl, en, fr, de or es. By default the language of your account.
localeslistExtra languages of the bot. Languages the bot finds on the website are added automatically.
plantextlite, plus, pro or business. Without a plan we choose the smallest plan that fits the website.
support_emailtextAddress for questions the bot forwards; by default your account's support address.
auto_approveyes/noUse new and changed pages right away (default yes) or have them approved in the portal first.
for_customeryes/noPartners only: a bot for a customer, not covered by your own subscription.
confirm_priceyes/noConfirms the price if the bot costs extra.
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"
  }
}

Ask a question

POST /bots/{id}/messages

Sends a message to the bot and returns the answer. Without conversation a new conversation is started. Send the conversation from the response along with the next question, and the bot will remember what the conversation is about.

FieldTypeMeaning
messagetextThe user's question or answer, max. 4,000 characters.
conversation optionaltextId of a conversation started with this key.
locale optionaltextLanguage of a new conversation (nl, fr, en, …). Defaults to the bot's language. If the user replies in a different language, the bot follows.
office_id optionalnumberLocation a new conversation is about.
user optionaltextYour own identifier for the user, max. 40 characters. This lets you find their conversations in the portal. No names or e-mail addresses.
stream optionalbooleantrue to receive the answer word by word. See 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

With "stream": true you get a text/event-stream (Server-Sent Events), so you can show the answer while the bot is still writing:

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… }}

If something goes wrong along the way, an event: error follows with a message.

Retrieve conversation

GET /bots/{id}/conversations/{conversation}

All messages of a conversation started with this key.

{
  "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": "…" }
    ]
  }
}

Forward leads

If a visitor asks to be contacted, the bot creates a lead with name, email address, phone number, question and summary. With webhooks, every new lead goes straight to your CRM, Zapier, Make or custom software. You can also fetch leads and events yourself.

Leads contain visitor contact details. Therefore, the calls below only work with a key where Access to leads is enabled, and only for the bots of that key. Without this access, you will get a 403.

Webhooks

Create a webhook in the portal under Connections → Webhooks, or via the API. Choose the URL, events and bots. For each event, we send a POST with JSON to that URL.

EventWhen
lead.createdA visitor requested to be contacted.
lead.updatedA lead’s follow-up status changed (new, contacted, closed), in the portal or via the API. Includes previous_status.
report.createdThe bot recorded a complaint or bug report. Five minutes later, so any follow-up from the visitor is included.
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"
    }
  }
}

Verify signature

Each webhook has a secret (whsec_…), visible in the portal under Secret. The X-ChatOnPage-Signature header contains t (timestamp, Unix seconds) and v1: the HMAC-SHA256 of t, a dot and the raw body, using the secret as the key. Compare them, and reject messages where t is more than 5 minutes off.

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

Fetch events

GET /events

Same events as webhooks, oldest first, for those who prefer to fetch themselves (e.g. every 5 minutes). Store next_after and send it as after: you’ll only get what’s new. Events are kept for 30 days.

ParameterMeaning
after optionalOnly events with a higher ID.
type optionalOne or more events, separated by a comma, for example lead.created.
bot optionalOnly events from this bot.
limit optional1 to 100, default 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 from the bots of the key, newest first. Filter by status (new, contacted, closed), bot and since (date or time, ISO 8601). If there is a next page, send next_before as before.

GET /leads/{id}

One lead, with the same fields as in lead.created, plus bot.

PATCH /leads/{id}

Updates the follow-up, e.g. {"status": "contacted"} once the lead is in your CRM. This is also visible in the portal and triggers a lead.updated event.

Webhooks via the API

This is how automation platforms like Zapier and Make connect and disconnect. A key only sees the webhooks it created itself. If the key is revoked, its webhooks are also removed.

POST /webhooks
FieldTypeMeaning
urltextPublic HTTPS address. Required.
events optionallistAll events by default.
bots optionallistBot IDs. Defaults to the bots of the key.
name optionaltextName in the portal. Defaults to the key’s name.
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_…"
  }
}

The secret is only in this response. Next: GET /webhooks (list), DELETE /webhooks/{id} (unsubscribe) and POST /webhooks/{id}/test (test event). Maximum 25 webhooks per account.

Choices

In a choice flow (for example enrolling) the bot returns choices. Show them as buttons and send the chosen text back verbatim as the next message, with the same conversation. The user may also type something else: the bot will work out whether it is a choice or a new question.

Errors and limits

On an error you get an HTTP status with {"error": "…"}. For invalid fields (422) the details are in errors.

StatusMeaning
401Missing, invalid or revoked key.
402The account is not active (for example, the trial has ended), or for POST /bots: the price must be confirmed ("code": "price_confirmation_required").
403This key cannot create bots or has no access to leads.
404The bot, conversation, lead or webhook does not exist, or this key has no access to it.
422A field is missing or invalid.
429Too many questions: maximum 60 per minute per key, plus the daily limit per bot.

Every answer counts towards the answers in your subscription, just like in the chat on your website. Conversations via the API are listed in the portal under Conversations.

Examples

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