Skip to content

newCall() silently drops customHeaders — wrong shape passed to @telnyx/react-native-voice-sdk #84

Description

@andrewkouri

Packages:

  • @telnyx/react-voice-commons-sdk 0.4.3
  • @telnyx/react-native-voice-sdk 0.4.5

Summary

TelnyxVoipClient.newCall() types customHeaders as Record<string, string>, but passes
it through unconverted to @telnyx/react-native-voice-sdk, whose CallOptions.customHeaders
expects { name: string; value: string }[]. The object is serialized verbatim into the
custom_headers field of the SIP INVITE message, which Telnyx ignores because it isn't the
expected array shape.

Result: any X-headers passed to newCall() never reach Telnyx, so they are absent from the
call.initiated Call Control webhook. There is no error or warning — the headers just vanish.

Impact

Apps that use custom SIP headers to correlate outbound WebRTC calls with backend state
(e.g. an X-CallId that the Call Control webhook handler uses to attach client_state)
silently lose that correlation. In our case every mobile-originated outbound call left a
permanently stale call record and was never recorded, because call.initiated /
call.answered / call.hangup all arrived without the header / client_state.

The same call placed via @telnyx/webrtc in the browser (which natively accepts the
{ name, value }[] shape) works correctly.

Where it happens

  1. telnyx-voip-client.ts — public API accepts a record:
newCall(
  destination: string,
  callerName?: string,
  callerNumber?: string,
  customHeaders?: Record<string, string>
): Promise<Call>;
  1. internal/calls/call-state-controller.ts (newCall) — passed through as-is:
const callOptions = {
  destinationNumber: destination,
  callerIdName: callerName,
  callerIdNumber: callerNumber,
  customHeaders, // <-- still Record<string, string>
};
const telnyxCall = await this._sessionManager.telnyxClient.newCall(callOptions);
  1. @telnyx/react-native-voice-sdk call-options.ts — expects an array:
customHeaders?: { name: string; value: string }[];
  1. @telnyx/react-native-voice-sdk messages/call.ts (createInviteMessage) — serialized verbatim:
custom_headers: callOptions.customHeaders,

Notably, the conversion IS done correctly on the CallKit answer path
(client.ts, executePendingAnswer):

// Convert Record<string, string> to { name: string; value: string }[] format
const customHeaders = Object.entries(this.pendingCustomHeaders).map(([name, value]) => ({
  name,
  value,
}));

…but the equivalent conversion is missing on the newCall path.

Repro

  1. Place an outbound call from a credential connection via the commons SDK:
await voipClient.newCall("+15551234567", undefined, "+15557654321", {
  "X-CallId": "abc123",
});
  1. Inspect the call.initiated Call Control webhook for the resulting call.

Expected: payload.custom_headers contains { "name": "X-CallId", "value": "abc123" }.
Actual: payload.custom_headers is absent/empty.

Suggested fix

In CallStateController.newCall() (or TelnyxVoipClient.newCall()), mirror the
executePendingAnswer conversion:

customHeaders: customHeaders
  ? Object.entries(customHeaders).map(([name, value]) => ({ name, value }))
  : undefined,

Workaround

Pass the array shape and cast past the public type:

const headers = [
  { name: "X-CallId", value: callId },
] as unknown as Record<string, string>;
await voipClient.newCall(destination, undefined, callerNumber, headers);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions