UnifiApi

Elixir HTTP client for UniFi Dream Machine APIs, covering both the Network API (v10.1.84) and the Protect API (v6.2.88). Built on Req.

Installation

Add unifi_api to your list of dependencies in mix.exs:

def deps do
  [
    {:unifi_api, "~> 0.2.0"}
  ]
end

Configuration

Application config

# config/config.exs
config :unifi_api,
  base_url: "https://192.168.1.1",
  api_key: "your-api-key",
  verify_ssl: false,
  # UDM defaults — for Cloud Key, set both to "/integration"
  network_path: "/proxy/network/integration",
  protect_path: "/proxy/protect/integration"

Environment variables

# config/runtime.exs
config :unifi_api,
  base_url: System.get_env("UNIFI_BASE_URL", "https://192.168.1.1"),
  api_key: System.get_env("UNIFI_API_KEY", ""),
  verify_ssl: System.get_env("UNIFI_VERIFY_SSL", "false") == "true",
  network_path: System.get_env("UNIFI_NETWORK_PATH", "/proxy/network/integration"),
  protect_path: System.get_env("UNIFI_PROTECT_PATH", "/proxy/protect/integration")

Path prefixes

On UDM / UDM Pro / UDM SE (UniFi OS), the API runs behind a reverse proxy:

API Default path Env var
Network /proxy/network/integrationUNIFI_NETWORK_PATH
Protect /proxy/protect/integrationUNIFI_PROTECT_PATH

On Cloud Key or standalone controllers, set both to "/integration":

config :unifi_api,
  network_path: "/integration",
  protect_path: "/integration"

Runtime override

Pass options directly when creating a client to override application config:

client = UnifiApi.new(
  base_url: "https://192.168.0.1",
  api_key: "my-api-key",
  verify_ssl: false
)

Quick Start

# Create a client (uses application config)
client = UnifiApi.new()

# Or with explicit options
client = UnifiApi.new(base_url: "https://192.168.1.1", api_key: "my-key")

# Check the controller version
{:ok, info} = UnifiApi.Network.Info.get_info(client)
# => {:ok, %{"applicationVersion" => "10.1.84"}}

# List all sites
{:ok, sites} = UnifiApi.Network.Sites.list(client)

# List devices on a site
{:ok, devices} = UnifiApi.Network.Devices.list(client, "site-uuid")

# List Protect cameras
{:ok, cameras} = UnifiApi.Protect.Cameras.list(client)

Network API

All Network API functions require a site_id (except Info, Resources.list_dpi_categories/2, Resources.list_dpi_applications/2, Resources.list_countries/2, and Devices.list_pending/2).

Sites

{:ok, sites} = UnifiApi.Network.Sites.list(client)
# => {:ok, [%{"id" => "abc-123", "name" => "Default", "internalReference" => "default"}]}

Devices

# List all devices on a site
{:ok, devices} = UnifiApi.Network.Devices.list(client, site_id)

# Get a specific device
{:ok, device} = UnifiApi.Network.Devices.get(client, site_id, device_id)

# Get latest device statistics
{:ok, stats} = UnifiApi.Network.Devices.get_statistics(client, site_id, device_id)

# Adopt a new device
{:ok, _} = UnifiApi.Network.Devices.adopt(client, site_id, %{mac: "aa:bb:cc:dd:ee:ff"})

# Execute a device action (restart, locate, etc.)
{:ok, _} = UnifiApi.Network.Devices.execute_action(client, site_id, device_id, %{action: "restart"})

# Execute a port action (PoE cycle, etc.)
{:ok, _} = UnifiApi.Network.Devices.execute_port_action(client, site_id, device_id, 3, %{action: "cycle"})

# Remove a device
{:ok, _} = UnifiApi.Network.Devices.remove(client, site_id, device_id)

# List pending devices (not site-scoped)
{:ok, pending} = UnifiApi.Network.Devices.list_pending(client)

Clients

# List connected clients
{:ok, clients} = UnifiApi.Network.Clients.list(client, site_id)
# Each client has: type (WIRED/WIRELESS/VPN/TELEPORT), id, name, connectedAt, ipAddress, access

Networks

# List all networks
{:ok, networks} = UnifiApi.Network.Networks.list(client, site_id)

# Get a specific network
{:ok, network} = UnifiApi.Network.Networks.get(client, site_id, network_id)

