Skip to main content

Build an embedded agent with a widget connector

Create a document-backed agent and embed it on your website using a widget connector. Index documentation excerpts, configure the agent, route it through an alias, and embed Popup Agent Chat. You do not need to build chat components or expose an operator API key in the browser.

What you will build​

The embedded agent searches three short documentation excerpts about widget setup, alias routing, and allowed origins. These excerpts form a small test corpus, not a copy of the full Vectara documentation. You will check answers against the indexed text before testing the widget on your website.

ResourceIts job in this tutorial
CorpusHolds the documents the agent can search.
AgentDefines the search tool and instructions for answering questions.
AliasRoutes new sessions to the agent.
Widget connectorHolds presentation and access settings for the embedded UI.

The connector belongs to the alias, not directly to the agent. The embed needs both the alias key and the connector ID.

This example enables anonymous access

Start with the documentation excerpts provided below. Before substituting private documents, review the agent's accessible data and tools and decide whether users must sign in. Allowed origins are not user authentication.

Already have a tested document-backed agent? Complete Before you begin, set AGENT_KEY to its key, choose a new alias key, then skip to Step 3. Review its data and tools before enabling anonymous access, and do not delete reused resources during cleanup. The verification questions below assume the supplied excerpts; use questions with known answers from your own documents for a reused agent.

Before you begin​

You need:

  • A Vectara account and a personal API key for a user with corpus_administrator and agent_administrator API roles, or an account role of administrator or owner. A personal key inherits its user's permissions; a query-only credential is not sufficient for this setup.
  • Bash, curl 7.76.0 or later (for --fail-with-body), and jq.
  • An agent model available to your account. The example names gpt-4o; check GET /v2/llms and select an available model before creating the agent.
  • A website where you can publish an HTML page over HTTPS. Use a lowercase hostname without a custom port. You must know the page's origin, such as https://chat.example.com, and be able to edit the page's HTML.

Use a new working directory and keep the same terminal open for the commands. Only the site/ subdirectory will be published.

Bash
mkdir -p widget-chat-tutorial/site
cd widget-chat-tutorial

API_BASE='https://api.vectara.io/v2'
CORPUS_KEY='widget_docs'
AGENT_KEY='docs-chat'
ALIAS_KEY='docs-chat-web'
MODEL_NAME='gpt-4o'
SITE_ORIGIN='https://chat.example.com'

read -rsp 'Vectara API key: ' VECTARA_API_KEY
printf '\n'

Replace SITE_ORIGIN with an HTTPS origin you control, with no trailing slash or path. For a page at https://chat.example.com/index.html, the origin is https://chat.example.com. The allowed-origin format does not accept http://localhost:5173; use your HTTPS test site for this walkthrough.

The corpus, agent, and alias keys above are example names you choose. If those keys are already in use, choose new ones before running the creation steps. Do not delete an existing resource just to reuse an example name. The connector ID, in contrast, comes from the API response.

Keep the API key in this terminal, not in HTML, browser storage, or a published configuration file. The requests use the documented x-api-key header.

Bash
curl --fail-with-body --silent --show-error \
"$API_BASE/llms" \
-H "x-api-key: $VECTARA_API_KEY"

Check the returned models and change MODEL_NAME if necessary. Run each section in order. If a request fails, read its response and fix the problem before continuing; do not treat a failed creation as a successful setup. Commands with --output save error responses to the named file too.

Step 1. Set up your corpus with sample data​

Create the corpus​

Generate the request from the key you chose, then create the corpus:

Bash
jq -n --arg key "$CORPUS_KEY" '{
key: $key,
name: "Widget documentation excerpts",
description: "Documentation excerpts for the widget connector tutorial"
}' > corpus-request.json

curl --fail-with-body --silent --show-error \
-X POST "$API_BASE/corpora" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @corpus-request.json \
--output corpus.json && jq '{key, name}' corpus.json

Check: a successful create returns 201. The printed key must match CORPUS_KEY. The command prints only the key and name from the response.

Add the documents​

