Build incoming-call Answer and Decline controls with the Webphone SDK

Build reliable Answer and Decline controls for IllyVoIP SDK incoming calls, including duplicate clicks, caller cancellation and UI cleanup.

Use the Webphone SDK’s incoming-call event to show an Answer and a Decline action for the exact ringing call. Keep those actions separate from ending a connected call. The caller can hang up while the buttons are visible, so your interface must follow SDK events as well as button clicks.

This guide assumes you already have one SDK instance owned by your application, a backend-authorized browser token, HTTPS and a registered eligible SIP identity. Route an IllyVoIP number or queue to that identity for incoming calls. Registration alone does not create incoming routing. The SDK is not the server-side Voice API.

1. Subscribe before you make the phone available

Attach incoming, answered, state and ended listeners before connecting/registering the phone. Keep the call object or its exact ID for each visible call. A generic “current call” lookup can select a different call than the one a user clicked.

Render caller information as text, never as HTML. Display names and numbers are not proof of identity; avoid showing them in shared logs or notifications without an appropriate privacy policy. Provide visible status even if a browser blocks the ringtone.

2. Track ringing calls and serialize the buttons

The helper below manages only incoming controls. render receives a list of pending calls and their busy state; notice displays a plain-text status message. Implement those two UI functions in your application. Other parts of your phone owner continue to manage connected calls, audio and logout.

function incomingControls(phone, render, notice) {
  const pending = new Map();
  const busy = new Set();
  let disposed = false;
  const paint = () => {
    if (!disposed) render([...pending.values()].map(call => ({
      id: call.id, state: call.state, busy: busy.has(call.id)
    })));
  };
  const remove = ({ call }) => {
    pending.delete(call.id); paint();
  };
  const off = [
    phone.on('call:incoming', ({ call }) => {
      pending.set(call.id, call); paint();
    }),
    phone.on('call:answered', remove),
    phone.on('call:ended', remove),
    phone.on('call:state', ({ call }) => {
      if (['active', 'held', 'ending', 'ended'].includes(call.state)) {
        pending.delete(call.id);
      }
      paint();
    })
  ];
  return {
    async act(id, action) {
      if (disposed || !['answer', 'reject'].includes(action)) return false;
      const call = pending.get(id);
      if (!call || busy.has(id)) return false;
      if (['active', 'held', 'ending', 'ended'].includes(call.state)) {
        pending.delete(id); paint(); return false;
      }
      busy.add(id); paint();
      try {
        await call[action]();
        pending.delete(id);
        if (!disposed) notice('Request completed; follow the call status.');
        return true;
      } catch {
        if (!disposed) notice('Action failed. Check the current call state.');
        return false;
      } finally {
        busy.delete(id); paint();
      }
    },
    dispose() {
      disposed = true;
      off.forEach(unsubscribe => unsubscribe());
      pending.clear(); busy.clear();
    }
  };
}

Connect Answer to act(callId, "answer") and Decline to act(callId, "reject"). Disable both while the selected call is busy. Do not automatically retry a failed answer or decline: the caller may already have ended the call or another authorized endpoint may have handled it.

3. Keep call completion owned by events

When call:answered arrives, remove the incoming prompt and let your active-call interface take over. When call:ended arrives, clear the prompt even if a button promise is still pending. The helper does not reinsert a call after a late promise result, which avoids resurrecting a cancelled invitation.

A successful button promise does not certify two-way audio or that the user heard the caller. Continue displaying the actual connected-call state and handle microphone and speaker problems through the device and troubleshooting guides.

4. Handle navigation and sign-out deliberately

Dispose these listeners when their UI owner is removed. Disposing the helper does not reject calls, destroy the SDK or unregister the phone. Your single phone owner must decide what happens to an active or ringing call during navigation, logout and account changes.

Do not create a second SDK instance just to rebuild the Answer panel. Framework remounts and duplicate listeners can otherwise trigger competing actions. The React, Vue, Next.js and Angular guides explain ownership; reuse that structure.

5. Test races before enabling incoming calls

Check one incoming event, two quick Answer clicks, Answer followed by Decline, caller cancellation before a click, cancellation while answer is pending, a rejected action and disposal before a promise settles. Confirm the UI clears terminal calls and never issues a second action while the first is pending.

Then, during an authorized live acceptance check, verify number routing, registration, ringing indication, answer, two-way audio, decline and remote hangup. Browser background restrictions and mobile suspension require separate testing; this implementation does not promise native-phone background reliability.

Related guides and reference

Reviewed 10 October 2026 against the current IllyVoIP reference and implementation. SDK examples target version 1.4.2 where applicable. Local example checks do not certify a completed live call, routing change or third-party integration.