Developer Documentation

API reference and code examples for building integrations.

TL;DR

Fetch all services in 30 seconds:

curl https://services.aprslive.com/api/v1/registry

No authentication required. Returns JSON with all registered services.

API Overview

The APRS Service Registry provides a RESTful API for querying and managing service registrations. All endpoints return JSON responses.

Base URL

https://services.aprslive.com

Versioning

The API is versioned via URL path. Current version: /api/v1/

Authentication

No authentication required. The API is public and read operations are available to everyone.

Note: Authentication may be added for write operations in the future. Subscribe to the project repository for updates.

Rate Limits

Rate limit: 60 requests per minute per IP address.

Exceeding this limit returns HTTP 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.

Best practices:

  • Cache responses where appropriate
  • Avoid polling more frequently than once per minute for real-time applications
  • Use webhooks (when available) instead of polling for change detection

Endpoints Summary

Method Endpoint Description Since
Core Registry (Sprint 1)
GET /api/v1/registry List all services v1.13
GET /api/v1/registry/{callsign} Get a specific service v1.13
POST /api/v1/registry Register or update a service v1.13
DELETE /api/v1/registry/{callsign} Remove a service (soft-delete) v1.13
GET /api/v1/health Application health check v1.13
POST /api/v1/health-check/{callsign} Trigger health check for a service v1.13
POST /api/v1/health-check Trigger health check for all services v1.13
Command Catalog (Sprint 2)
POST /api/v1/services/{callsign}/commands Submit a command for a service v1.14
GET /api/v1/services/{callsign}/commands List commands for a service v1.14
Public API Value (Sprint 3)
GET /api/v1/registry/{callsign}/health-history Paginated health-check probe results v1.16
GET /api/v1/registry/{callsign}/uptime Uptime percentage + stats over a period v1.16
GET /api/v1/registry/{callsign}/badge.svg Embeddable shields.io-style SVG badge v1.16
GET /api/v1/stats Aggregate registry statistics v1.16
GET /api/v1/count Lightweight polling (ETag/304 support) v1.16
APRS-Native Discovery (Sprint 4)
POST /api/v1/query Execute APRS query commands via HTTP v1.17
Real-Time & Voting (Sprint 6)
GET /api/v1/events SSE stream for real-time registry events v1.18
POST /api/v1/registry/{callsign}/vote Cast/toggle a vote (passcode-authenticated) v1.18
GET /api/v1/registry/{callsign}/votes Vote summary + trust score for a service v1.18
GET /api/v1/registry/{callsign}/trust Trust score breakdown (uptime/votes/age) v1.18

Error Responses

The API uses standard HTTP status codes:

Code Meaning
200 Success
201 Created (for new registrations)
400 Bad request (invalid input)
404 Not found (service doesn't exist)
500 Server error

Error responses include a JSON body:

{"detail": "Service 'UNKNOWN' not found"}

List All Services

Retrieve all registered APRS services.

Request

GET /api/v1/registry

Query Parameters

Parameter Type Default Description
include_deleted boolean false Include deleted services
include_all boolean false Include all services regardless of status

Response

{
  "count": 5,
  "timestamp": "2024-01-15T10:30:00Z",
  "services": [
    {
      "callsign": "REPEAT",
      "description": "Find the nearest N repeaters to your current location",
      "service_website": "http://aprs-repeat.hemna.com",
      "software": "APRSD version 3.3.0",
      "callsign_owner": "WB4BOR",
      "status": "active",
      "last_health_check": {
        "timestamp": "2024-01-15T10:00:00Z",
        "success": true,
        "response_time_ms": 1250,
        "error": null
      }
    }
  ]
}

Code Examples

curl

curl https://services.aprslive.com/api/v1/registry

Python

import requests

response = requests.get("https://services.aprslive.com/api/v1/registry")
data = response.json()

print(f"Found {data['count']} services")
for service in data['services']:
    print(f"  {service['callsign']}: {service['description']}")

JavaScript

const response = await fetch("https://services.aprslive.com/api/v1/registry");
const data = await response.json();

console.log(`Found ${data.count} services`);
data.services.forEach(service => {
  console.log(`  ${service.callsign}: ${service.description}`);
});

Go

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type RegistryResponse struct {
    Count    int       `json:"count"`
    Services []Service `json:"services"`
}

type Service struct {
    Callsign    string `json:"callsign"`
    Description string `json:"description"`
}

func main() {
    resp, err := http.Get("https://services.aprslive.com/api/v1/registry")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var data RegistryResponse
    json.NewDecoder(resp.Body).Decode(&data)

    fmt.Printf("Found %d services\n", data.Count)
    for _, svc := range data.Services {
        fmt.Printf("  %s: %s\n", svc.Callsign, svc.Description)
    }
}

Get a Specific Service

Retrieve details for a single service by callsign.

Request

GET /api/v1/registry/{callsign}

Response

{
  "callsign": "REPEAT",
  "description": "Find the nearest N repeaters to your current location",
  "service_website": "http://aprs-repeat.hemna.com",
  "software": "APRSD version 3.3.0",
  "callsign_owner": "WB4BOR",
  "status": "active",
  "last_health_check": {
    "timestamp": "2024-01-15T10:00:00Z",
    "success": true,
    "response_time_ms": 1250,
    "error": null
  }
}

Code Examples

curl

curl https://services.aprslive.com/api/v1/registry/REPEAT

Python

import requests

callsign = "REPEAT"
response = requests.get(f"https://services.aprslive.com/api/v1/registry/{callsign}")

if response.status_code == 200:
    service = response.json()
    print(f"{service['callsign']}: {service['description']}")
elif response.status_code == 404:
    print(f"Service {callsign} not found")

JavaScript

const callsign = "REPEAT";
const response = await fetch(`https://services.aprslive.com/api/v1/registry/${callsign}`);

if (response.ok) {
  const service = await response.json();
  console.log(`${service.callsign}: ${service.description}`);
} else if (response.status === 404) {
  console.log(`Service ${callsign} not found`);
}

Go

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    callsign := "REPEAT"
    url := fmt.Sprintf("https://services.aprslive.com/api/v1/registry/%s", callsign)
    
    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode == 200 {
        var service map[string]interface{}
        json.NewDecoder(resp.Body).Decode(&service)
        fmt.Printf("%s: %s\n", service["callsign"], service["description"])
    } else if resp.StatusCode == 404 {
        fmt.Printf("Service %s not found\n", callsign)
    }
}

