Skip to content

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_rx and mode_rx for split. Older programs send downlink_freq and downlink_mode, which are stored the same way. uplink_freq and uplink_mode are 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 as SAT.
  • 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.