The sample text comes from the following guides. Each document has an explicit ID so you can compare a response with its source during verification.

Document IDSourceContent used here
widget-setupEmbed an agent on your websiteWhere to create the connector and what not to include in the embed.
alias-routingRoute sessions with an aliasAlias purpose and how changing routing weights affects existing sessions.
widget-originsEnd-user sessionsAllowed-origin format, limits, and behavior.

Save these three core documents. Each explicitly supplied document_parts entry is a searchable part. These are selected excerpts, not an automatic import or a synchronized copy of the linked pages.

Bash
cat > documents.json <<'JSON'
[
{
"id": "widget-setup",
"type": "core",
"metadata": {"document_type": "documentation_excerpt"},
"document_parts": [
{"text": "The agent defines behavior, the alias routes new sessions to an agent, and the widget connector holds the widget's presentation and access configuration. Create the connector on the alias, not on the agent's settings page."},
{"text": "The snippet does not contain an operator API key; do not add one to your page."}
]
},
{
"id": "alias-routing",
"type": "core",
"metadata": {"document_type": "documentation_excerpt"},
"document_parts": [
{"text": "An alias is a stable public name whose policy decides which agent owns each new session."},
{"text": "To shift more traffic to the new version, replace the policy again with new weights, for example 50 and 50, then 0 and 100. The change is atomic. Sessions already created keep the agent they resolved to. Only new sessions use the updated weights."}
]
},
{
"id": "widget-origins",
"type": "core",
"metadata": {"document_type": "documentation_excerpt"},
"document_parts": [
{"text": "Use up to 20 unique lowercase HTTPS origins, each at most 255 characters, without ports, paths, or trailing slashes. Wildcards match subdomains at any depth, excluding the apex and origins with ports."},
{"text": "On create, omitted or [] allows any origin. On update, omitted preserves the list; null or [] clears it. Null entries are rejected."},
{"text": "Requests without Origin are allowed, so this setting does not restrict non-browser clients."}
]
}
]
JSON

jq -c '.[]' documents.json | while IFS= read -r document; do
curl --fail-with-body --silent --show-error \
-X POST "$API_BASE/corpora/$CORPUS_KEY/documents?wait_for=searchable" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary "$document" || exit 1
printf '\n'
done

Check: each successful request returns the document's id. Confirm that all three IDs were returned without an error. wait_for=searchable returns success only after the document is indexed and searchable.

Step 2. Create and check the agent​

Give the agent a search tool scoped to your corpus. Define its entry point with steps and first_step_name. The instructions ask it to search the sample documents, identify its sources, and acknowledge missing information.

Bash
jq -n \
--arg agent "$AGENT_KEY" \
--arg corpus "$CORPUS_KEY" \
--arg model "$MODEL_NAME" '{
key: $agent,
name: "Documentation agent",
description: "Agent for searching the indexed widget documentation excerpts",
tool_configurations: {
search_docs: {
type: "corpora_search",
description_template: "Search the indexed documentation excerpts for widget setup, alias routing, and allowed origins.",
query_configuration: {
search: {
corpora: [{corpus_key: $corpus}],
limit: 10,
reranker: {
type: "customer_reranker",
reranker_name: "Rerank_Multilingual_v1"
}
}
}
}
},
model: {name: $model},
first_step_name: "answer",
steps: {
answer: {
instructions: [{
type: "inline",
name: "Documentation response instructions",
template: "Answer questions about the indexed documentation excerpts. Use search_docs before answering. Base answers only on the retrieved text and identify the source document. If the excerpts do not answer the question, state that the indexed material does not contain the answer. Do not invent product behavior, configuration fields, or limits. Keep answers concise and preserve exact identifiers and numeric limits from the source."
}],
output_parser: {type: "default"}
}
}
}' > agent-request.json

curl --fail-with-body --silent --show-error \
-X POST "$API_BASE/agents" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @agent-request.json \
--output agent.json && jq '{key, name, first_step_name}' agent.json

Check: the returned key must match AGENT_KEY, and first_step_name must be answer. The example uses the documented multilingual reranker name, not the deprecated reranker_id field.