Register a Service

Register a new service or update an existing one.

Request

POST /api/v1/registry

Request Body

{
  "callsign": "MYCALL",
  "description": "My APRS service description",
  "service_website": "http://example.com",
  "software": "APRSD 3.3.0",
  "callsign_owner": "N0CALL"
}

Code Examples

curl

curl -X POST https://services.aprslive.com/api/v1/registry \
  -H "Content-Type: application/json" \
  -d '{
    "callsign": "MYCALL",
    "description": "My APRS service",
    "service_website": "http://example.com",
    "software": "APRSD 3.3.0"
  }'

Python

import requests

data = {
    "callsign": "MYCALL",
    "description": "My APRS service",
    "service_website": "http://example.com",
    "software": "APRSD 3.3.0",
}

response = requests.post(
    "https://services.aprslive.com/api/v1/registry",
    json=data
)
print(response.json())

JavaScript

const data = {
  callsign: "MYCALL",
  description: "My APRS service",
  service_website: "http://example.com",
  software: "APRSD 3.3.0",
};

const response = await fetch("https://services.aprslive.com/api/v1/registry", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(data),
});

console.log(await response.json());

Go

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    data := map[string]string{
        "callsign":        "MYCALL",
        "description":     "My APRS service",
        "service_website": "http://example.com",
        "software":        "APRSD 3.3.0",
    }

    jsonData, _ := json.Marshal(data)
    resp, err := http.Post(
        "https://services.aprslive.com/api/v1/registry",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    fmt.Println(result)
}

Trigger Health Check

Manually trigger a health check for a specific service.

Request

POST /api/v1/health-check/{callsign}

Response

{
  "status": "ok",
  "callsign": "REPEAT",
  "health_check": {
    "timestamp": "2024-01-15T10:30:00Z",
    "success": true,
    "response_time_ms": 1250,
    "error": null
  }
}

Code Examples

curl

curl -X POST https://services.aprslive.com/api/v1/health-check/REPEAT

Python

import requests

callsign = "REPEAT"
response = requests.post(f"https://services.aprslive.com/api/v1/health-check/{callsign}")
result = response.json()

if result["health_check"]["success"]:
    print(f"Health check passed in {result['health_check']['response_time_ms']}ms")
else:
    print(f"Health check failed: {result['health_check']['error']}")

Command Catalog (Sprint 2)

Services can register their supported APRS commands so users know what messages to send.

Submit a Command

POST /api/v1/services/{callsign}/commands

Requires APRS passcode authentication. Commands are queued for admin approval.

curl -X POST https://services.aprslive.com/api/v1/services/REPEAT/commands \
  -H "Content-Type: application/json" \
  -d '{
    "passcode": "12345",
    "name": "find",
    "description": "Find nearest repeaters to your location",
    "example": "find 5",
    "response_format": "Multi-message reply with repeater info"
  }'

List Commands

GET /api/v1/services/{callsign}/commands

curl https://services.aprslive.com/api/v1/services/REPEAT/commands

Response

{
  "callsign": "REPEAT",
  "commands": [
    {
      "name": "find",
      "description": "Find nearest repeaters to your location",
      "example": "find 5",
      "response_format": "Multi-message reply with repeater info"
    },
    {
      "name": "help",
      "description": "List available commands"
    }
  ]
}

Health History & Uptime (Sprint 3)

Access historical health-check data, uptime percentages, and embeddable status badges.

Health History

GET /api/v1/registry/{callsign}/health-history

