Quickstart Tutorial

Get started with the Vail ReRBN API in 5 minutes. This tutorial walks you through making your first API calls and understanding the responses.

Step 1: Your First API Call

Let's start with the simplest possible request: searching for spots of a specific callsign.

Search for W1AW on 20m CW
curl "https://vailrerbn.com/api/v1/spots?call=W1AW&mode=CW&band=20m"
Search for W1AW on 20m CW
import requests

response = requests.get(
    "https://vailrerbn.com/api/v1/spots",
    params={
        "call": "W1AW",
        "mode": "CW",
        "band": "20m"
    }
)
data = response.json()
print(f"Found {data['total']} spots")

for spot in data["spots"]:
    grid = spot.get('grid', 'Unknown')
    print(f"{spot['callsign']} ({grid}) on {spot['frequency']} kHz")
Search for W1AW on 20m CW
const params = new URLSearchParams({
  call: 'W1AW',
  mode: 'CW',
  band: '20m'
});

const response = await fetch(
  `https://vailrerbn.com/api/v1/spots?${params}`
);
const data = await response.json();

console.log(`Found ${data.total} spots`);

data.spots.forEach(spot => {
  const grid = spot.grid || 'Unknown';
  console.log(`${spot.callsign} (${grid}) on ${spot.frequency} kHz`);
});
Search for W1AW on 20m CW
<?php
$params = http_build_query([
    'call' => 'W1AW',
    'mode' => 'CW',
    'band' => '20m'
]);

$response = file_get_contents(
    "https://vailrerbn.com/api/v1/spots?$params"
);
$data = json_decode($response, true);

echo "Found {$data['total']} spots\n";

foreach ($data['spots'] as $spot) {
    echo "{$spot['callsign']} on {$spot['frequency']} kHz\n";
}
Search for W1AW on 20m CW
require 'net/http'
require 'json'

uri = URI('https://vailrerbn.com/api/v1/spots')
uri.query = URI.encode_www_form({
  call: 'W1AW',
  mode: 'CW',
  band: '20m'
})

response = Net::HTTP.get(uri)
data = JSON.parse(response)

puts "Found #{data['total']} spots"

data['spots'].each do |spot|
  puts "#{spot['callsign']} on #{spot['frequency']} kHz"
end

Find recent spots for W1AW on the 20-meter band in CW mode

Understanding the Response

The API returns a JSON object with the following structure:

{
  "spots": [
    {
      "id": 12345678,
      "timestamp": "2025-01-09T15:30:00Z",
      "spotter": "K1TTT",
      "spotter_grid": "FN32",
      "callsign": "W1AW",
      "grid": "FN31pr",
      "frequency": 14025.3,
      "mode": "CW",
      "snr": 25,
      "wpm": 28
    }
  ],
  "total": 247,
  "offset": 0,
  "limit": 100
}

Field Explanations

  • id: Unique spot identifier (used for pagination)
  • timestamp: When the spot was received (ISO 8601 UTC)
  • spotter: The RBN skimmer that detected the signal
  • spotter_grid: Maidenhead grid of the skimmer (null if unknown)
  • callsign: The station being spotted
  • grid: Maidenhead grid of the spotted station (null if unknown)
  • frequency: Frequency in kHz
  • mode: Operating mode (CW, FT8, FT4, RTTY)
  • snr: Signal-to-Noise Ratio in dB (higher = stronger)
  • wpm: Words per minute (CW only, null for digital modes)

Step 2: Adding Filters

Let's narrow down the results by adding mode and band filters:

Search multiple modes
curl "https://vailrerbn.com/api/v1/spots?mode=CW,FT8&band=40m"
Search multiple modes
import requests

response = requests.get(
    "https://vailrerbn.com/api/v1/spots",
    params={
        "mode": "CW,FT8",
        "band": "40m"
    }
)
data = response.json()

# Group by mode
for mode in ["CW", "FT8"]:
    spots = [s for s in data["spots"] if s["mode"] == mode]
    print(f"{mode}: {len(spots)} spots")
Search multiple modes
const response = await fetch(
  'https://vailrerbn.com/api/v1/spots?mode=CW,FT8&band=40m'
);
const data = await response.json();

// Group by mode
for (const mode of ['CW', 'FT8']) {
  const spots = data.spots.filter(s => s.mode === mode);
  console.log(`${mode}: ${spots.length} spots`);
}
Search multiple modes
<?php
$response = file_get_contents(
    "https://vailrerbn.com/api/v1/spots?mode=CW,FT8&band=40m"
);
$data = json_decode($response, true);

// Group by mode
foreach (['CW', 'FT8'] as $mode) {
    $spots = array_filter($data['spots'], function($s) use ($mode) {
        return $s['mode'] === $mode;
    });
    echo "$mode: " . count($spots) . " spots\n";
}
Search multiple modes
require 'net/http'
require 'json'

uri = URI('https://vailrerbn.com/api/v1/spots')
uri.query = URI.encode_www_form({
  mode: 'CW,FT8',
  band: '40m'
})

response = Net::HTTP.get(uri)
data = JSON.parse(response)

# Group by mode
['CW', 'FT8'].each do |mode|
  spots = data['spots'].select { |s| s['mode'] == mode }
  puts "#{mode}: #{spots.length} spots"
end

Filter for both CW and FT8 modes using comma-separated values

Available Filter Parameters

Parameter Description Example
call Callsign to search (partial match) W1AW
spotter Filter by skimmer callsign K1TTT
mode Operating mode (comma-separated) CW,FT8
band Amateur band 20m, 40m
limit Max results (1-1000, default 100) 500

Step 3: Understanding Time Ranges

Important: Default Time Range

By default, the API only returns spots from the last 1 hour. To query historical data, you must explicitly set the since parameter.

