Call Control

Build a browser-based call controller using the Nextiva Web SDK

Overview

In this tutorial, you’ll learn how to:

  • Place and receive calls
  • Mute, unmute, and hold calls
  • Transfer calls to another agent
  • End a call

Prerequisites

Before starting, make sure you have:

  • A Nextiva developer account and API credentials
  • Node.js installed
  • A package manager (npm or yarn)
  • A working Nextiva tenant with call-enabled users
  • Browser support for WebRTC

Install the SDK


Make a call

After initializing the SDK, you can place outbound calls using the userService.dial() method. This example demonstrates how to:

  • Retrieve available outbound campaigns from the Data Service.
  • Determine the user’s default campaign from the State Service.
  • Construct a DialPayload and initiate the call using the User Service.

Make a call flow

  1. Data Service
  • Fetches the list of outbound campaigns available for the current tenant.
  1. State Service
  • Retrieves the user’s local settings to identify their default campaign.
  1. User Service
  • Executes the outbound call with the prepared payload.
  1. Dial Event
  • The SDK emits callStarted and callConnected events once dialing succeeds.
DialPayload (reference)
PropertyTypeRequiredDescription
campaignIdstring/nullNoOptional campaign ID to associate with the call.
conferenceWorkitemIdstring/nullYesConference work item ID (if applicable).
contactIdstring/nullNoOptional contact ID linked to the call.
contextanyYesOptional metadata, e.g., CRM record or custom object.
conversationIdstring/nullNoOptional conversation ID.
forcebooleanYesWhether to force the call regardless of restrictions.
namestring/nullNoOptional display name for the caller.
optionstringYesDial option (e.g., DialTypes.Campaign.toString()).
spoofWorkitemIdstring/nullNoOptional spoofed work item ID.
ticketIdstring/nullNoOptional ticket ID (e.g., support ticket reference).
tostringYesDestination phone number (E.164 format recommended).
workitemIdstring/nullYesWork item ID related to the call.

Example: make a call

// Assume sdk is initialized & authenticated
const userService = sdk.getUserService()
const dataService = sdk.getDataService()
const stateService = sdk.getStateService()

/**
 * Places an outbound call using the user's default campaign or a fallback.
 * @param to - Destination phone number in E.164 format (e.g., +1234567890)
 */
async function makeCall(to: string) {
  try {
    // 1) Retrieve list of outbound campaigns
    const outboundCampaigns = await dataService.getCampaigns('outbound')

    // 2) Retrieve user settings
    const userSettings = stateService.getUserSettings()

    // 3) Select campaign: default → first available
    const campaignId =
      userSettings?.general?.defaultCampaignId ?? outboundCampaigns?.[0]?._id

    if (!campaignId) throw new Error('No outbound campaign available.')

    // 4) Build payload
    const payload = {
      campaignId,
      conferenceWorkitemId: null,
      contactId: null,
      context: null,
      conversationId: '66fef3e78b64a853830343bb', // Example; replace in production
      force: true,
      name: null,
      option: DialTypes.Campaign.toString(),
      spoofWorkitemId: null,
      to,
      workitemId: null,
    }

    // 5) Dial
    const response = await userService.dial(payload)
    console.log('Call response:', response)
    console.log(`Dialing ${to} via campaign ${campaignId}`)
  } catch (err) {
    console.error('Failed to start call:', err)
  }
}

Sample call response (may vary by SDK version)

{
  "canDial": true,
  "consentType": 2,
  "conversationId": "66fef3e78b64a853830343bb",
  "overwriteUserId": "68d6710dcae6ac1855cd3680",
  "workitemId": "8b96c29c-9fd8-4578-a95f-4148033b3610"
}
FieldDescription
canDialWhether the call can proceed
consentTypeNumeric code representing contact consent
conversationIdConversation identifier
overwriteUserIdUser ID overriding default user (if applicable)
workitemIdWork item representing this call

Notes

  • Always check that campaigns exist before dialing.
  • Replace the hardcoded conversationId with a dynamic value.
  • Sanitize phone number input.
  • Handle 401 Unauthorized by refreshing the token via your backend.

Possible issues

  • Missing Campaigns: If no outbound campaign exists, getCampaigns('outbound') may return an empty array. Add fallback logic or validation.
  • Token Expiry: If you see 401 Unauthorized, refresh the SDK token via your backend before calling dial().
  • Permissions: Ensure your user has permission to initiate outbound calls.

