External API Integrations
How to let a Kaltura agent call an external REST API: write a support ticket, update a booking system, look up inventory, capture a CRM lead, or call any other HTTP endpoint. Includes the OAuth2 flow for endpoints that need it.
Every integration is an api tool (tools.api()) that the model can call. The same three steps apply to a CRM, a support desk, a booking system or a MAM (media asset management) API.
To forward the viewer's own submitted form data, read Structured Data Forms first. session.submitStructuredDataForm() alone does not give you durable, retrievable data. A tool call the model makes lands the data on infrastructure you control.
The building blocks
An external API integration is a custom api tool linked to your intellect through tool_ids. Three steps, in this order:
- Store the credential as a secret.
mgmt.intellects.secrets.set(configId, {NAME: value}, adminKs). Secrets are write-only. Every read masks values as"***". - Build and register the tool.
tools.api({..., request: {..., headers: {Authorization: 'Bearer {{secrets.NAME}}'}}}), thenmgmt.tools.add(tool, adminKs). A tool is its own partner-level entity, not part of the intellect. - Link it.
mgmt.intellectConfig.setToolIds(configId, [toolId], adminKs).
- Secret references use one exact syntax. Write
{{secrets.<name>}}inside any string field of therequestblock. Do not write{{variables.secrets.<name>}}. The OAuth2client_secretuses the bare formsecrets.<name>(see below). - Validate after wiring.
mgmt.intellects.secrets.validate(configId, adminKs)scans every tool and prompt for secret references. It lists references to secrets that are not stored (unresolved) and the wrong prefix above (badPrefix).
To run your own page-side JavaScript instead of a server-side HTTP call, use a type: "client" tool (tools.client()) and session.onToolCall(name, handler). See Client-Side Commands. Everything below assumes a server-side api tool.
Authenticating the call
tools.api() supports two authentication shapes in its request block:
-
Static bearer token or API key. A secret you mint once (a private-app token, a personal access token, a long-lived key), injected as a header. You own rotation.
request: { headers: { Authorization: 'Bearer {{secrets.API_TOKEN}}' } } -
OAuth2 authorization-code flow. For providers that need viewer consent and issue an expiring, refreshable token. See the next section.
When you need OAuth2 (authorization-code flow)
The authentication block supports the authorization-code flow only. The viewer grants consent through an auth_url your app opens. Pass the block instead of a static bearer header. All four fields are required: client_id, client_secret (a secrets.<name> reference), token_url and auth_url. There is no flow or scopes option.
import { api } from '@kaltura/intelligent-agents/management';
const tool = api({
name: 'update_crm_contact',
description: "Update the user's contact record once you have their email.",
args: { email: { type: 'str', prompt: "The user's email", required: true } },
request: {
url: 'https://api.example.com/v1/contacts',
method: 'POST',
authentication: {
type: 'oauth2',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'secrets.EXAMPLE_CLIENT_SECRET',
token_url: 'https://auth.example.com/oauth/token',
auth_url: 'https://auth.example.com/oauth/authorize',
},
body: { email: '{email}' },
},
responseMapping: { result: 'result' },
});
The SDK validates the block before any network call. type is optional, and must be 'oauth2' if set. Unknown keys are rejected. token_url and auth_url must be http(s) URLs. client_secret must match /^secrets\.[A-Za-z_][A-Za-z0-9_]*$/. A plaintext secret is rejected, so it never lands in a tool config.
What to build for:
- First call, no token yet: handle the consent redirect. The call comes back as an
interruptionstream segment (metadata.subtype:"oauth_required") with anauth_url. Parse it withparseOAuthRequiredorsession.onOAuthRequired. See Wire Protocol · Events Catalog for the shape. Open the URL in a new tab so the viewer can finish the provider's consent screen. MCP servers behind OAuth use the same mechanism: MCP Integrations. - After consent, later calls just work. The viewer is not asked again.
- Refresh is automatic. An expired token is refreshed with no redirect. Only when the refresh fails does another
interruptionsend the viewer back through consent. Do not assume a fixed validity window. Treat every call as one that can return a freshauth_url.
With a static bearer token you own rotation but never show a consent link. With OAuth2 the platform handles consent and refresh, and your app must handle the interruption segment.
Don't skip kaltura_genie_experiences: 'off'
Any intellect that references tool_ids (an external-API tool is no exception) must set capabilities: {kaltura_genie_experiences: 'off'} at creation time.
The default-on capability adds an experiences instruction. It competes with your tool for the same decision about what to do with a turn. mgmt.tools.clientToolReadiness(body) is a pure lint you can run over a create or update body. It warns when tools are referenced but the capability is not explicitly off. intellects.create() and intellects.update() run this lint and log its warnings.
Set the capability at creation. Flipping it on an existing intellect does not apply right away.
Verifying the wiring
Run both checks after setup and before you rely on the integration:
mgmt.intellects.secrets.validate(configId, adminKs)flags unresolved references and thebadPrefixmistake.- A live test conversation where you supply the field values yourself. Confirm the tool fires (
collectConverse(...).toolCallsheadless, or thetype:"tool"segment on a live session) and that the target API shows the expected write. A tool config can validate and still fail at the HTTP layer: wrong URL, expired token, wrong field names.
Example: CRM / marketing-automation integration
The SDK ships three builders for the common CRM writes. They are pure config builders. They make no network call and hold no secret value. Each throws a typed error for a missing secretName or instanceUrl before anything is written.
| Builder | CRM object | What the call does |
|---|---|---|
salesforceLeadUpsert |
Lead | Upsert on a key (default Email). LastName and Company are required. |
salesforceContactUpsert |
Contact | Upsert on a key (default Email). |
hubspotContactUpsert |
Contact | Create only. HubSpot rejects the call with a conflict if the email exists. |
| Your situation | Use |
|---|---|
| Visitor is a new prospect and sales works Leads | salesforceLeadUpsert |
| The person is already an account contact, or your org has no Lead workflow | salesforceContactUpsert |
| HubSpot | hubspotContactUpsert, new contacts only |
HubSpot
hubspotContactUpsert() creates a contact with a HubSpot private-app token sent as a static bearer token. These tokens are long-lived, so no OAuth2 flow is needed. It is a create, not an upsert. Use it for new-lead capture, not to update a contact.
import { hubspotContactUpsert } from '@kaltura/intelligent-agents/management';
await mgmt.intellects.secrets.set(configId, { HUBSPOT_TOKEN: process.env.HUBSPOT_TOKEN }, adminKs);
const tool = hubspotContactUpsert({
secretName: 'HUBSPOT_TOKEN',
propertiesToCapture: ['email', 'firstname', 'lastname'],
});
const { id } = await mgmt.tools.add(tool, adminKs);
await mgmt.intellectConfig.setToolIds(configId, [id], adminKs);
Every propertiesToCapture entry becomes a tool argument and a field in the request body. Only email is required. On success the tool returns contact_id.
Salesforce
import { salesforceLeadUpsert } from '@kaltura/intelligent-agents/management';
const tool = salesforceLeadUpsert({
secretName: 'SF_TOKEN',
instanceUrl: 'https://yourorg.my.salesforce.com',
});
await mgmt.intellects.secrets.set(configId, { SF_TOKEN: accessToken }, adminKs);
const { id } = await mgmt.tools.add(tool, adminKs);
await mgmt.intellectConfig.setToolIds(configId, [id], adminKs);
instanceUrl must be an https: origin with no path, query or credentials. The table lists the Lead defaults. salesforceContactUpsert takes externalIdField and fieldsToCapture (default Email, FirstName, LastName) and has no consent arg, LeadSource or Description.
| Item | Default | Option |
|---|---|---|
| Captured fields | FirstName, LastName, Company, Email, Phone, Country |
fieldsToCapture (must keep LastName, Company and the key) |
| Upsert key | Email |
externalIdField (a custom External ID field needs field access for the integration user) |
LeadSource |
Web |
leadSource (must be a valid value in your org) |
Description |
Captured by a Kaltura AI agent. Thread: <thread id>. Consent to be contacted: <answer>. |
not configurable |
| Consent | A required boolean consent arg. The agent calls the tool only after the visitor agrees. |
requireConsent: false |
| API version | v68.0 |
apiVersion |
Salesforce has no standard Lead consent field, so consent is recorded in Description. Country takes the full name, such as United States. An org with country picklists rejects a code such as US with FIELD_INTEGRITY_EXCEPTION.
What the agent can say
The tool maps result (the record id) and success. The default description tells the agent what to say.
| Salesforce answer | Tool result | Agent says |
|---|---|---|
201 with an id (new record) |
success: true, result is the id |
Saved |
200 with an id (existing record) |
success: true, result is the id |
Saved |
Any error status (missing field, invalid email, expired token, 300) |
No mapped values. The agent gets only the generic error line. | Could not save |
An empty result is never treated as saved. A custom description replaces the default guidance, so keep these rules in it.
If two or more Leads share the email, an Email-keyed upsert is ambiguous. Salesforce answers 300 and changes nothing. Merge or delete the duplicates.
Two argument rules apply to every api tool:
- Percent-encode any value you put in a URL (
@as%40,+as%2B,/as%2F). The recipe prompts already tell the agent to do this for the upsert key. - Mark an arg
requiredwhen the call cannot work without it. The recipe prompts ask the agent to send an empty string for optional args. Do the same in your own tools.
Placeholder rules for args: Tools and Secrets. Headers take {{secrets.NAME}} and request variables such as {{ sys__thread_id }}.
Access tokens
These builders use a static secret, not the OAuth2 block. A Salesforce access token expires, and after that Salesforce answers 401 and the agent reports a failed save. Pick one option:
| Option | How | Use when |
|---|---|---|
| Customer endpoint (recommended) | The agent calls your own HTTPS endpoint. It holds the Salesforce credentials, refreshes the token and does the upsert. The tool holds only a long-lived secret for your endpoint. | Production. Kaltura never holds a Salesforce token. |
| Scheduled refresh | Your server refreshes the token and calls mgmt.intellects.secrets.set(...) again before it expires. The SDK does not ship this job. |
You want the agent to call Salesforce directly and can run the refresh well inside the token lifetime. |
The direct tools put the visitor's email into the request URL, and the visitor controls that text. Use them for a quick start and for dev orgs. In production, your endpoint validates and encodes the email before it calls Salesforce.
A customer endpoint is a plain api tool. It takes the fields as JSON and answers 200 with { "saved": true, "lead_id": "..." } or { "saved": false }. A 4xx or 5xx reaches the agent only as the generic error line, so use 200 with saved: false for failures the agent should handle:
import { tools } from '@kaltura/intelligent-agents/management';
const tool = tools.api({
name: 'save_lead',
description: 'Save the visitor as a sales lead after they agree to be contacted. Say it is saved ONLY if the result has saved true. Otherwise say you could not save it.',
args: {
LastName: { type: 'str', prompt: 'Last name', required: true },
Company: { type: 'str', prompt: 'Company', required: true },
Email: { type: 'str', prompt: 'Email', required: true },
},
request: {
url: 'https://your-server.example.com/leads',
method: 'POST',
headers: { Authorization: 'Bearer {{secrets.LEADS_ENDPOINT_TOKEN}}', 'Content-Type': 'application/json' },
body: { LastName: '{LastName}', Company: '{Company}', Email: '{Email}', threadId: '{{ sys__thread_id }}' },
},
responseMapping: { saved: 'saved', lead_id: 'lead_id' },
});
Your endpoint must authenticate every request itself. See the security note in Agent Components · Tools and Secrets.
Marketo: two integration paths
- Forms submission with no token (Munchkin). Marketo's embeddable forms submit leads through a public POST endpoint tied to a Munchkin account ID. To capture a lead only, build a
clienttool (tools.client()). Yoursession.onToolCallhandler does thefetch()to that endpoint. No secret is needed. - REST API (updating a lead, triggering a campaign). Marketo uses a client-credentials token, which the
authenticationblock does not support. Use one of these:- Bearer secret. Get a token from Marketo's
identity/oauth/tokenendpoint, store it withmgmt.intellects.secrets.set(), and sendAuthorization: 'Bearer {{secrets.MARKETO_TOKEN}}'from anapitool. You refresh the secret before it expires. - Client tool. The handler in
session.onToolCallcalls your own server, which holds the Marketo credentials.
- Bearer secret. Get a token from Marketo's
Use Munchkin to get a lead into Marketo with no secret management. Use the REST API when the model must do more than submit a form.
Other targets
Each is a plain api tool with the same three steps:
- Airtable. A personal access token as a
Bearerheader.POSTtohttps://api.airtable.com/v0/{baseId}/{tableName}withbody: {fields: {...}}. DeclarebaseIdandtableNameas tool args, because the tool fills{baseId}and{tableName}in the url from declared args. - Google Sheets. The Sheets API requires OAuth2. Viewer consent fits the
authenticationblock. A service account does not. - Google Forms. Forms has no lead-write REST endpoint. Use a
clienttool that opens a pre-filled Forms URL (viewform?usp=pp_url&entry.<id>=<value>) for the viewer. This is a UX handoff, not a server-side write. - Any other REST API. A static secret in the
Authorizationheader, or the OAuth2 block for the authorization-code flow.
Related docs
| Doc | What it adds |
|---|---|
| Structured Data Forms | Collecting the values you forward with these tools |
| Dynamic Data Injection | Feeding data into the conversation, the opposite direction |
| Client-Side Commands | Driving your UI from the agent with a client-side tool |
| MCP Integrations | Wiring a whole MCP server's tools at once |