How-to Guide

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:

  1. Store the credential as a secret. mgmt.intellects.secrets.set(configId, {NAME: value}, adminKs). Secrets are write-only. Every read masks values as "***".
  2. Build and register the tool. tools.api({..., request: {..., headers: {Authorization: 'Bearer {{secrets.NAME}}'}}}), then mgmt.tools.add(tool, adminKs). A tool is its own partner-level entity, not part of the intellect.
  3. Link it. mgmt.intellectConfig.setToolIds(configId, [toolId], adminKs).

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:

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:

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:

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:

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

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:

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
Click to talk with Nova — she knows this whole SDK.
Nova AI assistant — knows this whole site

Nova is an AI assistant and can make mistakes. Check important details in the docs. Reloading starts a fresh chat.