> ## 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.

# Desk-phone fleet rollout: bulk zero-touch provisioning at scale

> The operator runbook for a 200-handset install: pre-stage extensions and the MAC list, bulk-register by CSV inside the rate window, pair phones to extensions, run zero-touch day per vendor redirect server, audit the floor, and recover the rows that did not come online.

# Desk-phone fleet rollout

[Extensions and desk phones](/guides/extensions-and-desk-phones) and
[Desk phones: zero-touch provisioning](/guides/voice-desk-phone-provisioning)
cover the console flow end to end. This guide is the runbook for the
other half of a real install: the monitor and recovery loop of a bulk
rollout, from the MAC export to handing facilities a working floor.

It assumes one workspace, one site, and a fleet of Polycom, Yealink,
Cisco, or Grandstream handsets. Everything here is available to the
**owner** and **admin** roles.

## Pre-stage the fleet

Do this before any phone is unboxed:

* **Handsets on hand.** Confirm every SKU is one of the four supported
  vendors: **Polycom**, **Yealink**, **Cisco**, **Grandstream**. The
  importer rejects any other vendor row by row.
* **MAC list exported.** Pull the MACs from the vendor shipping manifest
  or the carton labels into one spreadsheet, one row per phone, and
  dedupe it before upload. The importer normalises `:` , `-` and `.`
  separators, so `00:04:f2:aa:bb:cc` and `00-04-F2-AA-BB-CC` are the same
  phone; dedupe on the normalised form, not the raw string.
* **Extensions pre-created.** Create one extension per phone (plus a
  handful of spares) under **Voice → Extensions** before zero-touch day.
  Give each a 2–8 digit extension number from a reserved range (for
  example `2101`–`2300` for a 200-seat floor) so colleagues can
  short-dial, and keep a label convention that matches the phone label
  column below (`FL1-101`, `FL1-102`, …). Pairing later is a picker, not a
  form: the extension has to exist to be picked.
* **Network ready.** The redirect servers are the vendor clouds
  (Polycom ZTP, Yealink RPS, Cisco EDOS, Grandstream GDMS), so there is
  no on-prem TFTP server and no DHCP option to maintain. The handsets
  need HTTPS egress to the vendor portal and to the Orbit SIP edge over
  TLS, power (PoE switch ports or adapters), and whatever voice-VLAN
  hygiene you already run. Disable SIP-ALG on the site router; it
  rewrites SIP headers and breaks REGISTER bindings.
* **Rate window planned.** Registration is limited to **50 phones per
  hour per workspace**, and the bulk importer posts rows one at a time
  against that window. A 200-phone fleet is four uploads, an hour apart.
  Schedule them; do not fight the window. Uploads of more than 50 rows
  run until the window fills, and the surplus rows land in the failed
  list with a rate-limit reason.

The CSV template below is the file you will upload in the next step;
build it from the deduped MAC export and your label convention:

```csv theme={null}
mac,vendor,label,model
00:04:f2:aa:bb:cc,polycom,FL1-101,VVX 450
00:04:f2:aa:bb:cd,polycom,FL1-102,VVX 450
80:5e:c0:11:22:33,yealink,FL1-103,T46U
80:5e:c0:11:22:34,yealink,FL1-104,T46U
aabb.ccdd.ee01,cisco,FL2-201,8851
aabb.ccdd.ee02,cisco,FL2-202,8851
0c:3a:2b:99:00:11,grandstream,FL2-203,GRP2612
0c:3a:2b:99:00:12,grandstream,FL2-204,GRP2612
```

## Bulk-register by MAC

In **Voice → Desk phones**, click **Bulk import** and upload the CSV.
The header row is required; `mac` and `vendor` are required columns,
`label` and `model` optional. Column order and case do not matter, and
common aliases are accepted (`mac address`, `make`, `manufacturer`,
`name`, `description`).

The preview stage shows how many rows are ready and lists parse
rejections with line numbers before anything is posted. After the
import, the result panel reports three counts:

* **Registered** — the row created a device. The phone now has a
  provisioning URL and a 30-day claim window.
* **Skipped** — the row never reached the server: a cell that does not
  reduce to a 12-digit hex MAC, or a vendor outside the four supported.
  Fix the line and re-upload.