Track call status

The SDK emits WorkitemStateChangeNotification for work item state changes. Listen to it to keep UI state in sync.

React

import { useEffect } from 'react'
import { EventTypes, ofType, WorkitemStates } from '@nextiva/ncx-react-sdk'

// assuming ncxSDK is initialized
useEffect(() => {
  const sub = ncxSDK.events$
    .pipe(ofType([EventTypes.WorkitemStateChangeNotification]))
    .subscribe((event) => {
      const state = event?.data?.workitem?.state
      if (!state) return

      console.log('Call State Changed:', state)
      switch (state) {
        case WorkitemStates.Active:
          console.log('Call is active.')
          break
        case WorkitemStates.Terminated:
          console.log('Call has been terminated.')
          break
        default:
          console.log(`Call is in state: ${state}`)
      }
    })
  return () => sub.unsubscribe()
}, [])

Vanilla JS

import { EventTypes, WorkitemStates } from '@nextiva/ncx-web-sdk'

let subscription: { unsubscribe(): void } | null = null

function startListeningToCallStatus() {
  subscription = ncxSDK.events$.subscribe((event: any) => {
    if (event.name !== EventTypes.WorkitemStateChangeNotification) return
    const state = event.data?.workitem?.state

    if (state === WorkitemStates.Active) {
      console.log('Call status = Active')
    } else if (state === WorkitemStates.Terminated) {
      console.log('Call status = Terminated')
    } else {
      // other states
    }
  })
}

function cleanupOnUnload() {
  subscription?.unsubscribe()
}

startListeningToCallStatus()
window.addEventListener('beforeunload', cleanupOnUnload)

Vue 3 (composition API)

<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import { EventTypes, WorkitemStates } from '@nextiva/ncx-web-sdk'

let subscription: { unsubscribe(): void } | null = null

onMounted(() => {
  subscription = ncxSDK.events$.subscribe((event: any) => {
    if (event.name !== EventTypes.WorkitemStateChangeNotification) return
    const state = event.data?.workitem?.state

    if (state === WorkitemStates.Active) {
      console.log('call status = Active')
    } else if (state === WorkitemStates.Terminated) {
      console.log('call status = Terminated')
    } else {
      // other states
    }
  })
})

onBeforeUnmount(() => subscription?.unsubscribe())
</script>

Explanation

  • Event
    • WorkitemStateChangeNotification is emitted on every work item state change.
  • Property
    • The call status is available in event.data.workitem.state.
  • States
    • Use the WorkitemStates constants for reliable comparison (Active, Terminated).
  • Subscription
    • Always unsubscribe on component unmount (or teardown) to prevent memory leaks.
Data typeValueDescription
Campaign1Outbound campaign-based call (default)
User2Direct user-to-user dial
State3State-triggered dial
Locked4Locked work item dial
📘

In most contact-center use cases, DialTypes.Campaign is used to initiate calls associated with a specific outbound campaign.

Receive and handle incoming calls (offers)

When an inbound call is received, the ACD Service publishes an offer event. This event represents an available call that an agent or application can accept or reject.

If you’re building with React, the easiest way to manage call offers is through the useOffers hook provided by the @nextiva/ncx-react-sdk package. This hook automatically listens for new inbound call offers and updates your component state, allowing you to present an Accept / Reject modal or other UI pattern.

For non-React applications, you can subscribe directly to the SDK’s event stream by listening to sdk.events$. Watch for the OffersChangedNotification event type to handle updates and changes to incoming call offers manually.

Example: incoming offer modal with accept / reject

import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useNcxSdk, useOffers } from '@nextiva/ncx-react-sdk'
import type { Offer } from '@nextiva/ncx-core-sdk';

type Offer = {
  _id: string
  workitem?: { from?: string }
}

