---
title: "Discover Speakers"
method: POST
path: "/sites/{site_id}/speakers/discover"
tags: ["speakers"]
---

# Discover Speakers

`POST /sites/{site_id}/speakers/discover`

## Request

- Base URL: `https://api.verdun.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.verdun.ai/sites/{site_id}/speakers/discover`
- Auth: HTTP bearer

Probe one host for Axis speakers: which device answers on each
candidate port (serial, model, firmware - read without credentials), and,
when credentials are available, whether they work there and which clip ids
the unit holds. Built for the 2026-10-03 install where two horns sat behind
one IP and the record pointed at the wrong one.

WRITES NOTHING, but it is a POST that makes outbound calls to a customer
address with device credentials, so it takes exactly the guard of creating
a speaker on the site (`require_manage_sites_cameras` + the service's site
scope check), and read_only is refused by the global write guardrail like
any other POST - deliberately no allowlist entry. Not activity-logged:
nothing changed, and the request carries a password.

## Path parameters

- `site_id` string, required

## Request body

- DiscoverSpeakersRequest: Probe one public IP for Axis speakers. READ-ONLY: nothing is written. WHY (2026-10-03): two horns sat behind one IP on two forwarded ports, the record pointed at the wrong one, and nobody could tell from the console. This asks every candidate port which device answers (serial, model, firmware - no credentials needed for that) and, where credentials are available, whether they work there and which clip ids the unit holds. The password is write-only and used for this one probe: it is never stored, logged or returned.
  - `host` string, required: Public IP address to probe (IPv4 or IPv6 literal; hostnames are refused, as on speaker create). Validated by the same SSRF guard as a speaker record.
  - `ports` integer[], nullable: Extra ports to try on top of the common web ports (80, 443, 8080, 8081, 8443, 8000, 8888) and the speaker's own port. At most 16.
  - `speaker_id` string, uuid, nullable: A live speaker on THIS site. Its port is added to the probe, and its stored credentials are used unless a password is supplied here.
  - `username` string, nullable: Device account to try. Defaults to the speaker's stored username when speaker_id is given. Only used together with a password, or with the speaker's stored credentials.
  - `password` string, password, nullable: Device password to try for this probe only. Never stored, logged or returned.

## Response `200`

Successful Response

- SpeakerDiscoveryResponse: Every candidate port on one host, speakers first. Nothing was written.
  - `host` string, required: The host probed, in canonical form.
  - `probed_at` string, date-time, required: When the probe started.
  - `credentials` 'stored' | 'supplied' | 'none', required: Which credentials were tried: the speaker's stored ones, the ones in the request, or none.
  - `results` SpeakerDiscoveryResult[], required: One row per port: speakers first, then other answers, then closed/timed-out ports; by port within each group.
    - `port` integer, required: The TCP port probed.
    - `scheme` 'https' | 'http', nullable: The scheme that got an HTTP answer (HTTPS is tried first). Null when nothing answered.
    - `status` 'speaker' | 'other' | 'closed' | 'timeout', required: speaker: an Axis device answered. other: something answered over HTTP but not an Axis device API (e.g. a router page). closed: no HTTP answer on either scheme. timeout: the probe did not finish inside the overall time budget.
    - `product` string, nullable: Product name the device reported (read without credentials).
    - `serial` string, nullable: Serial number the device reported. Two rows with different serials are two physical units.
    - `firmware` string, nullable: Firmware version the device reported.
    - `note` string, nullable: Short detail when the answer was not a plain Axis identity, e.g. a web page's title. Never response headers.
    - `auth` 'ok' | 'rejected' | 'error' | 'not_tried', required: Whether the credentials worked on THIS device: ok, rejected (401), error (could not tell), or not_tried (no credentials, or not a speaker).
    - `clips` DiscoveredClip[], nullable: Clips on the device, when the credentials worked. Null otherwise.
      - `id` string, required: Clip id to put in the speaker's clip_id.
      - `name` string, nullable: The clip's name on the device.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/verdun/apis/verdun-api.md) - [All operations](https://skmtc.dev/verdun/apis/verdun-api/llms.txt) - [OpenAPI document](https://skmtc.dev/verdun/apis/verdun-api/revisions/a19366be50a3?raw)
