API Documentation
Add Caretta Assistant — WAPI's smart assistant — to your product over a plain HTTPS API. One key, no SDK — send a message, get an answer generated from your own content. You build and own the UI around it.
Overview
The WAPI API gives you programmatic access to Caretta Assistant, WAPI's smart assistant. It answers questions from your own knowledge base over HTTPS. Authenticate with a single API key; there is no SDK to install.
This API is AI-only — it answers questions and returns JSON. Everything else (the chat UI, when to show it, what to do with a low-confidence answer) is yours to build.
Authentication
Send your API key as a Bearer token on every request. The X-WAPI-Key header is accepted as an alternative.
curl "https://www.wapiso.com/api/v1/chat/start" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY"fetch("https://www.wapiso.com/api/v1/chat/start", {
headers: { "Authorization": "Bearer wapi_pk_YOUR_KEY" }
});curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer wapi_pk_YOUR_KEY"
]);headers = { "Authorization": "Bearer wapi_pk_YOUR_KEY" }Always call the API from your own backend so the key never ships in browser code. A missing, invalid, or regenerated (old) key returns HTTP 401. Find and rotate your key in the dashboard under Developer → API Key.
Response format
Every response is JSON. On success, the payload is nested under a data object alongside "success": true. On error, you get "success": false with an error code and a human-readable message, plus a matching HTTP status.
{
"success": true,
"data": { /* the action’s fields */ }
}{
"success": false,
"error": "Error",
"message": "Message is required"
}Quickstart
The whole core of the API is one call: send a message, read the assistant reply from data.messages. Here it is end to end — call this from your server.
curl -X POST "https://www.wapiso.com/api/v1/chat/send" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY" \
-d "message=How do I renew my domain?"const res = await fetch(
"https://www.wapiso.com/api/v1/chat/send",
{
method: "POST",
headers: {
"Authorization": "Bearer wapi_pk_YOUR_KEY",
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({ message: "How do I renew my domain?" })
}
);
const json = await res.json();
const reply = json.data.messages.find(m => m.role === "assistant");
console.log(reply.body);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"],
CURLOPT_POSTFIELDS => http_build_query(["message" => "How do I renew my domain?"]),
]);
$json = json_decode(curl_exec($ch), true);
foreach ($json["data"]["messages"] as $m) {
if ($m["role"] === "assistant") echo $m["body"];
}import requests
res = requests.post(
"https://www.wapiso.com/api/v1/chat/send",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"},
data={"message": "How do I renew my domain?"},
)
json = res.json()
reply = next(m for m in json["data"]["messages"] if m["role"] == "assistant")
print(reply["body"]){
"success": true,
"data": {
"conversationId": 1234,
"messages": [
{ "id": 9001, "role": "user", "body": "How do I renew my domain?", "source": "system" },
{ "id": 9002, "role": "assistant", "body": "You can renew it from your domains page…", "source": "ai" }
],
"lowConfidence": false
}
}Conversations & state
You do not pass a conversation id. send takes only message; the server resolves the conversation from the API-key owner. In practice that means one rolling conversation per API key — messages accumulate in the same thread.
The assistant reads the last 30 messages of that thread for context; older turns are summarized automatically, so nothing breaks on long threads — just keep important facts recent.
Reply language follows the message
The assistant answers in the language each message is written in. On every send the message language is auto-detected (Turkish, English or German), overrides the conversation’s previous language, and is saved on the conversation for the next turns. You don’t pass a language parameter on this surface — just write in the language you want answered. Supported: tr, en, de.
Caretta Chat API
Four conversational actions plus two read-only account endpoints. All are called as /api/v1/chat/<action> with your Bearer key. The older /api.php?endpoint=chat&action=… URLs keep working, so existing integrations need no change.
Conversational
No parameters.
curl "https://www.wapiso.com/api/v1/chat/start" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY"const res = await fetch("https://www.wapiso.com/api/v1/chat/start", {
headers: { "Authorization": "Bearer wapi_pk_YOUR_KEY" }
});
const { data } = await res.json();
console.log(data.conversationId, data.messages);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/start");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"],
]);
$data = json_decode(curl_exec($ch), true)["data"];import requests
data = requests.get(
"https://www.wapiso.com/api/v1/chat/start",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"},
).json()["data"]{
"success": true,
"data": {
"conversationId": 1234,
"lang": "en",
"messages": []
}
}| Parameter | Type | Description |
|---|---|---|
messagerequired |
string | The visitor’s message. Max 2,000 characters. |
curl -X POST "https://www.wapiso.com/api/v1/chat/send" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY" \
-d "message=How do I renew my domain?"const res = await fetch(
"https://www.wapiso.com/api/v1/chat/send",
{
method: "POST",
headers: {
"Authorization": "Bearer wapi_pk_YOUR_KEY",
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({ message: "How do I renew my domain?" })
}
);
const json = await res.json();
const reply = json.data.messages.find(m => m.role === "assistant");
console.log(reply.body);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"],
CURLOPT_POSTFIELDS => http_build_query(["message" => "How do I renew my domain?"]),
]);
$json = json_decode(curl_exec($ch), true);
foreach ($json["data"]["messages"] as $m) {
if ($m["role"] === "assistant") echo $m["body"];
}import requests
res = requests.post(
"https://www.wapiso.com/api/v1/chat/send",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"},
data={"message": "How do I renew my domain?"},
)
json = res.json()
reply = next(m for m in json["data"]["messages"] if m["role"] == "assistant")
print(reply["body"]){
"success": true,
"data": {
"conversationId": 1234,
"messages": [
{ "id": 9001, "role": "user", "body": "How do I renew my domain?", "source": "system" },
{ "id": 9002, "role": "assistant", "body": "You can renew it from your domains page…", "source": "ai" }
],
"lowConfidence": false
}
}No parameters.
curl "https://www.wapiso.com/api/v1/chat/suggestions" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY"const { data } = await fetch("https://www.wapiso.com/api/v1/chat/suggestions", {
headers: { "Authorization": "Bearer wapi_pk_YOUR_KEY" }
}).then(r => r.json());
console.log(data.seeds, data.articles);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/suggestions");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"]]);
$data = json_decode(curl_exec($ch), true)["data"];import requests
data = requests.get("https://www.wapiso.com/api/v1/chat/suggestions",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"}).json()["data"]{
"success": true,
"data": {
"articles": [
{ "id": 12, "slug": "renew-domain", "title": "Renewing a domain", "excerpt": "…", "category_slug": "domains", "category_name": "Domains", "view_count": 1250 }
],
"seeds": [ "How do I renew a domain?", "How do I add balance?" ]
}
}| Parameter | Type | Description |
|---|---|---|
sinceIdoptional |
int | Return only messages with id greater than this. Defaults to 0. |
curl "https://www.wapiso.com/api/v1/chat/sync?sinceId=9002" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY"const { data } = await fetch(
"https://www.wapiso.com/api/v1/chat/sync?sinceId=9002",
{ headers: { "Authorization": "Bearer wapi_pk_YOUR_KEY" } }
).then(r => r.json());
console.log(data.messages);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/sync?sinceId=9002");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"]]);
$data = json_decode(curl_exec($ch), true)["data"];import requests
data = requests.get("https://www.wapiso.com/api/v1/chat/sync?sinceId=9002",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"}).json()["data"]{
"success": true,
"data": {
"conversationId": 1234,
"messages": [
{ "id": 9003, "role": "assistant", "body": "…", "source": "ai" }
]
}
}| Parameter | Type | Description |
|---|---|---|
messageIdrequired |
int | The assistant message id being rated (from start/send/sync). |
ratingrequired |
string | helpful or not_helpful. |
commentoptional |
string | Optional note shown to the reviewer. Max 500 characters. |
{
"success": true,
"data": {
"saved": true
}
}The message object
start, send and sync all return message objects with these fields:
| Field | Type | Description |
|---|---|---|
id | int | Unique message id (use the largest as sinceId for sync). |
role | string | who wrote it: user, assistant, or system. |
body | string | The message text. |
source | string | how it was produced: ai, cache_tenant, cache_shared, system, fallback. |
created_at | string | Timestamp (UTC). |
products | array | assistant messages only — product cards attached to the reply (see below). Absent or empty when no products apply. |
The product object (products[])
When your assistant’s Sales Mode is on and the visitor asks about products, assistant messages carry up to 8 product cards learned from your own pages. The API returns a plain array — render it however you like (the widget shows a horizontal carousel). Product data enters the system from your site during crawling (including schema.org JSON-LD you already publish); the API output itself is this plain array, never JSON-LD.
| Field | Type | Description |
|---|---|---|
name | string | Product name. The only field that is always present. |
url | string | Link to the product page, when learned. |
price | string | Display price as text, when learned. |
category | string | Category label, when learned. |
image | string | Product image URL, when learned. |
Account (read-only)
curl "https://www.wapiso.com/api/v1/chat/apiKey" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY"const { data } = await fetch("https://www.wapiso.com/api/v1/chat/apiKey", {
headers: { "Authorization": "Bearer wapi_pk_YOUR_KEY" }
}).then(r => r.json());
console.log(data.balance, data.pricing);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/apiKey");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"]]);
$data = json_decode(curl_exec($ch), true)["data"];import requests
data = requests.get("https://www.wapiso.com/api/v1/chat/apiKey",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"}).json()["data"]{
"success": true,
"data": {
"apiKey": { "id": 1, "api_key_prefix": "wapi_pk_6c52c2d1", "status": "active", "last_used_at": "2026-05-30 14:12:52", "created_at": "2026-05-01 09:00:00" },
"balance": 12.40,
"pricing": { "input_per_m_usd": 1.10, "output_per_m_usd": 4.40, "typical_ai_usd": 0.0021 }
}
}curl "https://www.wapiso.com/api/v1/chat/usageSummary" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY"const { data } = await fetch("https://www.wapiso.com/api/v1/chat/usageSummary", {
headers: { "Authorization": "Bearer wapi_pk_YOUR_KEY" }
}).then(r => r.json());
console.log(data.total_cost, data.by_event);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/usageSummary");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"]]);
$data = json_decode(curl_exec($ch), true)["data"];import requests
data = requests.get("https://www.wapiso.com/api/v1/chat/usageSummary",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"}).json()["data"]{
"success": true,
"data": {
"window_days": 30,
"by_event": { "ai_call": { "count": 42, "cost": 0.0735 } },
"total_cost": 0.0993,
"total_events": 195,
"all_time": { "events": 500, "cost": 0.35 },
"recent": [ { "id": 1, "event_type": "ai_call", "cost_usd": 0.00175, "created_at": "2026-05-30 10:31:00" } ],
"price_per_message_usd": 0.00175
}
}{
"success": true,
"data": {
"preset": "warm",
"custom": "keep answers short",
"presets": ["professional", "warm", "energetic"]
}
}Embed tokens (manage via API)
{
"success": true,
"data": {
"tokens": [ {
"id": 3, "token": "wapi_emb_…", "label": "Company site",
"allowed_domains": ["example.com"], "daily_budget_usd": "1.0000", "status": "active",
"today_spend_usd": 0.0421, "budget_used_pct": 4,
"conversations": 12, "visitors": 9,
"last_used_at": "2026-06-07 18:40:11", "created_at": "2026-06-01 09:00:00"
} ]
}
}| Parameter | Type | Description |
|---|---|---|
domainsrequired | string | Allowed domains, comma-separated (e.g. example.com, www.example.com). Subdomains of an allowed domain pass too. |
labeloptional | string | Display label (max 80 chars). |
daily_budget_usdoptional | float | Hard daily budget in USD (0.05–100, default 1.00). Spend at the cap pauses the widget until next UTC day. |
{
"success": true,
"data": {
"token": "wapi_emb_f3a91c…"
}
}| Parameter | Type | Description |
|---|---|---|
idrequired | int | Token id (from embedTokens). |
labeloptional | string | Display label (max 80 chars). |
domainsoptional | string | Allowed domains, comma-separated (e.g. example.com, www.example.com). Subdomains of an allowed domain pass too. |
daily_budget_usdoptional | float | Hard daily budget in USD (0.05–100, default 1.00). Spend at the cap pauses the widget until next UTC day. |
{
"success": true,
"data": {
"updated": true
}
}| Parameter | Type | Description |
|---|---|---|
idrequired | int | Token id (from embedTokens). |
{
"success": true,
"data": {
"revoked": true
}
}Marketing & Intelligence API
A separate API family from the Caretta chat above — a distinct model surface you call to CREATE, not to converse. Same Bearer key. Generate marketing copy and complete strategies (marketingGenerate) and run structured intelligence over your KB (intelligence). Grounded on your own tenant and never-invent, just like the chat.
Marketing generator
| Parameter | Type | Description |
|---|---|---|
briefrequired | string | The marketing brief / topic. Max 2,000 characters. Describes intent only — never treated as fact. |
productoptional | string | Product or plan name to feature. |
audienceoptional | string | Target audience (e.g. small businesses, developers). |
goaloptional | string | Campaign goal (e.g. signups, awareness, sales). |
toneoptional | string | Desired voice (e.g. energetic, warm, professional). |
formatoptional | string | Output format hint (e.g. social post, email, ad copy). |
variantsoptional | int | How many alternative headlines to return (1–5, default 3). |
site_idoptional | int | One of your site ids to ground on (from mySites). You must own it; an unowned or omitted id falls back to your account — never another tenant’s data. |
langoptional | string | Output language: tr, en or de. Defaults to your account language. |
depthoptional | string | full → a saturated deliverable (positioning, angles, objections, channel adaptations, proof, rationale, A/B). strategy → a COMPLETE 12-section marketing strategy (market analysis + personas + JTBD forces, positioning, messaging, full funnel, channel mix, content plan, measurement/KPIs, roadmap, experiments). Omit for the lean output below. |
curl -X POST "https://www.wapiso.com/api/v1/chat/marketingGenerate" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY" \
-d "brief=Introduce our email marketing plans" \
-d "audience=small businesses" \
-d "goal=signups" \
-d "site_id=5"const res = await fetch(
"https://www.wapiso.com/api/v1/chat/marketingGenerate",
{
method: "POST",
headers: {
"Authorization": "Bearer wapi_pk_YOUR_KEY",
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
brief: "Introduce our email marketing plans",
audience: "small businesses",
goal: "signups",
site_id: "5"
})
}
);
const { data } = await res.json();
console.log(data.content.headline, data.content.body);<?php
$ch = curl_init("https://www.wapiso.com/api/v1/chat/marketingGenerate");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"],
CURLOPT_POSTFIELDS => http_build_query([
"brief" => "Introduce our email marketing plans",
"audience" => "small businesses",
"goal" => "signups",
"site_id" => 5,
]),
]);
$content = json_decode(curl_exec($ch), true)["data"]["content"];import requests
data = requests.post(
"https://www.wapiso.com/api/v1/chat/marketingGenerate",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"},
data={
"brief": "Introduce our email marketing plans",
"audience": "small businesses",
"goal": "signups",
"site_id": 5,
},
).json()["data"]
print(data["content"]["headline"]){
"success": true,
"data": {
"content": {
"headline": "Smart Email Marketing for Small Businesses",
"body": "WMail sends from your own domain with AI templates and real-time analytics…",
"cta": "Start free",
"variants": [ "Automate your email with WMail", "Big impact for small teams" ],
"hashtags": [ "EmailMarketing", "SmallBusiness", "WMail" ],
"strategy_used": "Kotler STP + AIDA"
},
"meta": { "lang": "en", "grounded": true, "kb_used": 24, "products": 3, "tokens": 3379 }
}
}The content object
By default (lean) marketingGenerate returns data.content with these fields, plus data.meta (depth, grounded, kb_used, products, tokens). With depth=full it instead returns the saturated set: positioning, segment, funnel_stage, angles[], objections[], channel_adaptations[], proof_points[], hashtags[], strategy_rationale, next_experiment.
| Field | Type | Description |
|---|---|---|
headline | string | Punchy, benefit-first headline. |
body | string | The main marketing copy. |
cta | string | One clear call to action. |
variants | array | Alternative headlines/hooks. |
hashtags | array | Relevant hashtags (without #), or empty. |
strategy_used | string | Which framework(s) were applied. |
Intelligence API
One grounded engine, many shapes. Pick an operation, a format and a depth and get a grounded, never-invent result over your own KB. Path: /api/v1/intelligence/<op>.
| Parameter | Type | Description |
|---|---|---|
oprequired | path | The operation is the URL path: answer, summarize, compare, extract, or data (data is a direct, LLM-free query of your products/faq/categories). |
formatoptional | string | text | markdown | json | table | list. Shapes the output (table → {columns, rows}; list → {items}; json → {data}; text/markdown → {result}). |
depthoptional | string | lean (default) or full (a more thorough result). Applies to the LLM ops. |
(inputs)optional | — | Op-specific inputs alongside the above: question (answer), topic (summarize), items[] or query (compare), fields + topic (extract), type=products|faq|categories + page/limit/category/query (data). |
site_id, langoptional | int, string | One of your site ids to ground on (you must own it); otherwise your account KB. lang: tr|en|de. |
curl -X POST "https://www.wapiso.com/api/v1/intelligence/compare" \
-H "Authorization: Bearer wapi_pk_YOUR_KEY" \
-d "query=email plans" \
-d "format=table" \
-d "depth=full"const res = await fetch(
"https://www.wapiso.com/api/v1/intelligence/answer",
{
method: "POST",
headers: {
"Authorization": "Bearer wapi_pk_YOUR_KEY",
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
question: "Can I send from my own domain?",
format: "markdown"
})
}
);
const { data } = await res.json();
console.log(data.result);<?php
// data op — no LLM, a direct query of your own products
$ch = curl_init("https://www.wapiso.com/api/v1/intelligence/data");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer wapi_pk_YOUR_KEY"],
CURLOPT_POSTFIELDS => http_build_query([
"type" => "products", "format" => "table", "limit" => 20,
]),
]);
$data = json_decode(curl_exec($ch), true)["data"];import requests
data = requests.post(
"https://www.wapiso.com/api/v1/intelligence/summarize",
headers={"Authorization": "Bearer wapi_pk_YOUR_KEY"},
data={"topic": "your product", "format": "list", "depth": "full"},
).json()["data"]
print(data["result"]){
"success": true,
"data": {
"op": "compare",
"format": "table",
"result": {
"columns": [ "Feature", "Starter", "Growth" ],
"rows": [ [ "Own domain", "Yes", "Yes" ], [ "Real-time analytics", "—", "Yes" ] ]
},
"meta": { "depth": "full", "lang": "en", "grounded": true, "kb_used": 24, "products": 3, "tokens": 2100 }
}
}Returns data.result shaped by format, plus data.meta (op, format, depth, grounded, kb_used, products, tokens). The data op returns rows and a total in meta.
Responses & errors
The lowConfidence flag
send returns data.lowConfidence: true when the assistant could not answer confidently (low confidence, or the request was declined — e.g. the balance ran out). Use it to decide what to show on your side: a fallback message, or a link to your own contact page. The API only answers — what happens next is entirely up to your application.
Status codes
200 | Success (including the “out of balance” reply — inspect the message body and lowConfidence). |
400 | Bad request (e.g. empty or too-long message). |
401 | Missing, invalid, or regenerated (old) API key. |
403 | Embed surface only — the call came from a domain outside the token’s allowed list, a write arrived without an Origin header, or the action is outside the token’s scope. |
429 | Rate limited — you exceeded a request rate (see Limits). The response carries a Retry-After header with the seconds to wait. |
500 | Server error. Safe to retry shortly. |
Balance exhausted
When your balance can’t cover a request, the call still returns HTTP 200 — it is not an error. The assistant message explains the balance is insufficient, data.lowConfidence is true, and you are not charged. Top up to resume.
// HTTP 200 — NOT an error, and NOT charged
{
"success": true,
"data": {
"conversationId": 1234,
"messages": [
{ "id": 9004, "role": "assistant",
"body": "Your account balance isn't enough to answer this message.",
"source": "system" }
],
"lowConfidence": true
}
}Limits
Limits are counted per API key (account-wide, shared by all your site’s visitors) — not per visitor.
Repeat questions are free against your quota
The daily limit only counts unique questions that miss the knowledge base and reach the AI. Answers served from your knowledge base (repeated or similar questions) are returned from cache before the quota is checked — they never consume it. So 1,000 visitors asking mostly the same things barely touch the daily cap once your knowledge base is warm. Build it up by crawling your site and adding Q&A.
Daily unique-answer limit by plan
Other limits
| Limit | Value | At the limit |
|---|---|---|
| Message length | 2,000 characters | A longer message returns HTTP 400. Trim or split before sending. (The embed surface has its own 1,000-character limit — see Embed Widget.) |
| Send rate | 15 sends/min per key | Faster sending returns HTTP 429 with a Retry-After header — wait that many seconds, or queue on your side. |
| Context window | last 30 messages | The assistant reads the most recent 30 messages of the conversation; older turns are summarized automatically. |
Per-message pricing
Messages are billed by real token usage. The figure below is the typical all-in cost of one answer — the main reply plus the automatic planning, quality-check and context-summary sub-calls that run on a normal send. This is the same number shown on your dashboard.
| Estimated spend at the typical rate | Est. cost |
|---|---|
| 1,000 messages | $35.50 |
| 10,000 messages | $355.00 |
| 100,000 messages | $3,550.00 |
| Token type | Per 1M tokens |
|---|---|
| Input tokens | $3.00 |
| Output tokens | $25.00 |
Embed Widget
Drop Caretta Assistant onto any page of yours with one script tag. No backend needed: the widget uses a PUBLIC embed token (wapi_emb_…) that is safe to ship in HTML — protection comes from a server-side domain lock, per-visitor/IP rate limits and a hard daily budget you set.
Install
Create a token in the dashboard (Developer → Site Assistant (Embed)), then paste:
<script src="https://www.wapiso.com/embed.js?v=1788352057"
data-wapi-token="wapi_emb_YOUR_TOKEN"
data-wapi-lang="en"
data-wapi-color="#0061FF" defer></script>How it works
- Public token (wapi_emb_…) — designed to be visible in your page source. It is NOT a secret; never confuse it with your wapi_pk_ server key.
- Domain lock — the server only accepts requests whose Origin matches your token’s allowed domains (subdomains included). Other sites get HTTP 403.
- Per-visitor conversations — each visitor gets an anonymous UUID (localStorage) and an isolated thread; visitors can never read each other’s messages.
- Daily budget — you set a hard USD cap per token. When spend reaches it, the widget is NOT switched off: it degrades to cache-only mode — frequent questions keep getting answered from your knowledge base for free, new AI calls pause until the next UTC day, and you get an email notification. Answers are billed to YOUR balance at the same per-message rates above.
- Honest answers only — the widget answers from your knowledge base. When it can’t, it says so; there is no human-handover promise to your visitors (a lowConfidence hint suggests rephrasing).
Widget rate limits
| Per visitor: 10 messages/min, 100/day |
| Per token: 60 requests/min |
| Per IP: 30 requests/min |
| Per message: 1,000 characters (longer returns HTTP 400) |
Manage tokens
Create, revoke and budget tokens under Developer → Site Assistant (Embed) in your dashboard — the snippet is generated there for copy-paste. Prefer code? The same operations are available server-side with your wapi_pk_ key — see “Embed tokens” in the API reference above.
Security
- Never put your wapi_pk_ key in public front-end code or a public repository. Call the API from your backend.
- Rotate the key any time from the dashboard. The previous key is revoked the instant you regenerate.
- Every request is rate-limited and billed; monitor usage under Developer → API Usage.
Need help?
Open the chat on your dashboard and type your question, or reach our team from the Contact page.