Parameter Type Default Description
limit integer 100 Max records to return (max 1000)
since ISO timestamp Only return checks after this time
curl "https://services.aprslive.com/api/v1/registry/WXBOT/health-history?limit=50&since=2026-08-01T00:00:00Z"

Uptime Statistics

GET /api/v1/registry/{callsign}/uptime

Parameter Type Default Description
period string 7d Time period: 1d, 7d, 30d, 90d
curl https://services.aprslive.com/api/v1/registry/WXBOT/uptime?period=30d

Response

{
  "callsign": "WXBOT",
  "period": "7d",
  "total_checks": 168,
  "successful_checks": 165,
  "failed_checks": 3,
  "uptime_percent": 98.21,
  "avg_response_time_ms": 142.5
}

Status Badge

GET /api/v1/registry/{callsign}/badge.svg

Returns an SVG badge (shields.io style) with color-coded uptime. Cached for 5 minutes.

<img src="https://services.aprslive.com/api/v1/registry/WXBOT/badge.svg" alt="uptime">

Registry Statistics

GET /api/v1/stats

Aggregate statistics: total services, active/down counts, overall uptime.

curl https://services.aprslive.com/api/v1/stats

Lightweight Count (Polling)

GET /api/v1/count

Returns service count with ETag support. Use If-None-Match header for conditional GETs (returns 304 when unchanged). Rate limit: 120/minute.

# First request
curl -i https://services.aprslive.com/api/v1/count
# → ETag: "abc123"

# Subsequent poll (returns 304 if unchanged)
curl -H 'If-None-Match: "abc123"' https://services.aprslive.com/api/v1/count

APRS-Native Discovery (Sprint 4)

Radio-only users can query the registry by messaging RGSTRY over APRS — no internet required. The same command interface is also available via HTTP for web/mobile clients.

Over APRS Radio

Send a message to RGSTRY on the APRS network:

You Send RGSTRY Replies
? or help Service count + available commands + web URL
list Paginated comma-separated callsigns (up to 5 pages)
list 2 Just page 2
list weather Only services in the "weather" category
find <keyword> Matching callsigns
info REPEAT Description, owner, and status
cmds REPEAT Command catalog for that service

Constraints: All responses ≤67 chars. Rate-limited to 1 query per 60s per station. SSID-agnostic (RGSTRY and RGSTRY-10 both work).

Via HTTP API

POST /api/v1/query

Execute the same APRS commands via HTTP (for web UI, mobile apps, or testing).

Request Body

{
  "command": "find weather",
  "from_call": "WEBCLIENT"
}

Response

{
  "command": "find weather",
  "from_call": "WEBCLIENT",
  "responses": ["Found:WXBOT,WXNOW,WXYO"],
  "count": 1
}

Code Examples

curl
curl -X POST https://services.aprslive.com/api/v1/query \
  -H "Content-Type: application/json" \
  -d '{"command": "help"}'
Python
import requests

response = requests.post(
    "https://services.aprslive.com/api/v1/query",
    json={"command": "list weather"}
)
data = response.json()
for msg in data["responses"]:
    print(msg)
JavaScript
const response = await fetch("https://services.aprslive.com/api/v1/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ command: "info WXBOT" }),
});
const data = await response.json();
data.responses.forEach(msg => console.log(msg));

Real-Time Events & Voting (Sprint 6)

SSE Event Stream

Connect to receive real-time push notifications:

const es = new EventSource('/api/v1/events');
es.addEventListener('service.status_changed', (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.data.callsign}: ${data.data.old_status} → ${data.data.new_status}`);
});
es.addEventListener('vote.cast', (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.data.voter} voted on ${data.data.callsign}`);
});

Event types: service.registered, service.updated, service.deleted, service.status_changed, health_check.completed, vote.cast.

30-second keepalive heartbeats. Max 10 connections per IP.

Community Voting

Vote on services using your APRS passcode:

# Upvote a service
curl -X POST /api/v1/registry/WXBOT/vote \
  -H "Content-Type: application/json" \
  -d '{"callsign": "KM6LYW", "passcode": 12345, "vote": "up"}'

# Response:
# {"callsign":"WXBOT","voter":"KM6LYW","action":"created",
#  "current_vote":1,"upvotes":1,"downvotes":0,"score":1,"voter_count":1}

# Vote same direction again to toggle off (remove vote)
# Switch "up" to "down" to change direction

Trust Score

Trust score (0–100) combines uptime, community votes, and service age:

GET /api/v1/registry/WXBOT/trust

# {"callsign":"WXBOT","trust_score":65.3,
#  "components":{"uptime_weight":50,"vote_weight":30,"age_weight":20},
#  "votes":{"upvotes":3,"downvotes":0,"score":3,"voter_count":3}}

Rate Limits

ActionLimitScope
Votes10/hourPer callsign
Vote endpoint30/minPer IP
SSE connections10 concurrentPer IP

Full API Reference

For the complete API specification with all parameters and response schemas, see:

  • Swagger UI — Interactive API explorer
  • ReDoc — Clean API documentation