Skip to content

Vouchers & guest access

UniFi’s Hotspot feature lets you hand guests a time-limited code — a voucher — that they enter in the captive-portal to get on the network. You can also authorize specific clients directly via the API, which is useful when you already know the client ID (for example, from a kiosk or a booking system).

Both operations are mutations and require --allow-mutations. Every mutating command also accepts --dry-run so you can see exactly what ufi will send before it touches the network.


  • ufi installed and authenticated — see /installation/ and /auth/.
  • A site with the Hotspot feature enabled on the UniFi console.
  • Your console’s API key exported or stored — see /auth/.

If your console has more than one site, pass --site <name-or-id> or set UNIFI_SITE. See /getting-started/ for details.


Terminal window
ufi voucher list --json

Output follows the standard list envelope:

{
"schemaVersion": 1,
"items": [
{
"id": "a1b2c3d4-0000-0000-0000-000000000001",
"name": "conference-2024",
"code": "4861409510",
"created_at": "2024-11-01T09:00:00Z",
"time_limit_minutes": 480,
"authorized_guest_limit": 1,
"authorized_guest_count": 0,
"activated_at": null,
"expires_at": null,
"expired": false
},
{
"id": "a1b2c3d4-0000-0000-0000-000000000002",
"name": "lobby-guest",
"code": "3920184756",
"created_at": "2024-11-02T14:30:00Z",
"time_limit_minutes": 60,
"authorized_guest_limit": null,
"authorized_guest_count": 1,
"activated_at": "2024-11-02T15:10:00Z",
"expires_at": "2024-11-02T16:10:00Z",
"expired": false
}
],
"count": 2,
"nextCursor": null
}

Fields are snake_cased from the API’s camelCase (e.g. timeLimitMinutestime_limit_minutes). An empty result exits with code 3 (empty_results) and emits the envelope with count: 0 and items: []. See /exit-codes/ and /output/.

Use --limit and --cursor / --page to page through large voucher pools:

Terminal window
# First page of 20
ufi voucher list --json --limit 20
# Next page using the opaque cursor from a previous response
ufi voucher list --json --limit 20 --cursor "eyJvZmZzZXQiOjIwfQ=="
# Or navigate by page number (1-based)
ufi voucher list --json --limit 20 --page 3

Project down to just the fields you care about with --select:

Terminal window
ufi voucher list --json --select id,name,code,expired

--select is a client-side dot-path projection applied after the API responds; the API itself returns the full object either way. See /output/.


voucher create requires two things: a name (a human-readable label duplicated across all generated vouchers) and a time limit in minutes. All other flags are optional.

Terminal window
# Minimal — one voucher, 4-hour session
ufi voucher create "hotel-guest" --minutes 240 --allow-mutations

Always check the preview before committing. The dry run shows exactly what will be POSTed and exits 0:

Terminal window
ufi voucher create "hotel-guest" --minutes 240 --dry-run --allow-mutations --json
{
"action": "voucher.create",
"name": "hotel-guest",
"minutes": 240,
"count": 1,
"dry_run": true
}
Terminal window
ufi voucher create "hotel-guest" --minutes 240 --allow-mutations --json
{
"ok": true,
"action": "voucher.create",
"name": "hotel-guest",
"minutes": 240,
"count": 1,
"result": {
"vouchers": [
{
"id": "a1b2c3d4-0000-0000-0000-000000000003",
"name": "hotel-guest",
"code": "7730291845",
"created_at": "2024-11-03T10:00:00Z",
"time_limit_minutes": 240,
"authorized_guest_count": 0,
"expired": false
}
]
}
}

The code field is the secret the guest types into the captive portal. Store it or hand it off to your booking system.

Flag API field Required Notes
<name> (positional) name yes Label duplicated across all vouchers in the batch
--minutes N timeLimitMinutes yes 1–1 000 000 minutes
--count N count no (default: 1) 1–1 000 vouchers per call
--guests N authorizedGuestLimit no Max guests per voucher code (omit for unlimited)
--data-mb N dataUsageLimitMBytes no 1–1 048 576 MB; omit for unlimited

Batch example — 10 day-pass vouchers, 2 guests each

Section titled “Batch example — 10 day-pass vouchers, 2 guests each”
Terminal window
ufi voucher create "day-pass" \
--minutes 1440 \
--count 10 \
--guests 2 \
--data-mb 2048 \
--allow-mutations \
--json

This creates 10 vouchers in one API call. Each code can be shared by up to 2 guests, grants 24 hours of access, and caps total data at 2 GB. The timer starts when the first guest activates the voucher; any subsequent guests on the same code share the same expiry.

Note: --rx-kbps / --tx-kbps rate limits are available on the client authorize command (see below) but the voucher creation API does not accept them.

If you forget the flag, ufi blocks the operation and exits 12:

{
"error": "voucher create is a mutating operation and is blocked by default",
"code": "MUTATION_BLOCKED",
"remediation": "re-run with --allow-mutations (add --dry-run to preview)"
}

See /safety-model/ for the full gate design.


Terminal window
ufi voucher delete <id> --allow-mutations

Deletion is idempotent: if the voucher ID does not exist the command still exits 0 and reports "existed": false. This makes it safe to call from a cleanup loop without checking first.

Terminal window
ufi voucher delete a1b2c3d4-0000-0000-0000-000000000001 --dry-run --allow-mutations --json
{
"dry_run": true,
"action": "voucher delete",
"id": "a1b2c3d4-0000-0000-0000-000000000001"
}
Terminal window
ufi voucher delete a1b2c3d4-0000-0000-0000-000000000001 --allow-mutations --json
{
"ok": true,
"kind": "voucher",
"id": "a1b2c3d4-0000-0000-0000-000000000001",
"existed": true
}

