Open Meet Exchange

Version 1.0. A minor version only adds; only a major breaks. Comments to [email protected].

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.kind says which one it is.
  • Every update is whole where it counts. A results document is either full, the whole meet as it stands, or partial, 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 live destination 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

  1. 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.
  2. 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 an ETag and answering If-None-Match with 304 is recommended: a timer may fetch the link again for late entries.
  3. Show the link to the meet director with the words “give this to your timer”. Also offering the same document as a downloadable .omx.json file 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"]
	}
]
  • deliver is how often you want results: final (the default) once the meet is official, progressive as each round is seeded, goes official, or changes, or live, which adds an update every few seconds while a round runs. See When a handoff happens.
  • content is what you want: results (the default), and start_lists if 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 every 1.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_ids under 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 whose athlete_id names the athlete carrying "examplereg": "a-1001" and whose event_id names 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. full replaces everything you hold for the meet; partial replaces only the rounds it carries. If you never ask for more than final, you only ever get full.
  • Read meet.origin. It names the product the meet started in. When its id differs from omx.generator.id, the meet came from that Registration product, and records that came from it carry its id in external_ids under origin.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 under anet, so you can match results without ever having supplied an entry.

Meet Operations

  1. Accept a pasted URL or an opened file, check its version and kind, and build the meet from it with no wizard.
  2. 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.
  3. 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 deliver and content. Your first document to a destination is full; after that you may send partial documents, or always full ones.

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-v1 is 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 of e, scr, or wait. No results, rounds, places, team scores, track, or markers.
  • results, written by Meet Operations: what happened. One entry per athlete or relay per round, with round required. It also carries omx.update, omx.updated_at, and meet.origin, and never publish.

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-level omx object and its kind, not by its name, so a plain .json file 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 id is an opaque string. No reader parses it.
  • An id never changes for the life of the meet. Regenerating ids on each export breaks the results join.
  • An id is 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 their id, for references within the document.
  • A writer never lists itself: its own id for the record is id.
  • In a registration document, id is Registration’s id, and external_ids holds any ids it knows on other systems, such as an athlete’s Athletic.net id.
  • In a results document, id is Meet Operations’ id. Every meet, event, team, and athlete that came from Registration carries Registration’s id unchanged in external_ids under 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 by display, and its team scores as sent, and prints an nr entry in it as NR, 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 a tf-indoor meet, 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_ms is elapsed from the same start as result: the gun for the athlete’s race or wave. A relay leg’s split is the running clock when that leg finished, with 1 for 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 entries with status: "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, a label such as “Final”, final, status, scheduled_at, timing, and place_by. Entries point at a round by number. A round may list heats with what belongs to a heat rather than an entry: when it started_at, and its official wind_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 no heat belongs 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):

  • e is a heat-sheet entry: round, heat, lane, seed, wave_id, and no result. Meet Operations writes heat sheets only to a destination whose content asks for start_lists (OMX-C118).
  • s is in the round with nothing to place yet: no result, no place, and it may carry splits or a field_series.
  • A placed entry carries overall_place and no status. In a started round 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, and nr are 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 result in pt and their place in the standings as overall_place. While the multi-event is started, both are the total and place so far.
  • Each component entry is that event’s result. It carries the time or mark as result and the points it earned as multi_points.
  • points on any entry means team points, a multi-event entry’s included.
  • An athlete who abandons the multi-event is dnf on the multi-event entry and holds no place in its standings. Jake Ortiz pulls a hamstring after 7 of 10 decathlon events: his DEC entry is dnf, and his component entries keep their marks and multi_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:

  • dnf is for races and for a multi-event’s own entry. A thrower or jumper with no valid mark is nr, never dnf.
  • fs is for timed track events only. A false start found only after the race, from the start blocks, is dq: that athlete ran the race and keeps the time.
  • Every other status applies to every event.