The API accepts timestamps in two formats:

Seconds since January 1, 1970 00:00:00 UTC

1704825600

How to get current Unix timestamp:

Bash
date +%s
Python
import time
now = int(time.time())
JavaScript
const now = Math.floor(Date.now() / 1000);
PHP
$now = time();
Ruby
now = Time.now.to_i

Example: Last 24 Hours

Search last 24 hours
# Get Unix timestamp for 24 hours ago
SINCE=$(date -d '24 hours ago' +%s)

curl "https://vailrerbn.com/api/v1/spots?since=$SINCE"
Search last 24 hours
import requests
import time

# Get timestamp for 24 hours ago
since = int(time.time()) - (24 * 60 * 60)

response = requests.get(
    "https://vailrerbn.com/api/v1/spots",
    params={"since": since}
)
data = response.json()
print(f"Found {data['total']} spots in last 24 hours")
Search last 24 hours
// Get timestamp for 24 hours ago
const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60);

const response = await fetch(
  `https://vailrerbn.com/api/v1/spots?since=${since}`
);
const data = await response.json();

console.log(`Found ${data.total} spots in last 24 hours`);
Search last 24 hours
<?php
// Get timestamp for 24 hours ago
$since = time() - (24 * 60 * 60);

$response = file_get_contents(
    "https://vailrerbn.com/api/v1/spots?since=$since"
);
$data = json_decode($response, true);

echo "Found {$data['total']} spots in last 24 hours\n";
Search last 24 hours
require 'net/http'
require 'json'

# Get timestamp for 24 hours ago
since = Time.now.to_i - (24 * 60 * 60)

uri = URI('https://vailrerbn.com/api/v1/spots')
uri.query = URI.encode_www_form({ since: since })

response = Net::HTTP.get(uri)
data = JSON.parse(response)

puts "Found #{data['total']} spots in last 24 hours"

Get spots from the past 24 hours using Unix timestamp

Step 4: Real-Time Monitoring Preview

For continuous monitoring (like tracking when a station comes on the air), use cursor-based pagination with the after_id parameter:

How it works:

  1. Make initial request to get recent spots
  2. Track the highest id from the response
  3. Poll every 5-10 seconds with after_id=LAST_ID
  4. Process new spots and update tracked ID
Real-time monitoring with after_id
# Not practical with curl - use Python or JavaScript for continuous polling
Real-time monitoring with after_id
import requests
import time

last_id = 0

print("Monitoring for W1AW spots (Ctrl+C to stop)...")

while True:
    params = {"limit": 500, "call": "W1AW"}
    if last_id > 0:
        params["after_id"] = last_id

    response = requests.get("https://vailrerbn.com/api/v1/spots", params=params)
    data = response.json()

    for spot in data["spots"]:
        grid = spot.get('grid', '????')
        print(f"{spot['timestamp']} - {spot['callsign']} ({grid}) on {spot['frequency']} kHz by {spot['spotter']}")
        last_id = max(last_id, spot["id"])

    time.sleep(5)  # Poll every 5 seconds
Real-time monitoring with after_id
let lastId = 0;

console.log('Monitoring for W1AW spots (Ctrl+C to stop)...');

async function pollSpots() {
  const params = new URLSearchParams({
    limit: 500,
    call: 'W1AW'
  });

  if (lastId > 0) {
    params.set('after_id', lastId);
  }

  const response = await fetch(`https://vailrerbn.com/api/v1/spots?${params}`);
  const data = await response.json();

  for (const spot of data.spots) {
    const grid = spot.grid || '????';
    console.log(`${spot.timestamp} - ${spot.callsign} (${grid}) on ${spot.frequency} kHz by ${spot.spotter}`);
    lastId = Math.max(lastId, spot.id);
  }
}

// Poll every 5 seconds
setInterval(pollSpots, 5000);
pollSpots(); // Initial call
Real-time monitoring with after_id
<?php
$lastId = 0;

echo "Monitoring for W1AW spots (Ctrl+C to stop)...\n";

while (true) {
    $params = ['limit' => 500, 'call' => 'W1AW'];
    if ($lastId > 0) {
        $params['after_id'] = $lastId;
    }

    $url = "https://vailrerbn.com/api/v1/spots?" . http_build_query($params);
    $response = file_get_contents($url);
    $data = json_decode($response, true);

    foreach ($data['spots'] as $spot) {
        echo "{$spot['timestamp']} - {$spot['callsign']} on {$spot['frequency']} kHz by {$spot['spotter']}\n";
        $lastId = max($lastId, $spot['id']);
    }

    sleep(5); // Poll every 5 seconds
}
Real-time monitoring with after_id
require 'net/http'
require 'json'

last_id = 0

puts 'Monitoring for W1AW spots (Ctrl+C to stop)...'

loop do
  uri = URI('https://vailrerbn.com/api/v1/spots')
  params = { limit: 500, call: 'W1AW' }
  params[:after_id] = last_id if last_id > 0
  uri.query = URI.encode_www_form(params)

  response = Net::HTTP.get(uri)
  data = JSON.parse(response)

  data['spots'].each do |spot|
    puts "#{spot['timestamp']} - #{spot['callsign']} on #{spot['frequency']} kHz by #{spot['spotter']}"
    last_id = [last_id, spot['id']].max
  end

  sleep 5 # Poll every 5 seconds
end

Continuously poll for new spots using cursor pagination

Learn more about pagination strategies →

What's Next?

Try the API Playground

Build and test queries interactively with our visual tool

Explore All Endpoints

Complete reference for /stats, /skimmers, /charts, and more

See Common Use Cases

Real-world examples like band analysis, DX alerting, and CSV export

Troubleshooting Guide

Solutions to common errors and gotchas