Skip to main content
Back to Racey School
12 min read
Race directors and admins doing one-off entries

Manual Result Import

Long-form reference for hand-entered or JSON-driven result imports — field reference, multiclass, stages, validation rules, and common errors.

Hand-enter a complete round result without a CSV export
Understand field requirements for multiclass and stage races
Diagnose validation errors during manual entry
Use this guide during beta
1
Manually enter results for one round end-to-end
2
Trigger and resolve at least one validation error
3
Verify DNF/DSQ/DNS handling matches what you expected
Open quick reference

This guide provides the complete reference for importing race results into Racey, including the JSON and CSV field formats, handling special cases, and troubleshooting common errors.


Table of Contents

  1. Overview
  2. Import Methods
  3. Field Reference
  4. JSON Format
  5. CSV Format
  6. Handling DNFs, DSQs, and DNS
  7. Multiclass Races
  8. Stage Results (NASCAR-style)
  9. Heat + Feature Races
  10. Validation Rules
  11. Common Errors and Fixes
  12. Best Practices

1. Overview

Racey currently supports manual result import for race sessions. Results are imported per session within a round (e.g., you import qualifying results and race results separately).

The import pipeline:

  1. You prepare results data (JSON or CSV).
  2. You import via the League Admin Results page or the API.
  3. Racey validates the data and creates RaceResult records.
  4. You review the imported data.
  5. You publish the results, which triggers the scoring engine.
  6. Standings are recalculated and drivers are notified.

2. Import Methods

UI Import (League Admin > Results)

  1. Navigate to League Admin > Results.
  2. Click Import Results.
  3. Select the Target Round and Target Session.
  4. Paste the results as a JSON array.
  5. Click Import.

API Import

Send a POST request to the results API endpoint:

POST /api/results
Content-Type: application/json

{
  "sessionId": "session_cuid",
  "roundId": "round_cuid",
  "results": [ ... ]
}

The results array uses the same format as the UI import.


3. Field Reference

FieldTypeRequiredDescription
userIdstringYesThe Racey user ID (CUID format) of the driver. Must match a registered driver.
positionintegerYesOverall finishing position (1-based).
startPositionintegerYesGrid starting position (1-based).
lapsCompletedintegerNoNumber of laps completed. Defaults to 0.
lapsLedintegerNoNumber of laps the driver led. Defaults to 0.
fastestLapTimefloatNoDriver's fastest lap time in seconds (e.g., 91.234). null if no valid lap.
avgLapTimefloatNoAverage lap time in seconds. null if not available.
incidentsintegerNoIncident count (iRacing-style 0x, 1x, 2x, 4x system). Defaults to 0.
finishStatusstringNoOne of: running, dnf, dsq, dns. Defaults to running.
intervalstringNoTime interval to the car ahead (e.g., "+1.234", "+1 Lap"). null for the leader.
gapstringNoTime gap to the overall leader (e.g., "0.000", "12.456", "2 Laps").
carClassIdstringNoThe car class CUID for multiclass races. null for single-class.

4. JSON Format

The primary import format is a JSON array of result objects:

[
  {
    "userId": "clx1234567890abcdef",
    "position": 1,
    "startPosition": 3,
    "lapsCompleted": 42,
    "lapsLed": 18,
    "fastestLapTime": 91.234,
    "avgLapTime": 92.567,
    "incidents": 2,
    "finishStatus": "running",
    "interval": null,
    "gap": "0.000"
  },
  {
    "userId": "clx0987654321zyxwvu",
    "position": 2,
    "startPosition": 1,
    "lapsCompleted": 42,
    "lapsLed": 24,
    "fastestLapTime": 91.456,
    "avgLapTime": 92.789,
    "incidents": 0,
    "finishStatus": "running",
    "interval": "+1.234",
    "gap": "1.234"
  },
  {
    "userId": "clx1111111111aaaaaa",
    "position": 3,
    "startPosition": 5,
    "lapsCompleted": 42,
    "lapsLed": 0,
    "fastestLapTime": 91.789,
    "avgLapTime": 93.012,
    "incidents": 4,
    "finishStatus": "running",
    "interval": "+3.456",
    "gap": "4.690"
  }
]

Minimal Example

Only the required fields:

[
  { "userId": "clx1234567890abcdef", "position": 1, "startPosition": 3 },
  { "userId": "clx0987654321zyxwvu", "position": 2, "startPosition": 1 },
  { "userId": "clx1111111111aaaaaa", "position": 3, "startPosition": 5 }
]

All optional fields default to their zero values (0, null, or "running").


5. CSV Format

If you are preparing results from a spreadsheet, use the following CSV format. Convert to JSON before importing via the UI, or use a script to transform to the API format.

CSV Header

position,userId,startPosition,lapsCompleted,lapsLed,fastestLapTime,avgLapTime,incidents,finishStatus,interval,gap,carClassId

CSV Example