export function IncomingOfferPanel() {
  const ncxSDK = useNcxSdk()
  const offers = useOffers<Offer>() // remove generic if not supported

  const activeOffer = useMemo(() => offers?.[0] ?? null, [offers])
  const fromLabel = activeOffer?.workitem?.from ?? 'Unknown caller'

  const [isSubmitting, setSubmitting] = useState(false)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    if (activeOffer?._id) console.log('Incoming offer:', activeOffer)
  }, [activeOffer?._id])

  const onAccept = useCallback(async () => {
    if (!activeOffer) return
    setSubmitting(true); setError(null)
    try {
      await ncxSDK.getACDService().accept(activeOffer._id)
    } catch (err) {
      console.error('Failed to accept offer', err)
      setError('Failed to accept the offer. Please try again.')
    } finally {
      setSubmitting(false)
    }
  }, [activeOffer?._id, ncxSDK])

  const onReject = useCallback(async () => {
    if (!activeOffer) return
    setSubmitting(true); setError(null)
    try {
      await ncxSDK.getACDService().reject({ offerId: activeOffer._id })
    } catch (err) {
      console.error('Failed to reject offer', err)
      setError('Failed to reject the offer. Please try again.')
    } finally {
      setSubmitting(false)
    }
  }, [activeOffer?._id, ncxSDK])

  if (!activeOffer) return null

  return (
    <div role="dialog" aria-modal="true" aria-labelledby="offer-title" className="ncx-offer-modal">
      <h2 id="offer-title">Incoming Call</h2>
      <p><strong>From:</strong> {fromLabel}</p>

      {error && <p className="ncx-error" role="alert">{error}</p>}

      <div className="ncx-modal-actions">
        <button onClick={onAccept} disabled={isSubmitting} aria-busy={isSubmitting}>Accept</button>
        <button onClick={onReject} disabled={isSubmitting} aria-busy={isSubmitting}>Reject</button>
      </div>
    </div>
  )
}

Notes

  • The sample uses offers?.[0] as the active offer. In production, you may need a queue or rules (by priority or timestamp).
  • Prioritize useEffect deps like [activeOffer?._id] (or [offers]) over [offers.length] so you also react to offer content changes, not just count changes.

Offer source details

const label = activeOffer?.workitem?.from ?? 'Unknown'
console.log('Offer coming from:', label)

Non-React (OffersChangedNotification)

Subscribe to SDK events after successful login/init. Always unsubscribe on teardown.

Vanilla JS

import { EventTypes, Nextiva } from '@nextiva/ncx-web-sdk'

const sdk = new Nextiva({
  baseURL: `https://${import.meta.env.VITE_APP_HOST}`,
  debug: true,
  language: import.meta.env.VITE_APP_LANGUAGE,
  translator: new Translator(),
})

let subscription: { unsubscribe(): void } | undefined

async function initializeSDK() {
  try {
    await sdk.login(username, password)
    await sdk.init()

    subscription = sdk.events$.subscribe((event: any) => {
      if (event.name !== EventTypes.OffersChangedNotification) return
      const offers = event.data?.offers
      if (Array.isArray(offers)) {
        console.log('📞 Received incoming offers:', offers)
        // update your UI/store here
      }
    })

    console.log('SDK initialized and event listener attached.')
  } catch (error) {
    console.error('SDK initialization failed:', error)
  }
}

function cleanup() { subscription?.unsubscribe() }
window.addEventListener('beforeunload', cleanup)
initializeSDK()

Vue 3

import { onMounted, onBeforeUnmount } from 'vue'
import { EventTypes, Nextiva } from '@nextiva/ncx-web-sdk'

const sdk = new Nextiva({ /* ... */ })
let subscription: { unsubscribe(): void } | null = null

const initializeSDK = async () => {
  await sdk.login(username, password)
  await sdk.init()

  subscription = sdk.events$.subscribe((event: any) => {
    if (event.name !== EventTypes.OffersChangedNotification) return
    const offers = event?.data?.offers
    if (Array.isArray(offers)) {
      console.log('📞 Offers received:', offers)
      // store.commit('setOffers', offers) or a reactive ref
    }
  })
}

onMounted(() => initializeSDK())
onBeforeUnmount(() => subscription?.unsubscribe?.())

Possible issues

  • Guard your data: check event?.data?.offers to avoid runtime errors on unrelated events.
  • Handle multiple offers: design for multiple concurrent offers (queue UI, or a selector to pick which offer to act on).
  • Disable during action: while accepting/rejecting, disable buttons to prevent double-click races.
  • Error reporting: surface accept/reject errors to the UI and log the error object for debugging.

Accept/reject methods (reference)

/** Accept an inbound offer */
ncxSDK.getACDService().accept(offerId: string): Promise<void>

/** Reject an inbound offer */
ncxSDK.getACDService().reject(
  /** Payload for rejection */
  { offerId: string }
): Promise<void>

Return types may vary by SDK version. Treat them as Promise<void> unless your SDK exposes a richer result.

