The API¶
Other programs talk to Cloudlog over HTTP. Cloudlog Aurora does it, and so do the smaller CAT programs listed under other programs. This page is the list of calls those programs use, taken from the current code.
You need an API key. Create it under your callsign, API Keys. The API URL on that page is the address of this Cloudlog. On a normal install the calls sit under index.php/api/. If pretty URLs are switched on and index.php was cleared from the config, leave index.php/ out.
A key looks like cl followed by a unique id. Give each program its own key, and a description you will recognise later.
Create a Read & Write key for a program that sends contacts or radio reports. Create a Read-Only key for a display that only reads. The calls below accept an active key of either kind. Hand a sending program a read and write key so you can see, from the description and from Last Used, which program is writing.
POST bodies are JSON. A body Cloudlog cannot read comes back as wrong JSON. A missing or disabled key comes back as HTTP 401.
Each call below is a complete program in PHP, Go, and curl. Change the address and the key at the top, then run it. PHP needs the curl extension (php script.php). Go uses only the standard library (go run script.go). Choosing a language tab once keeps that language selected for the rest of the page.
The samples use https://your-cloudlog.example/index.php/api and the key cl6a1b2c3d4e5f6.
Test the key¶
auth¶
GET api/auth/{key} answers in XML. Valid and the rights (r or rw) means the key is live. Key Invalid means it is missing or disabled. This is the page the test button on a key row opens.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$ch = curl_init($base . '/auth/' . rawurlencode($key));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n$body\n";
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
resp, err := http.Get(base + "/auth/" + url.PathEscape(key))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/auth/cl6a1b2c3d4e5f6"
check_auth¶
GET api/check_auth/{key} is the same test as JSON. status is valid or failed, and rights is r or rw. A bad key is HTTP 401.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$ch = curl_init($base . '/check_auth/' . rawurlencode($key));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
resp, err := http.Get(base + "/check_auth/" + url.PathEscape(key))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/check_auth/cl6a1b2c3d4e5f6"
Station locations¶
station_info¶
GET api/station_info/{key} lists the locations that belong to the key's owner. Each row has station_id, station_profile_name, station_callsign, station_gridsquare, and station_active. You need station_id before you can post a contact.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$ch = curl_init($base . '/station_info/' . rawurlencode($key));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
resp, err := http.Get(base + "/station_info/" + url.PathEscape(key))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/station_info/cl6a1b2c3d4e5f6"
Logbooks¶
The worked checks further down want the public slug, so a logbook with that field empty will not be found.
logbook_public_slugs¶
GET api/logbook_public_slugs/{key} lists the logbooks you own that have a public slug. Each row has logbook_id, logbook_name, and public_slug.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$ch = curl_init($base . '/logbook_public_slugs/' . rawurlencode($key));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
resp, err := http.Get(base + "/logbook_public_slugs/" + url.PathEscape(key))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/logbook_public_slugs/cl6a1b2c3d4e5f6"
logbook_public_slugs_accessible¶
GET api/logbook_public_slugs_accessible/{key} is the same list, plus logbooks someone has shared with you. Those rows also have access_level.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$ch = curl_init($base . '/logbook_public_slugs_accessible/' . rawurlencode($key));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
resp, err := http.Get(base + "/logbook_public_slugs_accessible/" + url.PathEscape(key))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/logbook_public_slugs_accessible/cl6a1b2c3d4e5f6"
Sending a contact¶
qso¶
POST api/qso
type is adif. string is one or more ADIF records. station_profile_id has to be a location owned by the key, from station_info. If the record contains STATION_CALLSIGN, it has to match that location. An empty CALL is rejected.
A good post answers HTTP 201, status created, imported_count, and messages for any record that was skipped. The same contact posted twice is handled by the importer. Read messages before you assume every record landed.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$payload = [
'key' => $key,
'station_profile_id' => '1',
'type' => 'adif',
'string' => '<CALL:6>2M0SQL<BAND:3>20M<MODE:3>SSB<QSO_DATE:8>20261022<TIME_ON:4>1930<EOR>',
];
$ch = curl_init($base . '/qso');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
payload := map[string]string{
"key": "cl6a1b2c3d4e5f6",
"station_profile_id": "1",
"type": "adif",
"string": "<CALL:6>2M0SQL<BAND:3>20M<MODE:3>SSB<QSO_DATE:8>20261022<TIME_ON:4>1930<EOR>",
}
raw, _ := json.Marshal(payload)
resp, err := http.Post(base+"/qso", "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/qso' <<'EOF'
{
"key": "cl6a1b2c3d4e5f6",
"station_profile_id": "1",
"type": "adif",
"string": "<CALL:6>2M0SQL<BAND:3>20M<MODE:3>SSB<QSO_DATE:8>20261022<TIME_ON:4>1930<EOR>"
}
EOF
Has this been worked?¶
These all POST JSON with key and logbook_public_slug. A slug Cloudlog does not know is HTTP 404, logbook not found. A logbook with no locations linked is Empty Logbook.
logbook_check_callsign¶
Needs callsign. band is optional. The answer is result: Found or Not Found.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$payload = [
'key' => 'cl6a1b2c3d4e5f6',
'logbook_public_slug' => 'your-public-slug',
'callsign' => '2M0SQL',
'band' => '20m',
];
$ch = curl_init($base . '/logbook_check_callsign');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
payload := map[string]string{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"callsign": "2M0SQL",
"band": "20m",
}
raw, _ := json.Marshal(payload)
resp, err := http.Post(base+"/logbook_check_callsign", "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/logbook_check_callsign' <<'EOF'
{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"callsign": "2M0SQL",
"band": "20m"
}
EOF
logbook_check_grid¶
Needs grid. band is optional. The answer uses gridsquare and the same Found or Not Found result.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$payload = [
'key' => 'cl6a1b2c3d4e5f6',
'logbook_public_slug' => 'your-public-slug',
'grid' => 'IO87',
'band' => '20m',
];
$ch = curl_init($base . '/logbook_check_grid');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
payload := map[string]string{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"grid": "IO87",
"band": "20m",
}
raw, _ := json.Marshal(payload)
resp, err := http.Post(base+"/logbook_check_grid", "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/logbook_check_grid' <<'EOF'
{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"grid": "IO87",
"band": "20m"
}
EOF
logbook_check_country¶
Needs callsign. Cloudlog works out the country from the callsign, then looks for that country on the band and mode you send. Send type as sat when you want satellite contacts. The answer is workedBefore, and confirmed for qsl, lotw, eqsl, and qrz.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$payload = [
'key' => 'cl6a1b2c3d4e5f6',
'logbook_public_slug' => 'your-public-slug',
'callsign' => '2M0SQL',
'band' => '20m',
'mode' => 'SSB',
// 'type' => 'sat',
];
$ch = curl_init($base . '/logbook_check_country');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
payload := map[string]string{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"callsign": "2M0SQL",
"band": "20m",
"mode": "SSB",
}
raw, _ := json.Marshal(payload)
resp, err := http.Post(base+"/logbook_check_country", "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/logbook_check_country' <<'EOF'
{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"callsign": "2M0SQL",
"band": "20m",
"mode": "SSB"
}
EOF
logbook_worked_status¶
Needs callsign. band, mode, and gridsquare are optional. The answer is the country, then worked (the callsign, the country, and the grid, overall and on that band) and confirmed (LoTW and a paper QSL, for the callsign and for the country). A field you did not send comes back as null for the band-specific checks.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$payload = [
'key' => 'cl6a1b2c3d4e5f6',
'logbook_public_slug' => 'your-public-slug',
'callsign' => '2M0SQL',
'band' => '20m',
'mode' => 'SSB',
'gridsquare' => 'IO87',
];
$ch = curl_init($base . '/logbook_worked_status');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
payload := map[string]string{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"callsign": "2M0SQL",
"band": "20m",
"mode": "SSB",
"gridsquare": "IO87",
}
raw, _ := json.Marshal(payload)
resp, err := http.Post(base+"/logbook_worked_status", "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/logbook_worked_status' <<'EOF'
{
"key": "cl6a1b2c3d4e5f6",
"logbook_public_slug": "your-public-slug",
"callsign": "2M0SQL",
"band": "20m",
"mode": "SSB",
"gridsquare": "IO87"
}
EOF
Recent contacts, with no key¶
recent_qsos¶
GET api/recent_qsos/{public_slug}/{limit}
This one is public. It uses the slug, not a key. limit defaults to 10 and is capped at 50. The answer is qsos, count, and logbook_slug. Each contact has date, time, callsign, mode (the submode when there is one), band (the satellite name when the contact is a satellite contact), rst_sent, and rst_rcvd. Gridsquare, name, QTH, contest exchange, country, and lat/long are included when the contact has them.
The HTML table for a club website is the widget. This call is the same contacts as JSON.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$slug = 'your-public-slug';
$limit = 10;
$ch = curl_init($base . '/recent_qsos/' . rawurlencode($slug) . '/' . $limit);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
slug := "your-public-slug"
limit := 10
endpoint := fmt.Sprintf("%s/recent_qsos/%s/%d", base, url.PathEscape(slug), limit)
resp, err := http.Get(endpoint)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/recent_qsos/your-public-slug/10"
Statistics¶
statistics¶
GET api/statistics/{key}
Counts for the key owner's active logbook: Today, total_qsos, month_qsos, and year_qsos.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$ch = curl_init($base . '/statistics/' . rawurlencode($key));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
resp, err := http.Get(base + "/statistics/" + url.PathEscape(key))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/statistics/cl6a1b2c3d4e5f6"
Radio reports¶
radio¶
POST api/radio
This is how a CAT program tells Cloudlog where the radio is. The radio then shows under Hardware Interfaces.
radio is the name Cloudlog stores. The same name updates the same row. frequency is in hertz, so 14.075 MHz is 14075000. Optional fields:
frequency_rxandmode_rxfor split. Older programs senddownlink_freqanddownlink_mode, which are stored the same way.uplink_freqanduplink_modeare the older names for the transmit frequency and mode.power, in watts.sat_name. If you send a satellite and no propagation mode, Cloudlog stores the propagation asSAT.prop_mode, when you want to set it yourself.
A frequency or mode that is not a real value is stored blank. The answer is {"status":"success"}.
Cloudlog does not open the COM port. Something on the shack PC has to read the radio and post this.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$payload = [
'key' => 'cl6a1b2c3d4e5f6',
'radio' => 'FT-991A',
'frequency' => 14075000,
'mode' => 'USB',
'power' => 50,
];
$ch = curl_init($base . '/radio');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
payload := map[string]any{
"key": "cl6a1b2c3d4e5f6",
"radio": "FT-991A",
"frequency": 14075000,
"mode": "USB",
"power": 50,
}
raw, _ := json.Marshal(payload)
resp, err := http.Post(base+"/radio", "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/radio' <<'EOF'
{
"key": "cl6a1b2c3d4e5f6",
"radio": "FT-991A",
"frequency": 14075000,
"mode": "USB",
"power": 50
}
EOF
Queued tune commands¶
Clicking a spot asks Cloudlog to queue a tune. Aurora, or another program, polls the queue and sends the command to the radio. The commands are SET_FREQ, SET_MODE, SET_VFO, and SET_POWER. A frequency in the queue is in hertz. A command expires after 30 minutes.
A program that only reports the dial can ignore this queue. A program that tunes the radio from a spot click has to poll it.
radio_commands_pending¶
GET api/radio_commands_pending/{key} lists pending commands for every radio. Add /{radio_id} to limit it to one radio.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$radioId = null; // set to an id, for example 1, to limit the list
$url = $base . '/radio_commands_pending/' . rawurlencode($key);
if ($radioId !== null) {
$url .= '/' . rawurlencode((string) $radioId);
}
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
// Set radioID to limit the list, for example "1". Leave it empty for every radio.
radioID := ""
endpoint := base + "/radio_commands_pending/" + url.PathEscape(key)
if radioID != "" {
endpoint += "/" + url.PathEscape(radioID)
}
resp, err := http.Get(endpoint)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/radio_commands_pending/cl6a1b2c3d4e5f6"
# One radio:
# curl -sS "https://your-cloudlog.example/index.php/api/radio_commands_pending/cl6a1b2c3d4e5f6/1"
radio_commands_pending_by_name¶
GET api/radio_commands_pending_by_name/{key}/{radio_name} is the same lookup by the name you posted to api/radio. The name is URL-encoded if it contains a space.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$radio = 'FT-991A';
$url = $base . '/radio_commands_pending_by_name/' . rawurlencode($key) . '/' . rawurlencode($radio);
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
radio := "FT-991A"
endpoint := base + "/radio_commands_pending_by_name/" + url.PathEscape(key) + "/" + url.PathEscape(radio)
resp, err := http.Get(endpoint)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/radio_commands_pending_by_name/cl6a1b2c3d4e5f6/FT-991A"
radio_commands_all_pending¶
GET api/radio_commands_all_pending/{key} is every pending command for this user.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$ch = curl_init($base . '/radio_commands_all_pending/' . rawurlencode($key));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
resp, err := http.Get(base + "/radio_commands_all_pending/" + url.PathEscape(key))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/radio_commands_all_pending/cl6a1b2c3d4e5f6"
radio_commands_get¶
GET api/radio_commands_get/{key}/{command_id} is one command.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$commandId = 1;
$url = $base . '/radio_commands_get/' . rawurlencode($key) . '/' . $commandId;
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
commandID := 1
endpoint := fmt.Sprintf("%s/radio_commands_get/%s/%d", base, url.PathEscape(key), commandID)
resp, err := http.Get(endpoint)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS "https://your-cloudlog.example/index.php/api/radio_commands_get/cl6a1b2c3d4e5f6/1"
radio_commands_update_status¶
POST api/radio_commands_update_status/{key} with command_id and status. Status is PROCESSING, COMPLETED, or FAILED. error_message is optional.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$payload = [
'command_id' => 1,
'status' => 'COMPLETED',
];
$ch = curl_init($base . '/radio_commands_update_status/' . rawurlencode($key));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
payload := map[string]any{
"command_id": 1,
"status": "COMPLETED",
}
raw, _ := json.Marshal(payload)
endpoint := base + "/radio_commands_update_status/" + url.PathEscape(key)
resp, err := http.Post(endpoint, "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/radio_commands_update_status/cl6a1b2c3d4e5f6' <<'EOF'
{
"command_id": 1,
"status": "COMPLETED"
}
EOF
radio_commands_queue¶
POST api/radio_commands_queue/{key} adds a command. Send command_type, and either radio_id or radio_name. frequency, mode, vfo, power, and station_id are optional. The radio must already have reported in, or the name lookup answers radio not found. Frequency is in hertz.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$payload = [
'radio_name' => 'FT-991A',
'command_type' => 'SET_FREQ',
'frequency' => 14075000,
'mode' => 'USB',
];
$ch = curl_init($base . '/radio_commands_queue/' . rawurlencode($key));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
payload := map[string]any{
"radio_name": "FT-991A",
"command_type": "SET_FREQ",
"frequency": 14075000,
"mode": "USB",
}
raw, _ := json.Marshal(payload)
endpoint := base + "/radio_commands_queue/" + url.PathEscape(key)
resp, err := http.Post(endpoint, "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/radio_commands_queue/cl6a1b2c3d4e5f6' <<'EOF'
{
"radio_name": "FT-991A",
"command_type": "SET_FREQ",
"frequency": 14075000,
"mode": "USB"
}
EOF
radio_commands_cleanup¶
POST api/radio_commands_cleanup/{key} drops expired commands. Send cleanup_old as true, and days_old, when finished commands older than that many days should go as well. The default for days_old is 7.
<?php
$base = 'https://your-cloudlog.example/index.php/api';
$key = 'cl6a1b2c3d4e5f6';
$payload = [
'cleanup_old' => true,
'days_old' => 7,
];
$ch = curl_init($base . '/radio_commands_cleanup/' . rawurlencode($key));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP $code\n";
echo json_encode(json_decode($body), JSON_PRETTY_PRINT), "\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
base := "https://your-cloudlog.example/index.php/api"
key := "cl6a1b2c3d4e5f6"
payload := map[string]any{
"cleanup_old": true,
"days_old": 7,
}
raw, _ := json.Marshal(payload)
endpoint := base + "/radio_commands_cleanup/" + url.PathEscape(key)
resp, err := http.Post(endpoint, "application/json", bytes.NewReader(raw))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var parsed any
if json.Unmarshal(body, &parsed) == nil {
out, _ := json.MarshalIndent(parsed, "", " ")
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
return
}
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, body)
}
curl -sS -X POST \
-H 'Content-Type: application/json' \
--data @- \
'https://your-cloudlog.example/index.php/api/radio_commands_cleanup/cl6a1b2c3d4e5f6' <<'EOF'
{
"cleanup_old": true,
"days_old": 7
}
EOF
What is not a public API¶
api/lookup is the callsign lookup the logging page uses while you are logged in. It wants a browser session. A program with only an API key should use the worked checks above.