Outlap Docs
Open Outlap

Custom integration

Build your own connector: push laps and penalties from any timing system, decoder or spreadsheet using the Live Timing API.

A step-by-step walkthrough for pushing laps and penalties from MyLaps Speedhive, RaceHero, AMB decoders, or your own timing code into an Outlap event. For the field-by-field reference, see Ingest laps.

  1. Create an API key.

    In your club dashboard, go to Settings → API keys and mint one. The plaintext key is shown once; store it in the timing PC’s secrets manager. Keys are scoped to a single club. See Create an API key.

  2. Map transponders to drivers.

    In the event’s Paddock tab, enter each attendee’s transponder. Laps can then be resolved from a decoder ID alone. Or skip this and send user_id directly.

  3. Mark the session live.

    So the public live board shows it in real time:

    curl -X PATCH https://fzfstcyowovukscxgdrh.supabase.co/functions/v1/timing-api/v1/events/EVENT_ID/sessions/SESSION_ID \
      -H "Authorization: Bearer $OUTLAP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "status": "live" }'
  4. Post laps.

    Batch up to 1,000 per request. All times are in milliseconds. Send incrementally as your decoder emits passings, or in bulk after the session.

    curl -X POST https://fzfstcyowovukscxgdrh.supabase.co/functions/v1/timing-api/v1/events/EVENT_ID/sessions/SESSION_ID/laps \
      -H "Authorization: Bearer $OUTLAP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "laps": [
          { "lap_number": 1, "lap_time_ms": 92345, "s1_ms": 28110, "s2_ms": 31900, "s3_ms": 32335,
            "speed_trap_kph": 214.6, "transponder_id": "MX-4421", "recorded_at": "2026-06-02T14:05:11Z" },
          { "lap_number": 2, "lap_time_ms": 91980, "transponder_id": "MX-4421" }
        ]
      }'
    curl -X POST https://fzfstcyowovukscxgdrh.supabase.co/functions/v1/timing-api/v1/events/EVENT_ID/laps \
      -H "Authorization: Bearer $OUTLAP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "laps": [
          { "lap_number": 5, "lap_time_ms": 88420, "user_id": "USER_UUID" },
          { "lap_number": 6, "lap_time_ms": 88210, "user_id": "USER_UUID", "is_valid": false, "invalid_reason": "cut T7" }
        ]
      }'
  5. Post penalties.

    Time penalties are added to the driver’s total. Points deductions come off championship points. Both recompute results. See Add penalties.

    curl -X POST https://fzfstcyowovukscxgdrh.supabase.co/functions/v1/timing-api/v1/events/EVENT_ID/penalties \
      -H "Authorization: Bearer $OUTLAP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "penalties": [
        { "user_id": "USER_UUID", "type": "time", "value_ms": 5000, "reason": "track limits" },
        { "user_id": "USER_UUID", "type": "points_deduction", "value_int": 3, "reason": "jump start" }
      ] }'
  6. Close the session.

    Set status to finished when timing is final. The leaderboard is frozen for public viewers; organisers can still edit.

  7. Verify on the live board.

    Open the event’s live page and confirm the order and best laps. Check Race Control for any laps that didn’t match a driver.

Tips

  • Parse mm:ss.SSS with (m * 60 + s) * 1000 + ms.
  • Out-of-order lap numbers are fine. Results recompute on every push.
  • POST /laps doesn’t de-duplicate. Re-posting the same lap creates a second one. Track what you’ve sent, or use POST /stream with passings, which skips laps already ingested.
  • Respect limits. 120 writes a minute per key. Batch 50 to 200 laps per request, honour Retry-After on 429, and back off on 5xx. See Rate limits.
  • Report health. Send a heartbeat every 30 seconds so Race Control can show your feed’s status.

A minimal relay in Node

const API = "https://fzfstcyowovukscxgdrh.supabase.co/functions/v1/timing-api";
const { OUTLAP_API_KEY, EVENT_ID } = process.env;

async function post(path, body) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${API}${path}`, {
      method: "POST",
      headers: { Authorization: `Bearer ${OUTLAP_API_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, (Number(res.headers.get("Retry-After")) || 5) * 1000));
    } else if (res.status >= 500 && attempt < 5) {
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
    } else {
      throw new Error(`${res.status}: ${(await res.json()).error}`);
    }
  }
}

// passings from your own decoder
export const sendPassings = (passings) =>
  post(`/v1/events/${EVENT_ID}/stream`, { vendor: "generic", source_label: "My relay", passings });
esc
↑↓ to navigate ↵ to open esc to close