Website widget
What this surface is
Everything under /api/public authenticates with a widget-scoped API key and is scoped to your organization by that key alone. It sets Access-Control-Allow-Origin: *, because a widget is embedded on domains we cannot know in advance.
- Create a key with only
chat:read,chat:write,bookings:readandbookings:write. Such a key is refused everywhere else, which is what makes publishing it safe. - The same routes are also mounted at
/api/v1/public. Both work; prefer the shorter form for new code. - Widget conversations are keyed by an anonymous session id, never a phone number.
Never publish a key with messages:send
Assistant chat
/api/public/chat/messagechat:writeAlso mounted at /api/public/message. Rate limited to 20 per minute.
const res = await fetch('https://api.wpai.co.in/api/public/chat/message', {
method: 'POST',
headers: {
'X-API-Key': 'wpai_pub123.xyz…',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'Do you deliver to Bandra?',
sessionId: localStorage.getItem('wpai_session') ?? undefined,
name: 'Priya',
}),
});
const data = await res.json();
localStorage.setItem('wpai_session', data.sessionId); // reuse it for the whole conversation
render(data.reply);{
"ok": true,
"sessionId": "web_sess_1756201234_a9f2x",
"userMessage": "Do you deliver to Bandra?",
"reply": "Yes — we deliver across the western suburbs. What is your address?",
"replySource": "ai",
"leadCaptured": false,
"bookingCreated": false
}- Omit
sessionIdon the first message and one is generated. Store it and send it back on every subsequent message, or each turn starts a new conversation. leadCapturedandbookingCreatedtell you the assistant did something beyond replying — worth showing a confirmation for.- This runs the same assistant configured for WhatsApp: same knowledge base, same handoff keywords, same active hours.
Widget chat is not metered
History and reset
/api/public/chat/history?sessionId=…&limit=50chat:readOldest first. limit is clamped to 1–100, default 50.
{
"ok": true,
"sessionId": "web_sess_1756201234_a9f2x",
"messages": [
{ "id": "68d1…", "fromMe": false, "text": "Do you deliver to Bandra?", "timestamp": "…" },
{ "id": "68d2…", "fromMe": true, "text": "Yes — we deliver across…", "timestamp": "…" }
]
}fromMe is from the business's point of view: true is the assistant, false is the visitor.
/api/public/chat/session?sessionId=…chat:writeDeletes the conversation. Use it for a “start over” control.
Bookings
Offering slots
/api/public/bookings/slots?date=YYYY-MM-DDbookings:read{
"ok": true,
"date": "2026-09-01",
"timezone": "Asia/Kolkata",
"isOpenDay": true,
"businessHours": { "start": "09:00", "end": "18:00" },
"slotDurationMinutes": 30,
"maxBookingsPerSlot": 3,
"slots": [ { "time": "09:00", "available": 2, "capacity": 3 } ]
}An empty slots array means closed, not full
isOpenDay: false with no slots is a day the business does not work. Say "closed" rather than "fully booked" — they are different answers to give a customer.Taking a booking
/api/public/bookingsbookings:writeRate limited to 20 per minute.
curl -X POST https://api.wpai.co.in/api/public/bookings \
-H "X-API-Key: wpai_pub123.xyz…" \
-H "Content-Type: application/json" \
-d '{
"name": "Priya Sharma",
"phone": "919876543210",
"date": "2026-09-01",
"time": "14:30",
"service": "Consultation",
"email": "[email protected]"
}'time must be a two-digit hour — 09:00, not 9:00. Times are wall-clock in the business's booking timezone, not UTC.
Unlike the staff-facing booking endpoint, this one enforces the schedule:
400 Bookings are not accepted on SUNDAYs400 Bookings are only accepted between 09:00 and 18:00409 Selected slot (2026-09-01 14:30) is fully booked
A successful booking upserts a contact tagged website-booking, confirms the slot, and notifies the business.
Embedding the ready-made widget
If you do not want to build the interface, the hosted widget does all of the above. Drop it in with your public key:
<script
src="https://api.wpai.co.in/public/widget.js"
data-api-key="wpai_pub123.xyz…"
data-title="Chat with us"
data-accent="#25D366"
defer
></script>Configure it from the Website Widget screen in the dashboard, which generates the snippet with your key and current settings already filled in.