Layer One Robotics

Type at least two characters to search.

Skip to examples

Public API · v1

Build your competition viewer

Get matches, score breakdowns, robot standings, awards, brackets, and the public match timer in one JSON snapshot. No account, API key, or cookies required.

Start with a static sample

Add mock=true to either endpoint. It works without a database connection and always returns the same sample event and timestamps. The competition selector is ignored in mock mode. Try ticker ROBOT or A&B; other tickers return 404.

In the interactive reference, choose Test Request, include mock under Query Parameters, and select true from its dropdown.

Remove mock for real data. Use live, or copy the competition ID from /competition/COMPETITION_ID/view.

Choose an event and team

live prefers the ongoing event with the latest start date. If none is ongoing, it chooses the most recently started event; if none has started, it chooses the nearest upcoming event. Equal start dates prefer the newest-created ongoing/past event, or oldest-created upcoming event. Pin a competition ID to keep watching the same event.

my-team trims and uppercases your ticker. It includes every registered robot on the team, official robot-level ranks, matches with both alliances, upcoming matches, and earned awards. A match appears only once even if two of your robots play in it. The presentation timer remains event-wide.

Request examples

The examples use mock data. For curl and Python, replace localhost with the website origin. JavaScript uses the current website origin. Always encode tickers: A&B becomes A%26B.

Full competition

GET /api/v1/competitions/{competition}/full

Full curl
# Replace localhost with your website origin; remove mock for real data.
curl --get 'http://localhost:3000/api/v1/competitions/live/full' \
  --data-urlencode 'mock=true'
JavaScript and Python polling examples
Full JavaScript
// Run in a browser on the website, or replace location.origin.
const url = new URL('/api/v1/competitions/live/full', location.origin);
url.search = new URLSearchParams({ mock: 'true' }).toString();
const controller = new AbortController();
let delay = 5000;
async function poll() {
  try {
    const response = await fetch(url, {
      credentials: 'omit', cache: 'no-store',
      signal: controller.signal,
    });
    if (response.status === 400 || response.status === 404) {
      console.error(await response.json());
      return; // Fix the selector/ticker before retrying.
    }
    if (!response.ok) throw new Error('Temporarily unavailable');
    const snapshot = await response.json();
    console.log(snapshot); // Update your viewer here.
    delay = 5000;
  } catch (error) {
    if (controller.signal.aborted) return;
    console.error(error);
    delay = Math.min(delay * 2, 60000);
  }
  if (!controller.signal.aborted) setTimeout(poll, delay);
}
poll();
// Call controller.abort() when leaving the viewer.
// Remove mock from url.searchParams to use real competition data.
Full Python
import json
import os
import time
from urllib.parse import urlencode
from urllib.request import urlopen
from urllib.error import HTTPError, URLError

base = os.environ.get('BASE_URL', 'http://localhost:3000')
params = {'mock': 'true'}  # Remove mock for real data.
url = base.rstrip('/') + '/api/v1/competitions/live/full?' + urlencode(params)
delay = 5
while True:
    try:
        with urlopen(url, timeout=15) as response:
            snapshot = json.load(response)
        print(snapshot)  # Update your viewer here.
        delay = 5
    except HTTPError as error:
        if error.code in (400, 404):
            raise  # Fix the selector/ticker before retrying.
        delay = min(delay * 2, 60)
    except (URLError, TimeoutError):
        delay = min(delay * 2, 60)
    time.sleep(delay)

My team

GET /api/v1/competitions/{competition}/my-team?ticker=ROBOT

Team curl
# Replace localhost with your website origin; remove mock for real data.
curl --get 'http://localhost:3000/api/v1/competitions/live/my-team' \
  --data-urlencode 'mock=true' \
  --data-urlencode 'ticker=A&B'
JavaScript and Python polling examples
Team JavaScript
// Run in a browser on the website, or replace location.origin.
const url = new URL('/api/v1/competitions/live/my-team', location.origin);
url.search = new URLSearchParams({ mock: 'true', ticker: 'A&B' }).toString();
const controller = new AbortController();
let delay = 5000;
async function poll() {
  try {
    const response = await fetch(url, {
      credentials: 'omit', cache: 'no-store',
      signal: controller.signal,
    });
    if (response.status === 400 || response.status === 404) {
      console.error(await response.json());
      return; // Fix the selector/ticker before retrying.
    }
    if (!response.ok) throw new Error('Temporarily unavailable');
    const snapshot = await response.json();
    console.log(snapshot); // Update your viewer here.
    delay = 5000;
  } catch (error) {
    if (controller.signal.aborted) return;
    console.error(error);
    delay = Math.min(delay * 2, 60000);
  }
  if (!controller.signal.aborted) setTimeout(poll, delay);
}
poll();
// Call controller.abort() when leaving the viewer.
// Remove mock from url.searchParams to use real competition data.
Team Python
import json
import os
import time
from urllib.parse import urlencode
from urllib.request import urlopen
from urllib.error import HTTPError, URLError