# Create a network
{:ok, network} = UnifiApi.Network.Networks.create(client, site_id, %{
  name: "Guest VLAN",
  vlanId: 100
})

# Update a network
{:ok, _} = UnifiApi.Network.Networks.update(client, site_id, network_id, %{name: "New Name"})

# Delete a network
{:ok, _} = UnifiApi.Network.Networks.delete(client, site_id, network_id)

WiFi

# List WiFi broadcasts (SSIDs)
{:ok, ssids} = UnifiApi.Network.Wifi.list(client, site_id)

Firewall

# --- Zones ---
{:ok, zones} = UnifiApi.Network.Firewall.list_zones(client, site_id)
{:ok, zone} = UnifiApi.Network.Firewall.get_zone(client, site_id, zone_id)
{:ok, zone} = UnifiApi.Network.Firewall.create_zone(client, site_id, %{name: "DMZ", networkIds: [net_id]})
{:ok, _} = UnifiApi.Network.Firewall.update_zone(client, site_id, zone_id, %{name: "DMZ-Updated"})
{:ok, _} = UnifiApi.Network.Firewall.delete_zone(client, site_id, zone_id)

# --- Policies ---
{:ok, policies} = UnifiApi.Network.Firewall.list_policies(client, site_id)
{:ok, policy} = UnifiApi.Network.Firewall.get_policy(client, site_id, policy_id)
{:ok, policy} = UnifiApi.Network.Firewall.create_policy(client, site_id, %{
  name: "Block IoT to LAN",
  enabled: true,
  action: "BLOCK",
  source: %{zoneId: iot_zone_id},
  destination: %{zoneId: lan_zone_id}
})
{:ok, _} = UnifiApi.Network.Firewall.update_policy(client, site_id, policy_id, %{enabled: false})
{:ok, _} = UnifiApi.Network.Firewall.delete_policy(client, site_id, policy_id)

Hotspot Vouchers

# List all vouchers
{:ok, vouchers} = UnifiApi.Network.Hotspot.list_vouchers(client, site_id)

# Get a specific voucher
{:ok, voucher} = UnifiApi.Network.Hotspot.get_voucher(client, site_id, voucher_id)

# Create vouchers (1-1000 at a time)
{:ok, vouchers} = UnifiApi.Network.Hotspot.create_vouchers(client, site_id, %{
  count: 10,
  name: "Event Pass",
  timeLimitMinutes: 1440,
  authorizedGuestLimit: 1,
  dataUsageLimitMBytes: 500,
  rxRateLimitKbps: 5000,
  txRateLimitKbps: 1000
})

# Delete a specific voucher
{:ok, _} = UnifiApi.Network.Hotspot.delete_voucher(client, site_id, voucher_id)

# Delete all vouchers
{:ok, _} = UnifiApi.Network.Hotspot.delete_vouchers(client, site_id)

ACL Rules

# List ACL rules
{:ok, rules} = UnifiApi.Network.ACL.list(client, site_id)

# Create an ACL rule
{:ok, rule} = UnifiApi.Network.ACL.create(client, site_id, %{
  type: "IPV4",
  name: "Block SSH",
  enabled: true,
  action: "BLOCK",
  protocolFilter: %{protocol: "TCP", dstPort: 22}
})

# Update and delete
{:ok, _} = UnifiApi.Network.ACL.update(client, site_id, rule_id, %{enabled: false})
{:ok, _} = UnifiApi.Network.ACL.delete(client, site_id, rule_id)

# Manage rule ordering
{:ok, ordering} = UnifiApi.Network.ACL.get_ordering(client, site_id)
{:ok, _} = UnifiApi.Network.ACL.update_ordering(client, site_id, %{ids: ["rule-1", "rule-2"]})

DNS Policies

# List DNS policies
{:ok, policies} = UnifiApi.Network.DNS.list(client, site_id)

# Create a DNS record
{:ok, policy} = UnifiApi.Network.DNS.create(client, site_id, %{
  type: "A_RECORD",
  name: "app.local",
  value: "192.168.1.50"
})

# Supported types: A_RECORD, AAAA_RECORD, CNAME_RECORD, MX_RECORD,
#                  TXT_RECORD, SRV_RECORD, FORWARD_DOMAIN

Traffic Matching

{:ok, lists} = UnifiApi.Network.TrafficMatching.list(client, site_id)
# Types: PORTS, IPV4_ADDRESSES, IPV6_ADDRESSES

