Tutorials
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.
| Resource | Its job in this tutorial |
|---|---|
| Corpus | Holds the documents the agent can search. |
| Agent | Defines the search tool and instructions for answering questions. |
| Alias | Routes new sessions to the agent. |
| Widget connector | Holds 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.
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_administratorandagent_administratorAPI roles, or an account role ofadministratororowner. A personal key inherits its user's permissions; a query-only credential is not sufficient for this setup. - Bash,
curl7.76.0 or later (for--fail-with-body), andjq. - An agent model available to your account. The example names
gpt-4o; checkGET /v2/llmsand 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.
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.
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:
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 ID | Source | Content used here |
|---|---|---|
widget-setup | Embed an agent on your website | Where to create the connector and what not to include in the embed. |
alias-routing | Route sessions with an alias | Alias purpose and how changing routing weights affects existing sessions. |
widget-origins | End-user sessions | Allowed-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.
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.
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.
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)
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.
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.
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.
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.
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.
- Inspect
site/index.html: the two identifiers should be filled in, and it must contain no operator credential. - Upload only
site/index.htmlto the HTTPS website you chose. Open its URL in a browser; for example, use/index.htmlon yourSITE_ORIGIN. Do not open the file with afile://URL or publish the entire working directory. - If the website has a content security policy, check that it permits the
widget module and its shared JavaScript chunks from
https://widgets.vectara.comand API connections tohttps://api.vectara.io. The widget also carries its color palette in an inline<style>element; astyle-srcpolicy 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:
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:
-
Confirm the widget script loads and the launcher opens the chat panel.
-
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.
Question What to check Source document Where should I create a widget connector? On the alias, not on the agent's settings page. widget-setupShould I put my operator API key in the embedded page? No. The answer should not recommend putting the operator key in the page. widget-setupHow many allowed origins can I configure, and can they include ports? Up to 20 unique lowercase HTTPS origins; ports are not accepted. widget-originsAnd 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-originsIf 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-routingWhat 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 -
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:
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:
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
| Symptom | What to check |
|---|---|
| A provisioning request fails | Read 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 use | Choose new keys for a separate walkthrough. Do not delete or overwrite an existing resource to reuse a tutorial name. |
| The terminal agent test fails | Inspect 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 load | Check 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 hidden | This 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 403 | Read 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 fails | Check 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 facts | Inspect 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:
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}'
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.
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.
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:
unset VECTARA_API_KEY