Four rules:

  • NT, NM, and NH are how nr prints, 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) is nr, in any event. Render it as NT for a race, NM for a throw or horizontal jump, and NH for a vertical jump, never as NR, which on a results sheet reads as National Record. Never write NT, NM, or NH into status.
  • Only a placed entry and dq carry a result. A dq keeps the time or mark it was recorded with and holds no place. dnf, dns, fs, nr, s, and e carry no result. A false start never ran the race that counted, so an fs entry carries no splits, while a dq may keep its splits with its result. Whether an entry placed is decided by overall_place, never by whether result is 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, and qualified appear only beside overall_place.
  • Two entries in one round with the same overall_place are 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:

  • result is 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_result is 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:

  • scorer counted in the team total; displacer did not count but pushed other teams’ runners back.
  • The number is on the entry. An entry’s points is 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, no place, and no points, and lists no entries. Its runners, and runners on an unattached team, carry no points, 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): Q by place, q by time or mark, qD won a draw for the last spot, qR advanced by the referee, qJ advanced by the Jury of Appeal. A field-event qualifier is q, because a field Q means a qualifying standard was met. Absent on a final and on any result that prints no letter. Maya Chen’s 60m prelim entry carries qualified: "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 its qualified, 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 an order and an outcome: o valid, x foul or miss, - pass, r retired. For a horizontal jump or throw, mark is the measured distance and is present when the outcome is o. For a vertical jump, mark is the bar height and is present on every attempt, so a reader can print the height card. A horizontal jump attempt may carry its own wind_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.version before validating. A version you do not implement gets 422 with a message naming the version offered and the versions you accept, not a schema error. Put the versions you accept in the body as accepts, 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 not results gets 422.

  • Accept a document of at least 64 MB, uncompressed. 413 is for a document beyond any meet’s size.

  • Key a document on omx.generator.id plus meet.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 in meet.external_ids finds your meet; it does not decide what to replace. A document for your meet under a generator.id and meet.id you 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. A full document 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 their id, adds or updates the meet, divisions, groups, waves, events, teams, and athletes it carries by id, and changes nothing else (OMX-C111). An event it carries keeps the rounds the document leaves out: rounds merge by their round number. A partial document deletes nothing.