Supporting Resources

# WAN interfaces
{:ok, wans} = UnifiApi.Network.Resources.list_wans(client, site_id)

# VPN
{:ok, tunnels} = UnifiApi.Network.Resources.list_vpn_tunnels(client, site_id)
{:ok, servers} = UnifiApi.Network.Resources.list_vpn_servers(client, site_id)

# RADIUS
{:ok, profiles} = UnifiApi.Network.Resources.list_radius_profiles(client, site_id)

# Device tags
{:ok, tags} = UnifiApi.Network.Resources.list_device_tags(client, site_id)

# DPI (not site-scoped)
{:ok, categories} = UnifiApi.Network.Resources.list_dpi_categories(client)
{:ok, apps} = UnifiApi.Network.Resources.list_dpi_applications(client)

# Countries (not site-scoped)
{:ok, countries} = UnifiApi.Network.Resources.list_countries(client)

Protect API

Protect endpoints are not site-scoped.

Cameras

# List all cameras
{:ok, cameras} = UnifiApi.Protect.Cameras.list(client)

# Get a specific camera
{:ok, camera} = UnifiApi.Protect.Cameras.get(client, camera_id)

# Update camera settings
{:ok, _} = UnifiApi.Protect.Cameras.update(client, camera_id, %{
  name: "Front Door",
  micVolume: 80,
  videoMode: "highFps",
  ledSettings: %{isEnabled: false}
})

# Take a snapshot (returns JPEG binary)
{:ok, jpeg} = UnifiApi.Protect.Cameras.snapshot(client, camera_id)
File.write!("snapshot.jpg", jpeg)

# High quality snapshot
{:ok, jpeg} = UnifiApi.Protect.Cameras.snapshot(client, camera_id, high_quality: true)

# PTZ controls
{:ok, _} = UnifiApi.Protect.Cameras.ptz_goto(client, camera_id, 1)           # Go to preset slot 1
{:ok, _} = UnifiApi.Protect.Cameras.ptz_patrol_start(client, camera_id, 0)   # Start patrol slot 0
{:ok, _} = UnifiApi.Protect.Cameras.ptz_patrol_stop(client, camera_id)       # Stop patrol

NVR

{:ok, nvr} = UnifiApi.Protect.NVR.get(client)
# => {:ok, %{"id" => "...", "name" => "UNVR", "doorbellSettings" => %{...}}}

Viewers

{:ok, viewers} = UnifiApi.Protect.Viewers.list(client)
{:ok, viewer} = UnifiApi.Protect.Viewers.get(client, viewer_id)
{:ok, _} = UnifiApi.Protect.Viewers.update(client, viewer_id, %{liveview: liveview_id})

Liveviews

{:ok, liveviews} = UnifiApi.Protect.Liveviews.list(client)
# Each has: id, name, isDefault, isGlobal, owner, layout (1-26), slots

Sensors

{:ok, sensors} = UnifiApi.Protect.Sensors.list(client)
# Each has: id, name, state, mountType, batteryStatus, stats,
#           isOpened, isMotionDetected, temperature/humidity/light/leak settings

Lights

{:ok, lights} = UnifiApi.Protect.Lights.list(client)
# Each has: id, name, state, isDark, isLightOn, lastMotion,
#           lightModeSettings, lightDeviceSettings, camera

Chimes

{:ok, chimes} = UnifiApi.Protect.Chimes.list(client)
# Each has: id, name, state, cameraIds, ringSettings

Streaming & Pagination

Every list endpoint has a stream variant that returns a lazy Stream powered by Stream.resource/3. Pages are fetched on demand — no data is pulled until you consume the stream with Enum or Stream functions.

Lazy streaming (recommended)

# Stream ALL devices across pages — fetches 200 per page automatically
UnifiApi.Network.Devices.stream(client, site_id)
|> Enum.to_list()

# Only the first page is fetched
UnifiApi.Network.Devices.stream(client, site_id)
|> Enum.take(5)

# Filter + stream — composable with the full Stream/Enum API
UnifiApi.Network.Clients.stream(client, site_id, filter: "type.eq(WIRELESS)")
|> Stream.map(& &1["name"])
|> Enum.to_list()

# Count all clients without loading them all into memory at once
UnifiApi.Network.Clients.stream(client, site_id)
|> Enum.count()

# Custom page size
UnifiApi.Network.Devices.stream(client, site_id, limit: 50)
|> Enum.to_list()