base = os.environ.get('BASE_URL', 'http://localhost:3000')
params = {'mock': 'true', 'ticker': 'A&B'}  # Remove mock for real data.
url = base.rstrip('/') + '/api/v1/competitions/live/my-team?' + urlencode(params)
delay = 5
while True:
    try:
        with urlopen(url, timeout=15) as response:
            snapshot = json.load(response)
        print(snapshot)  # Update your viewer here.
        delay = 5
    except HTTPError as error:
        if error.code in (400, 404):
            raise  # Fix the selector/ticker before retrying.
        delay = min(delay * 2, 60)
    except (URLError, TimeoutError):
        delay = min(delay * 2, 60)
    time.sleep(delay)

Read results and standings

Matches are ordered by match number. Unpublished future schedules and cancelled matches are excluded everywhere, including bracket match references and presentation selections. Active and completed brackets include seeded alliances and series topology; draft brackets are excluded. Awards appear before recipients are chosen.

score is null until an active score revision exists. A completed result can still change; only finalized qualifying matches affect official standings. Corrections replace the active revision ID. Use outcome as the result: a disqualification can override the numerical winner. Practice, elimination, excluded qualification matches, and surrogate appearances do not earn ranking points.

score.gameData contains a schema key, version, and optional payload. Cash Out v1 exposes red/blue chip counts in ground, low bank, high bank, ground safe, and elevated safe locations, plus minor/major penalties, disqualifications, and unscored chips. Ground and ground-safe chips score 1, low-bank and elevated-safe chips 2, high-bank chips 3; opponent minor/major penalties add 3/5 points. Stored totals remain authoritative. Unknown formats retain their key/version with a null payload. Unfinished scoring workspaces and score revision history are not included.

Robot standings use finalized results: wins earn 2 ranking points, ties 1, losses 0; disqualified alliances earn 0. No-contests are excluded. Played robots sort before unplayed robots, then ranking points and total eligible score descending, then ticker, robot name, and entry ID for deterministic ties.

Each standings row also includes offensivePowerRating(OPR): a least-squares estimate of that robot's contribution to its alliance score across finalized, eligible qualification matches. Robots without enough qualifying score data return 0.

Poll and display timers

Poll every 5 seconds and use exponential backoff up to 60 seconds after failures. Keep the last successful snapshot visible with a stale indicator. Each response is a complete snapshot with apiVersion: "1", resolved competitionId, and generatedAt. Times are Unix milliseconds; unavailable values are null. Responses are not cached.

currentMatches contains running or paused matches. upcomingMatches contains queued and scheduled matches. nextMatch prefers the lowest-numbered queued match, then the lowest-numbered scheduled match, or null. None of these imply an estimated start time.

The presentation state can be null if no state exists or its selected match is not public. For live/countdown timers, estimate server time from generatedAt and elapsed time since receipt; count down to goAt or endsAt. A paused timer uses the fixed remainingMs. Network latency makes this an estimate; refresh it with each poll.

Timer interpolation
let receivedAt = performance.now();
function remainingMs(snapshot) {
  const state = snapshot.presentation;
  if (!state) return null;
  if (state.mode === 'paused') return state.remainingMs;
  const target = state.mode === 'countdown' ? state.goAt
    : state.mode === 'live' ? state.endsAt : null;
  if (target === null) return null;
  const serverNow = snapshot.generatedAt + performance.now() - receivedAt;
  return Math.max(0, target - serverNow);
}
// Reset receivedAt for each new snapshot.

400 means invalid input; 404 means the event is missing or the team has no registered robot. Fix those inputs before retrying. For 503, back off and retry. Read error.code and error.message for details. CORS allows cross-origin GET/OPTIONS without credentials.

The API returns all public event data without pagination or silent truncation. Very large events can exceed backend limits and return 503; smaller team responses still read the same complete event snapshot.

Explore every response field in the interactive reference →