@interhumanai/sdk
v0.15.0
Published
Official TypeScript SDK for the Interhuman API (auth, upload analysis, live stream analysis, and realtime multi-track analysis).
Maintainers
Readme
Interhuman TypeScript SDK
Official TypeScript/JavaScript SDK for the Interhuman API. It wraps the public API surfaces behind a small, typed client:
- Auth — exchange API-key credentials for a short-lived access token (
POST /v1/auth), or mint/revoke capped client tokens for direct browser use (POST /v1/client_tokens). - Upload — analyze a complete video file (
POST /v1/upload/analyze). - Stream — analyze a live feed over a WebSocket (
WS /v1/stream/analyze, orWS /v2/stream/analyzefor the Inter-2 model). - Realtime — analyze a live feed with multi-track analysis and periodic
recommendations (
WS /v0/realtime/analyze).
The SDK handles the token exchange, multipart upload, and the WebSocket envelope protocol for you, so you give it your API key once and make a single call per surface.
This SDK targets the API exactly as it ships today. The realtime endpoint ships under the
/v0path, which may change when it graduates to v1, and requires theinterhumanai.realtimescope.
Installation
npm install @interhumanai/sdkThe package ships both ESM and CommonJS builds with TypeScript declarations.
Supported runtimes
| Surface | Requirement |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Auth, Upload | A global fetch, FormData, and Blob — Node.js ≥ 18 or any modern browser. On older Node, pass a fetch implementation via the fetch option. |
| Stream, Realtime | A WebSocket. Node.js ≥ 22 (global WebSocket) or any modern browser. On older Node, pass a webSocket factory (e.g. backed by the ws package). |
The SDK has no runtime dependencies.
Quickstart
import { InterhumanClient, IncludeFlag } from "@interhumanai/sdk";
const client = new InterhumanClient({
credentials: {
keyId: process.env.INTERHUMAN_KEY_ID!,
keySecret: process.env.INTERHUMAN_KEY_SECRET!,
},
// environment: "staging", // defaults to "production"
});
// Upload a finished file
const report = await client.upload.analyze({
file: { data: bytes, filename: "clip.mp4", contentType: "video/mp4" },
include: [IncludeFlag.ConversationQualityOverall],
});
console.log(report.signals, report.conversation_quality?.overall);InterhumanClient exchanges your credentials at /v1/auth and refreshes the
resulting token automatically before it expires. If you already have a bearer
token — including a direct API key, which the API accepts as a bearer token on
every request — pass accessToken instead of credentials:
const client = new InterhumanClient({ accessToken: process.env.IH_API_KEY! });Authentication
Get an API key (keyId + keySecret) from the Interhuman customer platform.
You can let the client manage tokens for you (recommended), or mint one
yourself:
import { AuthClient, Scope } from "@interhumanai/sdk";
const auth = new AuthClient();
const token = await auth.createToken({
keyId,
keySecret,
scopes: [Scope.Upload, Scope.Stream],
});
// token.access_token, token.expires_in (≈900s), token.scopeScopes:
Scope.Upload(interhumanai.upload) — required for upload.Scope.Stream(interhumanai.stream) — required for stream.Scope.Realtime(interhumanai.realtime) — required for realtime.
Each scope grants only its own endpoint, so request every scope the client needs — holding one never implies another.
Client tokens (browser use)
To let a browser call the upload, stream, or realtime API without shipping your API key, mint a short-lived, capped client token on your backend and hand only that token to the browser:
const token = await auth.createClientToken({
apiKey: process.env.INTERHUMAN_API_KEY!,
scopes: [Scope.Stream], // optional; defaults to stream
expiresIn: 300, // optional; clamped to 60–3600s
maxConcurrent: 1, // one session at a time (the default)
maxVideoSeconds: 600, // total video budget across upload/stream/realtime
allowedOrigins: ["https://app.example.com"],
});
// token.access_token — safe to hand to the browser; the API enforces the caps.The token grants the requested scopes (default stream), lives 60–3600s, and carries per-token caps (duration, video bytes, concurrency, video-second budget, origin allow-list) that the API enforces across upload, stream, and realtime (the video budget spans all three). Cut a token off early by revoking it — after which it is rejected on new requests and any live session it opened is closed:
await auth.revokeClientToken({
apiKey: process.env.INTERHUMAN_API_KEY!,
token: token.access_token,
});There is no "extend a token" call — mint a new one when a token nears expiry or its video budget runs low.
Upload API
const report = await client.upload.analyze({
file: { data: bytes, filename: "clip.mp4", contentType: "video/mp4" },
include: ["conversation_quality_overall", "conversation_quality_timeline"],
goalDimensions: ["clarity", "energy"], // triggers interaction feedback
// conversationContext: "Sales discovery call", // alternative feedback driver
});fileaccepts aBlob/Fileor{ data, filename?, contentType? }wheredatais aUint8Array,ArrayBuffer, orBlob.- Formats: mp4, avi, mov, mkv, mpeg-ts, webm. The clip must be ≥ 3 seconds and the upload ≤ 32 MB.
- The response is an
AnalysisResultwithsignals,engagement_state, and optionalconversation_quality/feedback. - Every
Signalcarries amodalitynaming the analysis modalities that detected it. When several tracks detect the same signal, every contributing modality is included, e.g.["audio", "visual"].
Stream API
const stream = client.stream();
stream.on("signal.detected", (e) => console.log(e.data.signal_type, e.data.start));
stream.on("signal.updated", (e) => console.log(e.data.signal_type, e.data.probability));
stream.on("signal.ended", (e) => console.log(e.data.signal_type, e.data.end));
stream.on("engagement.updated", (e) => console.log(e.data.state));
stream.on("conversation_quality.updated", (e) => console.log(e.data.overall));
stream.on("coverage.dropped", (e) => console.warn(e.data.ranges));
stream.on("coverage.degraded", (e) => console.warn(e.data.ranges, e.data.reason));
stream.on("error", (e) => console.error(e.data.code, e.data.message));
stream.on("session.ended", (e) => console.log("analysis closed:", e.data.reason));
stream.on("close", (info) => console.log(info.code, info.reason));
await stream.connect();
await stream.waitForSessionReady();
// Optional per-session settings; the last config sent wins.
stream.updateConfig({ include: ["conversation_quality_overall"], goal_dimensions: ["clarity"] });
// Send WebM or fragmented-MP4 video chunks as they are recorded (each ≤ 32 MB).
stream.sendVideo(chunk); // Uint8Array | ArrayBuffer | Blob
// Graceful shutdown: the server drains the accepted video, ends still-active
// signals, sends session.ended, and closes the socket itself (code 1000).
stream.requestClose();- Authentication uses the
Sec-WebSocket-Protocol: access_token, <token>subprotocol pair. This is the portable method that works in browsers (where the standardWebSocketconstructor cannot set anAuthorizationheader) and in Node. - Server→client frames are typed envelopes
{ type, timestamp, correlation_id, data }.on(type, …)is fully typed per event;on("message", …)receives every envelope as theStreamEventunion. - Client→server: video is sent as binary frames; session config as a JSON text frame.
session.ready(available viawaitForSessionReady()) reports the session's limits: idle timeout (5 min default), max duration (1 hour default), and the min/max chunk size.waitForSessionReady()rejects with anInterhumanErrorif the connection closes before the session becomes ready — for example when the server accepts the handshake, sends anerrorenvelope, and closes — so awaiting it never hangs on a refused session.requestClose()starts the graceful shutdown handshake: the server replies withsession.closing(itsmax_drain_secondsis the longest you should wait), rejects any further video, finishes analyzing what it already accepted, emitssignal.endedfor still-active signals, sendssession.ended, and closes the socket.close()remains the immediate, non-draining teardown.
Choosing the model: v1 and v2
client.stream() opens WS /v1/stream/analyze, analyzed by the Inter-1
model. Pass { apiVersion: "v2" } to open WS /v2/stream/analyze and have
the same session analyzed by the Inter-2 model:
const stream = client.stream({ apiVersion: "v2" });Everything else is identical — the scope, the session.ready limits, session
config, the video you send, the events you receive and their order, and the
graceful close — so the handlers and types above work unchanged. The one
difference is how the client names itself: errors raised for a v2 session say
"Inter-2 stream" where a v1 session says "Stream", so a message names the
endpoint the session actually opened. If a
deployment has no Inter-2 backend configured, a v2 session is refused right
after the handshake with an error envelope (ih1003) and close code 1013.
Limitations
- The stream protocol accepts WebM and fragmented MP4 chunks — the two
shapes browsers produce via
MediaRecorder(video/webmon most browsers,video/mp4on Safari). A server-side pipeline must encode to one of the two. Clips cut at arbitrary byte offsets mid-structure are only supported for WebM. coverage.droppedis informational, not an error: under heavy load some media is skipped (and not billed) while the session continues.coverage.degradedis likewise informational: the listed ranges were analyzed (with their audio) and are billed normally, but their windows decoded materially less video than they span — typically a screen share or other long-GOP stream whose keyframes are sparser than the analysis window.- An abruptly disconnected session does not emit a trailing
signal.endedfor still-active signals — treatcloseas ending them at the last emitted time. End the session withrequestClose()to receive them explicitly.
Realtime API
The Realtime API is a sibling of the Stream API:
same ingest (send video chunks, receive signal.* events), plus multi-track
analysis, a transcript channel in both directions, and periodic written
guidance (recommendations). client.realtime() mirrors client.stream():
const realtime = client.realtime();
realtime.on("signal.detected", (e) => console.log(e.data.signal_type, e.data.start));
realtime.on("realtime_recommendation.generated", (e) => console.log("guidance:", e.data.text));
realtime.on("error", (e) => console.error(e.data.code, e.data.message));
await realtime.connect();
const ready = await realtime.waitForSessionReady();
console.log(ready.data.supported_session_config_options.analysis_groups);
// Recommendations are opt-in: the instructions are the switch that enables them.
realtime.updateConfig({
analysis_groups: ["audio", "visual"], // subset of what session.ready advertises
realtime_recommendation_instructions: "Goal: help me close a sales call.",
realtime_recommendation_frequency: "medium", // high|medium|low = every 10/20/30s of analyzed video
});
// Optionally supply your own transcript; it feeds the recommendation prompt.
realtime.sendTranscript([{ start: 0.4, end: 2.3, text: "Yeah, exactly.", speaker: 0 }]);
realtime.sendVideo(chunk); // same binary chunks as the stream client
// Graceful shutdown, same handshake as the stream client: the server drains
// the accepted video and any in-flight recommendations, sends session.ended, and
// closes the socket itself (code 1000).
realtime.requestClose();Notes on the realtime client:
- The session config takes
analysis_groups,realtime_recommendation_instructions, andrealtime_recommendation_frequency. The instructions shape the guidance's goal, domain, and tone. realtime_recommendation.generatedcarries the periodic guidance.- The
visualanalysis group reports its negative signal as"tension"and theaudiogroup reports"frustration". They describe the same state seen versus heard, and both can be active at once, each with its own lifecycle, so code that branches onsignal_typeshould handle both. sendTranscript(segments)sends atranscript.updatedframe; the most recent transcript replaces any prior one. Text frames — config, transcript,session.close— remain accepted while a graceful close drains, and a transcript sent during the drain still feeds recommendation runs started by draining windows.- Scope and path: the route is
WS /v0/realtime/analyze(not v1) and requires tokens holdingScope.Realtime, so include it in the scopes the client requests alongside the ones you still use — a token grants exactly what was requested, soscopes: [Scope.Realtime]alone would close offupload()andstream(). Expect the path to change when the endpoint graduates to v1.
Errors
- HTTP failures throw
InterhumanApiErrorcarryingstatus,errorId(e.g."ih2001"),correlationId,link, and the rawbody. - Misconfiguration throws
InterhumanConfigErrorbefore any network call. - Stream
errorenvelopes arrive on the"error"event; transport-level socket failures arrive on"socketError".
See the error handling reference.
SDK attribution
Every request the SDK makes tells the API which SDK sent it, so Interhuman can report SDK adoption and spot clients stuck on an old release:
- HTTP requests carry
X-Interhuman-SDK: typescript/<version>. - Stream and realtime WebSocket handshakes carry
ih_sdk=typescript&ih_sdk_version=<version>in the URL, because a browserWebSocketcannot set custom handshake headers. TheSec-WebSocket-Protocol: access_token, <credential>authentication contract is unchanged, and any query parameters yourbaseUrlalready had are kept. (Releases before 0.15.0 sent the same values assdk/sdk_version; the API accepts both spellings.)
The version is read from the package's own package.json, so it always matches
the release you installed. Nothing else is sent — no device, OS, runtime,
hostname, application name, or end-user identifier — and because any caller can
send the same values, the API treats the metadata as a self-declared hint that
never affects authentication, authorization, quotas, or billing. Servers that
predate it ignore it.
SDK_NAME, SDK_VERSION, SDK_HEADER_NAME, and sdkHeaderValue() are
exported if you want to inspect exactly what is sent.
Environments
new InterhumanClient({ credentials, environment: "production" }); // api.interhuman.ai (default)
new InterhumanClient({ credentials, environment: "staging" }); // staging-api.interhuman.ai
new InterhumanClient({ credentials, baseUrl: "http://localhost:8080" }); // local devExamples
Runnable examples live in examples/: auth.ts,
client-tokens.ts, upload.ts, stream.ts, realtime.ts.
Development
npm install
npm run typecheck
npm test
npm run build # emits dist/ (ESM + CJS + .d.ts)The build and test toolchain needs Node.js ≥ 20.19 or ≥ 22.12 (Vite's floor); CI runs Node 22. That is a contributor prerequisite only — the published package still supports the runtimes in Supported runtimes above.
The unit tests live in the repository's top-level test tree at
tests/sdk/typescript/ (mirroring this package's path), while the runner
config stays here — run them with npm test from this directory.
API reference docs
The reference documentation on docs.interhuman.ai
is generated from this package's TSDoc comments — do not hand-write it. Edit the
comments in src/, then regenerate:
npm run docs # TypeDoc → Mintlify MDX in docs-dist/mintlify/On every push to main that touches the SDK, the Sync Docs to interhuman-docs
workflow regenerates this page and opens a PR against the interhuman-docs
repository. That workflow keeps a single rolling docs-sync PR open — a later run
updates it in place instead of opening another one.
Releasing
This package is versioned independently from the API service using
semantic versioning, but in lockstep with the
Python SDK (interhumanai on PyPI): both always carry the same version and
release together from a single workflow. Publishing is automatic on a
version bump, gated on a successful deploy:
- Bump
versioninpackage.jsonand__version__insdk/python/src/interhumanai/_version.pyto the same value, and add aCHANGELOG.mdentry to each package. - Merge to
main. - After the
Deployworkflow succeeds, theSDK releaseworkflow builds, tests, and publishes both packages. It is idempotent per registry — it publishes only when the version changes — and tags the releasesdk-v<version>.
You can also publish manually by pushing an sdk-vX.Y.Z tag (must match both
SDK versions) or via workflow_dispatch (with an optional dry run). The
sdk-v* tag namespace is intentionally separate from the API service's
automatic release tags so the two never collide.
Setup, the NPM_TOKEN secret, and token rotation (our policy re-mints the npm
token about every 90 days) are documented in
docs/sdk-release.md.