# Stream firewall policies, vouchers, ACL rules, DNS, etc.
UnifiApi.Network.Firewall.stream_policies(client, site_id)
|> Stream.filter(& &1["enabled"])
|> Enum.to_list()

UnifiApi.Network.Hotspot.stream_vouchers(client, site_id)
|> Stream.reject(& &1["expired"])
|> Enum.map(& &1["code"])

UnifiApi.Network.Resources.stream_dpi_categories(client)
|> Enum.to_list()

Stream functions raise on API errors, making them safe to compose in pipelines.

Available stream functions

Module Function
Sites stream/2
Devices stream/3, stream_pending/2
Clients stream/3
Networks stream/3
Wifi stream/3
Firewall stream_zones/3, stream_policies/3
Hotspot stream_vouchers/3
ACL stream/3
DNS stream/3
TrafficMatching stream/3
Resources stream_wans/3, stream_vpn_tunnels/3, stream_vpn_servers/3, stream_radius_profiles/3, stream_device_tags/3, stream_dpi_categories/2, stream_dpi_applications/2, stream_countries/2

Manual pagination

If you need per-page control, use list with :offset and :limit:

{:ok, page1} = UnifiApi.Network.Devices.list(client, site_id, limit: 50, offset: 0)
{:ok, page2} = UnifiApi.Network.Devices.list(client, site_id, limit: 50, offset: 50)

# Filter (UniFi filter expression syntax)
{:ok, wireless} = UnifiApi.Network.Clients.list(client, site_id,
  filter: "type.eq(WIRELESS)"
)

Filter syntax

Filters use the format property.function(args) and can be combined:

Function Example
eqname.eq(Office)
netype.ne(WIRELESS)
gt, ge, lt, leconnectedAt.gt(1700000000)
in, notIntype.in(WIRED,VPN)
likename.like(cam*)
isNull, isNotNullipAddress.isNotNull()
isEmptyname.isEmpty()
contains, containsAny, containsAll, containsExactlytags.contains(vip)

Combine with and(), or(), not().

Data Extraction Recipes

Common patterns for pulling structured data out of your UniFi controller.

Export all clients to a list of maps

client = UnifiApi.new()

all_clients =
  UnifiApi.Network.Sites.stream(client)
  |> Enum.flat_map(fn site ->
    UnifiApi.Network.Clients.stream(client, site["id"])
    |> Enum.map(&Map.put(&1, "site", site["name"]))
  end)

# Filter only wireless clients
wireless = Enum.filter(all_clients, &(&1["type"] == "WIRELESS"))

Build a device inventory CSV

client = UnifiApi.new()

rows =
  UnifiApi.Network.Sites.stream(client)
  |> Enum.flat_map(fn site ->
    UnifiApi.Network.Devices.stream(client, site["id"])
    |> Enum.map(fn device ->
      [site["name"], device["name"], device["mac"], device["model"], device["state"]]
      |> Enum.join(",")
    end)
  end)

csv = ["site,name,mac,model,state" | rows] |> Enum.join("\n")
File.write!("devices.csv", csv)

Scrape all camera snapshots

client = UnifiApi.new()
{:ok, cameras} = UnifiApi.Protect.Cameras.list(client)

for camera <- cameras, camera["state"] == "CONNECTED" do
  case UnifiApi.Protect.Cameras.snapshot(client, camera["id"], high_quality: true) do
    {:ok, jpeg} ->
      name = camera["name"] |> String.replace(~r/[^\w]/, "_")
      File.write!("snapshots/#{name}.jpg", jpeg)

    {:error, reason} ->
      IO.puts("Failed #{camera["name"]}: #{inspect(reason)}")
  end
end

Collect network topology (sites, networks, devices)

client = UnifiApi.new()

topology =
  UnifiApi.Network.Sites.stream(client)
  |> Enum.map(fn site ->
    sid = site["id"]

    %{
      site: site["name"],
      networks:
        UnifiApi.Network.Networks.stream(client, sid)
        |> Enum.map(&Map.take(&1, ["id", "name", "vlanId", "subnet"])),
      devices:
        UnifiApi.Network.Devices.stream(client, sid)
        |> Enum.map(&Map.take(&1, ["id", "name", "mac", "model", "state"]))
    }
  end)

Monitor connected client count over time

