-
Notifications
You must be signed in to change notification settings - Fork 31
TypeScript types for error event payload (replace any, document nested shape) #157
Description
The error listener on the web SDK is typed as (error: any) in the published typings (VapiEventListeners['error']). That forces consumers to maintain hand-rolled types and makes it easy to miss contract changes until runtime. This issue exists for other events as well (especially the message event), but I just want to limit the scope.
We depend on the error payload for product behavior (concurrency limits, “meeting ended” / ejection), not only for logging. Official, versioned types (and ideally a documented error schema) would let us rely on the compiler and catch breaking changes at build time.
A real payload we captured shows duplicated nested objects and message as a structured object (not a string). This is hard to model correctly without official types.
Environment
- Package:
@vapi-ai/web 2.5.2 - TypeScript consumer app using strict typing; we avoid
anyin app code where possible
Current typings (reference)
In dist/vapi.d.ts, several event listeners use any, including:
error: (error: any) => void;The same pattern appears for message, camera-error, network-quality-change, and network-connection.
Why this matters to us
We branch on fields nested on the error object, for example:
- Concurrency / subscription limits - we read
evt.error?.error?.subscriptionLimitsand handleconcurrencyBlockedso we can show a capacity state instead of a generic failure. - Call lifecycle - we treat certain errors as graceful “meeting ended” (e.g., ejected for due to inactivity while a user is intentionall paused/muted will still run some post-processing).
Because the SDK types the callback as any, we maintain a local VapiErrorEvent interface and have had to manually reproduce errors or pull logs to infer shape. That’s fragile and incomplete.
Ambiguity in the payload
The runtime object appears to duplicate or nest the same concepts in multiple places (e.g. human-readable text on both outer and inner fields, and nested error objects). Without official types or docs, it’s unclear which fields are:
- stable public API vs internal detail
- mutually exclusive vs always present together
- the canonical place to read
subscriptionLimitsvs a string message
Example error event payload
This is a recent error we logged (not related to call concurrency even though subscriptionLimits is present). Observed shape from a start-method-error:
{
"type": "start-method-error",
"stage": "unknown",
"error": {
"message": {
"statusCode": 400,
"message": "unsupported Unicode escape sequence",
"error": "Bad Request",
"subscriptionLimits": {
"concurrencyBlocked": false,
"concurrencyLimit": 20,
"remainingConcurrentCalls": 19
}
},
"data": null,
"error": {
"statusCode": 400,
"message": "unsupported Unicode escape sequence",
"error": "Bad Request",
"subscriptionLimits": {
"concurrencyBlocked": false,
"concurrencyLimit": 20,
"remainingConcurrentCalls": 19
}
}
},
"totalDuration": 323,
"timestamp": "2026-03-09T19:30:32.997Z",
"context": {
"hasAssistant": true,
"hasSquad": false,
"hasWorkflow": false,
"isMobile": false
}
}Questions this raises for typing / docs
error.messageis sometimes a structured object (HTTP-stylestatusCode,message,error, plussubscriptionLimits), not necessarily a stringconsumer code that assumes a string will be wrong; the SDK should encode that union explicitly.- The same logical payload appears twice under
error.messageanderror.errorin this sample. Which is canonical? Are they always mirrors? Can they diverge? error.dataisnullhere; when is it populated and what type does it have?- Top-level fields (
type,stage,totalDuration,timestamp,context) look like start-phase diagnostics; are these the same TypeScript event as mid-callerrorevents, or a different variant? A discriminated union (e.g. bytype) would help consumers narrow safely.
Requested outcome
- Export accurate TypeScript types for the
errorevent argument (and ideally otheranyevent payloads), reflecting what the SDK actually emits today. - Document the error payload (field meanings, nesting, and which branches are for Daily vs Vapi vs subscription errors) in README or reference docs.
- Treat the payload as part of the semver contract where possible. Breaking field renames or moves should be called out in release notes so TS consumers get compile-time signal.
Workaround (today)
We define a local VapiErrorEvent (and related types) in our app and cast/assert at the boundary; we would like to remove that once the SDK exports the real type.
Acceptance criteria (suggestion)
on('error', …)/ typedEventEmitteroverloads use a named interface or discriminated union, notany.- Changelog mentions any intentional shape changes.