Accept or reject a call (offer)

When an incoming call (offer) arrives, the ACD Service emits the offer details to its subscribers. You can retrieve active offers using the useOffers hook from @nextiva/ncx-react-sdk and display a UI modal with Accept and Reject buttons that show the caller’s information for a better user experience (demonstrated above).

Accept an incoming offer

const onAccept = async (offerId: string) => {
  try {
    await ncxSDK.getACDService().accept(offerId);
    console.log('Offer accepted successfully.');
  } catch (error: unknown) {
    console.error('Failed to accept offer:', error);
  }
};

Reject an incoming offer

const onReject = async (offerId: string) => {
  try {
    await ncxSDK.getACDService().reject({ offerId });
    console.log('Offer rejected successfully.');
  } catch (error: unknown) {
    console.error('Failed to reject offer:', error);
  }
};

Additional info

You can access caller information directly from the offer object:

const callerName = offer.workitem?.from ?? 'Unknown Caller';
console.log('Incoming call from:', callerName);
📘

The from field within offer.workitem typically contains the caller’s display name or phone number. Use this value to personalize your incoming call modal or notification banner.


Call transfers and call end

This section demonstrates how to:

  • Validate which workitem types represent active calls
  • Fetch the latest active call workitem
  • End (hang up) a call and optionally submit a disposition
  • Mute / Unmute, Hold / Resume, and Transfer calls (both warm transfer and agent transfer)
  • Integrate these controls into a React UI

Everything below is TypeScript-friendly and can be dropped directly into your SDK tutorial or application code.

WorkItem type validation

Not all workitem types represent phone calls. Use a type guard to ensure that actions like hang up, hold, or transfer are applied only to valid call workitems.

KeyValueDescription
Chat'Chat'Standard chat work item.
ConversationMessage'ConversationMessage'Individual message in a conversation thread.
Facebook'Facebook'Facebook-based interaction or message.
InboundCall'InboundCall'Incoming voice call.
InboundEmail'Email'Incoming email message.
InboundExtensionCall'InboundExtensionCall'Incoming internal extension call.
InboundFax'InboundFax'Incoming fax document.
InboundSMS'InboundSMS'Incoming SMS message.
Instagram'Instagram'Instagram message or interaction.
OutboundCall'OutboundCall'Outgoing voice call.
OutboundEmail'OutboundEmail'Outgoing email message.
OutboundExtensionCall'OutboundExtensionCall'Outgoing internal extension call.
OutboundFax'OutboundFax'Outgoing fax.
OutboundSMS'OutboundSMS'Outgoing SMS message.
OutboundVideoMeeting'OutboundVideoMeeting'Outgoing video meeting or conference.
PredictiveCall'PredictiveCall'Predictive dialer call.
PredictiveSMS'PredictiveSMS'Predictive SMS outreach.
PreviewCall'PreviewCall'Preview dialer call.
ProgressiveCall'ProgressiveCall'Progressive dialer call.
Survey'Survey'Post-interaction survey item.
SystemCall'SystemCall'System-initiated call (e.g., automated).
Twitter'Twitter'Twitter-based message or mention.
Unrecognized'Unrecognized'Fallback type for unknown work items.
Whatsapp'Whatsapp'WhatsApp chat message.
Workflow'Workflow'Workflow automation item.
Youtube'Youtube'YouTube-related interaction.
export type WorkitemType = (typeof WorkitemTypes)[keyof typeof WorkitemTypes]

type ValidCallWorkitemTypes =
  | typeof WorkitemTypes.InboundCall
  | typeof WorkitemTypes.OutboundCall
  | typeof WorkitemTypes.InboundExtensionCall
  | typeof WorkitemTypes.OutboundExtensionCall
  | typeof WorkitemTypes.PredictiveCall
  | typeof WorkitemTypes.PreviewCall
  | typeof WorkitemTypes.ProgressiveCall
  | typeof WorkitemTypes.SystemCall

const validCallWorkitemTypesSet = new Set<WorkitemType>([
  WorkitemTypes.InboundCall,
  WorkitemTypes.OutboundCall,
  WorkitemTypes.InboundExtensionCall,
  WorkitemTypes.OutboundExtensionCall,
  WorkitemTypes.PredictiveCall,
  WorkitemTypes.PreviewCall,
  WorkitemTypes.ProgressiveCall,
  WorkitemTypes.SystemCall,
])