Test before adding the website​

Create a diagnostic session, then send a question. This is a terminal check of the agent, not code that belongs in the browser.

Bash
curl --fail-with-body --silent --show-error \
-X POST "$API_BASE/agents/$AGENT_KEY/sessions" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"name":"Documentation agent setup check","tti_minutes":60}' \
--output check-session.json && \
CHECK_SESSION_KEY=$(jq -er '.key' check-session.json)
Bash
curl --fail-with-body --silent --show-error \
-X POST "$API_BASE/agents/$AGENT_KEY/sessions/$CHECK_SESSION_KEY/events" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"type": "input_message",
"messages": [{"type":"text","content":"Where should I create a widget connector?"}],
"stream_response": false
}' \
--output check-response.json && \
jq -r '.events[] | select(.type == "agent_output") | .content' check-response.json

Compare the answer with widget-setup: create the connector on the alias, not on the agent's settings page. Wording can vary. Inspect check-response.json for the search tool's input/output and any error events. A plausible answer alone does not confirm retrieval; check the retrieved text as well. If the answer is missing, incorrect, or unsupported by that text, fix the data, tool, or model configuration before making the agent public.

Web search is optional. This example leaves it out so you can verify the corpus-backed answer first. To add it later, see agent tools and update the instructions to distinguish indexed documentation from external information. Tool selection is model-driven; test it with representative questions rather than assuming a fixed order.

Step 3. Create an alias and configure routing​

Create a separate public handle for the agent. This policy has one catch-all rule and one target: new sessions through this alias route to AGENT_KEY.

Bash
jq -n --arg alias "$ALIAS_KEY" --arg agent "$AGENT_KEY" '{
key: $alias,
name: "Documentation agent website",
policy: {
type: "routed",
rules: [{targets: {type: "single", agent_key: $agent}}]
},
enabled: true
}' > alias-request.json

curl --fail-with-body --silent --show-error \
-X POST "$API_BASE/agent_aliases" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @alias-request.json \
--output alias.json && jq '{key, enabled, policy}' alias.json

Check: the returned key matches ALIAS_KEY, enabled is true, and the single target's agent_key matches your agent. Keep the alias key distinct from the agent key throughout the remaining steps. For more routing options, see Route sessions with an alias.

Step 4. Create the widget connector​

Create the connector on that alias. Keep public_access false while preparing the page; you will explicitly enable anonymous access in Step 5.

Bash
jq -n --arg origin "$SITE_ORIGIN" '{
name: "Documentation agent widget",
enabled: true,
configuration: {
type: "widget",
public_access: false,
allowed_origins: [$origin],
presentation: {version: "beta"}
}
}' > connector-request.json

curl --fail-with-body --silent --show-error \
-X POST "$API_BASE/agent_aliases/$ALIAS_KEY/connectors" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @connector-request.json \
--output connector.json && \
CONNECTOR_ID=$(jq -er '.id' connector.json)

jq '{id, alias_key, enabled, configuration}' connector.json

Check: alias_key matches your alias, allowed_origins contains your actual website origin, and public_access is false. Keep the returned id in CONNECTOR_ID; do not substitute the connector's display name.

The widget uses its default appearance with this minimal presentation object. The current presentation format uses version: "beta". For branding and welcome-message configuration, use the widget guide after the widget answers a question.

An origin restriction is not a login requirement

Omitting allowed_origins, or setting it to an empty list, allows any origin. Requests without an Origin header are admitted by the origin check. Keep an explicit origin for this example, but do not rely on it to identify users or protect private documents.

Step 5. Embed Popup Agent Chat​

Generate a complete HTML page with the alias key and returned connector ID. The unquoted heredoc substitutes your shell variables into the file; the API key is deliberately not referenced.