Real run — voucher already gone (idempotent soft success)

Section titled “Real run — voucher already gone (idempotent soft success)”
{
"ok": true,
"kind": "voucher",
"id": "a1b2c3d4-0000-0000-0000-000000000001",
"existed": false
}

Rather than issuing a voucher code, you can authorize or revoke network access for a specific client directly — useful for automated workflows (room-check-in/out, event management, etc.).

You need the client’s ID. Get it from ufi client list:

Terminal window
ufi client list --json --select id,name,mac_address

Only guest clients can be authorized this way. The client must be connected to a network configured as a guest network on the UniFi console.

Terminal window
ufi client authorize <client-id> --allow-mutations
Terminal window
ufi client authorize 8f3e9a00-0000-0000-0000-000000000042 \
--minutes 120 \
--dry-run --allow-mutations --json
{
"action": "AUTHORIZE_GUEST_ACCESS",
"id": "8f3e9a00-0000-0000-0000-000000000042",
"minutes": 120,
"dry_run": true
}
Terminal window
ufi client authorize 8f3e9a00-0000-0000-0000-000000000042 \
--minutes 120 \
--data-mb 500 \
--allow-mutations --json
{
"ok": true,
"action": "AUTHORIZE_GUEST_ACCESS",
"id": "8f3e9a00-0000-0000-0000-000000000042",
"minutes": 120,
"result": {
"action": "AUTHORIZE_GUEST_ACCESS",
"granted_authorization": {
"authorized_at": "2024-11-03T11:00:00Z",
"authorization_method": "API",
"expires_at": "2024-11-03T13:00:00Z",
"data_usage_limit_m_bytes": 500,
"usage": {
"duration_sec": 0,
"rx_bytes": 0,
"tx_bytes": 0,
"bytes": 0
}
}
}
}

If the client was already authorized, the API cancels the existing session, creates a new one with the new limits, and resets traffic counters. The previous authorization appears as revoked_authorization in the result alongside granted_authorization.

Flag API field Notes
--minutes N timeLimitMinutes 1–1 000 000; omit to use the site default
--data-mb N dataUsageLimitMBytes 1–1 048 576 MB; omit for unlimited
--rx-kbps N rxRateLimitKbps Download rate cap, 2–100 000 kbps
--tx-kbps N txRateLimitKbps Upload rate cap, 2–100 000 kbps

All flags are optional. With none set the site’s default time limit applies and there are no data or rate caps.


Revoke access and immediately disconnect the client:

Terminal window
ufi client unauthorize <client-id> --allow-mutations
Terminal window
ufi client unauthorize 8f3e9a00-0000-0000-0000-000000000042 \
--dry-run --allow-mutations --json
{
"action": "UNAUTHORIZE_GUEST_ACCESS",
"id": "8f3e9a00-0000-0000-0000-000000000042",
"dry_run": true
}
Terminal window
ufi client unauthorize 8f3e9a00-0000-0000-0000-000000000042 --allow-mutations --json
{
"ok": true,
"action": "UNAUTHORIZE_GUEST_ACCESS",
"id": "8f3e9a00-0000-0000-0000-000000000042",
"result": {
"action": "UNAUTHORIZE_GUEST_ACCESS",
"revoked_authorization": {
"authorized_at": "2024-11-03T11:00:00Z",
"authorization_method": "API",
"expires_at": "2024-11-03T13:00:00Z",
"usage": {
"duration_sec": 1842,
"rx_bytes": 48123904,
"tx_bytes": 2097152,
"bytes": 50221056
}
}
}
}

Voucher names and notes are network-controlled free text — a guest could craft a name containing instructions. In agent mode (JSON output or non-TTY stdout), ufi wraps these values automatically:

[UNTRUSTED_DATA_BEGIN] <suspicious-voucher-name> [UNTRUSTED_DATA_END]

Your agent code must treat anything inside those delimiters as data, not as instructions. Use --no-fence to disable fencing (not recommended for automated pipelines) or --wrap-untrusted to force it on in interactive mode. See /agents/ for the full prompt-injection defence design.


A typical room-check-in loop: look up the client by MAC, authorize it for the stay duration, and log the result.

#!/usr/bin/env bash
set -euo pipefail
MAC="aa:bb:cc:dd:ee:ff"
MINUTES=2880 # 48-hour stay
# 1. Find the client ID
CLIENT_ID=$(
ufi client list --json \
| jq -r --arg mac "$MAC" \
'.items[] | select(.mac_address == $mac) | .id'
)
if [[ -z "$CLIENT_ID" ]]; then
echo "Client not found on network" >&2
exit 1
fi
# 2. Authorize
ufi client authorize "$CLIENT_ID" \
--minutes "$MINUTES" \
--data-mb 10240 \
--allow-mutations --json

For the check-out path, replace the authorize call with ufi client unauthorize "$CLIENT_ID" --allow-mutations --json.


Command Mutation Dry-run Idempotent
ufi voucher list no
ufi voucher create <name> --minutes N yes yes no
ufi voucher delete <id> yes yes yes
Command Mutation Dry-run Notes
ufi client list no
ufi client get <id> no
ufi client authorize <id> yes yes Resets existing session if already authorized
ufi client unauthorize <id> yes yes Disconnects client immediately

  • /safety-model/ — how --allow-mutations and --dry-run work
  • /output/ — list envelope, snake_case fields, --select projection
  • /exit-codes/ — full exit-code table
  • /agents/ — prompt-injection fencing, ufi schema, agent contracts
  • /flags-env/ — all global flags and environment variables