What a partial document carries (OMX-C115):

  • omx and meet;
  • 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 full next when a Publisher answers 412. See Responses.
  • It may always send full instead of partial. 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 final destination only ever gets full documents, so the simplest Publisher needs one rule. final is the default so that a destination that says nothing is not handed numbers that are still moving.
  • deliver and content are open lists. An unknown deliver reads as final, and an unknown content value 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, and accepts.
  • A Publisher shows the card as text with a copy button, or offers it as a file named <label>.omx-destination.json. It validates against destination-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 url is not https://. Pasting a card whose url the 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 every 1.x.
  • generator (required) names the Publisher exactly as a document names its writer: its system id, its name, and an optional version.
  • A probe’s accepts wins. It is newer than the one on the destination or card, so Meet Operations goes by it, as it does by the accepts in a 422.
  • 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 answers 404. On a 410 Meet 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, and chip, and a meet’s or event’s notes, 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’s official_note.
  • Age, not birth date. A results document carries the athlete’s age as 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 live update 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 flags say what the timer typed, such as MR or PB, 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_at is 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.type whose 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.version is MAJOR.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.x document, and a destination that accepts 1.0 is sent any 1.x (OMX-R187). A destination’s accepts may come from its card or publish item, from a probe, or from a 422; 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_at with 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 against registration-document-v1, and passes omx-check.mjs
  • Every id is a stable string, unique within its list
  • Every team_id, athlete_id, event_id, division_id, group_id, and wave_id resolves 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
  • ETag sent and If-None-Match honoured with 304 (recommended)
  • Cross-origin headers on the document URL: Access-Control-Allow-Origin: *, ETag exposed, If-None-Match allowed
  • The URL you give out answers directly, not through a redirect (a bare domain to www. included)
  • omx.generator.id is your system id, a name you pick once and never change
  • Ids you know on other systems are in external_ids on 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 only e, scr, and wait
  • 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 code defined for the meet’s sport, at its advertised distance (HMAR and MAR for 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 age or birth_date, never both
  • Nothing assumes the URL will be fetched more than once

Registration that wants results back, in addition

  • Each publish destination has a stable id, a human label, and a token
  • deliver and content are set deliberately, knowing deliver defaults to final
  • Finds its meet, events, teams, and athletes in a results document by external_ids under its own system id, and its entries through their athlete and event
  • Each destination’s token is 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 POST of one results document with bearer auth; 401 on a token it does not recognise, 410 on 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, and Content-Digest, and exposes Retry-After
  • The url in your destination answers directly, not through a redirect
  • Reads omx.version before validating, and answers 422 naming both versions to one it does not implement, with accepts in the body
  • Answers 422 to a document whose kind is not results, or whose update it does not know
  • Removes null values before validating; answers 400 or 422, with errors, to a document it cannot accept
  • Shows its user a destination card to give the timer
  • May answer the probe: GET on the destination URL with the token gets 200 with accepts and generator, or 503 while not ready; a refused token gets 401 or 410
  • Issues tokens with at least 128 bits of randomness
  • Keys each document on omx.generator.id and meet.id
  • Applies a newer omx.updated_at, treats an equal one as a no-op, and refuses an older one with 409, comparing instants rather than text, whatever the document’s update
  • Applies a full document by replacing everything held for the meet; the same document twice is a no-op
  • Applies a partial document by replacing only the rounds it carries and the team score blocks it carries by their id, adding or updating the records it carries by id, and keeping everything else
  • Answers 412 to a partial it cannot apply, until a full arrives
  • Creates a meet it has never seen, without Registration existing
  • Reads meet.origin rather than inferring provenance from missing ids
  • Joins records by an id in external_ids under 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, a started round as running with its s entries on the course, and an unknown status as not official
  • Compares marks on result.value, never on display, and shows a mark in a unit it does not know by its display
  • 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 from status
  • Ignores fields it does not know
  • Treats null as absent, and an absent result on 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 nr there as NR
  • Never lets one Meet Operations product’s document replace another’s: a document for its meet under a new generator.id or meet.id is 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, sets meet.origin honestly, writes its own ids in id, and carries Registration’s id unchanged in external_ids, under Registration’s system id, on every meet, event, team, and athlete that came from Registration
  • Writes back every external_ids entry it received on the meet, events, teams, and athletes
  • Writes no external_ids on 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, and updated_at on every results document
  • Writes one entry per round, with round, and exactly one of status and overall_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 content asks for start_lists
  • Marks the round whose places are the event’s result with final: true, and gives every round its status, cancelled included
  • Writes a multi-event at both levels, with every component event contested
  • Writes the gun time in result, any net time in net_result, and place_by on 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 points on each entry, a displacer’s included, one value in every block that lists it; gives each team score block an id that stays the same; lists each standing’s entries by entry_id and role; 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, or publish, and no other product’s extra fields
  • Writes each round of a partial document 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 full first from any copy of the meet and after any removal
  • Respects each destination’s deliver: full only for final, and a live update at most every few seconds while a round runs
  • Every handoff is one document that validates against results-document-v1, sent uncompressed or with Content-Encoding: gzip
  • Every handoff carries omx.updated_at to the millisecond, moved forward by any change and never backwards
  • Retries 429, 5xx, and network failures with backoff; never retries 400, 401, 404, 409, 410, 413, 422, or a 4xx the table does not list
  • Sends full next when a Publisher answers 412
  • May probe a destination when the timer adds it, tells the timer what it learned, and goes by a probe’s accepts over the destination’s; a destination that does not answer is not an error
  • Publishes stored values in result.value, with display alongside
  • Omits what it does not know; never writes null
  • Never re-encodes a status, and writes no NT, NH, or NM into a status (they are how nr prints)
  • Offers the timer a handoff cadence, with the destination’s deliver preselected
  • 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].