* **Failed** — the server refused the row: a duplicate MAC (with the
  row's line number), or a MAC live in another workspace. Everything
  else in the batch is unaffected.

Re-uploading is safe and is the intended recovery loop: rows that
registered on a previous upload report as duplicates in the failed
list, not as errors, and the new rows register. Keep the same file,
fix the rejected lines, upload again.

For the 200-phone template above, upload rows in batches of 50 or
fewer per hour. The result panel's per-line reasons are your source of
truth; export the failed list before closing the dialog.

## Pair phones to extensions

A registered phone is inert until it is paired with a SIP credential.
There are two lanes; pick per batch, not per phone:

* **Single create with pairing** — the **Register desk phone** dialog
  (or `POST /api/v1/voice/devices` with `sipCredentialId`) registers and
  pairs in one step. Use it for day-two additions, replacements, and
  small batches where the extension is known at register time.
* **Bulk CSV now, pair after** — the CSV lane registers only
  (`mac`, `vendor`, `label`, `model`; there is no credential column).
  After the batch registers, pair each device from its row in
  **Voice → Desk phones**, or with
  `PATCH /api/v1/voice/devices/:id` setting `sipCredentialId`. Use this
  for the fleet: registration is the rate-limited step, pairing is not,
  so register the whole batch first and pair at your pace.

Two rules for the pairing pass:

* An extension pairs to a phone by picking it; Orbit does not reserve
  an extension to one device. If you pair two phones to the same
  extension, both ring as that extension. For one phone per seat, pair
  a dedicated extension and keep shared extensions deliberate.
* Pairing a credential that does not exist in this workspace fails
  with a 404. The guard does not confirm or deny credentials outside
  your workspace; double-check the id came from
  **Voice → Extensions**.

Pair every row before zero-touch day where you can. A phone that boots
**unpaired** fetches its config and receives a refusal; that attempt
does not spend the one-shot provisioning URL, so once you pair the row
the next boot provisions cleanly. Booting paired is still the calmer
order.

## Zero-touch day

Each phone already knows how to reach its vendor cloud on first boot;
the vendor cloud hands it your provisioning URL. For every MAC, the
record the portal carries is the MAC plus the device's provisioning
URL (on the device row in **Voice → Desk phones**, and in the create
response if you registered via API):

```text theme={null}
mac:       00:04:f2:aa:bb:cc
redirect:  https://api.orbit.devotel.io/api/v1/voice/provisioning/0004f2aabbcc.xml?token=<token>
```

| Vendor | Redirect service | Entry |
| - | - | - |
| Polycom | Polycom ZTP | MAC exactly as printed on the label, URL pasted as the redirect target |
| Yealink | Yealink RPS | The phone's MAC, URL pasted as the redirect target |
| Cisco | Cisco EDOS | The phone's MAC, URL pasted as the redirect target |
| Grandstream | Grandstream GDMS | The phone's MAC, URL pasted as the redirect target |

Enter the MAC in the notation the portal prints; the URL is pasted
verbatim, token included. Where the vendor cloud accepted the mapping
automatically at create time (the create response and the audit log
say so), the record is already there and the portal entry is a
verification, not a setup step. For vendors without auto-registration,
the manual entry is the setup.

<Warning>
  The provisioning URL carries a signed, single-use token bound to the
  MAC. The export of MAC→URL records is a credentials file: store it like
  one, and never email it as plain text in the body.
</Warning>

On the day, facilities powers the phones (network + PoE), and each
handset factory-reset boots through its redirect, pulls its rendered
config, and registers. "Came online" is two signals agreeing:

1. **Voice → Desk phones** — the device row shows a last provisioning
   fetch with timestamp and source IP. The config left the building.
2. **Voice → Extensions → the paired extension** — the **Live status**
   card reads **Registered**, with the UTC time and source IP of the
   last REGISTER. The phone is on the SIP edge.

Expect the floor to come up in waves as phones boot; a 200-phone
floor typically settles within the hour.

## Verify the floor

Spot-check online status from the API instead of clicking 200 rows.
`GET /api/v1/voice/devices` returns each device with `macAddress`,
`label`, `sipCredentialId`, `provisioningLockedAt` (set when the phone
fetched its config), `claimExpiresAt`, and `disabled`, paginated by
cursor (`limit` up to 200; follow `next_cursor` for larger fleets):

```bash theme={null}
curl -s "https://api.orbit.devotel.io/api/v1/voice/devices?limit=200" \
  -H "Authorization: Bearer $ORBIT_TOKEN" | jq -r '
  .data[] |
  [ .macAddress,
    (.label // "-"),
    (if .sipCredentialId     then "paired"     else "UNPAIRED"  end),
    (if .provisioningLockedAt then "fetched"   else "NOT-FETCHED" end),
    (if .disabled            then "DISABLED"  else "active"    end)
  ] | @tsv'
```

Every row should read `paired / fetched / active`. The three bad
readings map to three recovery loops:

* **UNPAIRED** — the pairing pass missed the row. Pair it from the
  device row; the phone provisions on its next boot with no further
  action.
* **NOT-FETCHED** — the phone never reached its redirect. Confirm the
  vendor portal carries the exact MAC→URL record, confirm the handset
  is powered and has egress, then factory-reset it. If the claim
  window expired in transit (the row reads **Expired**), run **Rotate
  provisioning URL** on the row and reset the phone; rotation re-opens
  the window.
* **Fetched but Not registered** — the config left our side and the
  REGISTER is failing at the site. Check the extension's **Live
  status** card: if it flips `Registered` → `Not registered`, the
  re-register is failing. Confirm the password has not rotated since
  the phone fetched, that SIP-ALG is off, and that the site allows TLS
  egress to the SIP edge. If an allowed-source-IP list is set on the
  extension, the phone's egress IP must match it.

The phone-side indicator for the last class is the extension's **Not
registered** state (the binding expired); brief gaps self-heal because
phones re-REGISTER every 60–300 seconds, so act on persistent readings,
not a single poll. When a handset is genuinely wedged, the clean
re-provision is: rotate the extension's password, delete the device
row, re-register the MAC (it re-tickets as a fresh device), re-pair,
and factory-reset the phone.

## Failure reference

| Symptom | What it is | Recovery |
| - | - | - |
| Failed row, line number named, "already registered to your organization" | The MAC is already live in this workspace; on a re-upload this is expected for rows that registered earlier. | Leave it. Only act when the MAC was not in a previous result panel. |
| Failed row, "already registered (contact support to transfer)" | The MAC is live in another workspace: mis-ticketed stock or a reseller batch. | Support transfers the row; do not delete and retry locally. |
| Re-registering a torn-down MAC succeeds as a new device | A deleted row still holds the MAC until it is re-ticketed; re-registering revives it with a fresh token and claim window. | Not an error. Re-pair and reset the phone. |
| 404 pairing a `sipCredentialId` | The credential is not in this workspace; the guard neither confirms nor denies foreign rows. | Copy the id from **Voice → Extensions** and retry. |
| Create response says the vendor-cloud redirect was not registered | The vendor portal was unreachable or refused at create time; the device itself registered fine. | Paste the MAC→URL record into the vendor portal by hand before boot. |
| Phone fetch returns a generic 403 ("provisioning unavailable") | One refusal body covers every failure by design: unknown MAC, expired claim, wrong or spent token, disabled device, unpaired row. It also answers attackers probing for live MACs, so it stays generic on purpose. | Work the row in the dashboard: claim state, paired, disabled. **Rotate provisioning URL** mints a fresh token after a leak or a spent URL; reset the phone after. |
| Registered, fetched, then REGISTER loops | Provisioning worked; the SIP binding is failing at the network. | The "fetched but Not registered" loop above; [SIP trunk troubleshooting](/troubleshooting/sip-trunk) covers the REGISTER loop in depth. |

## Handoff and day two

Hand facilities a one-page walk-through so day one does not need you
in the room:

1. Connect network and power (PoE or adapter); wait for the boot
   sequence to finish. The phone shows its label and extension once
   registered.
2. Place an internal call to a colleague's extension number from the
   reserved range; then one external call.
3. If the remote phonebook or line keys are enabled for the workspace,
   the colleague list and BLF keys arrive with the config; a missing
   directory is a phonebook setting, not a failed rollout.
4. A phone stuck mid-boot gets one factory reset before it becomes
   your problem; the recovery loop above is yours.

Day-two additions and single replacements use the single-phone flow:
**Register desk phone**, pair, paste or confirm the redirect entry,
reset the handset. The full per-step console and API reference is
[Desk phones: zero-touch provisioning](/guides/voice-desk-phone-provisioning);
the credential lifecycle (rotate, disable, delete) is
[Extensions: reference](/voice/extensions).

All SIP traffic the provisioned phones carry exits through the
Devotel wholesale softswitch; the rendered config points handsets at
the Devotel-routed SIP edge only. There is nothing to configure for
this; it is stated so a network team auditing the floor knows what to
expect.

## More

* [Extensions and desk phones](/guides/extensions-and-desk-phones) —
  create the SIP credentials the fleet pairs with.
* [Desk phones: zero-touch provisioning](/guides/voice-desk-phone-provisioning) —
  the single-phone console flow and devices API reference.
* [SIP trunk troubleshooting](/troubleshooting/sip-trunk) — REGISTER
  loops and binding failures.
* [Voice quickstart](/voice/quickstart) — numbers, routing, and test
  calls.


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