export function validateCallWorkitemType(
  workitemType: WorkitemType,
): workitemType is ValidCallWorkitemTypes {
  return validCallWorkitemTypesSet.has(workitemType)
}

Get latest active call WorkItem

/**
 * Returns the most recent *call* workitem in "active" or "directconnect" state.
 */
export const getLatestCallWorkItem = async (sdk: Nextiva): Promise<any | null> => {
  try {
    const response = await sdk.getWorkitemsService().fetchWorkitems()
    const workitems: any[] = response.objects ?? []

    return [...workitems]
      .reverse()
      .find(
        (w) =>
          validateCallWorkitemType(w.type) &&
          (w.state === 'active' || w.state === 'directconnect'),
      ) ?? null
  } catch (error) {
    console.error('Failed to fetch latest call work item:', error)
    return null
  }
}

Retrieves all workitems from the SDK, filters for call-type workitems that are currently active or directconnect, and returns the latest match. Returns null when no active call exists.

Hang up (with optional Disposition)

This is the end-call handler you’ll call from your UI. It supports both workitem object and workitemId use.

/**
 * Ends the call and (optionally) submits a disposition.
 */
export const handleHangUp = async (
  ncxSDK: any,
  latestCallWorkItem: any | null,
  number: string,
) => {
  if (!latestCallWorkItem) return

  const workitemsService = ncxSDK.getWorkitemsService?.()
  const dispositionService = ncxSDK.getDispositionService?.()
  const userService = ncxSDK.getUserService?.()

  if (!workitemsService?.hangUp) {
    console.error('handleHangUp - hangUp not available on workitemsService')
    return
  }

  try {
    await workitemsService.hangUp(latestCallWorkItem)
    // OR: await workitemsService.hangUp(latestCallWorkItem.workitemId)

    if (userService?.hasDispositionEnabled?.()) {
      if (!dispositionService?.disposition) {
        console.error('handleHangUp - disposition method not available')
        return
      }
      try {
        await dispositionService.disposition({
          action: DispositionActions.ConnectedCallback,
          callbackDate: Date.now(),
          callbackTime: Date.now(),
          connectAgain: false,
          phoneNumber: number,
          useCallbackNumberOnly: false,
          workitemId: latestCallWorkItem.workitemId,
        })
      } catch (err) {
        console.error('handleHangUp - disposition error', err)
      }
    }
  } catch (err) {
    console.error('handleHangUp - hangUp failed', err)
  }
}

Example

<button
  aria-label="Hang up"
  onClick={() => handleHangUp(ncxSDK, latestCallWorkItem, number)}
>
  Hang Up
</button>

Mute / unmute

Mute/unmute is issued via UserService. Confirmation comes asynchronously via PhoneStateNotification events.

export const handleMute = async (sdk: any) => {
  try {
    const response = await sdk.getUserService().mute()
    console.log('Mute response:', response)
  } catch (error) {
    console.error('Mute failed:', error)
  }
}

export const handleUnmute = async (sdk: any) => {
  try {
    const response = await sdk.getUserService().unMute()
    console.log('Unmute response:', response)
  } catch (error) {
    console.error('Unmute failed:', error)
  }
}

Subscribe for confirmation (recommended)

You can listen for mute/unmute updates through the SDK’s event stream. This ensures your UI stays synchronized with the real-time phone state.

const subscription = sdk.events$.subscribe((event: any) => {
  if (event.name === EventTypes.PhoneStateNotification) {
    const { mute } = event.data || {}
    if (typeof mute === 'boolean') {
      console.log('Muted:', mute)
      // Update UI state accordingly (e.g., toggle mute icon)
    }
  }
})

// Later, to clean up:
subscription.unsubscribe()

Confirm via PhoneStateNotification

import { EventTypes } from '@nextiva/ncx-web-sdk'

const subscription = sdk.events$.subscribe((event: any) => {
  if (event.name === EventTypes.PhoneStateNotification) {
    const { mute } = event.data || {}
    if (typeof mute === 'boolean') {
      console.log('Muted:', mute)
    }
  }
})
// unsubscribe later
subscription.unsubscribe()

React Hook version

If you’re using React, you can encapsulate mute-state tracking in a simple custom hook. This keeps your UI in sync with the phone’s real-time mute state automatically.

import { useEffect, useState } from 'react'
import { EventTypes } from '@nextiva/ncx-web-sdk'

