Open Meet Exchange
Overview
Open Meet Exchange (OMX) is a JSON document format for moving a track and field meet, cross country meet, or road race between the products that handle it: the entry site, the timing software, and the results site. One format replaces a separate import and export between every pair of them.
Registration ──── entries ────▶ Meet Operations ──── results ────▶ Publisher
(who entered) (what happened) (shows it)
- Registration collects entries and serves them as a registration document at a secret URL.
- Meet Operations fetches the document, seeds, times, and scores the meet, and hands results documents to every Publisher the document names.
- The Publisher shows the results to coaches, athletes, and fans, and recomputes nothing.
The meet moves in one direction only: Meet Operations does not send entries back to Registration, and a Publisher does not send results back to Meet Operations.
A typical meet: the timer imports the registration link a week out and assigns bibs, then fetches it again on race morning for late registrations. During the meet, results go to each Publisher as often as it asked for them: start lists before a round, updates every few seconds while it runs, and the round again when it goes official and whenever it is corrected.
One product may run more than one stage. A meet host that takes entries and shows results is both Registration and Publisher: it hands entries out and gets results back. A meet does not need every stage: a timer who builds a meet from a spreadsheet starts at Meet Operations and hands straight to a Publisher.
Three design choices shape the rest:
- Two kinds of document, one shape. A registration document says who entered; a results
document says what happened. Both describe the meet with the same records (venue, divisions,
events, teams, athletes, entries), and
omx.kindsays which one it is. - Every update is whole where it counts. A results document is either
full, the whole meet as it stands, orpartial, only some rounds, each of them whole. There are no field-level deltas and no revision to keep in step. - The URL is the setup. No accounts, API keys, or per-vendor adapters. A registration document carries everything Meet Operations needs, including where to send results.
What OMX is not
- Not a universal meet format. A document carries what the next stage needs, not everything a product holds. Meet Operations’ timing evidence, settings, and history stay in that product.
- Not a backup. To move a meet between computers, use the product’s own backup. A document cannot restore a meet in progress.
- Not a scoreboard protocol. A
livedestination gets the running round every few seconds (see Live updates), which is enough for a results page that follows a race. A stadium board that needs every tenth of a second needs its own connection, such as Open Meet Display. - Not a way to send entries back. A walk-up the timer adds reaches the Publisher in a results document, with no Registration id. Nothing in OMX asks Registration to add it to its roster; whether a product that runs both stages does so is its own decision.
Terms
| Term | Meaning |
|---|---|
| document | One JSON file describing one meet, validating against the schema |
| kind | omx.kind: registration or results |
| Registration | The stage that holds entries and serves a registration document |
| Meet Operations | The stage that fetches a registration document, runs the meet, and writes results documents |
| Publisher | The stage that accepts results documents from Meet Operations and shows them |
| timer | The person running Meet Operations at the meet |
| registration document | A document of kind registration: one entry per athlete or relay per event, no results |
| results document | A document of kind results: one entry per athlete or relay per round, with what happened |
| full / partial | omx.update on a results document: the whole meet, or only the rounds it carries |
| destination | One item in a registration document’s publish array, or a destination card: a Publisher and how it wants results |
| handoff | One delivery of one results document from Meet Operations to a destination |
| probe | A GET on a destination URL asking which versions it reads and who it is; optional to answer |
| system id | A product’s short, fixed name in ids, such as examplereg for ExampleRegistration: its omx.generator.id |
id |
The identifier chosen by the product that wrote the document |
external_ids |
The same record’s ids in other products, keyed by system id |
Roles
A product runs one or more stages. It does not have to run the others.
| Stage | What you do | Typical products |
|---|---|---|
| Registration | Serve a registration document at a secret URL. You own the meet’s identity, events, teams, athletes, and entries | Registration sites, entry systems, a school’s export |
| Registration + Publisher | Serve a registration document and name yourself as a destination, so the meet comes back to you with results | Meet hosts that also publish results |
| Publisher | Accept results documents from Meet Operations. You may never have supplied any entries | Results sites, league standings, a school’s site |
| Meet Operations | Fetch a registration document, run the meet, hand results on. You own rounds, heats, seeds, marks, places, statuses, scores, and the track | 2Timer, and any timing or meet-management product that adopts OMX |
Stages differ in what they are allowed to decide, not in the shape of the records they write. Registration says who entered. Meet Operations says what happened. A Publisher records what it is given and recomputes nothing.
Each stage takes only the handoff before it. Meet Operations reads registration documents, never a results document; a Publisher reads results documents, never a registration document. Two Meet Operations products timing parts of one meet each hand their own results to the Publisher; they do not hand to each other.
Quick start
Registration
- Build the document. Serialise the meet as one JSON document with
"kind": "registration"that validates against the registration check (registration-document-v1). Start from the example closest to your sport. - Serve it at a secret URL. HTTPS only,
Content-Type: application/json. Put a secret in the path or query (see Secrets), scope it to one meet, and let the meet director revoke it. Sending anETagand answeringIf-None-Matchwith304is recommended: a timer may fetch the link again for late entries. - Show the link to the meet director with the words “give this to your timer”. Also offering the
same document as a downloadable
.omx.jsonfile lets a timer import it without the link (see Encoding and files).
That is a complete, conformant registration product. Stop here if you never want the meet back.
Registration that wants results back
Add a publish array. Each item is one destination with its own id, credential, and preferences:
"publish": [
{
"id": "examplereg",
"label": "ExampleRegistration",
"url": "https://registration.example.com/omx/inbox/8812",
"token": "reg_pub_…",
"deliver": "live",
"content": ["results", "start_lists"]
}
]
deliveris how often you want results:final(the default) once the meet is official,progressiveas each round is seeded, goes official, or changes, orlive, which adds an update every few seconds while a round runs. See When a handoff happens.contentis what you want:results(the default), andstart_listsif you also want heat sheets before a round runs.accepts(optional) lists the versions your endpoint reads, such as["1.0"]. Listing a version accepts every later minor of its major too:["1.0"]reads 1.0, 1.1, and every1.x. A Meet Operations product sends only a major you list; with no list, it sends a version of the major of the document or destination card that named you (OMX-R187).
What comes back is a results document, written under Meet Operations’ ids, not yours. On the
meet, events, teams, and athletes, your id moves to external_ids, under your system id, the
omx.generator.id you wrote:
// You sent, with "generator": { "id": "examplereg", "name": "ExampleRegistration" }
{ "id": "a-1001", "first_name": "Maya", "last_name": "Chen" }
// You get back
{ "id": "at_301", "external_ids": { "examplereg": "a-1001" }, "first_name": "Maya", "last_name": "Chen" }
- Find your meet, events, teams, and athletes by
external_idsunder your system id."a-1001"comes back unchanged, every time, on every record that came from you. Attaching a result is a lookup, never a name match. - Find your entries through the athlete and the event. An entry carries no
external_ids. Maya Chen’s results in your Girls 60m are the entries whoseathlete_idnames the athlete carrying"examplereg": "a-1001"and whoseevent_idnames the event carrying your event’s id. A relay is found by its team, its letter, and its event. See Ids. - One of yours can come back as several. An athlete who ran a prelim and a final comes back as two entries, one per round. See One entry per round.
- Some come back with none of your ids. A walk-up the timer added was never yours. See Records Meet Operations creates.
Publisher
You need no document of your own. Implement an HTTPS endpoint that accepts handoffs (see Handing off) and show your user a destination card to give the timer, so Meet Operations knows where to send the meet. What arrives is a meet you can create from nothing.
- Apply each document by its
omx.update.fullreplaces everything you hold for the meet;partialreplaces only the rounds it carries. If you never ask for more thanfinal, you only ever getfull. - Read
meet.origin. It names the product the meet started in. When itsiddiffers fromomx.generator.id, the meet came from that Registration product, and records that came from it carry its id inexternal_idsunderorigin.id. When it is the same, the timer built the meet and no record carries a Registration id. Either way, a record may carry ids from other systems you know, such as an athlete’s Athletic.net id underanet, so you can match results without ever having supplied an entry.
Meet Operations
- Accept a pasted URL or an opened file, check its version and kind, and build the meet from it with no wizard.
- If the timer fetches the URL again for late entries, merge it however your product does. The format says nothing about how; it only promises the same shape and the same ids.
- Hand results to every destination the document names, and let the timer add destinations by
hand or by pasting a destination card. Respect each destination’s
deliverandcontent. Your first document to a destination isfull; after that you may sendpartialdocuments, or alwaysfullones.
The checklist at the end doubles as a test plan, and the conformance tests give each item a pass condition.
Check your work
Every stage has the same three tools, all under Files:
- Validate against your kind’s check. The registration check (
registration-document-v1) and the results check (results-document-v1) are strict: a value this version does not define, a date-time without seconds, or a field your stage never writes fails. A field the format does not define never fails any schema (see Extra fields), so check your spelling against the field reference.meet-document-v1is the lenient schema a reader validates with. - Run the checker for what no schema can see, such as references that resolve, entries that can
be joined, codes that belong to the meet’s sport, and statuses that fit their round:
node omx-check.mjs meet.omx.json. - Use the test page to check a document, a registration URL, a Publisher endpoint, or a destination card from your own browser.
The document
One JSON document, the same records in both kinds. A large invitational with a few thousand entries is well under a megabyte from Registration, and a few megabytes once results are on it.
A minimal cross country registration document:
{
"omx": {
"version": "1.0",
"kind": "registration",
"generator": { "id": "examplereg", "name": "ExampleRegistration", "version": "3.4" }
},
"meet": {
"id": "8812",
"name": "Riverbend XC Invitational",
"sport": "xc",
"start_date": "2026-10-03",
"time_zone": "America/New_York",
"venue": { "name": "Riverbend Park", "city": "Longwood", "region": "FL", "country": "US" }
},
"divisions": [
{
"id": "d-var",
"name": "Varsity",
"sequence": 1,
"gender_names": { "M": "Boys", "F": "Girls" }
}
],
"events": [
{
"id": "e-1",
"number": 1,
"name": "Varsity Girls 5K",
"code": "5000",
"division_id": "d-var",
"gender": "F"
},
{
"id": "e-2",
"number": 2,
"name": "Varsity Boys 5K",
"code": "5000",
"division_id": "d-var",
"gender": "M"
}
],
"teams": [{ "id": "t-lhs", "code": "LHS", "name": "Longwood High School" }],
"athletes": [
{
"id": "a-1001",
"first_name": "Maya",
"last_name": "Chen",
"gender": "F",
"grade": "11",
"team_id": "t-lhs",
"bib": "101",
"external_ids": { "anet": "18810342" }
}
],
"entries": [
{
"id": "n-1",
"event_id": "e-1",
"athlete_id": "a-1001",
"seed": { "value": 1183400, "unit": "ms", "display": "19:43.4" }
}
],
"publish": [
{
"id": "examplereg",
"label": "ExampleRegistration",
"url": "https://registration.example.com/omx/inbox/8812",
"token": "reg_pub_…",
"deliver": "live",
"content": ["results", "start_lists"]
}
]
}
Kinds
omx.kind says which handoff a document is, and it is required:
registration, written by Registration: who entered. One entry per athlete or relay per event, with its seed and an entry status ofe,scr, orwait. No results, rounds, places, team scores, track, or markers.results, written by Meet Operations: what happened. One entry per athlete or relay per round, withroundrequired. It also carriesomx.update,omx.updated_at, andmeet.origin, and neverpublish.
The kind is a closed list. A reader refuses a document whose kind is missing or one it does not know (OMX-C107), and a reader never guesses the kind from the fields a document carries. Meet Operations reads only registration documents; a Publisher reads only results documents.
Sections
| Section | Required | In | Contents |
|---|---|---|---|
omx |
yes | both | Format version, kind, and the generator (its system id, name, and optional version). A results document adds update and updated_at |
meet |
yes | both | Id, name, sport, dates, time zone, venue. Enough to create the meet without a wizard. A results document adds origin, and track for track and field |
divisions |
no | both | Groupings that events belong to: Varsity, Masters 40+, U12. With gendered names |
groups |
no | both | Award groups within a race, such as age groups or JV, with award counts |
waves |
no | both | Start waves in road and trail races, with each wave’s offset from the race’s start |
events |
yes | both | What is contested, in running order. A results document adds rounds, markers, and the component events of a multi-event |
teams |
yes | both | Id, code, name. An athlete with no team_id competes for no team |
athletes |
yes | both | Id, name, gender when stated, age or grade, team, bib, and an exhibition flag |
entries |
yes | both | Registration: who entered each event, with seeds. Results: one entry per round, with heat, lane, status, result, places, splits, and points |
publish |
no | registration only | Destinations the results are handed to. Absent or empty means one-way, which is a complete implementation |
team_scores |
no | results only | Standings, when the meet is scored |
The field reference lists every field with its type. The rest of this section covers the rules the schema cannot express.
Encoding and files
A document is UTF-8 JSON (RFC 8259) with no byte order
mark. Date-times are RFC 3339 with seconds and a UTC
offset (2026-10-03T09:00:00-04:00); dates are YYYY-MM-DD on the meet’s local calendar.
meet.time_zone, when present, names that calendar’s zone, so a reader can show times across a
daylight-saving change; it is optional, and without it a reader takes dates as written and reads
each date-time by its own offset. A reader also accepts a date-time with no seconds and reads it as
:00; a writer always sends seconds.
A document is also a file. A product that cannot serve or fetch a URL, such as desktop software or a meet with no connection, exchanges the same document as a file. Any product may also offer the file as a download next to the link, and any timer may import it instead of pasting the URL:
- Name it with the extension
.omx.json. A reader recognises a document by its top-levelomxobject and itskind, not by its name, so a plain.jsonfile is read the same way. - A file is read exactly like a fetched document.
- A registration file that names destinations holds their tokens. Handle it like the document URL: it is a credential.
Ids
Every record carries an id chosen by the product that wrote the document: the meet, each
division, group, wave, event, marker, team, athlete, entry, and publish destination.
- An
idis an opaque string. No reader parses it. - An
idnever changes for the life of the meet. Regenerating ids on each export breaks the results join. - An
idis unique within its own list. Two lists may share a value: an event and the meet may both be"8812", and two products may both write it. - A meet made from another meet, such as next year’s meet built from this year’s, is a new meet. It
gets new ids, and carries none of the old meet’s Registration ids or the old meet’s own
external_ids. An athlete’s or team’s ids on other systems, which name the person or the team, may stay.
id is always the writer’s id. Every other product’s id for the same record is in
external_ids, keyed by that product’s system id:
{
"id": "at_301",
"first_name": "Maya",
"last_name": "Chen",
"external_ids": { "examplereg": "a-1001", "anet": "18810342" }
}
- Only the meet, events, teams, and athletes carry
external_ids: the records a Publisher joins results through. Each value is a string, at most one per system. Divisions, groups, waves, markers, entries, and destinations carry only theirid, for references within the document. - A writer never lists itself: its own id for the record is
id. - In a registration document,
idis Registration’s id, andexternal_idsholds any ids it knows on other systems, such as an athlete’s Athletic.net id. - In a results document,
idis Meet Operations’ id. Every meet, event, team, and athlete that came from Registration carries Registration’s id unchanged inexternal_idsunder Registration’s system id. Records Meet Operations created carry no Registration id. - Meet Operations writes back every id it received, on the meet, events, teams, and athletes,
unchanged, whether or not it knows the system (OMX-C104). The timer may correct or remove one.
A results site that keys its athletes on its own system id, such as ExampleResults under
exampleresults, gets its ids back from any timing product.
A meet’s entries come from one Registration product, so meet.origin always names it, and every
Registration id in a results document is under that product’s system id.
An entry carries no external_ids. One registration entry can come back as several entries, a
prelim and a final, so an entry id from Registration would not name one record. A result joins back
to Registration through what it is about:
| Entry | Joined through |
|---|---|
| An individual entry | Its athlete’s external_ids and its event’s external_ids |
| A relay entry | Its team’s external_ids, its relay.letter, and its event’s external_ids |
| A multi-event component | Its athlete, and its component event, whose multi_event_id leads to the multi-event you entered |
So that each join lands on one entry, a registration document has at most one individual entry per
athlete per event, and at most one relay entry per team, letter, and event (OMX-C108). A relay entry
always carries its letter, in both kinds of document; a relay the timer created without one is
written A, or the team’s next free letter in that event.
Nothing is ever matched by name. A Publisher that never supplied an entry joins the same way, on
whichever system it knows: a results site keyed on Athletic.net ids reads external_ids.anet.
System ids
omx.generator.id is the writing product’s system id: any name it picks, in lowercase letters,
digits, and _, at most 32 characters, chosen once and never changed. Nobody assigns or registers
it; every document names its writer. The same id names that product’s ids in every other document’s
external_ids.
A system that writes no OMX document never names itself, so the ids it issues, such as an athlete’s Athletic.net id, go under the key below. Two products holding the same id then use the same key. A product holding ids from a system not listed picks a key and asks for it to be added. Nothing checks the list.
| System id | System | Ids it issues |
|---|---|---|
anet |
Athletic.net | Meet, athlete, and team ids |
milesplit |
MileSplit | Meet, athlete, and team ids |
tfrrs |
TFRRS | Athlete and team ids |
usatf |
USATF | Membership numbers |
aau |
AAU | Membership numbers |
rsu |
RunSignup | Race and registration ids |
rr |
RaceRoster | Participant ids |
da |
DirectAthletics | Athlete ids |
alinks |
Athlinks | Athlete ids |
trackie |
Trackie | Athlete and club ids |
usu |
UltraSignup | Participant ids |
Sports and event codes
meet.sport says what kind of meet this is. This version defines xc, tf-indoor, tf-outdoor,
and run; indoor and outdoor track are separate because their event catalogs differ. The list is
open: a later minor version adds sports. A writer sends only a sport this version defines.
Each event has a code, and the code is the event: what it is, how far, and how it ranks. No other
field says any of these, so nothing in an event can disagree with its code. Codes are defined per
sport, and a later sport may give a code such as 5000 its own meaning. The codes are an open
list too: a later minor version adds events. A writer sends only a code this version defines for the
meet’s sport, never one of its own (OMX-C14).
| Event | Code | Examples | Ranked |
|---|---|---|---|
| A race | metres, or miles with MILE |
100, 1600, 5000, 1MILE, 3.1MILE |
lowest time first (ms) |
| Half marathon, marathon | HMAR, MAR |
lowest time first (ms) |
|
| Hurdles | metres and H |
110H, 300H |
lowest time first (ms) |
| Steeplechase | metres and S |
3000S |
lowest time first (ms) |
| Race walk | metres and W, or miles and MILEW |
5000W, 1MILEW |
lowest time first (ms) |
| An even relay | legs X each leg, H for hurdles |
4X100, 4X400, 4X1MILE, 4X110H |
lowest time first (ms) |
| A medley relay | its total and R |
800R, 1000R, 1600R, 3200R, 4000R |
lowest time first (ms) |
| A field event | the field event table below | LJ, HJ, SP |
best mark first (mm, in) |
| A multi-event | the multi-event table below | DEC, HEP, PENT |
most points first (pt) |
Which codes each sport uses:
| Sport | Its codes |
|---|---|
tf-outdoor, tf-indoor |
Races, hurdles, steeplechases, race walks, even relays (shuttle hurdles included), medleys, field events, multi-events |
xc |
Races; even relays without hurdles |
run |
Races, HMAR, MAR; race walks; even relays without hurdles |
A race’s code is the distance it was advertised at, not what the course measured: the
Riverbend “5K” course measures 4.92 km, and its code is 5000. A race advertised in miles uses the
MILE form. Each distance has one code: a half marathon is always HMAR and a marathon always
MAR, never 21097, 13.1MILE, or 42195. A 6 km cross country course is 6000, and a 5K is
5000; 3.1MILE is only for a race advertised as 3.1 miles.
A hurdle race’s height is in hurdle_height_mm, and a throw’s implement in implement_weight_g,
because the code does not say them: a 4 kg shot and a 12 lb shot are both SP. Registration may
suggest either; the timer may change them, and a results document carries what was used.
A medley’s code gives its legs in their standard order:
| Code | Legs, in running order |
|---|---|
800R |
100, 100, 200, 400 |
1000R |
100, 200, 300, 400 |
1600R |
200, 200, 400, 800 |
3200R |
1200, 400, 800, 800 |
4000R |
1200, 400, 800, 1600 |
No event lists its legs; its code says them.
A value a reader does not know never fails the whole meet, and a reader never guesses an event from its name:
- An event code it does not know, or one not defined for the meet’s sport. Meet Operations
leaves that event and its entries out, tells the timer which events it left out, and reads the
rest of the meet: it would have to run an event it cannot define. A Publisher shows the event by
its
name, with places as given, marks bydisplay, and its team scores as sent, and prints annrentry in it asNR, since it cannot tell a race from a throw (OMX-R179). - A sport it does not know. A Meet Operations product that does not run the sport refuses the document and names the sport. A Publisher shows the meet’s events by their names and codes.
Field event codes
| Code | Event |
|---|---|
LJ |
Long jump |
TJ |
Triple jump |
SLJ |
Standing long jump |
HJ |
High jump |
PV |
Pole vault |
SHJ |
Standing high jump |
SP |
Shot put |
DT |
Discus |
HT |
Hammer |
JT |
Javelin |
WT |
Weight throw |
TJT |
Turbo javelin |
SBT |
Softball throw |
BBT |
Baseball throw |
CT |
Club throw |
A jump or throw ranks by its best mark: the longest jump or throw, or the highest bar cleared.
Multi-events and their components
DEC, HEP, and PENT may be sent for any gender in a track meet. Meet Operations builds the
component events by its own rules, with this table as the standard set:
| Code | Standard for | Components, in order |
|---|---|---|
DEC |
tf-outdoor, men |
100, LJ, SP, HJ, 400, 110H, DT, PV, JT, 1500 |
HEP |
tf-outdoor, women |
100H, HJ, SP, 200, LJ, JT, 800 |
HEP |
tf-indoor, men |
60, LJ, SP, HJ, 60H, PV, 1000 |
PENT |
tf-indoor, women |
60H, HJ, SP, LJ, 800 |
A masters meet that enters women in a decathlon sends DEC with gender F, and the timer runs it.
A multi-event Meet Operations cannot build, such as a youth pentathlon it has no rule for, is
reported to the timer and not created, as an unknown code is. A component’s hurdle height and
implement weight follow the event’s gender and division as for any event.
A registration document carries only the multi-event, never its component events. A results
document lists every component event actually contested, each naming its multi-event in
multi_event_id, so a Publisher never needs this table.
Adaptive events
A wheelchair race, a seated throw, a guided run, or another adaptive form of an event keeps the
event’s ordinary code and says how it is contested in adaptive.mode:
{
"id": "e-12",
"name": "Women Seated Shot Put",
"code": "SP",
"gender": "F",
"adaptive": { "mode": "seated" }
}
adaptive.mode |
Meaning |
|---|---|
wheelchair |
A wheelchair race |
seated |
A seated throw |
guided |
Run with a guide |
ambulatory |
Contested standing or running, without a chair or a guide |
An adaptive event is a different event from the standard event with the same code. A reader that cannot hold adaptive events tells the timer and never merges one into the standard event.
Event examples
{
"events": [
{ "id": "e-1", "name": "Men 100m", "code": "100", "gender": "M" },
{
"id": "e-2",
"name": "Men 110m Hurdles",
"code": "110H",
"gender": "M",
"hurdle_height_mm": 1067
},
{ "id": "e-3", "name": "Women Mile", "code": "1MILE", "gender": "F" },
{ "id": "e-4", "name": "Women 4x400m Relay", "code": "4X400", "gender": "F" },
{ "id": "e-5", "name": "Mixed Distance Medley", "code": "3200R", "gender": "C" },
{ "id": "e-6", "name": "Women Long Jump", "code": "LJ", "gender": "F" },
{
"id": "e-7",
"name": "Men Shot Put",
"code": "SP",
"gender": "M",
"implement_weight_g": 7260
},
{ "id": "e-8", "name": "Women Pentathlon", "code": "PENT", "gender": "F" }
]
}
e-5: a 3200R, whose legs are 1200, 400, 800, 800.e-8: in atf-indoormeet, Meet Operations builds the standard five events unless the meet calls for others.
Gender
An athlete’s gender says who they are; an event’s says who the contest is for. Each is one
letter, and a letter means the same gender wherever it appears:
| Value | On an athlete | On an event, award group, or team-score scope |
|---|---|---|
M |
Male | Men or boys |
F |
Female | Women or girls |
X |
Non-binary | Non-binary athletes |
C |
never | Everyone: mixed, combined, or co-ed, including a relay whose team may mix genders. Each athlete is also placed within their own gender (gender_place) |
An event’s, award group’s, and team-score scope’s gender is required. An athlete’s gender is optional. Leave it out when the athlete did not state one, as road
registration commonly allows; never guess it. A reader that does not tell an unstated gender from
non-binary may treat it as X.
Registration decides who may enter an event, and Meet Operations takes an entry as given: Riley
Adams (X) entered in the Varsity Boys 5K runs in it, and is placed within their own gender.
Marks and units
A seed or result is a value plus the unit you measured in, with optional display text:
{ "value": 198.25, "unit": "in", "display": "16-06.25" }
| Unit | Meaning | Value |
|---|---|---|
ms |
milliseconds | integer |
mm |
millimetres | integer |
in |
inches | decimal, two places max |
pt |
points | integer |
A mark’s unit is one its event’s code ranks by: ms for a running event, mm or in for a field
event, pt for a multi-event. Do not convert imperial marks to metric before sending. Converting
16-06.00 to 5029.2 mm and back loses precision under field-event rounding rules. Meet Operations
converts once, if its meet uses the other system.
display is the writer’s own formatting. It is shown, never parsed. A seed may also say where it
came from in source (season_best, coach_entered) and when in achieved_on; both are shown to
the timer, never interpreted.
The units are an open list: a later minor version adds a unit when an event needs one, such as a lap
count. A reader that meets a unit it does not know shows the mark’s display and never compares or
sorts on it (OMX-C110). Places arrive computed, so the results are still in the right order.
Bibs
The bib lives on the athlete. athlete.bib is the athlete’s bib for the meet. Registration may
assign it, or leave bibs to the timer. In a results document it is the bib the athlete raced under:
if Emma Walsh was given 201 and raced in 118, the results document says 118. A runner with two bibs
in one meet, such as a Saturday 5K and a Sunday half marathon at a race weekend, is two athletes.
Meet Operations imports Registration’s bibs through whatever conflict review it normally applies when two athletes claim one number.
Relays
A relay entry names a team, its squad letter, and legs in running order:
{
"id": "n-7",
"event_id": "e-4x1-f",
"relay": { "team_id": "t-lhs", "letter": "A", "legs": ["a-1", "a-2", "a-3", "a-4", "a-5"] },
"seed": { "value": 50120, "unit": "ms", "display": "50.12" }
}
letter is required: one upper-case letter, A for the team’s first squad in the event. Team,
letter, and event name one relay entry. legs may be shorter than the relay’s legs, may be empty,
and may list alternates after the running legs.
Waves
A road or trail race that starts in waves lists them in waves, and each entry names its wave with
wave_id:
"waves": [
{ "id": "w-1", "name": "Wave 1" },
{ "id": "w-2", "name": "Wave 2", "offset_ms": 300000 }
]
offset_ms is how long after the race’s start the wave went. In a results document, an entry’s
result is already timed from its own wave’s start, so a reader never subtracts the offset again.
Registration may assign waves; a results document carries the waves run.
Markers and splits
An event that records intermediate times lists its timing points in markers, in course order.
Markers are the timing side’s: they appear in a results document only.
"markers": [
{ "id": "m_1", "name": "1 Mile" },
{ "id": "m_2", "name": "2 Mile" }
]
A results entry’s splits each say where they were taken, at a marker_id, at the end of a relay
leg, or both, and carry one number, time_ms:
"splits": [
{ "marker_id": "m_1", "time_ms": 398200 },
{ "marker_id": "m_2", "time_ms": 800900 }
]
time_msis elapsed from the same start asresult: the gun for the athlete’s race or wave. A relay leg’s split is the running clock when that leg finished, with1for the first leg.- A stretch is a subtraction. Carla Mendez ran the second mile in 800900 − 398200 ms, 6:42.7. A Publisher works it out for display.
- A missed mat is absent. If Carla’s chip misses the 2-mile mat, her 3-mile split is still right, and the others still line up.
- A split with no marker and no leg cannot be placed on the course, so the schema refuses it.
Multi-events
A decathlon, heptathlon, or pentathlon is one event with code DEC, HEP, or PENT (see
multi-events and their components). Registration enters an
athlete in it once, with one entry on the multi-event:
{
"id": "reg_8",
"event_id": "e-pent-w",
"athlete_id": "ath_b2e1",
"seed": { "value": 3950, "unit": "pt", "display": "3950" }
}
- Registration sends the multi-event only. It writes the multi-event and one entry per athlete on it, never a component event or an entry on one. Meet Operations builds the component events and gives each athlete their component entries.
- A multi-event seed is a points total, in
pt.
Exhibition athletes
An athlete with exhibition: true competes and places normally, including awards, but never scores
for a team and never takes a scoring slot from someone who does, in every event they enter. It is a
property of the athlete, not the team or the entry. An athlete on an unattached team is not
exhibition, and an exhibition athlete may be on an ordinary team.
Names
last_name is the athlete’s family name, or their only name when they have one; first_name is
optional, so an athlete with one name is written as they are.
Name privacy
A name is published as it is sent. A registration product that must not publish an athlete’s full
name, such as a minor’s or an opt-out’s, sends the name the way it may appear: "last_name": "C."
for a last initial. Meet Operations may also apply its own meet-wide policy before anything is
published.
Award groups
groups lists award groups within a race: age groups in a road race, JV within a varsity cross
country race. A group may name the event it awards within. Its gender uses the event list: C
groups everyone in the span, and X is a non-binary group. Age and grade spans are inclusive at both
ends. An age span is matched against the athlete’s age.
In a registration document, an athlete carries age or birth_date, or neither, never both: how
an age follows from a birth date is Registration’s rule, so a registration product that knows the
age sends it. An entry may name the group Registration placed it in with group_id, when that does
not follow from age and gender alone.
In a results document, an entry’s group_places names each group it placed in, by its id in
groups, with the place. The athlete’s age is the age Meet Operations placed them by, whether
Registration sent an age or a birth date.
Extra fields
A product may add fields of its own to any record. A reader ignores any field it does not know and
never fails over one, and no schema fails over one either. To keep your field from clashing with one
a later version adds, you may prefix it with your product’s name, such as examplereg:season_id;
the format reserves no prefixes and lists none.
- Meet Operations does not carry another product’s extra fields into a results document. A registration product that needs a value back keeps it on its own side, keyed by its own id.
- A field that proves useful across products is added to the format in a later minor version.
A writer sends only the values this version defines, never one of its own (see Versioning).
Reading a registration document
Meet Operations reads only registration documents, told by omx.kind. It refuses a document of
kind results before creating or changing anything, and tells the timer that a meet is moved
between computers with the product’s own backup (OMX-C102). It refuses a document whose kind is
missing or unknown, naming it (OMX-C107).
A stray field is not a refusal. A registration document that carries a field Registration does
not write, such as a bib on Carla Mendez’s 5K entry or a result, is still read. Meet Operations
ignores the field and tells the timer what it ignored.
Fetching again. A timer may fetch Registration’s link more than once, to pick up late entries and scratches. Each fetch returns a complete registration document of the same shape. How Meet Operations merges a later fetch into the meet the timer is running is its own business; the format promises only what Registration does:
- Ids never change for the life of the meet.
- A scratch is a status. A withdrawn athlete stays in
entrieswithstatus: "scr", never removed (OMX-C10).
Registration must not assume its URL will be read a second time. If entries change late, tell the meet director to have the timer fetch the link again.
Records Meet Operations creates
Some records exist only in Meet Operations and carry no Registration id: an athlete who shows up on
the day, a club that enters unattached, a JV race Registration never listed. They reach a Publisher in the results document. meet.origin says how to read
that. It is { "id", "name" }, the product the meet started in, named as its generator was:
meet.origin |
Meaning | A record with no id under origin.id is… |
|---|---|---|
A Registration product, { "id": "examplereg", … } |
The meet was built from a registration document | a record the timer created |
Meet Operations itself, { "id": "2t", … } |
The timer built this meet; no Registration | every record. There are no Registration ids to expect |
A meet with no Registration is ordinary. Do not treat it as incomplete.
What a Publisher does with a record that has no Registration id:
- Keep it. The record is self-describing: name, team, bib, mark, place. A dropped entry means incomplete results and a wrong team score, shown as if correct.
- Never auto-match by name. You may offer the timer a match-or-create action; never guess.
- Key your storage on Meet Operations’
id, not on an external id. A record can acquire a Registration id mid-meet: the timer creates a walk-up, Registration’s next document includes that same athlete, and the timer merges them. Storage keyed on Meet Operations’ id sees a normal overwrite. Storage keyed on the external id sees a duplicate.
When the timer merges two athletes, the surviving athlete keeps its own Registration id.
The results document
Meet Operations hands on the meet with the competition filled in. A results entry is one athlete or relay in one round:
{
"id": "en_5001",
"event_id": "ev_41",
"athlete_id": "at_301",
"round": 1,
"result": { "value": 1176230, "unit": "ms", "display": "19:36.23" },
"overall_place": 3,
"gender_place": 3,
"points": 3,
"splits": [{ "marker_id": "m_1", "time_ms": 374200 }],
"group_places": [{ "group_id": "g-jv", "place": 1 }]
}
A results entry may carry id, event_id, athlete_id or relay, round (required), heat,
lane, wave_id, seed, status, result, net_result, overall_place, gender_place,
heat_place, group_places, qualified, flags, wind_mps, reaction_time_ms, points,
multi_points, splits, field_series, and official_note. It carries exactly one of status and
overall_place: a place means the athlete placed, and a status says everything else (see
Statuses). It never carries external_ids, a bib (the bib is the athlete’s), or a
group_id.
Two things exist only in a results document:
events[].rounds: one object per round, with the round number, alabelsuch as “Final”,final,status,scheduled_at,timing, andplace_by. Entries point at a round by number. A round may listheatswith what belongs to a heat rather than an entry: when itstarted_at, and its officialwind_mps. A round run as one race, such as a cross country or road race, lists at most one heat,1, to say when its gun went; an entry with noheatbelongs to that heat.team_scores: standings, when the meet is scored. See Team scores.
Who is in a results document
A results document lists only:
- the athletes its entries name, as the athlete or in a relay’s legs, a heat-sheet entry included;
- the teams of those athletes, and the teams of its relays and standings.
Noah Brooks scratched from the JV race: he has an entry with scr in the registration document,
no entry in the results document, and so no athlete record there either.
A results document never carries an athlete’s birth_date, email, phone, notes, or chip,
or a meet’s or event’s notes. See Personal data.
One entry per round
A registration document has one entry per athlete or relay per event. A results document has one
entry per athlete or relay per round, and round is required on it. A sprinter who ran a prelim and
a final comes back as two entries, each with its own id, with round: 1 and round: 2. Both are
found through the same athlete and event.
The round marked final: true is the event’s result. Its places are the event’s places. On
any other round, overall_place is the place across that round only. Meet Operations marks the
final as soon as it knows which round that is, including the only round of a single-round event,
and omits final on every other round. An event with no round marked final has no result yet. A
Publisher never infers the final from a round’s number or label: before the final runs, the
highest-numbered round is a prelim.
"rounds": [
{
"round": 1,
"label": "Prelims",
"status": "official",
"scheduled_at": "2026-10-03T09:00:00-04:00"
},
{
"round": 2,
"label": "Final",
"final": true,
"status": "upcoming",
"scheduled_at": "2026-10-03T14:00:00-04:00"
}
]
A round’s scheduled_at is the meet’s schedule. It says when the round is planned to start, so
prelims at 9:00 and the final at 2:00 each carry their own time. It is the only schedule in the
format: a registration document carries none, since Meet Operations sets the schedule, and a
results document carries it on each round it knows a time for. It is a plan, never a start: when a
heat actually went is that heat’s started_at, and no time in an entry counts from scheduled_at
except a rolling start’s, which has no start signal (see How a round was timed).
Round status
rounds[].status says where a round stands, from before it runs to official:
status |
Meaning | Example |
|---|---|---|
upcoming |
Not run yet | The Girls 3200 final, scheduled for 2:30 |
started |
Under way; some results may be in | Flight 1 of the long jump is done |
cancelled |
Will not run | The Boys 4x400 after lightning |
unofficial |
Every heat has run; results may still change | Varsity Girls 5K, just finished |
under_review |
A protest or review is open | The 100m after a lane-violation protest |
official |
Final, unless a later correction is handed on | Varsity Girls 5K once official |
The list is open, and it fails safe: a reader that meets a status it does not know treats the round
as not official, so a later state can never make results look final. A cancelled event stays
listed, with its rounds cancelled and no entries, so a Publisher prints “Cancelled” rather than
“Results to come” (OMX-C113).
A status or a place
A registration entry may carry a status; an entry with none is e, entered (OMX-R186). A
results entry carries exactly one of status and
overall_place, never both and never neither: a place means the athlete placed, and a status says
everything else (OMX-R177). There is no “finished” status to write beside a place.
What an entry may be depends on its document and round:
| Where | The entry may be |
|---|---|
| A registration document | e, scr, wait |
A results round that is upcoming |
e, a heat-sheet entry, for a start_lists destination only |
A results round that is started |
e (as above), s, placed so far, or dnf, dns, dq, fs, nr |
A results round that is unofficial, under_review, or official |
placed, or dnf, dns, dq, fs, nr |
A results round that is cancelled |
none |
One entry moves through these states (OMX-C117):
eis a heat-sheet entry:round,heat,lane,seed,wave_id, and no result. Meet Operations writes heat sheets only to a destination whosecontentasks forstart_lists(OMX-C118).sis in the round with nothing to place yet: noresult, no place, and it may carrysplitsor afield_series.- A placed entry carries
overall_placeand no status. In astartedround the place is the place so far, for a finisher in a race still running or an athlete still competing in a field event or multi-event; the round’s status says it may move. dnf,dns,dq,fs, andnrare where an entry ended without a place.
Example: Maya Chen in the Girls 100m prelims. At 9:10 she is heat 2, lane 4, seed 12.31, e; at
9:32, s; at 9:33, overall_place: 1 with 12.18, first in her heat. In the Riverbend Girls 5K, 12
minutes in, she is s with her 1-mile split. In the long jump, Ruth Okafor leads after her second
attempt: overall_place: 1 with her best mark, while the round is started.
Multi-events come back at both levels
A results document carries the multi-event and every component event actually contested. Meet
Operations built the component events, so they carry no Registration id; each names its multi-event
in multi_event_id.
- The multi-event entry is the standing. It carries the athlete’s points total as
resultinptand their place in the standings asoverall_place. While the multi-event isstarted, both are the total and place so far. - Each component entry is that event’s result. It carries the time or mark as
resultand the points it earned asmulti_points. pointson any entry means team points, a multi-event entry’s included.- An athlete who abandons the multi-event is
dnfon the multi-event entry and holds no place in its standings. Jake Ortiz pulls a hamstring after 7 of 10 decathlon events: hisDECentry isdnf, and his component entries keep their marks andmulti_points.
A Publisher can publish both levels: the standings from the multi-event, and each component’s
results from its own event. A Registration product finds an athlete’s component entries through the
athlete and the component events whose multi_event_id names its multi-event.
Unknown values are omitted
A key whose value is not known is left out. Do not write null to mean “no lane”, “no wind”, or
“no place”; the schema rejects it. A reader treats null exactly as an absent key, and removes
null values before validating so that one never fails an import.
A finisher placed by a recorded finish order with no time carries its place and no result key.
Statuses
status is a closed list. A placed athlete carries
overall_place instead of a status (see A status or a place).
| Code | Name | Meaning | Written in | Holds a place |
|---|---|---|---|---|
e |
Entered | Entrant, confirmed or not; nothing has happened yet | registration; results heat sheets | no |
scr |
Scratched | Withdrawn before competing | registration | no |
wait |
Wait list | Not an entrant yet; holding for a spot | registration | no |
s |
Started | In the round, with nothing to place yet | results | no |
dnf |
Did not finish | Started and did not complete. A race, or a multi-event the athlete abandoned | results | never |
dns |
Did not start | Expected to start, did not, or never took an attempt in a field event | results | never |
dq |
Disqualified | Competed, removed by rule; keeps the result it was recorded with | results | never |
fs |
False start | Charged with a false start and removed when the starter recalled the field | results | never |
nr |
No result | Competed and has no valid result | results | never |
Registration sends only e, scr, and wait. Each changes what Meet Operations does: the athlete
is on the start list, off it, or not in the race until a spot opens. An unconfirmed registration,
awaiting payment or a waiver, is sent as e, or left out. Checked In is set at the meet and is not
written in either kind: a checked-in athlete’s heat-sheet entry is e. There is no Pending.
Where each status applies:
dnfis for races and for a multi-event’s own entry. A thrower or jumper with no valid mark isnr, neverdnf.fsis for timed track events only. A false start found only after the race, from the start blocks, isdq: that athlete ran the race and keeps the time.- Every other status applies to every event.
Four rules:
NT,NM, andNHare hownrprints, not statuses. An athlete who competed and has no valid result (a runner with neither a time nor a recorded finish position, three fouls, no height cleared) isnr, in any event. Render it asNTfor a race,NMfor a throw or horizontal jump, andNHfor a vertical jump, never asNR, which on a results sheet reads as National Record. Never writeNT,NM, orNHintostatus.- Only a placed entry and
dqcarry a result. Adqkeeps the time or mark it was recorded with and holds no place.dnf,dns,fs,nr,s, andecarry noresult. A false start never ran the race that counted, so anfsentry carries nosplits, while adqmay keep itssplitswith its result. Whether an entry placed is decided byoverall_place, never by whetherresultis present. - A placed entry’s place comes from its result, a recorded finish order, or a place typed by an
official.
heat_place,gender_place,group_places, andqualifiedappear only besideoverall_place. - Two entries in one round with the same
overall_placeare tied. There is no separate flag.
Compare on value, render display
result carries the stored value and the display text. Times are stored to thousandths even where
an event displays hundredths. Given only 10.61, a reader cannot separate 10.605 from 10.614, so it
cannot order a season list. Compare on value; print display. Ties arrive as equal places.
How a round was timed
A round says how its times were taken in timing: hand (a stopwatch or hand-operated device),
fat (fully automatic, photo-finish), or transponder (chip). It lives on the round because one
race is timed one way. A reader needs it before comparing across rounds or meets: a hand 10.3 and an
automatic 10.30 are not the same performance.
timing says what captured the finish, not where the clock started. Those are separate:
resultis always the gun time: from the start signal for the athlete’s race or wave, or from its scheduled start when there was no signal, as in a rolling start where every runner is started by a mat. A cross country race timed with chips still produces gun times.net_resultis the net time: from the moment the athlete crossed the start line to the finish, where a start mat recorded it. Road and trail races have it; most other races do not.
A round with net times says which one decided places in place_by: overall (overall and gender
places) and groups (award group places), each gun or net. Road races commonly place overall
by gun and age groups by net. A round without place_by was placed by gun time.
Team scores
Each team_scores block has an id, which stays the same from one document to the next, and says
what it covers in scope: one event, one division across its events, or the whole meet, and
scope.gender says whose standings they are, always. When teams are scored separately by gender, as
a co-ed school’s boys and girls are, each gender’s score is its own block, M, F, or X;
standings that count every gender together are C:
{
"id": "ts_41",
"scope": { "type": "event", "id": "ev_41", "gender": "F" },
"standings": [
{
"team_id": "tm_7",
"place": 1,
"points": 45,
"complete": true,
"entries": [
{ "entry_id": "en_5001", "role": "scorer" },
{ "entry_id": "en_5002", "role": "scorer" },
{ "entry_id": "en_5003", "role": "scorer" },
{ "entry_id": "en_5004", "role": "scorer" },
{ "entry_id": "en_5005", "role": "scorer" },
{ "entry_id": "en_5006", "role": "displacer" },
{ "entry_id": "en_5007", "role": "displacer" }
]
}
]
}
Each standing names the team, its place, points, and complete, whether the team fielded enough
scorers to score. Two standings in one block with the same place are tied. entries lists the
entries that scored or displaced, each with its role:
scorercounted in the team total;displacerdid not count but pushed other teams’ runners back.- The number is on the entry. An entry’s
pointsis its team-scoring value whether or not it counted: in cross country its scoring position, in track and field the points it earned. An entry has one team-scoring value, and every block that lists it uses that value: a sprinter’s 8 points in the 100m are the same 8 in the event’s block and in the meet’s total (OMX-R178). - An incomplete team scores nothing. A team without enough runners to score is listed with
complete: false, noplace, and nopoints, and lists no entries. Its runners, and runners on anunattachedteam, carry nopoints, and the scoring positions of every other team’s runners skip them.
So a results page prints Longwood’s line at Riverbend, “45 (3-7-9-12-14) [18-22]”, by lookup. Emma
Walsh finished 21st overall, but three runners ahead of her ran for incomplete or unattached teams,
so her entry carries points: 18, and the standing lists her as a displacer.
A Publisher prints these values; it never recomputes them.
Advancement, flags, and field series
These ride on the entry and are written only when Meet Operations has them.
qualified: the letter the results print after the entry’s result in a round before the event’s last, as it advanced, following World Athletics’ notation (CR 25.4):Qby place,qby time or mark,qDwon a draw for the last spot,qRadvanced by the referee,qJadvanced by the Jury of Appeal. A field-event qualifier isq, because a fieldQmeans a qualifying standard was met. Absent on a final and on any result that prints no letter. Maya Chen’s 60m prelim entry carriesqualified: "Q"; her final entry carries no reason. A bye or an alternate is not carried.flags: the timer’s own flags on the result, as typed, in the order typed: a record (MR), a best (PB,SB), a standard met, a state qualifier at a regional (Q). A Publisher prints them after the result and itsqualified, as written:12.44 Q MR. They are never a qualifier letter in a round before the event’s last, and their absence does not mean a result is not a record.field_series: a field-event series in order. Each attempt has anorderand anoutcome:ovalid,xfoul or miss,-pass,rretired. For a horizontal jump or throw,markis the measured distance and is present when the outcome iso. For a vertical jump,markis the bar height and is present on every attempt, so a reader can print the height card. A horizontal jump attempt may carry its ownwind_mps.wind_mps: on a field entry, the wind for the mark that counted. A race’s wind is on its heat,rounds[].heats[].wind_mps, never copied onto each entry.reaction_time_ms: the reaction time from the start signal, where the start was monitored.official_note: a note from the officials, published with the result: the rule a disqualification was under, a jump-off, a protest outcome.
Handing off
A handoff is one POST of one results document.
POST {url}
Authorization: Bearer {token}
Content-Type: application/json
Content-Encoding: gzip
{ "omx": { "version": "1.0", "kind": "results", "update": "full", … }, "meet": { … }, … }
The body is the document. Meet Operations may compress it with gzip and say so in
Content-Encoding, or send it uncompressed; every Publisher accepts both (OMX-C109). A
25,000-runner marathon with eight splits per runner is about 32 MB of JSON, and a few MB
compressed.
Rules for a Publisher:
-
Read
omx.versionbefore validating. A version you do not implement gets422with a message naming the version offered and the versions you accept, not a schema error. Put the versions you accept in the body asaccepts, so Meet Operations knows what you read:{ "accepts": ["1.0"], "errors": [{ "path": "/omx/version", "message": "2.0 is not read here" }] } -
Read
omx.kind. A document that is notresultsgets422. -
Accept a document of at least 64 MB, uncompressed.
413is for a document beyond any meet’s size. -
Key a document on
omx.generator.idplusmeet.id. Two Meet Operations products can both send"id": "8812". The pair is unique, whatever credential it arrives on. -
One Meet Operations product never replaces another’s document. A large meet may be timed by more than one system. Each hands off documents of the meet as it holds it, under its own
meet.id. If you are also Registration, your id inmeet.external_idsfinds your meet; it does not decide what to replace. A document for your meet under agenerator.idandmeet.idyou have not seen before is a second Meet Operations product: keep both, tell your user, and apply each only to its own. -
Apply each document by its
update. See Full and partial documents. -
Never apply an older document. See Order.
-
A correction is another handoff. A DQ resolved three days later arrives as a new document.
-
Respond with the codes below.
Full and partial documents
omx.update says what a results document covers:
full: the whole meet as it stands. The Publisher replaces everything it holds for the meet; anything not in the document is gone. Afulldocument lists every event, including ones that have not run.partial: only the rounds it carries, each of them whole: the round’s status, its heats, and every entry in it. The Publisher replaces those rounds, replaces the team score blocks it carries by theirid, adds or updates the meet, divisions, groups, waves, events, teams, and athletes it carries byid, and changes nothing else (OMX-C111). An event it carries keeps the rounds the document leaves out: rounds merge by theirroundnumber. A partial document deletes nothing.
What a partial document carries (OMX-C115):
omxandmeet;- the events of its rounds, each with only those rounds. A component event brings its multi-event and the multi-event’s round, because the standing changes with every component;
- every entry in those rounds, and the athletes, teams, divisions, groups, and waves they name;
- the event-scope team scores of its events, and every division- and meet-scope team score, whole. A standing may name teams and entries the partial document does not carry; the Publisher already holds them.
There is no schedule message. A meet running 20 minutes behind sends one partial document carrying
the rounds that moved, each with its new scheduled_at. A cancelled race is a partial document carrying that round, cancelled.
A Publisher refuses an update value it does not know with 422, and never reads it as full,
which would delete every other round (OMX-C112).
Example: the Riverbend track meet. At 8:00, 2Timer’s first document to the registration site is full. At
9:10 the Girls 100m prelims are seeded: a partial carrying that round, with Maya Chen in heat 2,
lane 4. At 9:40 the round is official: a partial carrying it again. At 4:30 the meet is done:
full.
Order
A handoff can arrive late. A Meet Operations product on a poor meet-day connection queues it and
retries, and a retry can land after a newer document. omx.updated_at puts documents in order.
updated_at is when the meet’s data last changed, not when the copy was sent. Meet Operations
writes it on every results document:
- Millisecond precision with a UTC offset:
2026-10-03T14:41:52.418-04:00. Two handoffs can fall in the same second. - Any change moves it forward, including a deletion. Resending unchanged data keeps it the same.
- It never goes backwards. Stamp
max(now, previous + 1 ms), so a clock corrected when the laptop reconnects cannot make new results look old. Keep the previous stamp with the meet, so it survives a restore or a move to another computer.
There is one stamp per meet, whatever a document’s update. A Publisher compares updated_at as
an instant, not as text, against the last document it applied for that meet:
| Arriving document | Publisher does | Responds |
|---|---|---|
| Newer | Applies it | 200 or 204 |
| Equal | Nothing. It already holds this document | 200 or 204 |
| Older | Keeps what it holds | 409 |
That works because of what Meet Operations does for each destination (OMX-C116):
- It sends one document at a time. The next waits until the last is answered.
- It keeps the rounds changed since the Publisher last accepted a document. Every send, a retry included, is built at that moment and carries all of them. A missed delivery is always inside the next document, and a late retry that lands after a newer one holds nothing the newer one lacks.
- Its first document from any copy of the meet is
full, and so is any document after anything is removed: an event, round, team, athlete, division, group, wave, or team score block. A laptop swapped mid-meet starts with nothing pending. - It sends
fullnext when a Publisher answers412. See Responses. - It may always send
fullinstead ofpartial. A timing product never has to write partial documents.
updated_at orders documents. It is not a security control: replaying a document already requires
the token.
Responses
| Status | Meaning | Meet Operations |
|---|---|---|
200 or 204 |
Accepted, or already held | Is done |
400 or 422 |
The document is not valid, or not one this Publisher reads | Does not retry, and tells the timer |
401 |
The token is not recognised | Stops handing off to this destination and tells the timer |
404 |
No endpoint at this URL | Stops handing off to this destination and tells the timer |
409 |
The Publisher holds a newer document | Does not retry, and tells the timer |
410 |
The credential was retired, and will not return | Stops handing off to this destination and tells the timer |
412 |
The Publisher needs the whole meet before a partial |
Sends a full document next, at once |
413 |
The document is larger than the Publisher accepts | Does not retry, and tells the timer |
429 |
Too many requests | Waits for Retry-After, then retries |
5xx or no answer |
A temporary failure | Retries with backoff |
Meet Operations treats a 4xx this table does not list as 400, and a 5xx it does not list as a
temporary failure.
A Publisher answers a partial with 412 when it cannot apply it: it does not hold the meet,
or it cannot vouch that it holds everything it accepted, because it restored a backup or refused the
first full with 413. It answers every partial with 412 until a full arrives, and may say
so in the body (OMX-R180):
{ "need": "full" }
A Publisher may add a body to a 410 so the timer sees a clearer message:
{ "reason": "revoked" }
reason is revoked or expired. A 400 or 422 may say what was wrong, so the timer can pass
it on:
{ "errors": [{ "path": "/entries/14/result/value", "message": "must be a number" }] }
A Publisher may rate-limit. When it does, it answers 429 with Retry-After. A handful of
documents per event is normal for a progressive destination, and one every few seconds while a
round runs for a live one.
When a handoff happens
A destination says how often it wants results in deliver, and what in content. Meet Operations
preselects them for the timer, who may change the cadence:
deliver |
The destination gets |
|---|---|
final (default) |
full documents once every round is official or cancelled, and again for each correction |
progressive |
A first full document, then partial ones as each round is seeded, goes official, or changes |
live |
Everything progressive gets, plus a partial document every few seconds while a round runs |
content |
The destination gets |
|---|---|
results (the default) |
Entries once a round is under way |
start_lists |
Also heat-sheet entries for rounds that have not run yet |
- A
finaldestination only ever getsfulldocuments, so the simplest Publisher needs one rule.finalis the default so that a destination that says nothing is not handed numbers that are still moving. deliverandcontentare open lists. An unknowndeliverreads asfinal, and an unknowncontentvalue is ignored, so a later value never reaches a destination that did not ask for it.
Example: The registration site sets "deliver": "live", "content": ["results", "start_lists"] and shows heat
sheets, then each race as it is run. A school’s site sets nothing and gets the meet once, at 4:30,
when it is done.
Live updates
While a round is under way, each live update is a partial document carrying the round whole:
every entry, finishers with their places so far, and athletes still on the course as s with
their splits. A Publisher shows the round as running (OMX-C114). If the finish crew fixes a misread
(bib 181, not 118), the next update carries the round without the wrong entry. Leaving an
entry out of a round it carries whole is not a removal and needs no full; an athlete left with no
entries stays on the Publisher until the next full document.
How often needs no field. Meet Operations waits a few seconds between updates to one destination,
and a Publisher that wants fewer answers 429 with Retry-After. The Riverbend Girls 5K has 300
runners: about 100 KB per update, 15 KB compressed. A 2,000-runner road race is about 80 KB
compressed.
Destination cards
A Publisher that never wrote a document still has to tell Meet Operations where to send results. It does that with a destination card: one destination, with its token, wrapped in an object that names it.
{
"omx_destination": {
"id": "riverbend-results",
"label": "Riverbend Results",
"url": "https://results.example.org/omx/inbox",
"token": "rr_push_6Hq2Lk9Vb3Xc8Nm1Pz4Rt7Wy0Ad5Gf2J",
"deliver": "progressive",
"content": ["results"]
}
}
- A destination is
id,label,url,token,deliver,content, andaccepts. - A Publisher shows the card as text with a copy button, or offers it as a file named
<label>.omx-destination.json. It validates againstdestination-card-v1. - Meet Operations accepts a pasted or opened card as a destination the timer added by hand.
Pasting it is the timer’s approval. It refuses a card whose
urlis nothttps://. Pasting a card whoseurlthe meet already sends to changes that destination rather than adding a second. - A card holds a token, so it is a credential: show it only to the person setting up the meet, and let them revoke it.
Asking a destination first
A Publisher may answer a GET on its destination URL, with the same bearer token, so Meet Operations
can check a destination when the timer adds it, before the meet. This is the probe. Open Meet Display, the companion format
for scoreboards, uses the same probe.
GET {url}
Authorization: Bearer {token}
{
"accepts": ["1.0"],
"generator": { "id": "riverbend", "name": "Riverbend Results", "version": "2.1" }
}
| Answer | Meaning | Meet Operations |
|---|---|---|
200 with the body above |
Ready, and reads these versions | Tells the timer who it reached and which versions it reads, and sends what it accepts |
503 with Retry-After |
Running, but not ready yet | Tells the timer, and asks again later |
401 or 410 |
The token is not recognised, or retired | Tells the timer the token was refused, before the meet rather than at the first result |
404, 405, or another |
Does not answer probes | Nothing is wrong: uses the accepts the destination already gave |
accepts(required) lists the versions the Publisher reads, with the meaning it has on a destination:["1.0"]reads every1.x.generator(required) names the Publisher exactly as a document names its writer: its systemid, itsname, and an optionalversion.- A probe’s
acceptswins. It is newer than the one on the destination or card, so Meet Operations goes by it, as it does by theacceptsin a422. - A probe changes nothing. A Publisher never counts it as a handoff, and answers it the same however often it is asked.
- Answering is optional. A Publisher that does not answer probes is fully conformant (OMX-C125); one that answers does so as above.
Trying it by hand
Every exchange is plain HTTPS, so curl is enough to see one work. With a document URL in
$DOC, and a Publisher endpoint and token in $INBOX and $TOKEN:
# Ask the destination first (optional for a Publisher): 200 with accepts and generator, or 404/405.
curl -sS -w '\n%{http_code}\n' "$INBOX" -H "Authorization: Bearer $TOKEN"
# Fetch a registration document, keeping its ETag. Then ask again: a 304 means nothing changed.
curl -sS -D headers.txt -o meet.omx.json "$DOC"
curl -sS -o /dev/null -w '%{http_code}\n' -H "If-None-Match: $(grep -i '^etag:' headers.txt | cut -d' ' -f2- | tr -d '\r')" "$DOC"
# Hand off a results document. Send it twice: both answer 200 or 204, and the second changes nothing.
curl -sS -O https://2timer.app/omx/fixtures/c30-first.omx.json
curl -sS -w '%{http_code}\n' -X POST "$INBOX" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' --data-binary @c30-first.omx.json
# The same document, compressed: 200 or 204, and nothing changes.
gzip -c c30-first.omx.json | curl -sS -w '%{http_code}\n' -X POST "$INBOX" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-H 'Content-Encoding: gzip' --data-binary @-
# Hand off an older copy of the same meet: 409.
curl -sS -O https://2timer.app/omx/fixtures/c30-older.omx.json
curl -sS -w '%{http_code}\n' -X POST "$INBOX" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' --data-binary @c30-older.omx.json
The fixtures hold a crafted document for each conformance test that needs one, named by the test.
Security
The document URL is the credential. Whoever holds it can read the meet.
- HTTPS only. Meet Operations refuses a plain
http://document or destination URL. - Scope the secret to one meet. Let the meet director revoke it and mint a new one.
- You may expire it, but the timer needs it until the meet starts, and may fetch it again for late entries.
- A revoked or expired URL answers
410. An unknown one answers404. On a410Meet Operations keeps the meet and tells the timer the link no longer works.
Each destination’s token is a separate secret, carried in the registration document and sent as an
Authorization: Bearer header. Holding the read link never grants the right to write results, and
the token never appears in a query string where proxies log it. Destinations do not share a
credential, so revoking one does not touch another.
The Publisher owns its token’s life. It decides how long a token works and answers 410 once it
stops, as for any retired credential. Keep a token working long enough for late corrections: a DQ
at the Riverbend Invitational overturned three weeks later still has to arrive.
Secrets
A meet involves two kinds of secret. Whoever runs an endpoint issues its secret; everyone else treats it as an opaque string.
| Secret | Issued by |
|---|---|
| The secret in the document URL | Registration |
A destination’s token |
Whoever runs that destination: Registration for its own, a third party for theirs |
Every one of them:
- Has at least 128 bits from a cryptographically secure random generator. That is 22 or more base62 characters; 32 is recommended. A 6-digit code is not a secret.
- May carry a readable prefix such as
reg_pub_, which helps secret scanners catch a leak. The prefix does not count toward the 128 bits. - Is compared in constant time and redacted from logs.
The schema requires a destination’s token to be at least 22 characters, so validation catches a
short one. It cannot check randomness.
Browser-based products
Meet Operations may run in a web browser. A browser fetches a document or posts a handoff to another site only when that site allows it, so every OMX endpoint answers cross-origin requests:
| Endpoint | Answers OPTIONS with |
Adds to every response |
|---|---|---|
| Document URL | Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, Access-Control-Allow-Headers: If-None-Match |
Access-Control-Allow-Origin: *, Access-Control-Expose-Headers: ETag |
| A destination’s URL | Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: POST, GET, Access-Control-Allow-Headers: Authorization, Content-Type, Content-Encoding, Content-Digest |
Access-Control-Allow-Origin: *, Access-Control-Expose-Headers: Retry-After |
Allowing any origin is safe because OMX never uses cookies: the secret is in the URL or the bearer
token, and Access-Control-Allow-Credentials is never sent.
Answer at the URL you give out. A document URL and a destination URL answer the request
themselves, never with a redirect. A browser follows a cross-origin redirect only when the redirect
itself carries the headers above, never redirects a preflight, and drops Authorization when a
redirect leaves the origin. So a move from a bare domain to www., or from one path to another,
belongs in the URL you write into the document or hand to the timer. A redirect without those
headers looks to a browser exactly like a server that does not answer cross-origin requests at all.
Personal data
A document is personal data about athletes, many of them minors. Send what the meet needs: name, gender, team, and a birth date, age, or grade.
- What stays with the timer. An athlete’s
email,phone,notes,birth_date, andchip, and a meet’s or event’snotes, are for the timer’s use on meet day. Meet Operations leaves every one of them out of every results document (OMX-C65); the results check refuses them. A note such as “Hana S. has a custody order, do not release to her father” stays with the timer. Public text from the officials goes in an entry’sofficial_note. - Age, not birth date. A results document carries the athlete’s
ageas Meet Operations placed them, never a birth date. - Only the athletes the entries name are in a results document (see Who is in a results document).
- A Publisher never publishes a birth date or contact detail.
- Keep a document no longer than you keep the meet.
Integrity
OMX adds no checksum or signature. TLS already detects a corrupted or altered body in transit, and
a hash without a key would not stop anyone able to alter the body, who could recompute it.
The bearer token already says who sent the document. A sender may add a Content-Digest header
(RFC 9530); a Publisher may verify it and must not require it.
Not in this version
Each of these is a known gap, left out until an implementer needs it. Each can be added in a minor version without changing what anything already in the format means:
- Changes-only live updates. A
liveupdate carries the running round whole. A mode that sends only the entries that changed, for races of many thousands, waits for a destination to ask for it by name. - Records and bests as data. A timer’s
flagssay what the timer typed, such asMRorPB, not which record or best it is. Records with a name and type, and bests worked out from an athlete’s history, are the Publisher’s to compute from results. - Sessions. A named block of the schedule, such as “Saturday Morning” at the main stadium, is
not carried. Each round’s
scheduled_atis the schedule, and a Publisher groups rounds by day from it. - Webhooks. Registration cannot tell Meet Operations that its document changed.
- OAuth. Destinations use bearer tokens. An OAuth profile may be added later as new destination fields, without changing the document.
- Other ways to deliver. Every destination receives a
POST. A destination that would rather fetch a hosted document may come later with its own fields. - Para classification. An athlete’s sport classes (
T54,F37), which can differ between running and throwing, and a guide on an entry. Adaptive events are defined; the athlete’s classes are not. - Reasons beside a status. Lapped, pulled, or over the time limit, for lap races and ultras.
These are expected as an open reason beside the closed
status, never as new statuses. - Laps and fixed-time events. A lap count, a new mark unit, and a duration or lap target on an event (a one-hour run, a criterium, a backyard ultra).
- Yard events. A 600-yard run has no code yet. Event codes are an open list, so yard codes can come in a minor version.
- A measured course distance. A race’s code is its advertised distance; what the course actually measured may come as its own field.
- Segmented races. Triathlon and similar events build on
markers; a distance per marker and named segments wait for a producer. - Age grading and adjusted rankings. Age-graded results, and events ranked on points from raw marks.
- Dual-meet pairings. Pairwise scores inside a larger meet. They would come as a new
scope.typewhose standings carry their own points, which a 1.0 Publisher skips. - Which events a document covers. For a meet split between Meet Operations products; a Publisher keeps their documents apart meanwhile (see Handing off).
- Sanctions and certification. Sanction numbers and course certification. A meet’s ids on
other systems are already carried, in
meet.external_ids.
Versioning
omx.versionisMAJOR.MINOR.- Minor versions only add: optional fields, and values in open lists. A reader accepts any minor within a major it implements and ignores what it does not know.
- Major versions may break. A reader that meets a major it does not implement refuses the document with a clear message rather than importing what it half-understands.
- One schema URL per major, republished in place with each minor.
- A reader of 1.0 accepts every
1.xdocument, and a destination thataccepts1.0is sent any1.x(OMX-R187). A destination’sacceptsmay come from its card orpublishitem, from a probe, or from a422; the newest one counts (see Asking a destination first). - A badly formatted value is a warning, never a failed meet. The lenient reader schema checks
types, not formats: a country that is not two letters,
updated_atwith no milliseconds, a short token, a long team code. A reader warns about such a value and keeps reading; the strict writer schemas keep the formats, so a writer never sends one (OMX-R181, OMX-C123). A writer that holds a value it read that way, such as a 20-character team code, writes it in a form that fits, a shortened code unique in the meet, and never holds back the whole meet for one value (OMX-R185).
Open and closed lists
Every list of values is either closed or open, and an open one says what a reader does with a value it does not know.
Closed lists change only in a major version, because a reader branches on them: omx.kind,
athlete gender, event gender, entry status, and place_by.
Open lists gain values in minor versions. A writer sends only a value this version defines; the strict writer schemas enforce that. The lenient reader schema accepts any string, so a document from a later minor still reads.
| Open list | A value the reader does not know |
|---|---|
meet.sport |
Meet Operations refuses the document, naming the sport; a Publisher shows the events by name and code |
events[].code |
Meet Operations leaves the event and its entries out and tells the timer; a Publisher shows it by name |
Mark unit |
Show the mark’s display; never compare or sort on it |
omx.update |
Refuse the document with 422; never read it as full |
rounds[].status |
Treat the round as not official |
events[].adaptive.mode |
Show the event as adaptive with no mode |
publish[].deliver |
Treat as final |
publish[].content values |
Ignore the value |
team_scores[].scope.type |
Skip the block rather than show standings it cannot label |
field_series[].outcome |
An attempt with no valid mark |
qualified |
Show the value as written |
| Any other open list | Treat as absent |
The other open lists are timing, team_scores role, and meet.track.shape. flags are free
text, printed as written. System ids (omx.generator.id, meet.origin.id, and the keys of
external_ids) are not a list of values: Meet Operations writes back every id it received, and a
Publisher keeps or ignores an id under a system it does not know.
Pinned versions
- Every version’s raw files stay at
/omx/<version>/, for example/omx/1.0/spec.md, so an implementation can cite and re-check exactly what it was built against. The unversioned URLs always serve the newest version. - A document carrying a draft number (
0.x), written before 1.0, is refused by a 1.x reader. There are no converters.
Changelog
- 1.0. The first published version.
Checklist
Registration, whether or not you want the meet back
- Document carries
"kind": "registration", validates againstregistration-document-v1, and passesomx-check.mjs - Every id is a stable string, unique within its list
- Every
team_id,athlete_id,event_id,division_id,group_id, andwave_idresolves to a record in the same document - At most one entry per athlete per event; every relay has a letter, and at most one relay entry per team, letter, and event
- HTTPS,
application/json, revocable per meet - The URL secret has at least 128 bits of randomness; a revoked or expired URL answers
410 ETagsent andIf-None-Matchhonoured with304(recommended)- Cross-origin headers on the document URL:
Access-Control-Allow-Origin: *,ETagexposed,If-None-Matchallowed - The URL you give out answers directly, not through a redirect (a bare domain to
www.included) omx.generator.idis your system id, a name you pick once and never change- Ids you know on other systems are in
external_idson the meet, events, teams, and athletes, never your own, and never on an entry - Scratches are sent as
status: "scr", not omitted; entry statuses are onlye,scr, andwait - No
meet.origin, track, markers, rounds, results, places, or team scores: those are Meet Operations’ - Marks are in the unit you measured, with integer
ms,mm,pt - Every event has a
codedefined for the meet’s sport, at its advertised distance (HMARandMARfor those races); an athlete in a multi-event has one entry, on the multi-event - Every value is one this version defines
- An athlete’s gender is left out when they did not state one; a race or relay open to every
gender is
C, and every event, award group, and team-score scope has a gender - An athlete carries
ageorbirth_date, never both - Nothing assumes the URL will be fetched more than once
Registration that wants results back, in addition
- Each
publishdestination has a stableid, a humanlabel, and atoken deliverandcontentare set deliberately, knowingdeliverdefaults tofinal- Finds its meet, events, teams, and athletes in a results document by
external_idsunder its own system id, and its entries through their athlete and event - Each destination’s
tokenis a different secret from the document URL and from each other - Each token you issue has at least 128 bits of randomness, and keeps working long enough for late corrections
Publisher
- Accepts
POSTof one results document with bearer auth;401on a token it does not recognise,410on a retired one - Accepts a body sent with
Content-Encoding: gzip, and a document of at least 64 MB uncompressed - Answers cross-origin preflights, allowing
Authorization,Content-Type,Content-Encoding, andContent-Digest, and exposesRetry-After - The
urlin your destination answers directly, not through a redirect - Reads
omx.versionbefore validating, and answers422naming both versions to one it does not implement, withacceptsin the body - Answers
422to a document whosekindis notresults, or whoseupdateit does not know - Removes
nullvalues before validating; answers400or422, witherrors, to a document it cannot accept - Shows its user a destination card to give the timer
- May answer the probe:
GETon the destination URL with the token gets200withacceptsandgenerator, or503while not ready; a refused token gets401or410 - Issues tokens with at least 128 bits of randomness
- Keys each document on
omx.generator.idandmeet.id - Applies a newer
omx.updated_at, treats an equal one as a no-op, and refuses an older one with409, comparing instants rather than text, whatever the document’supdate - Applies a
fulldocument by replacing everything held for the meet; the same document twice is a no-op - Applies a
partialdocument by replacing only the rounds it carries and the team score blocks it carries by theirid, adding or updating the records it carries byid, and keeping everything else - Answers
412to apartialit cannot apply, until afullarrives - Creates a meet it has never seen, without Registration existing
- Reads
meet.originrather than inferring provenance from missing ids - Joins records by an id in
external_idsunder a system it knows, and entries through their athlete and event, never by name - Keeps entries with no Registration id
- Keys storage on Meet Operations’
id, so a record that later acquires a Registration id is an overwrite, not a duplicate - Expects one entry per round
- Takes an event’s result from the round marked
final, never from a round’s number or label - Shows a round by its
status: a cancelled round as cancelled, astartedround as running with itssentries on the course, and an unknown status as not official - Compares marks on
result.value, never ondisplay, and shows a mark in a unit it does not know by itsdisplay - Never recomputes a place, a tie, or a team score; reads a tie from equal places
- Reads a placed entry from
overall_place, and every other entry’s outcome fromstatus - Ignores fields it does not know
- Treats
nullas absent, and an absentresulton a finisher as “no time”, not an error - Never publishes a birth date or contact detail
- Treats any other unknown open value as its table says; shows a meet of a sport it does not know,
and an event whose code it does not know, by the events’ names, printing an
nrthere asNR - Never lets one Meet Operations product’s document replace another’s: a document for its meet under
a new
generator.idormeet.idis kept beside the first
Meet Operations
- Builds the meet from a pasted document URL or an opened file with no wizard
- Refuses a plain
http://document or destination URL - Refuses a major version it does not implement, naming both versions
- Refuses a document of kind
results, and one whose kind is missing or unknown, before creating anything - Reads a registration document carrying a field Registration does not write, ignores the field, and tells the timer
- Leaves out an event whose code it does not know, or that is not defined for the meet’s sport, with its entries, tells the timer, and reads the rest; refuses a sport it does not run, naming it; never merges an adaptive event into the standard event of the same code
- Builds a multi-event’s component events by its own rules, for any gender, and reports one it cannot build
- Writes its own system id as
omx.generator.id, setsmeet.originhonestly, writes its own ids inid, and carries Registration’s id unchanged inexternal_ids, under Registration’s system id, on every meet, event, team, and athlete that came from Registration - Writes back every
external_idsentry it received on the meet, events, teams, and athletes - Writes no
external_idson an entry, and a letter on every relay entry - Gives a meet made from another meet new ids, and none of the old meet’s Registration ids
- Writes
"kind": "results",update, andupdated_aton every results document - Writes one entry per round, with
round, and exactly one ofstatusandoverall_place: a place for a placed entry, and otherwise only a status its round’s status allows - Writes heat-sheet entries only for a destination whose
contentasks forstart_lists - Marks the round whose places are the event’s result with
final: true, and gives every round itsstatus,cancelledincluded - Writes a multi-event at both levels, with every component event contested
- Writes the gun time in
result, any net time innet_result, andplace_byon a round that has net times - Writes every split at a marker the event lists or at a relay leg, as elapsed
time_ms - Writes a race’s wind on its heat, and a field result’s wind on the entry
- Writes team-scoring
pointson each entry, a displacer’s included, one value in every block that lists it; gives each team score block anidthat stays the same; lists each standing’s entries byentry_idandrole; and writes an incomplete team with no place, points, or entries - Writes only the athletes and teams its entries name, with the age it placed each athlete by, and
no
birth_date,email,phone,notes,chip, orpublish, and no other product’s extra fields - Writes each round of a
partialdocument whole, with the athletes and teams its entries name and the team scores the spec lists - Sends one document at a time to a destination, builds every send (a retry included) at that
moment from everything pending, and sends
fullfirst from any copy of the meet and after any removal - Respects each destination’s
deliver:fullonly forfinal, and aliveupdate at most every few seconds while a round runs - Every handoff is one document that validates against
results-document-v1, sent uncompressed or withContent-Encoding: gzip - Every handoff carries
omx.updated_atto the millisecond, moved forward by any change and never backwards - Retries
429,5xx, and network failures with backoff; never retries400,401,404,409,410,413,422, or a4xxthe table does not list - Sends
fullnext when a Publisher answers412 - May probe a destination when the timer adds it, tells the timer what it learned, and goes by a
probe’s
acceptsover the destination’s; a destination that does not answer is not an error - Publishes stored values in
result.value, withdisplayalongside - Omits what it does not know; never writes
null - Never re-encodes a status, and writes no
NT,NH, orNMinto a status (they are hownrprints) - Offers the timer a handoff cadence, with the destination’s
deliverpreselected - Lets a timer add a destination by hand, and by pasting a destination card
Files
Rendered pages:
- Field reference: every field in the schema, as tables
- Worked examples: one validating registration document per sport, each with its results twin
- Conformance tests: every rule above with a pass condition, grouped by stage
- Test page: check a document, a registration URL, a Publisher endpoint, or a destination card from your browser
- Integration prompts: ready to paste into a coding assistant
- Field mapping: where Hy-Tek and other formats’ fields go in a document
Raw files, for tools and coding assistants. /omx/llms.txt lists them all.
| File | URL |
|---|---|
| Reader schema (lenient, JSON Schema 7) | /omx/schemas/meet-document-v1.schema.json |
| Registration check (strict) | /omx/schemas/registration-document-v1.schema.json |
| Results check (strict) | /omx/schemas/results-document-v1.schema.json |
| Destination card schema | /omx/schemas/destination-card-v1.schema.json |
| Checker (Node 18+, no dependencies) | /omx/tools/omx-check.mjs |
| Fixtures for the conformance tests | /omx/fixtures/index.json |
| TypeScript types | /omx/types/omx.d.ts |
| Python types (3.11+) | /omx/types/omx.py |
| This specification | /omx/spec.md |
| Conformance tests | /omx/conformance.md |
| Integration prompts | /omx/integration-prompt.md |
| Field mapping | /omx/mapping.md |
| Examples index | /omx/examples.md, listing each document under /omx/examples/ |
| This version, pinned | /omx/1.0/: the same raw files, never changed after a later version replaces it |
Run the checker against a document, with the schema too if your project has ajv installed:
curl -sS -O https://2timer.app/omx/tools/omx-check.mjs
node omx-check.mjs meet.omx.json # kind read from omx.kind
node omx-check.mjs --kind registration meet.omx.json # or check it as one kind
It prints each problem with its JSON path and the conformance test it fails, and exits non-zero when there is one.
Governance
OMX was started by 2Timer, which hosts this page, and is edited in the open with the products that implement it.
- Anyone may propose a change by email. A proposal says what problem it solves for which stage.
- A change is decided with the implementers it affects. Nothing that changes what an implementer reads or writes is made without asking them, and every change is published here with its reason in the changelog.
- The versioning rules apply: a minor only adds, and only a major breaks.
License
The text of this specification is licensed under Creative Commons Attribution 4.0 (CC BY 4.0): copy, translate, and adapt it, crediting Open Meet Exchange. The schema, the example documents, the conformance tests, and the integration prompts are dedicated to the public domain under CC0 1.0: copy them into your product or your test suite with no notice required.
Feedback
If something here only makes sense for one product, that is a bug in the format. Send thoughts to [email protected].
