Import Operations Guide
1. Purpose
This guide is for operators running bulk imports against a deployed admin service. It covers Event Participant, Membership, and Result imports. Scope:
-
Preparing the source file
-
Choosing the right endpoint, mode, and query parameters
-
Running the import against Dev / Stage / Prod
-
Reading the response and recognising common failure modes
-
Recovering from operator errors
For functional requirements see Event Participant Import Requirements and Result Import Requirements. For design see Event Participant Import Design, Membership Import Design, and Results Import Design.
2. Quick Reference
All three imports below are documented for release 2.4+ (asynchronous): the upload returns 202 Accepted + job id; poll the paired GET /import/{jobId} for the result DTO. See Async Delivery Mode.
On admin-service through 2.3.32 (current prod), the same endpoints exist but return the full result DTO inline on the PUT — see Sync Mode (pre-2.4) — applies through admin-service 2.3.32.
Each row’s Task cell links to the corresponding design doc’s API section (endpoint spec, request body, response DTO, invariants). The In this guide link jumps to the operational walkthrough below (file prep, curl example, how to read the response, recovery).
| Task (design doc) | Endpoint | Key query params | In this guide |
|---|---|---|---|
|
|
||
|
|
||
|
|
All endpoints require authentication — use the X-API-KEY header with an ADMIN-role key, or a valid JWT from admin-ui.
3. Environment URLs
Refer to Management API Access / reference_environment_urls.md for the current production API key and dev/stage/prod URLs. As of this writing:
| Environment | Base URL |
|---|---|
Production |
|
Stage |
|
Dev |
|
Test against Dev first for any new event or any file type you haven’t imported before. A failed import can delete pre-existing data (see Recovering from a Mode Mismatch). Dev is on the same MySQL cluster as Prod ( |
4. Sync Mode (pre-2.4) — applies through admin-service 2.3.32
|
Production currently runs admin-service 2.3.32 (commit Verified anchor: the import code ( Review on upgrade: when prod moves to 2.4+, re-diff these four files against the new release tag and either delete this section or refresh its anchor. |
4.1. Sync curl patterns
EP import (sync):
curl -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-F "[email protected]" \
"$BASE_URL/api/event-participants/import?eventId=<id>&sheetIndex=0&createCustom1=true&createCustom2=true&createCustom3=true"
Returns 200 OK + EventParticipantImportResultDTO inline.
Membership import (sync):
curl -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-F "[email protected]" \
"$BASE_URL/api/memberships/import?periodId=<id>&sheetIndex=0"
Returns 200 OK + MembershipImportResultDTO inline.
Bulk result import (sync):
curl -X PUT \
-H "X-API-KEY: ${API_KEY}" \
-F "[email protected]" \
"$BASE_URL/api/result-sets/import-bulk?eventId=<id>&participantIdMode=regid&pointsCalculator=wpca-road-league"
Returns 200 OK + BulkResultImportResponseDTO inline.
The DTO shape returned by the sync PUT is byte-identical to the DTO returned by the async GET /import/{jobId} once the job completes — see the per-import sections below for field-level interpretation. The only thing that changes between sync and async is how the DTO is delivered.
4.2. Sync mode constraints
-
180s nginx read timeout. Imports that take longer than that will appear to fail with a 504 even though the backend usually finishes the work. After a 504, query the database to confirm whether the rows landed before re-running.
-
No cancellation. There is no jobId, so
DELETE /api/imports/{jobId}does not apply. Plan file sizes accordingly: a couple of hundred EP rows or a few hundred result rows is the comfortable upper bound on sync. -
No
Teamheader on EP import at this version.EventParticipantImportXLSroutesSchoolyouareattending→custom_1_id(club) but has no mapping tocustom_2_id(team). To populate team on a 2.3.x prod, run a manualcustom_list_valueupsert +event_participant.custom_2_idUPDATE after the import lands. -
Bug #471 still live. The
matchPersonOTHER-branch creates duplicate Persons when theIDTypecolumn is absent. Always shipIDType=NATIONALas an explicit column on every row of the EP CSV until the fix deploys. -
No
RaceNumber/NumberTypeheader mapping on EP import at this version. The EP importer ignores those columns even when present in the source;event_participant.number_idandtag_idremain NULL after the import. Number/tag linkage is a separate post-import step (see Number & Tag Runbook). -
Result importer matches on
event_category.name, notrace.name. TheRegistration Eventcolumn in the bulk result CSV is matched againstevent_category.namewithin the event; the importer then finds the race that belongs to that category. A race named differently from its category will cause every CSV row with that race name to come back inunmatchedCategories. For combined-bucket races (e.g. a single "Youth" race spanning multiple age-band categories), create a syntheticevent_categoryof the same name and re-point the race’s FK at it — do not rely on the race’s own name to drive the match. -
EP import "rollback-only" errors in
issues[]are misleading. When Hibernate’s batch flush encounters a constraint violation, every subsequent row in the same batch can be reported withoutcome: ERRORandmessage: "Transaction silently rolled back because it has been marked as rollback-only", even though those rows actually persist via a retry transaction. Do not treaterrors_countfrom the response DTO as the true failure count. Verify against the database: true failures = (rows in source file) − (rows inevent_participantfor the event after the import). Real failures are typically duplicateRegistrationIDrows in the source, not the long tail of "rollback-only" messages.
5. Async Delivery Mode
From release 2.4 all three import endpoints (EP, Memberships, Bulk Results) are asynchronous. The upload returns HTTP 202 Accepted + an ImportJobDTO immediately; the response body you previously got inline is now fetched from a per-entity GET /import/{jobId} endpoint once the job reaches COMPLETED or FAILED.
Common shape:
# 1. Upload — returns immediately with a job identifier
PUT /api/{entity}/import[-bulk]?... → 202 Accepted
Location: /api/{entity}/import/{jobId}
{ "identifier": "<jobId>", "status": "PROCESSING", ... }
# 2. Poll until the job reaches a terminal status
GET /api/{entity}/import/{jobId}
→ 409 Conflict (still processing)
→ 200 OK + <EntityImportResultDTO> (COMPLETED or FAILED — payload carries the status)
→ 404 Not Found (unknown job id, wrong import type, or expired)
The response DTO returned by the GET is byte-identical to what the prior synchronous endpoint returned inline — the job summary, issues list, counters, number changes, etc., are all in the same shape. Only the delivery mode changed.
| Entity | Upload endpoint | Results endpoint |
|---|---|---|
Event Participants |
|
|
Memberships |
|
|
Results (bulk CSV) |
|
|
A generic status endpoint GET /api/imports/{jobId} is also available; it returns the framework-level state (status, counters, file-lines, skipMappingPhases) without the domain DTO. Useful during polling when you only need status.
5.1. Cancellation
DELETE /api/imports/{jobId} marks the job as CANCELLED. For result imports (whole-file, monolithic processor), the CANCELLED flag is observed between mapping phases but not mid-processing — the bulk CSV processor runs to completion once started, so a CANCELLED request on a RESULT job that is already in PROCESSING is effectively a no-op until the current file finishes. EP and Membership imports honour cancel between rows.
5.2. Polling example
# Upload
RESP=$(curl -sS -X PUT \
-H "X-API-KEY: $ADMIN_API_KEY" \
-F "file=@/path/to/125-ResultExport.csv" \
"$BASE_URL/api/result-sets/import-bulk?eventId=125&participantIdMode=regid")
JOB_ID=$(echo "$RESP" | jq -r '.identifier')
# Poll every 2 seconds until COMPLETED or FAILED
while :; do
STATUS=$(curl -sS -H "X-API-KEY: $ADMIN_API_KEY" \
"$BASE_URL/api/imports/$JOB_ID" | jq -r '.status')
case "$STATUS" in
COMPLETED|FAILED) break ;;
*) sleep 2 ;;
esac
done
# Fetch the per-entity result DTO
curl -sS -H "X-API-KEY: $ADMIN_API_KEY" \
"$BASE_URL/api/result-sets/import/$JOB_ID" | jq .
6. Event Participant Import
6.1. When to use it
You have a spreadsheet (from a club secretary, a legacy system export, or a manual roster) and want every row to become an EventParticipant in a named event. Re-running the same file with corrections should update existing records, not create duplicates.
6.2. Preparing the file
The import accepts XLSX (not CSV). Required columns:
-
FirstName(aliases:First Name,FN,Name) -
LastName(aliases:Last Name,Surname,SN,LN)
Strongly recommended columns for clean upsert on re-run:
-
RegistrationID(aliases:ExternalID,ExternalEPID) — the primary upsert key within the event. Without it, the import cannot recognise existing EPs and will create duplicates. -
IDNumber,IDType,IDCountry— used for Person matching across imports and events -
DOB,Email,Cellphone— secondary Person-matching signals -
Category—EventCategory.namefor the target event
Common extra columns: Gender, Address, Town, PostalCode, Bib / Number, OrderNumber, Schoolyouareattending (as a custom list).
See Event Participant Import Design for the full 32-column list and aliases.
6.3. Running the import
The upload is async (see Async Delivery Mode): this returns 202 Accepted with a job identifier; poll GET /api/event-participants/import/{jobId} for the result DTO.
curl -X PUT \
-H "X-API-KEY: $ADMIN_API_KEY" \
-F "file=@/path/to/participants.xlsx" \
"$BASE_URL/api/event-participants/import?eventId=123&sheetIndex=0&createCustom1=true&createCustom2=true&createCustom3=true"
Parameters:
-
eventId(required) — target event -
sheetIndex(optional, default0) — 0-based sheet index in the workbook -
orgId(optional) — defaults to the event’s organisation; only override for cross-org test data -
createCustom1/2/3(defaulttrue) — auto-create missing custom-list values (e.g. new school names)
6.4. Importing from an external system (source system & trust)
When the spreadsheet comes from a foreign system (a legacy WPCA export, a partner federation), identify that system so the importer can correlate people correctly and remember it for later result imports.
-
sourceSystemId(optional) — the external source registration system the file came from. The first such import records it onto the event, so result imports default their source-system selector from it (no need to re-pick — US #769). Omit for your own/manually-prepared rosters. -
trustPKs(defaultfalse) — settrueonly when you trust the foreign system’s id columns enough to resolve by them directly. Leftfalse, those ids are treated as hints and the importer matches on identity (ID number, then name+DOB) with a safety fingerprint check. -
updatePII(defaultfalse) — settrueto let the file overwrite existing name/DOB/contact details; otherwise existing PII is only filled where blank. -
acknowledgeFingerprintWarning(defaultfalse) — only set after you have reviewed a flagged person-match mismatch and confirmed it is genuinely the same person.
See the trust matrix for how these combine.
6.5. Reading the response
The DTO below is what comes back from GET /api/event-participants/import/{jobId} once the job has reached COMPLETED or FAILED — not from the upload response.
{
"totalRows": 185,
"created": 20,
"updated": 160,
"skipped": 0,
"warnings": 4,
"errors": 1,
"issues": [
{ "outcome": "WARNING", "firstName": "...", "lastName": "...",
"registrationId": "29364", "message": "Ambiguous person match — 2 candidates" },
{ "outcome": "ERROR", "firstName": "...", "lastName": "...",
"message": "CustomList 'School' value not found and createCustom1=false" }
]
}
Invariant: totalRows == created + updated + skipped + warnings + errors. If it doesn’t match, the import bailed mid-file — investigate logs.
Only non-success rows appear in issues — successful CREATED / UPDATED rows are counted but not listed.
7. Membership Import
7.1. When to use it
You have a spreadsheet of members to add to a MembershipPeriod — typically from a club secretary, a legacy export, or a renewal drive. The import creates Membership + User (Person) + optional Order + RaceNumber assignments in a single pass.
Unlike EP, the Membership import today has no upsert: re-running the same file will create duplicate memberships rather than updating existing ones. See Membership Outstanding #5.
7.2. Preparing the file
Accepts XLSX. Required columns:
-
FirstName(aliases:First Name,FN,Name) -
LastName(aliases:Last Name,Surname,SN,LN)
Strongly recommended columns for clean processing:
-
MembershipNumber(aliases:Member#,MemberNo,Membership#) — required today; the import throwsSKIPPEDif missing -
IDNumber,IDType,IDCountry— drive Person matching; an existing Person with a different identity number will reject the row asERROR -
MembershipTypeID,MembershipCriteriaID— FK ids into the period
Common extras: Gender, DOB, Email, Cellphone, Address, Town, PostalCode, OrderNumber, Status.
See Membership Import Design for the full 30-column list and aliases.
7.3. Running the import
The upload is async (see Async Delivery Mode): this returns 202 Accepted with a job identifier; poll GET /api/memberships/import/{jobId} for the result DTO.
curl -X PUT \
-H "X-API-KEY: $ADMIN_API_KEY" \
-F "file=@/path/to/members.xlsx" \
"$BASE_URL/api/memberships/import?periodId=17&sheetIndex=0"
Parameters:
-
periodId(required) — targetMembershipPeriod -
sheetIndex(optional, default0) — 0-based sheet index in the workbook -
orgId(optional) — defaults to the period’s organisation; only override for cross-org test data
7.4. Reading the response
The DTO below is what comes back from GET /api/memberships/import/{jobId} once the job has reached COMPLETED or FAILED — not from the upload response.
{
"totalRows": 120,
"created": 118,
"skipped": 1,
"errors": 1,
"issues": [
{ "lineNumber": 47, "outcome": "SKIPPED",
"message": "Membership number not supplied" },
{ "lineNumber": 93, "outcome": "ERROR",
"message": "Identity number does not match existing person" }
]
}
Invariant: totalRows == created + skipped + errors.
The issues list contains one entry per row that did not result in CREATED. Each issue carries the source lineNumber to help correct the file and re-run — trim the XLSX to only the failed rows before retrying, because the import today has no upsert and will duplicate previously-created memberships.
7.5. Success heuristics
-
Clean first run against a fresh period:
created ≈ totalRows,issues = [] -
Partial failure with recoverable rows: a small number of
issues— fix the source file and re-run only the failed rows (trim the XLSX) to avoid duplicates -
Systematic failure: every row produces the same
ERROR(e.g. "Identity number does not match") — stop, investigate the file’s identity columns, don’t blindly retry
8. Result Import (Bulk CSV)
8.1. When to use it
The timing system (RaceDay Scoring) has produced a multi-category CSV. You want every category’s ResultSet to reflect the finishing order in the file.
8.2. Preparing the file
The file comes straight from RaceDay Scoring. You should not hand-edit it. The expected shape:
Bib,First Name,Last Name,School,Registration Event,External Reference ID,Place,Total Time,Num. Laps
219,Freddie,Visser,,Category 1,29364,1,00:57:37.12,12
225,Imtiyaaz,Schultz,,Category 1,29349,2,00:57:58.56,12
...
(blank line)
Bib,First Name,... ← repeated header
103,Someone,Else,,Category 2,29400,1,00:52:11.03,11
...
Concatenated per-category sections (each with their own header and a blank separator) are expected. The importer classifies these as non-data rows automatically.
8.3. Choosing participantIdMode
This is the single most important decision. Getting it wrong can wipe pre-existing results (see Recovering from a Mode Mismatch).
| Mode | Column interpretation | When to use |
|---|---|---|
|
|
The timing system was told about EPs via the new-system PK (rare). Most clean-slate events start this way. |
|
|
The timing system holds legacy-system PKs because EPs were migrated from a legacy system. Use this for all migrated WPCA Road League events (119, 120, 121, and the rest of Series 10). |
|
|
The timing system holds the Person/User id (e.g. for cross-event standings). Uncommon in current operations. |
|
|
An external system feeds the timing system directly — the column holds its person id, not ours and not its per-event entry key. Requires a source registration system (see sourceSystemId (source registration system)). |
|
Identify the data source first, then the mode follows. Ask: whose ids are in the
When the event has a preferred timing identifier configured, the admin portal pre-selects the matching mode for you; confirm it still matches this file. |
|
Verify the mode AND the target event before firing. The CSV’s Spot-checking a single id is not enough. Run a coverage check that confirms the bulk of the file resolves under the chosen mode against the target event. If coverage is below ~95%, stop and investigate before firing. |
Coverage check (replaces the older single-id spot-check):
# 1. Extract the unique External Reference IDs from the CSV (column 6).
awk -F, 'NR>1 && $6!="" && $6!="External Reference ID"{print $6}' \
<result-file>.csv | sort -u > /tmp/xrids.txt
echo "Unique XRIDs in file: $(wc -l < /tmp/xrids.txt)"
-- 2. Load and check coverage against the target event in each candidate mode.
CREATE TEMPORARY TABLE tmp_xrid (rid VARCHAR(20));
LOAD DATA LOCAL INFILE '/tmp/xrids.txt' INTO TABLE tmp_xrid;
SELECT 'epid mode (EP.id)',
(SELECT COUNT(*) FROM tmp_xrid) AS total,
COUNT(DISTINCT x.rid) AS resolve_target
FROM tmp_xrid x
JOIN event_participant ep ON ep.id = x.rid AND ep.event_id = <eventId>;
SELECT 'regid mode (EP.registration_id)',
(SELECT COUNT(*) FROM tmp_xrid) AS total,
COUNT(DISTINCT x.rid) AS resolve_target
FROM tmp_xrid x
JOIN event_participant ep ON ep.registration_id = x.rid AND ep.event_id = <eventId>;
SELECT 'pid mode (Person.id)',
(SELECT COUNT(*) FROM tmp_xrid) AS total,
COUNT(DISTINCT x.rid) AS resolve_target
FROM tmp_xrid x
JOIN event_participant ep ON ep.person_id = x.rid AND ep.event_id = <eventId>;
-- 3. Sanity-check: where ELSE might these ids resolve? Catches wrong-target-event
-- cases where the file is in the right shape but aimed at the wrong event.
SELECT ep.event_id, e.name, COUNT(DISTINCT x.rid) AS hits
FROM tmp_xrid x
JOIN event_participant ep ON ep.registration_id = x.rid -- swap to ep.id for epid mode
JOIN event e ON e.id = ep.event_id
GROUP BY ep.event_id, e.name
ORDER BY hits DESC;
The mode whose resolve_target matches total (or nearly all of it) is the correct mode for this file. If the highest-coverage event in step 3 is not <eventId>, the file is aimed at the wrong event — do NOT import; either re-target the import or get a corrected file.
8.4. Choosing pointsCalculator
The default is the SA School Cycling calculator. Available alternatives:
| Short code | When to use |
|---|---|
(omit) |
SA School Cycling series points (scores down to ~position 67) |
|
WPCA Road League scheme: 50/45/40/36/34/32/30/28/26/24/22/20 for top 12; 2 participation points for positions 13+; 0 for non-finishers |
A fully-qualified class name is also accepted. See PointsCalculatorFactory in the admin service for the registered list.
8.5. applyNumberChanges
Defaults to false. Leave it off unless you are running a post-race reconciliation where you explicitly want to update participants' RaceNumber assignments to match the bibs they actually used on the day.
When true, only SIMPLE changes are applied automatically (target bib is unassigned stock). SWAP and CONFLICT are always reported but never auto-applied.
8.6. sourceSystemId (source registration system)
Only relevant for participantIdMode=xpid. It names the external system whose person ids the file
carries, so the system can resolve each id to the right athlete.
-
In the admin portal, the source-system selector is pre-filled from the event — it was set when participants were imported, and the screen tells you so.
-
If you change it, the portal warns and asks you to confirm, then saves the new value back onto the event. Only change it if you genuinely know the file came from a different source system.
-
Over the API, pass
&sourceSystemId=<id>; omit it to use the event’s stored value. -
For
epid/pid/regidyou do not need a source system — leave it unset.
8.7. reconciliationScope (how much the file is authoritative for)
Re-importing a category removes finishers who are no longer in the file (an athlete moved to the
right category, or withdrawn). reconciliationScope controls how far that removal reaches:
| Value | Use when |
|---|---|
|
You are loading one or a few categories' results. Only the categories present in the file are reconciled; every other category’s results are left alone. This is the safe everyday choice. |
|
You are uploading a complete event export and want it to be the single source of truth — a finisher missing from the file should be removed anywhere in the event. Use deliberately. |
A wholesale resolution failure can no longer empty a category: the delete pass is skipped for any category where nothing resolved or more than half the rows failed (the post-2026-04-20 guard). Removed results are shown in the import book-end so you can see exactly what was cleared.
8.8. Running the import
The upload is async (see Async Delivery Mode): the PUT below returns 202 Accepted with a job identifier; the full BulkResultImportResponseDTO is fetched from GET /api/result-sets/import/{jobId} once the job reaches COMPLETED.
curl -X PUT \
-H "X-API-KEY: $ADMIN_API_KEY" \
-F "file=@/path/to/125-ResultExport.csv" \
"$BASE_URL/api/result-sets/import-bulk?eventId=125&participantIdMode=regid&pointsCalculator=wpca-road-league"
8.9. Reading the response
The DTO below is what comes back from GET /api/result-sets/import/{jobId} once the job has reached COMPLETED or FAILED.
{
"eventId": 120,
"fileLines": 212,
"summary": {
"dataRows": 203,
"imported": 201,
"skipped": 2,
"nonDataRows": {
"blankLines": 4,
"repeatedHeaders": 4,
"malformedRows": 0,
"totalNonData": 8
}
},
"categories": [
{ "categoryName": "Category 1", "raceId": ..., "resultSetId": ...,
"rowCount": 51, "importedCount": 51, "skippedCount": 0 },
...
],
"unmatchedCategories": [],
"numberChanges": [ ... ]
}
Invariants to check before declaring success:
-
fileLines == 1 (header) + summary.nonDataRows.totalNonData + summary.dataRows -
summary.dataRows == summary.imported + summary.skipped -
unmatchedCategoriesis empty (or contains only values you expect to be missing)
Any non-zero skipped count is worth investigating — those rows were not imported. The detailed issues list for skipped rows is a work-in-progress (ADO-456); until it ships, inspect the application logs for WARN lines around the import timestamp.
8.10. Success heuristics
-
First import of an event:
imported == dataRows,skipped = 0,unmatchedCategories = [] -
Re-import after a correction: same as first-import, plus
RaceResult.idvalues in the database are unchanged compared to before the re-import (upsert-by-seq preserves identity) -
Normal "number change noise": if the event’s
NumberTypehas noRaceNumberstock loaded, every row triggers aCONFLICTnumber-change report. This is operational data config, not an import error.
9. Recovering from a Mode Mismatch
9.1. Symptom
You ran /api/result-sets/import-bulk and the response shows summary.imported = 0, summary.skipped == summary.dataRows, and the categories[].importedCount is 0 for every category. A follow-up database check shows RaceResult rows for that event have been deleted from the matched ResultSets.
9.2. Why it happens
The reconciliation logic expects the CSV to be the source of truth for each category’s ResultSet. When every row fails to resolve a participant, no seq is "touched", so the end-of-category delete pass removes every pre-existing RaceResult as "absent from the file". This is the correct behaviour for "participant genuinely removed" but the wrong behaviour for any of the failure shapes below. The importer currently cannot distinguish them.
Three failure shapes produce the same 0-imported / data-deleted outcome:
| Shape | Description | Diagnostic |
|---|---|---|
Wrong |
The mode you chose doesn’t match how the file’s |
Coverage in the correct mode is ~100%; coverage in the chosen mode is ~0%. |
Wrong target event |
The mode is right, but the file’s ids belong to a different event’s EPs (e.g. a timing operator exported last week’s race and re-saved it under this week’s filename, OR carry-over registration ids point at the prior event). |
Coverage in the chosen mode against |
ResultSet/category mismatch (partial) |
|
|
See FR-RI-012: Mode Mismatch Safety for the pending fix.
9.3. Immediate recovery
-
Identify the correct mode (see [choosing-participantidmode]).
-
Re-run the import with the correct mode. Because the reconciliation is idempotent and the CSV is authoritative, this restores every row that should have been there.
-
RaceResult.idvalues will be different after recovery — the ids you had before the bad run are gone for good. If you have any external system that referenced these ids (unlikely, but possible), it will need to be resynced.
9.4. Prevention
-
Run the coverage check before firing the import (see [choosing-participantidmode] for the SQL). Coverage below ~95% in the chosen mode against the target event is a hard stop — do not proceed.
-
Confirm the file is aimed at the right event. Step 3 of the coverage check shows which event the ids actually belong to. If it isn’t
<eventId>, do not import. -
Test against Dev first for any new event or any event whose timing-system configuration you haven’t seen before.
-
Check the response summary before walking away — if
imported = 0, stop and investigate before re-running. A second blind retry against a damaged event can’t undo the loss.
10. Troubleshooting Catalogue
| Symptom | Likely cause | Resolution |
|---|---|---|
|
|
Verify the event id via |
|
Missing |
Re-upload; check the file opens in Excel / a text editor; confirm |
|
Nginx 180s timeout on synchronous imports; backend usually finishes anyway |
Wait 2–5 minutes, then check the database for the expected rows. Avoid retrying blindly or you may double-apply number changes. |
|
Mode mismatch — see Recovering from a Mode Mismatch |
Re-run with correct |
|
Timing system’s Registration Event label does not match any |
Either add the missing |
Every row produces a |
No |
Operational data config issue — load the bib range into |
Some rows in |
Two or more existing |
Manually resolve via the admin UI — merge the duplicate persons, then re-run the import |
All |
File was imported against an admin-service version older than 2.3.31 (Bug #452) |
Re-import against 2.3.31+; upsert-by-seq will fill |
11. Pre-Flight Checklist
Before running against production:
-
[ ] File opens cleanly in Excel (EP import) or a text editor (Results import)
-
[ ] Required columns are present (
FirstName/LastNamefor EP;Registration Event+External Reference ID+Placefor Results) -
[ ] Target
eventIdis correct (double-check againstGET /api/events/{id}) -
[ ] For Results:
participantIdModeis confirmed via the coverage check (see [choosing-participantidmode]) — chosen mode resolves ≥95% of file XRIDs against<eventId>, and step 3 confirms the highest-coverage event IS the target event -
[ ] For Results:
pointsCalculatoris correct for this series -
[ ] Same file has been run successfully against Dev (for any new file shape)
-
[ ] You have a rollback plan — most commonly "re-run with the correct parameters"; occasionally "restore the pre-import snapshot from the DB backup"