position,userId,startPosition,lapsCompleted,lapsLed,fastestLapTime,avgLapTime,incidents,finishStatus,interval,gap,carClassId
1,clx1234567890abcdef,3,42,18,91.234,92.567,2,running,,0.000,
2,clx0987654321zyxwvu,1,42,24,91.456,92.789,0,running,+1.234,1.234,
3,clx1111111111aaaaaa,5,42,0,91.789,93.012,4,running,+3.456,4.690,
4,clx2222222222bbbbbb,2,40,0,,,6,dnf,+2 Laps,,
5,clx3333333333cccccc,4,0,0,,,0,dns,,,

CSV to JSON Conversion

Use this approach to convert your CSV:

// Example Node.js conversion
const csv = require('csv-parse/sync')
const fs = require('fs')

const raw = fs.readFileSync('results.csv', 'utf8')
const records = csv.parse(raw, { columns: true, skip_empty_lines: true })

const results = records.map((row) => ({
  userId: row.userId,
  position: parseInt(row.position),
  startPosition: parseInt(row.startPosition),
  lapsCompleted: parseInt(row.lapsCompleted) || 0,
  lapsLed: parseInt(row.lapsLed) || 0,
  fastestLapTime: row.fastestLapTime ? parseFloat(row.fastestLapTime) : null,
  avgLapTime: row.avgLapTime ? parseFloat(row.avgLapTime) : null,
  incidents: parseInt(row.incidents) || 0,
  finishStatus: row.finishStatus || 'running',
  interval: row.interval || null,
  gap: row.gap || null,
  carClassId: row.carClassId || null,
}))

console.log(JSON.stringify(results, null, 2))

6. Handling DNFs, DSQs, and DNS

DNF (Did Not Finish)

A driver who started the race but did not complete it.

{
  "userId": "clx2222222222bbbbbb",
  "position": 15,
  "startPosition": 8,
  "lapsCompleted": 28,
  "lapsLed": 0,
  "fastestLapTime": 92.345,
  "incidents": 6,
  "finishStatus": "dnf",
  "interval": "+14 Laps",
  "gap": null
}

Key points:

  • Set finishStatus to "dnf".
  • position should reflect their classified position (typically behind all running cars).
  • lapsCompleted should show how many laps they completed before retiring.
  • interval can show the lap deficit (e.g., "+14 Laps").

DSQ (Disqualified)

A driver disqualified from the race results.

{
  "userId": "clx4444444444dddddd",
  "position": 20,
  "startPosition": 6,
  "lapsCompleted": 42,
  "lapsLed": 5,
  "incidents": 12,
  "finishStatus": "dsq"
}

Key points:

  • Set finishStatus to "dsq".
  • position is typically last or based on the league's DSQ classification rules.
  • The driver's laps and stats are still recorded for historical purposes.
  • Scoring engine awards 0 points for DSQ results.

DNS (Did Not Start)

A driver who was entered but did not take the start.

{
  "userId": "clx3333333333cccccc",
  "position": 21,
  "startPosition": 4,
  "lapsCompleted": 0,
  "lapsLed": 0,
  "incidents": 0,
  "finishStatus": "dns"
}

Key points:

  • Set finishStatus to "dns".
  • lapsCompleted should be 0.
  • position is typically last.
  • DNS results receive 0 points.

Ordering

When importing a mix of running, DNF, DSQ, and DNS results, order positions as:

  1. Running cars (by finishing order)
  2. DNF cars (by laps completed descending, then by when they retired)
  3. DSQ cars
  4. DNS cars

7. Multiclass Races

For multiclass events, include the carClassId field to assign each driver to their class.

Prerequisites

Car classes must be created in the season configuration before importing results. Navigate to League Admin > Seasons and set up classes.

Example

[
  {
    "userId": "clx_gtp_driver1",
    "position": 1,
    "startPosition": 1,
    "lapsCompleted": 60,
    "lapsLed": 45,
    "fastestLapTime": 78.234,
    "incidents": 0,
    "finishStatus": "running",
    "carClassId": "cls_gtp_class_id"
  },
  {
    "userId": "clx_gtd_driver1",
    "position": 2,
    "startPosition": 4,
    "lapsCompleted": 58,
    "lapsLed": 0,
    "fastestLapTime": 82.567,
    "incidents": 2,
    "finishStatus": "running",
    "carClassId": "cls_gtd_class_id"
  },
  {
    "userId": "clx_gtp_driver2",
    "position": 3,
    "startPosition": 2,
    "lapsCompleted": 60,
    "lapsLed": 15,
    "fastestLapTime": 78.456,
    "incidents": 1,
    "finishStatus": "running",
    "carClassId": "cls_gtp_class_id"
  }
]

How Class Scoring Works

When the scoring system has multiClass.scoreSeparately enabled:

  • Each class gets its own points allocation based on in-class position (not overall).
  • Class standings are maintained separately.
  • The classPosition is computed automatically from the position field within each class group.

Finding Car Class IDs

  • Open League Admin > Seasons and view the car classes for the season.
  • Car class IDs are CUIDs (e.g., cls_abc123def456).
  • If you are using the API, query the season's car classes endpoint to retrieve IDs.

