> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# BLF presence: live extension state over REST and SSE

> Subscribe to per-extension busy, ringing, idle, and do-not-disturb state with GET /voice/blf (snapshot) and GET /voice/blf/stream (server-sent events) for desk-phone rosters, wallboards, and receptionist consoles.

# BLF presence

Busy Lamp Field (BLF) presence is the live busy/idle view of your
organization's extensions: who is on a call right now, whose phone is
ringing, who has do-not-disturb on, and whose device went quiet. It is the
same lamp pattern a desk phone lights next to a watched extension, exposed
as a REST + server-sent-events (SSE) surface you can build against from a
wallboard, a receptionist console, or any custom roster UI.

Every response is scoped to the organization that authenticates the
request. One organization's roster never appears in another organization's
snapshot or stream.

## What BLF presence is (and what it is not)

BLF presence describes the telephony line, not the contact-center agent.
Agent presence (available, auxiliary codes, shrinkage) answers "can this
agent take work right now?" and feeds routing decisions. BLF presence
answers "is this line free?" and is derived from three facts the platform
already maintains for every SIP extension:

* whether the extension's device is currently registered, and how fresh
  that registration is,
* whether do-not-disturb is switched on for the extension,
* whether a call involving the extension is ringing or in progress.

Use agent presence for staffing and routing views. Use BLF presence
whenever the state of the line is what matters: before offering a
transfer, on a front-desk screen, or on a team wallboard.

## The two consumption paths

| Path | Endpoint | Use it when |
| - | - | - |
| REST snapshot | `GET /api/v1/voice/blf` | Server-side checks, low-frequency polling, one-off lookups |
| SSE stream | `GET /api/v1/voice/blf/stream` | Interactive rosters and wallboards that must flip the moment a call starts or ends |

Both paths return the same envelope:

```json theme={null}
{
  "states": [],
  "emittedAt": "2026-10-15T09:14:31.772Z"
}
```

`states` holds one entry per SIP extension in your organization;
`emittedAt` stamps when the snapshot was derived. When you poll, treat
`emittedAt` as the freshness marker.

### The state entry

| Field | Type | Meaning |
| - | - | - |
| `credentialId` | string | Stable id of the SIP extension |
| `username` | string | SIP username the device registers with |
| `extensionNumber` | string or null | Short extension code; null when unassigned |
| `label` | string or null | Friendly name set by your admin |
| `state` | string | One of the wire state values below |
| `activeSince` | string or null | ISO start time of the current call; set only for `early` / `confirmed` |
| `direction` | `"inbound"` or `"outbound"` or null | Direction of the current call; set only for `early` / `confirmed` |

### Wire state values

| `state` | Meaning |
| - | - |
| `idle` | Device registered, no active call, do-not-disturb off |
| `early` | A call is ringing or being set up |
| `confirmed` | An answered call is in progress |
| `dnd` | Do-not-disturb is on for the extension |
| `disabled` | The SIP extension is disabled |
| `away` | Device registered but stale: no REGISTER refresh within the registration window |

### Poll or subscribe?

Poll the snapshot when a few seconds of staleness is fine and you want the
simplest possible client: a server-side cron that writes to a badge
system, a wallboard that refreshes on a timer, an admin script. Subscribe
to the stream when a person is watching and the lamp must flip as the call
flips: receptionist consoles, supervisor wallboards, in-browser rosters.
The stream removes both the polling load and the polling lag.

## Freshness: instant push with a 3-second safety net

The stream is event-driven first. Whenever a call involving one of your
extensions starts, rings, or ends, the change is published on an
organization-scoped notification channel and every open stream for your
organization wakes up immediately and re-derives a full snapshot. In
practice an active call transition reaches an open stream in well under a
second.

On top of that, each stream runs an independent reconciliation tick every
3 seconds that re-derives the snapshot from the source of truth. If a
pushed wake-up is ever lost, the tick catches the missed transition within
3 seconds, so the stream self-heals without any action from you.

Every frame carries a full snapshot, never a delta. There is no event
ordering to maintain and no replay to request: any single frame, including
the first one after a reconnect, is a complete current view of your
roster.

## Limits and connection behaviour

* **100 concurrent streams per organization**, enforced across the whole
  cluster. A 101st concurrent connection receives HTTP `429` with the
  error code `TOO_MANY_CONNECTIONS`. If your deployment needs more
  concurrent viewers than that, hold one stream in a backend and fan out
  to your clients from there.
* **Keepalive comments every 15 seconds.** A line reading `: ping` keeps
  proxies and load balancers from idling the connection out. SSE clients
  ignore comment lines automatically; a hand-rolled parser must skip
  them.
* **30-minute maximum stream duration.** The server closes the stream
  cleanly after 30 minutes. This is expected, not an error: reconnect, as
  in the pattern below. Size any client-side idle timeout above the 15s
  keepalive cadence, not above the 30-minute ceiling.
* **Frames arrive as unnamed events.** Each update is a single
  `data:` line holding the `{states, emittedAt}` JSON envelope. With
  `EventSource` that means `onmessage`; there are no named event types to
  register listeners for.
* **Browser origins are checked.** A browser connect from an origin that
  is not trusted for your organization receives HTTP `403`.

## Examples

### Fetch a snapshot

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/voice/blf \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