client = UnifiApi.new()
[site | _] = UnifiApi.Network.Sites.stream(client) |> Enum.take(1)

# Poll every 60 seconds
Stream.interval(60_000)
|> Stream.map(fn _ ->
  counts =
    UnifiApi.Network.Clients.stream(client, site["id"])
    |> Enum.group_by(& &1["type"])
    |> Map.new(fn {type, list} -> {type, length(list)} end)

  {DateTime.utc_now(), counts}
end)
|> Stream.each(fn {time, counts} ->
  IO.puts("#{time} | WIRED=#{counts["WIRED"] || 0} WIRELESS=#{counts["WIRELESS"] || 0} VPN=#{counts["VPN"] || 0}")
end)
|> Stream.run()

Export firewall rules

client = UnifiApi.new()

UnifiApi.Network.Sites.stream(client)
|> Enum.map(fn site ->
  sid = site["id"]

  %{
    site: site["name"],
    zones:
      UnifiApi.Network.Firewall.stream_zones(client, sid)
      |> Enum.map(&Map.take(&1, ["id", "name", "networkIds"])),
    policies:
      UnifiApi.Network.Firewall.stream_policies(client, sid)
      |> Enum.map(&Map.take(&1, ["id", "name", "enabled", "action", "source", "destination"]))
  }
end)

Export hotspot voucher codes

client = UnifiApi.new()
[site | _] = UnifiApi.Network.Sites.stream(client) |> Enum.take(1)

active =
  UnifiApi.Network.Hotspot.stream_vouchers(client, site["id"])
  |> Stream.reject(& &1["expired"])
  |> Enum.map(&Map.take(&1, ["code", "name", "timeLimitMinutes", "expiresAt"]))

# Print as a table
for v <- active do
  IO.puts("#{v["code"]}  #{v["name"]}  #{v["timeLimitMinutes"]}min")
end

Dump all Protect device info

client = UnifiApi.new()

{:ok, cameras} = UnifiApi.Protect.Cameras.list(client)
{:ok, sensors} = UnifiApi.Protect.Sensors.list(client)
{:ok, lights} = UnifiApi.Protect.Lights.list(client)
{:ok, chimes} = UnifiApi.Protect.Chimes.list(client)
{:ok, nvr} = UnifiApi.Protect.NVR.get(client)

protect_inventory = %{
  nvr: Map.take(nvr, ["id", "name", "modelKey"]),
  cameras: Enum.map(cameras, &Map.take(&1, ["id", "name", "state", "mac", "modelKey"])),
  sensors: Enum.map(sensors, &Map.take(&1, ["id", "name", "state", "batteryStatus"])),
  lights: Enum.map(lights, &Map.take(&1, ["id", "name", "state", "isLightOn"])),
  chimes: Enum.map(chimes, &Map.take(&1, ["id", "name", "state"]))
}

Dashboard data scraper

Pull everything you need for a custom dashboard in one shot — network overview, client breakdown, device health, WiFi status, and Protect camera states.

client = UnifiApi.new()

# Get controller info
{:ok, info} = UnifiApi.Network.Info.get_info(client)

# Collect per-site data
sites_data =
  UnifiApi.Network.Sites.stream(client)
  |> Enum.map(fn site ->
    sid = site["id"]

    # Clients grouped by type
    clients = UnifiApi.Network.Clients.stream(client, sid) |> Enum.to_list()

    client_breakdown =
      clients
      |> Enum.group_by(& &1["type"])
      |> Map.new(fn {type, list} -> {type, length(list)} end)

    # Devices with health status
    devices =
      UnifiApi.Network.Devices.stream(client, sid)
      |> Enum.map(fn d ->
        %{
          name: d["name"],
          mac: d["mac"],
          model: d["model"],
          state: d["state"],
          ip: d["ip"]
        }
      end)

    connected_devices = Enum.count(devices, & &1.state == "CONNECTED")
    disconnected_devices = Enum.count(devices, & &1.state == "DISCONNECTED")

    # Networks
    networks =
      UnifiApi.Network.Networks.stream(client, sid)
      |> Enum.map(&Map.take(&1, ["id", "name", "vlanId"]))

    # WiFi SSIDs
    ssids =
      UnifiApi.Network.Wifi.stream(client, sid)
      |> Enum.map(&Map.take(&1, ["id", "name", "enabled"]))

    # WANs
    wans =
      UnifiApi.Network.Resources.stream_wans(client, sid)
      |> Enum.map(&Map.take(&1, ["id", "name", "status"]))

    %{
      site_id: sid,
      site_name: site["name"],
      clients: %{
        total: length(clients),
        wired: client_breakdown["WIRED"] || 0,
        wireless: client_breakdown["WIRELESS"] || 0,
        vpn: client_breakdown["VPN"] || 0,
        teleport: client_breakdown["TELEPORT"] || 0
      },
      devices: %{
        total: length(devices),
        connected: connected_devices,
        disconnected: disconnected_devices,
        list: devices
      },
      networks: networks,
      ssids: ssids,
      wans: wans
    }
  end)

