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.md first — it explains why session.submitStructuredDataForm() alone does not get you durable, retrievable data with this toolkit's credentials, and 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 secretmgmt.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 tooltools.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 itmgmt.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-COMMANDS.md 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), and — 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, which a static bearer token never requires.

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. 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, because the default-on capability injects a "you MUST call get_experience_instructions" instruction that out-competes your tool for the same "what do I do with this turn" decision. 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 (MAM) 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 upsert 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):

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.md Collecting the values this doc shows you how to forward durably
DYNAMIC-DATA-INJECTION.md Feeding data into the conversation, the opposite direction from this doc
CLIENT-COMMANDS.md The avatar-driving-your-UI channel — a client-side, not server-side, mechanism
Click to talk with Nova — she knows this whole SDK.