8. Stage Results (NASCAR-style)

If your scoring system uses stage scoring, stage results are imported separately from the main race results.

Stage data is passed as part of the scoring pipeline configuration when results are published. The stage positions at each caution/stage end determine stage points.

Stage Result Format

{
  "stageResults": [
    { "userId": "clx_driver1", "stagePosition": 1, "stageNumber": 1 },
    { "userId": "clx_driver2", "stagePosition": 2, "stageNumber": 1 },
    { "userId": "clx_driver1", "stagePosition": 3, "stageNumber": 2 },
    { "userId": "clx_driver3", "stagePosition": 1, "stageNumber": 2 }
  ]
}

9. Heat + Feature Races

For the Heat + Feature scoring preset, heat and feature results are imported as separate sessions within the same round.

Setup

  1. When creating the round, add sessions:
    • Session type: heat, order: 0
    • Session type: feature, order: 0
  2. Import heat results into the heat session.
  3. Import feature results into the feature session.

The scoring engine automatically combines heat points and feature points when computing the round total.

Example

Heat results:

[
  {
    "userId": "clx_driver1",
    "position": 1,
    "startPosition": 3,
    "lapsCompleted": 15,
    "finishStatus": "running"
  },
  {
    "userId": "clx_driver2",
    "position": 2,
    "startPosition": 1,
    "lapsCompleted": 15,
    "finishStatus": "running"
  },
  {
    "userId": "clx_driver3",
    "position": 3,
    "startPosition": 2,
    "lapsCompleted": 15,
    "finishStatus": "running"
  }
]

Feature results (imported into the feature session):

[
  {
    "userId": "clx_driver2",
    "position": 1,
    "startPosition": 2,
    "lapsCompleted": 50,
    "lapsLed": 30,
    "finishStatus": "running"
  },
  {
    "userId": "clx_driver1",
    "position": 2,
    "startPosition": 1,
    "lapsCompleted": 50,
    "lapsLed": 20,
    "finishStatus": "running"
  },
  {
    "userId": "clx_driver3",
    "position": 3,
    "startPosition": 3,
    "lapsCompleted": 50,
    "lapsLed": 0,
    "finishStatus": "running"
  }
]

10. Validation Rules

The import system validates the following:

RuleError if Violated
Results must be an array"Results payload must be a JSON array"
Each entry must have userIdMissing required field
Each entry must have positionMissing required field
Each entry must have startPositionMissing required field
userId must match a registered user"User not found"
position must be a positive integerInvalid position value
startPosition must be a positive integerInvalid start position value
No duplicate userId per session"Duplicate userId in results"
finishStatus must be one of: running, dnf, dsq, dnsInvalid finish status
carClassId (if provided) must match a valid class"Car class not found"
lapsCompleted must be >= 0Invalid lap count
incidents must be >= 0Invalid incident count

11. Common Errors and Fixes

"Results payload must be a JSON array"

Cause: The pasted data is not valid JSON or is not wrapped in array brackets.

Fix: Ensure your data starts with [ and ends with ]. Validate the JSON using a tool like jsonlint.com.

"User not found" for a userId

Cause: The userId does not match any user in the Racey database.

Fix:

  • Verify the user ID is correct. User IDs are CUIDs (e.g., clx1234567890abcdef).
  • Ensure the driver has an account on Racey. They must have signed in at least once.
  • Check for copy-paste errors (trailing spaces, truncated IDs).

"Duplicate userId in results"

Cause: The same driver appears twice in the results array.

Fix: Remove the duplicate entry. Each driver can only have one result per session.

"Car class not found"

Cause: The carClassId does not match any class configured for the season.

Fix:

  • Verify car class IDs from League Admin > Seasons > Car Classes.
  • Ensure classes were created before importing results.

Import succeeds but standings are wrong

Cause: Results are imported but not yet published.

Fix: After importing, click Publish on the round from the Results page. Standings are only recalculated when results are officially published.

Points are zero for all drivers

Cause: No scoring system is assigned to the season.

Fix: Navigate to League Admin > Scoring and assign a scoring system to the active season.


12. Best Practices

  1. Prepare results immediately after the race. Import while the data is fresh and accurate.

  2. Use a spreadsheet. Maintain a Google Sheet or Excel template with the correct columns. Export to CSV, convert to JSON, and paste.

  3. Verify user IDs. Build a lookup table mapping driver names to their Racey user IDs at the start of the season. Keep it in your spreadsheet.

  4. Import practice/qualifying first. If you have multiple sessions, import them in order: practice, qualifying, then race.

  5. Review before publishing. After importing, check the results table in the admin UI. Verify positions, lap counts, and incidents before clicking Publish.

  6. Export a backup. After publishing, click Export CSV to save an offline copy of the official results.

  7. Handle corrections promptly. If errors are found after publishing, use the Result Corrections feature rather than re-importing. Corrections maintain an audit trail.

  8. Document your process. If multiple staff members import results, document your workflow so the format is consistent across rounds.