/api/v1/profilesList#
Responses
200List of all loaded profilesapplication/json
array of ProfileSummaryDRO
namerequiredstring
display_name string · nullable
modulationrequiredstring
symbol_rate_baud requirednumber · double
center_frequency_hz requiredinteger · int64
Request
curl "http://192.168.2.1:8080/api/v1/profiles"import requests
BASE_URL = "http://192.168.2.1:8080"
response = requests.get(f"{BASE_URL}/api/v1/profiles")
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let response = reqwest::Client::new()
.get(format!("{base_url}/api/v1/profiles"))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
[
{
"name": "string",
"display_name": "string",
"modulation": "string",
"symbol_rate_baud": 0.0,
"center_frequency_hz": 0
}
]/api/v1/profiles/activeActive#
Responses
200The active profile and whether it still describes the boardapplication/json
namestring · nullable
dirtyrequiredboolean
A manual change landed AFTER the profile was applied.
dirty_reason string · nullable
What made it dirty — the route or register. "dirty" alone sends the reader looking through everything.
Request
curl "http://192.168.2.1:8080/api/v1/profiles/active"import requests
BASE_URL = "http://192.168.2.1:8080"
response = requests.get(f"{BASE_URL}/api/v1/profiles/active")
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let response = reqwest::Client::new()
.get(format!("{base_url}/api/v1/profiles/active"))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
{
"name": "string",
"dirty": true,
"dirty_reason": "string"
}/api/v1/profiles/validateValidate#
Request body application/jsonrequired
Responses
200OKapplication/json
okrequiredboolean
errorsrequiredarray of string
Request
curl -X POST "http://192.168.2.1:8080/api/v1/profiles/validate" \
-H "Content-Type: application/json" \
-d '{}'import requests
BASE_URL = "http://192.168.2.1:8080"
response = requests.post(
f"{BASE_URL}/api/v1/profiles/validate",
json={},
)
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let response = reqwest::Client::new()
.post(format!("{base_url}/api/v1/profiles/validate"))
.json(&serde_json::json!({}))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
{
"ok": true,
"errors": [
"string"
]
}/api/v1/profiles/{name}Get one#
Path parameters
namerequiredstring
Profile identifier
Responses
200OKapplication/json
404Profile not foundapplication/json
Request
curl "http://192.168.2.1:8080/api/v1/profiles/<name>"import requests
BASE_URL = "http://192.168.2.1:8080"
name = "<name>" # Profile identifier
response = requests.get(f"{BASE_URL}/api/v1/profiles/{name}")
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let name = "<name>"; // Profile identifier
let response = reqwest::Client::new()
.get(format!("{base_url}/api/v1/profiles/{name}"))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
{}{}/api/v1/profiles/{name}Register#
Push a profile the daemon does not have.
WHY THIS EXISTS. Until now a profile could only reach the board inside a
firmware build: apply takes a NAME out of the set loaded from
profiles_dir at startup. So trying a profile meant a release cycle, and an
external client — a control centre, an example script — holding a perfectly
good profile had no way to use it.
THE BODY IS PARSED AS YAML, WHICH ALSO ACCEPTS JSON, because YAML is a superset of it. One parse path, and a client may send whichever it has; profiles are written as YAML files, so a script that reads one off disk can forward the bytes unchanged instead of converting them.
IT IS TRANSIENT, AND THE REPLY SAYS SO. Nothing is written to profiles_dir
— the rootfs is a ramdisk in any case. The client keeps the file and stays
the source of truth; a copy quietly persisted on the board would be a second
one, free to drift from the first.
Path parameters
namerequiredstring
Name to register the profile under
Request body text/plainrequired
The profile, as YAML or JSON
string
Responses
200OKapplication/json
namerequiredstring
replacedrequiredboolean
True when a profile of this name already existed and was replaced.
persistedrequiredboolean
Always false today, and present so a client never has to assume.
noterequiredstring
Request
curl -X PUT "http://192.168.2.1:8080/api/v1/profiles/<name>" \
-H "Content-Type: text/plain" \
--data-binary @body.txtimport requests
BASE_URL = "http://192.168.2.1:8080"
name = "<name>" # Name to register the profile under
response = requests.put(
f"{BASE_URL}/api/v1/profiles/{name}",
data=open("body.txt", "rb").read(),
headers={"Content-Type": "text/plain"},
)
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let name = "<name>"; // Name to register the profile under
let response = reqwest::Client::new()
.put(format!("{base_url}/api/v1/profiles/{name}"))
.header("Content-Type", "text/plain")
.body(std::fs::read("body.txt")?)
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
{
"name": "string",
"replaced": true,
"persisted": true,
"note": "string"
}/api/v1/profiles/{name}/applyApply#
Path parameters
namerequiredstring
Profile to apply
Responses
200OKapplication/json
profile_name requiredstring
cfg_epoch requiredinteger · int32
tuningrequiredstring
What the apply did to the RADIO, in one sentence — tuned, or why not.
Applying a profile used to leave the AD9361 exactly where it was while the reply said nothing about it (STE-994), so a UHF profile could be "applied" onto a part listening in S-band and every field in this struct stayed green. A tuning that did not happen has to be visible where the apply is.
404Profile not foundapplication/json
Request
curl -X POST "http://192.168.2.1:8080/api/v1/profiles/<name>/apply"import requests
BASE_URL = "http://192.168.2.1:8080"
name = "<name>" # Profile to apply
response = requests.post(f"{BASE_URL}/api/v1/profiles/{name}/apply")
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let name = "<name>"; // Profile to apply
let response = reqwest::Client::new()
.post(format!("{base_url}/api/v1/profiles/{name}/apply"))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
{
"profile_name": "string",
"cfg_epoch": 0,
"tuning": "string"
}{}/api/v1/profiles/{name}/revisionsList revisions#
Path parameters
namerequiredstring
Profile identifier
Responses
200Revision history sorted newest-firstapplication/json
array of ProfileRevisionDRO
versionrequiredinteger · int32
cfg_epoch requiredinteger · int32
applied_at requiredstring · date-time
applied_by string · nullable
messagestring · nullable
Request
curl "http://192.168.2.1:8080/api/v1/profiles/<name>/revisions"import requests
BASE_URL = "http://192.168.2.1:8080"
name = "<name>" # Profile identifier
response = requests.get(f"{BASE_URL}/api/v1/profiles/{name}/revisions")
response.raise_for_status()
print(response.json())let base_url = "http://192.168.2.1:8080";
let name = "<name>"; // Profile identifier
let response = reqwest::Client::new()
.get(format!("{base_url}/api/v1/profiles/{name}/revisions"))
.send()
.await?
.error_for_status()?;
let body: serde_json::Value = response.json().await?;
println!("{body:#}");Response
[
{
"version": 0,
"cfg_epoch": 0,
"applied_at": "2026-09-29T08:30:00Z",
"applied_by": "string",
"message": "string"
}
]