# Protect overview
{:ok, cameras} = UnifiApi.Protect.Cameras.list(client)
{:ok, sensors} = UnifiApi.Protect.Sensors.list(client)
{:ok, lights} = UnifiApi.Protect.Lights.list(client)
{:ok, nvr} = UnifiApi.Protect.NVR.get(client)

protect_data = %{
  nvr: Map.take(nvr, ["id", "name", "modelKey"]),
  cameras: %{
    total: length(cameras),
    connected: Enum.count(cameras, & &1["state"] == "CONNECTED"),
    list:
      Enum.map(cameras, fn c ->
        %{
          id: c["id"],
          name: c["name"],
          state: c["state"],
          model: c["modelKey"]
        }
      end)
  },
  sensors: %{
    total: length(sensors),
    open_doors: Enum.count(sensors, & &1["isOpened"]),
    motion_detected: Enum.count(sensors, & &1["isMotionDetected"])
  },
  lights: %{
    total: length(lights),
    on: Enum.count(lights, & &1["isLightOn"])
  }
}

# Full dashboard payload
dashboard = %{
  controller_version: info["applicationVersion"],
  scraped_at: DateTime.utc_now(),
  sites: sites_data,
  protect: protect_data
}

# Write to JSON
File.write!("dashboard.json", JSON.encode!(dashboard))

You can run this on an interval to feed a time-series database, or serve it from a Phoenix endpoint for a live dashboard:

# Poll every 30 seconds and write fresh data
Stream.interval(30_000)
|> Stream.each(fn _ ->
  # ... same scraper logic above ...
  File.write!("dashboard.json", JSON.encode!(dashboard))
  IO.puts("[#{DateTime.utc_now()}] Dashboard updated")
end)
|> Stream.run()

Formatted Output

UnifiApi.Formatter prints API responses as colored ANSI tables in iex.

Quick shortcuts

{:ok, sites} = UnifiApi.Network.Sites.list(client)
UnifiApi.Formatter.sites(sites)

{:ok, devices} = UnifiApi.Network.Devices.list(client, site_id)
UnifiApi.Formatter.devices(devices)
# State column is color-coded: green=CONNECTED, yellow=CONNECTING, red=DISCONNECTED

{:ok, clients} = UnifiApi.Network.Clients.list(client, site_id)
UnifiApi.Formatter.clients(clients)
# Type column is color-coded: blue=WIRED, magenta=WIRELESS, cyan=VPN

{:ok, cameras} = UnifiApi.Protect.Cameras.list(protect)
UnifiApi.Formatter.cameras(cameras)

{:ok, networks} = UnifiApi.Network.Networks.list(client, site_id)
UnifiApi.Formatter.networks(networks)

Custom tables

# Pick any columns
{:ok, devices} = UnifiApi.Network.Devices.list(client, site_id)
UnifiApi.Formatter.table(devices, ["name", "mac", "model", "state", "ip"],
  title: "My Devices",
  colors: %{"state" => :state}
)

# Detail view for a single record
{:ok, nvr} = UnifiApi.Protect.NVR.get(protect)
UnifiApi.Formatter.detail(nvr, title: "NVR Info")

Error Handling

All functions return {:ok, body} on success or {:error, reason} on failure:

case UnifiApi.Network.Devices.get(client, site_id, "bad-id") do
  {:ok, device} ->
    IO.inspect(device)

  {:error, {404, body}} ->
    IO.puts("Not found: #{inspect(body)}")

  {:error, {401, _}} ->
    IO.puts("Invalid API key")

  {:error, reason} ->
    IO.puts("Connection error: #{inspect(reason)}")
end

Generating Docs

mix deps.get
mix docs
open doc/index.html

License

Apache License 2.0 — see LICENSE for details.