Bash
cat > site/index.html <<HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Documentation agent</title>
</head>
<body>
<main>
<h1>Documentation agent</h1>
<p>Ask about widget setup, alias routing, and allowed origins.</p>
<p>This example searches three documentation excerpts, not the full documentation.</p>
</main>
<script
type="module"
async
src="https://widgets.vectara.com/v1/popup-agent-chat/popup-agent-chat.js"
data-agent-alias="$ALIAS_KEY"
data-connector-id="$CONNECTOR_ID"
data-api-host="https://api.vectara.io"
></script>
</body>
</html>
HTML

The popup creates its own floating launcher; no mount container or React project is needed. data-api-host is optional and defaults to https://api.vectara.io. It is the API origin, not the /v2 URL used by your terminal commands.

  1. Inspect site/index.html: the two identifiers should be filled in, and it must contain no operator credential.
  2. Upload only site/index.html to the HTTPS website you chose. Open its URL in a browser; for example, use /index.html on your SITE_ORIGIN. Do not open the file with a file:// URL or publish the entire working directory.
  3. If the website has a content security policy, check that it permits the widget module and its shared JavaScript chunks from https://widgets.vectara.com and API connections to https://api.vectara.io. The widget also carries its color palette in an inline <style> element; a style-src policy that blocks inline styles leaves the widget rendering with its colors unset, a styling failure rather than a connection failure. Inspect the browser's policy errors rather than disabling the site's policy wholesale.

Enable anonymous access deliberately​

After reviewing the agent's data and tools and confirming the allowed origin, open this connector to anonymous visitors:

Bash
curl --fail-with-body --silent --show-error \
-X PATCH "$API_BASE/agent_aliases/$ALIAS_KEY/connectors/$CONNECTOR_ID" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"configuration":{"type":"widget","public_access":true}}'

Widget configuration updates preserve omitted fields, so this request keeps the origin list and presentation. Confirm the response shows enabled: true and configuration.public_access: true. Both settings gate anonymous admission. Reload the hosted page for the next step.

Step 6. Verify the embedded agent​

A successful create response does not verify that the embedded widget works. On your actual website:

  1. Confirm the widget script loads and the launcher opens the chat panel.

  2. Ask the questions below. Compare the answers with the indexed excerpts, not with a fixed transcript. Ask the two origin questions consecutively in the same session: the second refers to the origins discussed in the first.

    QuestionWhat to checkSource document
    Where should I create a widget connector?On the alias, not on the agent's settings page.widget-setup
    Should I put my operator API key in the embedded page?No. The answer should not recommend putting the operator key in the page.widget-setup
    How many allowed origins can I configure, and can they include ports?Up to 20 unique lowercase HTTPS origins; ports are not accepted.widget-origins
    And what is the maximum length of each one?Each origin can be at most 255 characters. Check that the agent interprets "each one" as an allowed origin.widget-origins
    If I change routing weights, do existing sessions move to another agent?Existing sessions keep their resolved agent; the updated weights apply to new sessions.alias-routing
    What is the monthly price of the widget?The indexed excerpts do not specify pricing. Check that the answer acknowledges the missing information rather than supplying an unsupported price.Not covered by the excerpts
  3. Inspect the page source and browser requests to confirm that your operator API key has not been included. The widget's end-user credentials are separate from the key used to create these resources.

The instructions request source-grounded answers; they are not a guarantee of correct output. Investigate incorrect answers and retrieval failures before replacing the sample corpus or exposing additional data.

You can also check the configured origin restriction without sending an operator credential. First request the public connector view with the allowed origin:

Bash
curl --fail-with-body --silent --show-error \
"$API_BASE/agent_aliases/$ALIAS_KEY/connectors/$CONNECTOR_ID/public" \
-H "Origin: $SITE_ORIGIN"

Then use an origin that is not in your allowed list. This is an intentional negative check; print its HTTP status as well as the response body:

Bash
curl --silent --show-error --write-out '\nHTTP %{http_code}\n' \
"$API_BASE/agent_aliases/$ALIAS_KEY/connectors/$CONNECTOR_ID/public" \
-H 'Origin: https://not-allowed.example.com'

The origin check rejects an unmatched origin with 403. This tests that restriction only; it does not establish user authentication. Keep the allowed list explicit rather than clearing it to make a failed test pass.

