Publishing & widget
Publishing & widget API
These endpoints control an agent's public widget — the hosted page and embeddable button — and let an embedded widget start an anonymous session. See Publishing & embedding for the dashboard walkthrough and Embed the widget for the integration.
There are two surfaces, deliberately split so the public one carries no auth and no secrets:
- the authed config (
/api/orgs/{orgId}/agents/{agentId}/publish) — read/update settings, returns the{success,data}envelope; - the public widget (
/api/public/widget/{slug}) — unauthenticated, redacted, what the browser renders from.
Publish configuration (authenticated)
Read or update an agent's publish settings. Organization-scoped (API key or JWT + X-Org-Id).
| Method | Path | Purpose | Who |
|---|---|---|---|
GET | /api/orgs/{orgId}/agents/{agentId}/publish | Read (auto-creates a config with a slug on first read) | any org member |
PUT | /api/orgs/{orgId}/agents/{agentId}/publish | Replace publish settings | owner / admin only |
A PUT is a full replace of the editable fields — send the whole object, not a partial patch.
curl -X PUT https://api.telenow.ai/api/orgs/{orgId}/agents/{agentId}/publish \
-H "x-api-key: vai_live_…" -H "Content-Type: application/json" \
-d '{
"isPublic": true,
"apiEnabled": true,
"enableCall": true,
"enableChat": true,
"leadCapture": true,
"leadFields": [
{ "key": "name", "label": "Your name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": false }
],
"accessCode": null,
"allowedOrigins": ["https://acme.com", "https://app.acme.com"],
"themeColor": "#4f46e5",
"widgetTitle": "Talk to Acme",
"buttonLabel": "Start call",
"greeting": "Hi! Tap below to start."
}'
Editable fields
| Field | Type | Default | Notes |
|---|---|---|---|
isPublic | boolean | required | Enables the public page + widget. With it false, the slug resolves to "unavailable". |
apiEnabled | boolean | true | Allows the programmatic surfaces (init-web-call, chat) for this agent. |
enableCall | boolean | true | Allow browser voice calls. |
enableChat | boolean | false | Allow text chat. (Enable at least one of call/chat.) |
leadCapture | boolean | false | Collect fields before connecting. |
leadFields | array | [] | Field descriptors (see below) when leadCapture is on — max 12. |
accessCode | string | null | null | Require a code before a session starts. Empty/blank → no code. |
allowedOrigins | string[] | [] | Origins allowed to embed (empty = any) — max 50. Trimmed and de-duplicated on save. |
themeColor | string | null | null | Hex accent color, e.g. #4f46e5. |
widgetTitle | string | null | null | Heading shown in the widget/page. |
buttonLabel | string | null | null | Label on the start button. |
greeting | string | null | null | Short intro line shown before connecting. |
Free-text fields are trimmed (blank → null) and capped at 2,000 characters.
leadFields descriptor
| Key | Type | Notes |
|---|---|---|
key | string | Stable answer key (≤64 chars). If omitted, derived by slugifying label. Duplicate keys are dropped. |
label | string | What the visitor sees (≤120 chars). Required for the field to be kept. |
type | string | One of text (default), email, tel, number. phone is accepted as an alias for tel. Unknown types fall back to text. |
required | boolean | Whether the field must be filled before connecting (default false). |
Response
The full config (camelCase), including server-owned fields:
{
"success": true,
"data": {
"agentId": "…",
"orgId": "…",
"publicSlug": "a1b2c3d4e5f6",
"isPublic": true,
"apiEnabled": true,
"accessCode": null,
"allowedOrigins": ["https://acme.com"],
"themeColor": "#4f46e5",
"widgetTitle": "Talk to Acme",
"greeting": "Hi! Tap below to start.",
"buttonLabel": "Start call",
"enableCall": true,
"enableChat": true,
"leadCapture": true,
"leadFields": [ { "key": "name", "label": "Your name", "type": "text", "required": true } ]
}
}
publicSlug is the identifier used in the public URL …/p/{slug} and the embed snippet. It's assigned once and never changes (so a link you've already shared keeps working), and is not settable via PUT.
Public widget config (unauthenticated)
What the embedded widget fetches to render itself. Returns a redacted view (no access code, no org id, no allow-list internals). Resolves only when the agent is public and active — otherwise an indistinguishable 404 ("This link is unavailable"), so a private agent can't be probed.
GET /api/public/widget/{slug}
{
"success": true,
"data": {
"agentId": "…",
"agentName": "Acme Support",
"requiresAccessCode": false,
"themeColor": "#4f46e5",
"widgetTitle": "Talk to Acme",
"greeting": "Hi! Tap below to start.",
"buttonLabel": "Start call",
"enableCall": true,
"enableChat": true,
"leadCapture": true,
"leadFields": [ { "key": "name", "label": "Your name", "type": "text", "required": true } ],
"variables": [ { "name": "customer_name", "required": true } ]
}
}
requiresAccessCodeis a boolean — the code itself is never returned.variableslists the agent's context variables, derived from its prompt + opener. The widget collects the required ones before connecting; the agent's prompt is never exposed here.
Start a public session (unauthenticated)
Called by the embedded widget to begin a browser session. No API key — abuse is controlled by the access code, the allowedOrigins check, a per-slug/IP rate limit, and a per-org concurrency cap.
POST /api/public/widget/{slug}/session
// request
{
"accessCode": "1234", // required only if the agent has one
"lead": { "name": "Dana", "email": "[email protected]" }, // optional, keys match leadFields
"variables": { "customer_name": "Alex" } // optional; required context vars must be present
}
// response
{ "success": true, "data": { "sessionId": "…", "status": "active", "websocketUrl": "wss://…" } }
Connect a browser WebSocket to websocketUrl and follow the Web-call protocol. Captured lead values are attached to the call (visible under Lead details) and flow to your webhooks.
Guardrails (and the errors they raise)
These run in order before a session is created:
| Check | Failure |
|---|---|
| Agent is public + active | 404 "This link is unavailable" |
| Access code matches (if the agent has one) | 403 "Invalid access code" |
Origin header is in allowedOrigins (when the list is non-empty) | 403 "This site isn't allowed to embed this agent" |
| Per-slug + per-IP rate limit — 10 starts per 60 s | 400 "Too many attempts — please wait a moment and try again." |
| Per-org concurrent public web-call cap | 429 |
All required context variables present | 400 "Missing required variable(s): …" |
The lead object is bounded on the server (up to 20 entries, keys ≤64 chars, values ≤500 chars) before it's stamped onto the call.
Prefer the one-line widget embed unless you need a fully custom UI — it calls these endpoints for you and handles the audio.
Related
- Web-call API & WebSocket — the protocol the returned
websocketUrlspeaks. - Chat API — build a custom text UI on the agent from your server.
- Authentication — how the authed config call is keyed.