export function usePhoneMuteStatus(sdk: any) {
  const [isMuted, setIsMuted] = useState(false)

  useEffect(() => {
    const sub = sdk.events$?.subscribe((event: any) => {
      if (event.name === EventTypes.PhoneStateNotification) {
        const { mute } = event.data || {}
        if (typeof mute === 'boolean') setIsMuted(mute)
      }
    })
    return () => sub?.unsubscribe()
  }, [sdk])

  return isMuted
}
FeatureDetails
SDK methodsncxSDK.getUserService().mute() / .unMute()
DescriptionMutes or unmutes the agent’s microphone
ConfirmationPhoneStateNotification socket event
Return typePromise<void> (or SDK-specific)
Error handlingLog or surface to the UI
PrerequisitesSDK initialized, agent in an active call

Hold / resume

Hold is issued via WorkitemsService. The service supports a workitem object or string ID.

/**
 * Put a workitem (call) on hold.
 * @param model - Workitem object or workitemId string
 */
export async function holdWorkitem(workitemsService: any, model: any | string) {
  return workitemsService.hold(model)
}

/**
 * Resume a held workitem (call).
 * @param model - Workitem object or workitemId string
 */
export async function resumeWorkitem(workitemsService: any, model: any | string) {
return workitemsService.active(model);
}

Warm transfer

Warm transfer keeps the agent on the line while connecting another party. Payload fields are optional and workflow-dependent.

// React: import from '@nextiva/ncx-react-sdk'
// Non-React: import from '@nextiva/ncx-web-sdk'
import type { WorkitemRecordingOption, WarmTransferPayload } from '@nextiva/ncx-web-sdk'

/**
 * Handles a warm transfer operation.
 */
export const handleWarmTransfer = async (ncxSDK: any, payload: WarmTransferPayload) => {
  try {
    const workitemsService = ncxSDK.getWorkitemsService()
    const result = await workitemsService.warmTransfer(payload)
    console.log('Warm transfer result:', result)
  } catch (error) {
    console.error('Error during warm transfer:', error)
  }
}

Transfer to another agent

// React: import from '@nextiva/ncx-react-sdk'
// Non-React: import from '@nextiva/ncx-web-sdk'
import type { TransferToAgentPayload } from '@nextiva/ncx-web-sdk'

export const handleTransferToAgent = async (
  ncxSDK: any,
  agentName: string,
  activeWorkitem: { from?: string } | null,
) => {
  if (!agentName || !activeWorkitem?.from) {
    console.warn('Missing agent name or active workitem.')
    return
  }

  try {
    const schemaType = ncxSDK.getDataService().getSchemaType('user')
    const agents = await schemaType.search({ q: agentName })
    if (!Array.isArray(agents) || agents.length === 0) {
      console.warn('No agents found for query:', agentName)
      return
    }

    const targetAgent = agents[0] // TODO: improve selection logic
    const workitemId = activeWorkitem.from as string

    const payload: TransferToAgentPayload = {
      userId: targetAgent._id,
      workitemId,
      // recording: 'keepCurrent',
      // eventName: 'transferRequested',
    }

    await ncxSDK.getWorkitemsService().transferToAgent(payload)
    console.log('Work item transferred to agent:', targetAgent)
  } catch (error) {
    console.error('Transfer to agent failed:', error)
  }
}

UI wiring summary

  • Use getLatestCallWorkItem(sdk) to fetch the current call.
  • Call handleHangUp(ncxSDK, latestCallWorkItem, number) on End Call.
  • Mute/unmute via UserService.mute() / UserService.unMute() and confirm via PhoneStateNotification event.
  • Hold/resume via WorkitemsService.hold(model) / WorkitemsService.active(model).
  • Warm transfer with WorkitemsService.warmTransfer(payloadOrIds).
  • Agent transfer with WorkitemsService.transferToAgent(payload).

Best practices

  • Workitem shape: some APIs accept a workitemId. Normalize in helpers (see hold/active).
  • Null checks: always guard for null/undefined workitems and empty search results
  • Concurrency: disable action buttons while requests are in flight (prevents double clicks).
  • Events: subscribe after login/init; unsubscribe on unmount/teardown to avoid leaks.
  • Dispositions: only submit if your user/workflow requires it; surface failures but don’t block hang-up UX.
  • Agent selection: Improve agents[0] logic (exact match, skills, presence, load).