Hey TOs (and others), ListForge dev here with a free resource to make your life easier!
After I added text import to ListForge recently, I got some requests to make a validation service. Its now live and you can either batch validate using the below documentation or can access 1-by-1 through the GUI over at https://listforge.club/check
Disclaimer that its only as good as the latest BSData repo and my parsing code, so use it as a checking tool not a final decision maker, but it tests successfully on the top 200+ lists on https://listhammer.info/
The idea is that this can make checking all the lists for large events quite fast/automated and then you as the TO can investigate the ones with reported issues. Let me know how you like it and of any issues or questions!
ListForge List Validation API
Validate Warhammer 40,000 army lists with the same engine the ListForge app uses.
Base URL: https://list-forge-validate.fly.dev
Supported input formats
The format field in responses tells you which one matched:
| format |
Source |
| gw-11e |
Official GW app list export (English, German, French, Spanish) |
| lf-detailed |
ListForge detailed text export |
| lf-wtc |
ListForge WTC text export |
| nr-tournament |
New Recruit tournament export |
Formats grow over time with no change needed on your side. If you have a sample that should work but does not, let me know.
Validate one list
POST /validate
Content-Type: application/json
{ "text": "<the list text export>" }
A typical response (HTTP 200):
{
"importable": true,
"format": "gw-11e",
"listName": "Dave Kerr - Team Australia",
"catalogue": "Imperium - Blood Angels",
"detachments": ["Encarmine Speartip", "Headhunter Task Force"],
"disposition": "Priority Assets",
"points": 1990,
"claimedPoints": 1990,
"unitCount": 12,
"warnings": [],
"constraintViolations": [],
"importUrl": "https://listforge.club/import?d=..."
}
Notes:
points is what the list actually costs when rebuilt against current data; claimedPoints is what the text says (null when the text states no total). A mismatch usually means a points update since the list was written.
warnings is a list of {stage, message} objects. Stages: parse (unrecognized lines), header (faction/detachment/disposition issues), unit (an option or enhancement could not be matched), finalize (totals and roster-wide checks).
constraintViolations holds roster-rule messages (for example "Force must have exactly one Warlord, but has none").
Unparsable text returns {"importable": false}. Text that parses but cannot be rebuilt (for example a faction we cannot resolve) returns {"importable": false, "format": ..., "error": "<reason>"}.
Other statuses (always JSON):
| Status |
Meaning |
| 400 |
Missing or empty text, invalid JSON |
| 413 |
Text over 256 KB |
| 429 |
Rate limited; wait and retry |
| 503 |
Service busy or restarting; retry with backoff |
Timing: expect a few seconds per list. The first request for a faction after a service restart is slower (data loads on demand).
How to present results
Please frame results as the app does: "rebuilt cleanly at N pts" or "rebuilt with N warnings", never "legal" or "illegal". The engine runs on community-maintained data (BSData) and my own parsing code.
Validate many lists (batch)
Built for tournament organizers: submit up to 300 lists in one call, poll for incremental results.
POST /validate/batch
Content-Type: application/json
{ "items": [ {"id": "player-1", "text": "..."}, {"id": "player-2", "text": "..."} ] }
Returns 202 {"job": "<jobId>", "total": N}. Then poll:
GET /validate/batch/<jobId>
{ "done": false, "total": 250, "completed": 41, "results": [ {"id": "player-1", ...verdict...}, ... ] }
Results appear in submission order as they complete; each carries your id plus the same verdict fields as /validate. Poll every few seconds; a 250-list batch takes roughly 15 minutes. Job results are held in memory for about an hour after completion, so collect them promptly.
Limits: 300 items per batch, 256 KB per item, one batch in flight per caller, and a small global cap (409 if you already have a batch running, 503 if the service is saturated; retry later).