Skip to content

TypeScript types for error event payload (replace any, document nested shape) #157

@EnotionZ

Description

@EnotionZ

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 any in 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:

  1. Concurrency / subscription limits - we read evt.error?.error?.subscriptionLimits and handle concurrencyBlocked so we can show a capacity state instead of a generic failure.
  2. 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 subscriptionLimits vs 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.message is sometimes a structured object (HTTP-style statusCode, message, error, plus subscriptionLimits), 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.message and error.error in this sample. Which is canonical? Are they always mirrors? Can they diverge?
  • error.data is null here; 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-call error events, or a different variant? A discriminated union (e.g. by type) would help consumers narrow safely.

Requested outcome

  1. Export accurate TypeScript types for the error event argument (and ideally other any event payloads), reflecting what the SDK actually emits today.
  2. Document the error payload (field meanings, nesting, and which branches are for Daily vs Vapi vs subscription errors) in README or reference docs.
  3. 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', …) / typed EventEmitter overloads use a named interface or discriminated union, not any.
  • Changelog mentions any intentional shape changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions