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.
-
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.
-
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_iddirectly. -
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" }' -
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" } ] }' -
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" } ] }' -
Close the session.
Set
statustofinishedwhen timing is final. The leaderboard is frozen for public viewers; organisers can still edit. -
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.SSSwith(m * 60 + s) * 1000 + ms. - Out-of-order lap numbers are fine. Results recompute on every push.
POST /lapsdoesn’t de-duplicate. Re-posting the same lap creates a second one. Track what you’ve sent, or usePOST /streamwithpassings, which skips laps already ingested.- Respect limits. 120 writes a minute per key. Batch 50 to 200 laps per request, honour
Retry-Afteron429, and back off on5xx. 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 });