How-to Guide

External API Integrations

How to wire a Kaltura agent to call out to an external REST API — write a support ticket, update a booking system, look up inventory, upsert a CRM contact, or call anything else with an HTTP endpoint — including the real, backend-verified OAuth2 flow for endpoints that require it.

This is a general integration mechanism: any api tool (src/management/tools.js's tools.api()) the model can call, wired to whatever HTTP endpoint you point it at. CRM/marketing writes (HubSpot, Salesforce, Marketo) are one common use case, and get their own example section below. But the same three-step pattern applies equally to a support desk, a booking system, a MAM (media asset management) API, an inventory lookup, or any other REST integration.

If your use case is specifically getting the viewer's own submitted data (from a user_properties_forms prompt) onto external infrastructure, read Structured Data Forms first. It explains why session.submitStructuredDataForm() alone does not get you durable, retrievable data with this toolkit's credentials. Everything here is the alternative: a tool call the model makes directly, landing on infrastructure you control.

The building blocks

An external API integration is a custom api tool, linked to your intellect via tool_ids. Three pieces, always in this order:

  1. Store the credential as a secret — mgmt.intellects.secrets.set(configId, {NAME: value}, adminKs) (src/management/secrets.js). Secrets are write-only: every read masks values as "***", and there is no endpoint to read a plaintext value back. This is a genuine backend guarantee (server-encrypted at rest), not something the SDK layers on top.
  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 (/v1/tool/*), not embedded in the intellect.
  3. Link it — mgmt.intellectConfig.setToolIds(configId, [toolId], adminKs).

If instead you want the model to trigger your own page-side JS rather than a server-side HTTP call — e.g. push data into a client SDK already loaded in the browser — use a type: "client" tool (tools.client()) and session.onToolCall(name, handler) instead. See Client-Side Commands for that path; everything below assumes a server-side api tool.

Authenticating the call

Most external APIs need one of two authentication shapes, both supported directly by tools.api()'s request block:

When you actually need OAuth2 — the real, backend-managed flow

If your target API requires a proper three-legged OAuth2 authorization-code flow (a viewer must grant consent; the resulting token expires and needs refreshing), the platform has that — it's implemented, real, and lives entirely on the backend. Pass an authentication block instead of a static bearer header in an api tool's request:

import { api } from '@kaltura/intelligent-agents/management';

const tool = api({
  name: 'update_marketo_lead',
  description: "Update the user's lead record in Marketo once you have their email.",
  args: { email: { type: 'str', prompt: "The user's email", required: true } },
  request: {
    url: 'https://123-ABC-456.mktorest.com/rest/v1/leads.json',
    method: 'POST',
    authentication: {
      type: 'oauth2',
      client_id: 'YOUR_MARKETO_CLIENT_ID',
      client_secret: 'secrets.MARKETO_CLIENT_SECRET',
      token_url: 'https://123-ABC-456.mktorest.com/identity/oauth/token',
      auth_url: 'https://123-ABC-456.mktorest.com/identity/oauth/authorize',
    },
    body: { action: 'updateOnly', input: [{ email: '{{args.email}}' }] },
  },
  responseMapping: { result: 'result' },
});

buildAuth() (src/management/tools.js) validates this block before any network call. type must be 'oauth2' (the only scheme the backend supports today). The one hard rule: client_secret must be a secrets.<name> reference matching /^secrets\.[A-Za-z_][A-Za-z0-9_]*$/. A plaintext secret is rejected by construction, so there's no path for it to leak into a tool config at rest.

This is a genuine authorization-code exchange, not a pre-minted static token wearing an OAuth label. Here's what to build for and expect:

Unlike a static-bearer-token tool (where you own token rotation), a tool wired through authentication: {type: 'oauth2', ...} gets consent and refresh handled for you by the platform. The tradeoff is the interruption/consent UX: your app has to handle the interruption segment and show the viewer a link. A static bearer token never requires that.

Don't skip kaltura_genie_experiences: 'off'

Any intellect that references tool_ids (an external-API tool is no exception) should set capabilities: {kaltura_genie_experiences: 'off'} at creation time.

Here's why. The default-on capability injects a "you MUST call get_experience_instructions" instruction. That instruction out-competes your tool for the same "what do I do with this turn" decision. mgmt.tools.clientToolReadiness(body) (src/management/tools.js) is a pure lint you can run over your create/update body before sending it: it warns when tools are referenced but this capability isn't explicitly off. intellects.create() and intellects.update() already run this lint automatically and log its warnings.

But the fix (setting the capability) only takes effect immediately at creation. Flipping it on an existing intellect is defeated by the ~24h partner-config cache.

Verifying the wiring before you rely on it

Two read-only checks, both worth running after setup and before believing an integration works:

Example: CRM / marketing-automation integration

A CRM or marketing-automation write is a routine instance of the pattern above: the same secret → tool → link steps, pointed at a CRM's contact-upsert endpoint. The SDK ships two ready-made builders for the most common cases.

HubSpot

hubspotContactUpsert() (src/management/crm-recipes.js) wraps HubSpot's Contacts v3 create endpoint (POST /crm/v3/objects/contacts) with a static bearer token (a HubSpot private-app token, not an OAuth2 flow — HubSpot's private-app tokens are long-lived and don't need refresh). Despite the name, this is a create, not a true upsert: HubSpot rejects the call with a conflict if a contact with the same email already exists. Use it for new-lead capture, not for updating an existing 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);

This is a pure config builder. No network call happens inside hubspotContactUpsert() itself; it just assembles and validates the GenieToolConfig that mgmt.tools.add() then registers. Every propertiesToCapture entry becomes both a tool argument ({prompt: "Contact <prop>", type: 'str', required: prop === 'email'}) and a field in the outgoing properties body. The model fills them from the conversation and calls the tool. The server executes the actual HTTP request.

Salesforce

salesforceContactUpsert() (same file) wraps Salesforce's REST sobjects upsert-by-external-ID endpoint (PATCH {instanceUrl}/services/data/v59.0/sobjects/Contact/{externalIdField}/{value}), again using a static bearer token in the Authorization header:

const tool = salesforceContactUpsert({
  secretName: 'SF_TOKEN',
  instanceUrl: 'https://yourorg.my.salesforce.com',
  externalIdField: 'Email',
  fieldsToCapture: ['Email', 'FirstName', 'LastName'],
});

One real Salesforce quirk this builder accounts for: an upsert-by-external-ID PATCH returns 201 {id: ...} on insert but 204 with an empty body on update. There's no field guaranteed present on both, so its responseMapping only maps result: 'id' (present when it exists) rather than assuming a shape that breaks on the update path. The point of this tool is the side effect (the contact write), not what it echoes back.

This builder authenticates with a static secret, exactly like the HubSpot one — it does not use the OAuth2 authentication block described above. That's fine for a Salesforce Connected App access token you mint and rotate yourself, but it does mean you are responsible for refreshing that token before it expires; the platform won't refresh it for you unless you route through the real OAuth2 flow instead.

Marketo — two valid integration paths

Marketo supports both connection models, and which one fits depends on how much you're allowed to ask of the visitor's session:

Pick the first path when you just need "get this lead into Marketo" and want zero secret management; reach for the second only when the model needs to do more than a one-shot form submission.

Other DIY CRM/MAM/spreadsheet targets

None of these need a dedicated recipe — they're a plain api tool with a static bearer/API-key secret, following the exact same three-step pattern as HubSpot/Salesforce above:

Doc What it adds
Structured Data Forms Collecting the values this doc shows you how to forward durably
Dynamic Data Injection Feeding data into the conversation, the opposite direction from this doc
Client-Side Commands The avatar-driving-your-UI channel — a client-side, not server-side, mechanism
Click to talk with Nova — she knows this whole SDK.
Nova AI assistant — knows this whole site

Reloading starts a fresh chat. “New conversation” does the same without leaving the drawer.