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

MethodPathPurposeWho
GET/api/orgs/{orgId}/agents/{agentId}/publishRead (auto-creates a config with a slug on first read)any org member
PUT/api/orgs/{orgId}/agents/{agentId}/publishReplace publish settingsowner / 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

FieldTypeDefaultNotes
isPublicbooleanrequiredEnables the public page + widget. With it false, the slug resolves to "unavailable".
apiEnabledbooleantrueAllows the programmatic surfaces (init-web-call, chat) for this agent.
enableCallbooleantrueAllow browser voice calls.
enableChatbooleanfalseAllow text chat. (Enable at least one of call/chat.)
leadCapturebooleanfalseCollect fields before connecting.
leadFieldsarray[]Field descriptors (see below) when leadCapture is on — max 12.
accessCodestring | nullnullRequire a code before a session starts. Empty/blank → no code.
allowedOriginsstring[][]Origins allowed to embed (empty = any) — max 50. Trimmed and de-duplicated on save.
themeColorstring | nullnullHex accent color, e.g. #4f46e5.
widgetTitlestring | nullnullHeading shown in the widget/page.
buttonLabelstring | nullnullLabel on the start button.
greetingstring | nullnullShort intro line shown before connecting.

Free-text fields are trimmed (blank → null) and capped at 2,000 characters.

leadFields descriptor

KeyTypeNotes
keystringStable answer key (≤64 chars). If omitted, derived by slugifying label. Duplicate keys are dropped.
labelstringWhat the visitor sees (≤120 chars). Required for the field to be kept.
typestringOne of text (default), email, tel, number. phone is accepted as an alias for tel. Unknown types fall back to text.
requiredbooleanWhether 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 } ]
  }
}
  • requiresAccessCode is a boolean — the code itself is never returned.
  • variables lists 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:

CheckFailure
Agent is public + active404 "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 s400 "Too many attempts — please wait a moment and try again."
Per-org concurrent public web-call cap429
All required context variables present400 "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.