Skip to main content

Screen Pop

Open the customer record in your app on the very first ring — before anyone says hello.

Two event surfaces can drive a Screen Pop; pick the one that matches your architecture.

Option 1: frontend via Server-Sent Events (simplest for embedded integrations)​

If the customer record already lives in a browser tab, let the browser itself subscribe to DialStack events and react. No backend round-trip.

DialStack pushes real-time events to the browser over Server-Sent Events (SSE) — a long-lived HTTP connection that streams events one-way from server to client. It's simpler than WebSockets and reconnects automatically.

Use the SDK to open an SSE stream with a session token:

TypeScript
import { loadDialstackAndInitialize } from '@dialstack/sdk-js';

const dialstack = await loadDialstackAndInitialize({
publishableKey: 'pk_live_YOUR_KEY',
fetchClientSecret: async () => {
const res = await fetch('/api/dialstack/session');
const { client_secret, expires_at } = await res.json();
return { clientSecret: client_secret, expiresAt: expires_at };
},
});

dialstack.on('call.incoming', async (event) => {
const customer = await lookupByPhone(event.from_number);
if (customer) openCustomerRecord(customer.id);
});

Latency: sub-second from the first ring to the record opening. Connection stays alive as long as the browser tab is open. SSE reconnects automatically.

See Real-Time Events for the full SSE API, authentication, and reconnection behavior.

Option 2: backend webhook → your own realtime channel​

If the customer lookup requires your backend (to cross a firewall, join a DB, call a CRM), handle the Screen Pop server-side and push to the frontend over your own channel:

TypeScript
import { DialStack } from '@dialstack/sdk-server';

// POST /webhooks/dialstack — registered as a webhook endpoint
app.post('/webhooks/dialstack', express.raw({ type: 'application/json' }), async (req, res) => {
const event = DialStack.webhooks.constructEvent(
req.body,
req.header('x-dialstack-signature'),
process.env.DIALSTACK_WEBHOOK_SECRET
); // throws on a bad signature; see receiving-webhooks.md

if (event.type === 'call.incoming') {
const customer = await db.customers.findOne({ phone: event.data.from_number });
if (customer) {
// push to the user's open session via YOUR realtime channel
pusher.trigger(`user-${event.data.user}`, 'screen-pop', {
customerId: customer.id,
callId: event.data.call_id,
});
}
}

res.status(200).end();
});

Webhooks are fire-and-forget — respond 200 quickly. Don't do the customer lookup on the webhook's critical path if it can be slow; queue it.

What makes a good Screen Pop​

  • Speed over completeness. Pop the record in < 500ms. Enrich later.
  • Degrade gracefully. Unknown caller? Show the unknown-caller state, don't hide the Screen Pop entirely.
  • Idempotent. A webhook can be delivered more than once: de-dupe by the envelope's id. SSE events carry no id; key a pop on call_id when you read the stream directly, or on the caller and timestamp with the SDK, so a repeated event doesn't open the record twice.
  • No blocking UI. If lookup fails, log it and continue — never block the ring.

Where the data comes from​

The call.incoming event carries different fields on each surface:

FieldSDK (dialstack.on)SSE streamWebhook (event.data)Use for
from_numberyesyesyesMatch to a customer record (E.164)
from_nameyesyesyesFall-back display name when you have no match (may be null)
to_numberyesyesyesWhich of your numbers was dialed (useful for multi-location routing)
user_idyesyesas userThe user the call is routed to, when it targets a single user directly
call_idnoyesyesStable call identifier: attach the opened record to your activity log, and later fetch call details, recording, or transcript
timestampyes (a Date)yesstarted_atWhen the call arrived

See Real-Time Events for the SSE payload and Webhook Events for the webhook payload. Signature verification is covered in Receiving Webhooks.

See also​