```json theme={null}
{
  "states": [
    {
      "credentialId": "sip_cred_01jqx9km4e",
      "username": "alice-dev",
      "extensionNumber": "201",
      "label": "Alice - Support",
      "state": "idle",
      "activeSince": null,
      "direction": null
    },
    {
      "credentialId": "sip_cred_01jqx9kx2t",
      "username": "bob-support",
      "extensionNumber": "202",
      "label": "Bob - Support",
      "state": "confirmed",
      "activeSince": "2026-10-15T09:12:04.511Z",
      "direction": "inbound"
    }
  ],
  "emittedAt": "2026-10-15T09:14:31.772Z"
}
```

### Subscribe from a browser (EventSource)

`EventSource` cannot set request headers, so pass your credential as the
`token` query parameter, the same affordance the log stream exposes:

```javascript theme={null}
const source = new EventSource(
  `https://api.orbit.devotel.io/api/v1/voice/blf/stream?token=${apiKey}`,
);

source.onmessage = (event) => {
  const { states, emittedAt } = JSON.parse(event.data);
  renderRoster(states, emittedAt);
};
```

### Subscribe from Node

From a server you can send the `Authorization` header directly and parse
frames yourself. Frames are separated by a blank line; skip comment lines:

```javascript theme={null}
const res = await fetch(
  "https://api.orbit.devotel.io/api/v1/voice/blf/stream",
  { headers: { Authorization: `Bearer ${process.env.ORBIT_API_KEY}` } },
);

let buf = "";
for await (const chunk of res.body) {
  buf += chunk;
  let cut;
  while ((cut = buf.indexOf("\n\n")) !== -1) {
    const frame = buf.slice(0, cut);
    buf = buf.slice(cut + 2);
    const data = frame
      .split("\n")
      .filter((line) => line.startsWith("data: "))
      .map((line) => line.slice(6))
      .join("");
    if (!data) continue; // ": ping" keepalive comment
    const { states, emittedAt } = JSON.parse(data);
    renderRoster(states, emittedAt);
  }
}
```

### A full SSE frame

This is exactly what arrives on the wire: one `data:` line with the
complete envelope, and periodic `: ping` comments in between:

```text theme={null}
data: {"states":[{"credentialId":"sip_cred_01jqx9km4e","username":"alice-dev","extensionNumber":"201","label":"Alice - Support","state":"confirmed","activeSince":"2026-10-15T09:12:04.511Z","direction":"inbound"}],"emittedAt":"2026-10-15T09:14:31.772Z"}

: ping
```

### Watch a single extension

Every frame is a full roster, so filtering is a client-side one-liner:

```javascript theme={null}
const onmessage = (event) => {
  const { states } = JSON.parse(event.data);
  const frontDesk = states.filter((s) => s.extensionNumber === "1000");
  renderLamp(frontDesk[0]);
};
```

### Reconnect with backoff

Streams end by design after 30 minutes, and any network can drop a
connection at any moment. Treat every close as "reconnect", with capped
exponential backoff so a fleet of wallboards does not retry in lockstep:

```javascript theme={null}
let attempt = 0;

function connect() {
  const source = new EventSource(
    `https://api.orbit.devotel.io/api/v1/voice/blf/stream?token=${apiKey}`,
  );
  source.onopen = () => {
    attempt = 0; // healthy again: reset the backoff
  };
  source.onmessage = (event) => {
    renderRoster(JSON.parse(event.data).states);
  };
  source.onerror = () => {
    source.close();
    const wait = Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 500;
    attempt += 1;
    setTimeout(connect, wait);
  };
}

connect();
```

Because every frame is a full snapshot, the first frame after a reconnect
is already the complete current roster; you do not need to fetch a
baseline separately.

## Common topologies

**Desk-phone roster.** Hard-phone BLF keys (Polycom, Yealink, and similar)
are provisioned per device and light their lamps over SIP; the
organization-curated BLF and speed-dial list is configured under channel
settings, see
[Channel settings consoles](/guides/channel-settings-consoles) and
[Desk phone provisioning](/guides/voice-desk-phone-provisioning). Use the
REST + SSE surface documented here for everything browser- or
server-side: softphone rosters, custom consoles, wallboards.

**Supervisor wallboard.** Hold one SSE stream per wallboard client (or one
backend stream fanned out to many screens) and render each `state` as a
lamp: green for `idle`, flashing for `early`, solid red for `confirmed`,
grey for `away` / `disabled`. Pair it with queue metrics from
[Supervisor live monitoring](/guides/supervisor-live-monitoring-voice) for
a complete floor view.

**Receptionist console.** Sort the roster by `extensionNumber` and render
`label` next to each lamp so the front desk can see at a glance who can
take the next transfer. The in-product
[Attendant console](/voice/attendant-console) renders the same states;
this API is what you build against when you need a custom layout.

There is no webhook or third-party push delivery for BLF presence. The
subscription model is SSE only; if your architecture needs server-to-server
fan-out, hold a single stream in your backend and republish to your
internal consumers from there.

## Cross-references

* [Voice supervisor plane model](/concepts/voice-supervisor-plane-model):
  how supervisor surfaces (wallboards, monitoring, coaching) fit together.
* [Supervisor live monitoring](/guides/supervisor-live-monitoring-voice):
  queue- and agent-level live views that complement the line-level view
  on this page.
* [Colleagues](/voice/colleagues): the in-product directory built on the
  same snapshot and stream.
* [Extensions](/voice/extensions): create, disable, and manage the SIP
  extensions that populate the roster, including each extension's
  do-not-disturb switch.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.