Troubleshooting​

SymptomWhat to check
A provisioning request failsRead the response body. Check x-api-key, the user's roles, resource keys, model availability, and the request for that specific endpoint. Do not continue with identifiers from a failed request.
A resource key is already in useChoose new keys for a separate walkthrough. Do not delete or overwrite an existing resource to reuse a tutorial name.
The terminal agent test failsInspect check-response.json, including search tool output and error events. Resolve the data, model, or tool problem before debugging the website.
The widget script does not loadCheck network and content-security-policy errors for the script and shared chunks. Verify that your host kept type="module" and the script attributes.
The widget remains hiddenThis example starts with public access off. Confirm the deliberate enable step and connector state. For a signed-in application, troubleshoot sign-in instead of enabling anonymous access.
A browser request returns 403Read that response; compare its Origin with the allowed list and check the intended access mode. Not every 403 has the same cause.
The panel opens but a session request failsCheck the failed request, alias routing, agent health/configuration, and the alias key, connector ID, and API origin in the HTML.
Answers do not match the sample factsInspect retrieved content and tool activity. Recheck indexing, the corpus key in the search tool, and instructions; do not treat plausible wording as a successful test.

Before using your own data​

  • Choose the access model first. The anonymous example does not implement end-user sign-in. For identified users, see End-user sessions and the widget sign-in guidance.
  • Review every accessible document and tool. Substituting an internal corpus changes what this publicly accessible agent can retrieve. Adding an external tool also changes what it can do.
  • Review output visibility and session settings. Configure only what your use case needs; use the shared widget/session guides rather than assuming a successful anonymous demo establishes production readiness.
  • Keep provisioning separate from embedding. Operator credentials stay in protected setup tooling, not in the widget script or a public repository.

Clean up​

Remove the script from your website when you no longer need the example. To stop this connector serving its end-user surface, disable it:

Bash
curl --fail-with-body --silent --show-error \
-X PATCH "$API_BASE/agent_aliases/$ALIAS_KEY/connectors/$CONNECTOR_ID" \
-H "x-api-key: $VECTARA_API_KEY" \
-H 'Content-Type: application/json' \
--data '{"enabled":false}'
Delete only resources created for this walkthrough

The following deletions are permanent. Skip any resource you reused or that another application now depends on. Deleting a connector does not delete its alias; deleting an alias does not delete its agent.

Delete the diagnostic session when you are finished inspecting it. Skip this request if you skipped the diagnostic test or the session has already expired.

Bash
curl --fail-with-body --silent --show-error \
-X DELETE "$API_BASE/agents/$AGENT_KEY/sessions/$CHECK_SESSION_KEY" \
-H "x-api-key: $VECTARA_API_KEY"

For a dedicated tutorial setup, delete the connector, alias, agent, and corpus in that order. The commands are joined with && so a failed deletion stops the sequence. If a deletion fails, inspect the response before retrying. Remove only dependencies created for this tutorial, and resume at the failed command rather than repeating successful deletes. Do not remove other applications' resources to force deletion.

Bash
curl --fail-with-body --silent --show-error \
-X DELETE "$API_BASE/agent_aliases/$ALIAS_KEY/connectors/$CONNECTOR_ID" \
-H "x-api-key: $VECTARA_API_KEY" && \
curl --fail-with-body --silent --show-error \
-X DELETE "$API_BASE/agent_aliases/$ALIAS_KEY" \
-H "x-api-key: $VECTARA_API_KEY" && \
curl --fail-with-body --silent --show-error \
-X DELETE "$API_BASE/agents/$AGENT_KEY" \
-H "x-api-key: $VECTARA_API_KEY" && \
curl --fail-with-body --silent --show-error \
-X DELETE "$API_BASE/corpora/$CORPUS_KEY" \
-H "x-api-key: $VECTARA_API_KEY"

Successful deletions return 204 with no response body. For session inspection and removal, see Sessions and the API reference. When cleanup is complete, clear the key from this shell:

Bash
unset VECTARA_API_KEY

Next steps​