For contributors

API reference

Three surfaces, one implementation — the REST core, the /ff/ client surface, and the deprecated XML-RPC adapter.

The machine-readable description is at /openapi.json (OpenAPI 3.1), rendered interactively at /api/docs. This page is the orientation; that document is the contract.

Three surfaces

Surface Path Status
REST core /api/v1/… Current. Target this for new code.
Client surface /ff/… Supported. What the installed mofplus client speaks.
XML-RPC /API/user/xmlrpc, /API/admin/xmlrpc Deprecated, with a published sunset date.

All three call the same functions. The two older surfaces contain no logic of their own, which is what keeps them from drifting away from the REST core.

Both XML-RPC endpoints are public contracts — including the admin one, which has consumers outside the maintaining group. Neither is an internal convenience that can be withdrawn quietly.

Authentication

Reads on the REST core need no credential. Search, record pages, downloads and metadata are open.

Anything that writes, and everything under /ff/ and /API/, needs one of:

  • Authorization: Bearer mofpp_<prefix>_<secret> — a scoped token. Preferred.
  • Authorization: Basic <user:password> — the legacy bridge. XML-RPC clients reach this automatically, because credentials embedded in the URL are sent as a Basic header.

Scopes are the three tiers: user, weaver, admin. admin does not imply weaver — a curator who also deposits holds both explicitly, so that "what can this credential reach" is answerable by reading one field.

Mint a token: POST /api/v1/tokens, authenticating with anything the server accepts, including a legacy password. That is the bridge, and it is the supported way to migrate without a flag day.

REST core

Method Path Purpose
GET /api/v1/nets Search nets: name, coordination, exclusive, spacegroup_number, plane_group_number, pqrs, include_catenated, order
POST /api/v1/nets/identify Which net is this? Upload a .mfpx or .cgd; answers by canonical Systre key
POST /api/v1/nets/search-cs Search by coordination sequence and vertex symbol
GET /api/v1/nets/{name} One net
GET /api/v1/building-blocks Search building blocks
GET /api/v1/building-blocks/{name} One building block
GET /api/v1/frameworks Search frameworks
GET /api/v1/frameworks/{name} One framework and its structures
GET /api/v1/force-fields Force fields
GET /api/v1/force-fields/{name} One force field and its fits
GET /api/v1/force-fields/{name}/fits/{id}/params/{ric} Parameters
GET /api/v1/fragments Fragments, optionally filtered by atom type
POST /api/v1/tokens Mint a token
GET /api/v1/whoami What the presented credential can do

ric is one of bnd, ang, dih, oop, cha, vdw.

Nets

Everything in this section holds for the REST listing, the REST record, and the HTML pages at /data/nets and /net/{name} alike: they call the same service functions, so they cannot answer differently.

All 3077 nets are published; two licences, not one

Read this before you write a client. The collection is 3077 net records, loaded from the RCSR Systre archive and from MOF+ with full provenance, all of them served, under a licence that is split by source:

Records Source licence licence_source published A fetch answers
2438 mofplus or rcsr+mofplus CC-BY-4.0 the project decision true 200
634 rcsr RCSR-2019 the RCSR agreement true 200
5 null (seeded) CC-BY-4.0 null true 200

So an unfiltered GET /api/v1/nets answers "total": 3077 and "held_pending_licence": 0.

The MOF+ half is licensed by the project's own decision. MOF+ is this group's own service, so those nets are the project's own data to license, and the project licenses its data CC BY 4.0 — recorded in CONTEXT §6 and quoted in full in each record's licence_source. It is not a grant found in the source data, and the record says as much ("No third-party grant is claimed").

The RCSR half was held unpublished until 2026-09-09, when the repository owner confirmed the terms: those nets are © 2019 RCSR.net, redistributed by MOF++ under a private agreement with the rights holder, and RCSR-2019 is deliberately not a Creative Commons identifier — the agreement permits redistribution by MOF++ and grants a reader no onward licence, so the identifier carries open: false. See docs/reports/net-ingest-2026-09-09.md §3 in the repository for the resolution and what it does and does not cover.

The held mechanism itself is retained and still tested: a deposit whose provenance the licence decision does not recognise is licensed nothing and answers 403, not 404 — the identifier resolves, and the record states what is withheld. No record is in that state today, so every held_pending_licence below is 0 on this collection; the field stays in the envelope because the next ingest can put a record there. Held is a state of the collection, not of your credential: no token, scope or account changes it. The position is set out on the Licensing page.

Ordering

order selects the result order of GET /api/v1/nets.

order Result order
rcsr (default) rcsr_index ascending; nets with no RCSR symbol after the whole indexed block, by name
name Net name, ascending
complexity complexity_rank ascending — the ingest's simplest-first processing order; nets with no rank last, by name

Name is the tie-break in all three, so pages are stable. An unrecognised value is not an error: it is replaced by the default, and the order actually used is echoed in the response's order field, so a client never has to assume the default has not moved. It has moved once already — the listing sorted by name before rcsr_index existed.

rcsr_index is derived here, not published by RCSR: it is the position of the symbol in our sort of the archive's symbols. Every representation carrying the number also carries rcsr_index_provenance and a sentence saying so. Do not present it as RCSR's numbering — The two orderings a net carries is the whole rule.

POST /api/v1/nets/search-cs takes no order; it always answers in the default RCSR-index order.

The listing envelope

{"total": 3077, "limit": 50, "offset": 0, "order": "rcsr",
 "excluded_for_missing_data": 0, "excluded_for_unknown_catenation": 0,
 "held_pending_licence": 0, "results": [ ... ]}
  • total — matching records that are listed, which means published ones.
  • excluded_for_missing_data — records dropped only because a field the query filtered on (spacegroup_number, plane_group_number, pqrs) is blank on them. Neither matched nor ruled out; not the same as "did not match". 271 records have no spacegroup and the same 271 no p or q, for the reasons in docs/reports/net-analysis-2026-09-12.md.
  • The two symmetry-group filters are separate, and each sets aside the other's whole population. spacegroup_number is an ITA space-group number (1–230) and matches 3-periodic nets; plane_group_number is an ITA plane-group number (1–17) and matches the 200 nets here that are 2-periodic. No record carries both, and neither number is ever written into the other's field — which is the point of the separation: 11 means P21/m in one numbering and p4mm in the other, and a client reading spacegroup_number is reading an ITA space-group number or null, on every record, without first having to ask what dimension the net is. So of the 271 records a spacegroup_number filter sets aside, 200 are 2-periodic and are found by plane_group_number instead; the remaining 71 are the ones nothing resolved into a single net (52), the ones Systre refused or could not finish (18) and one with no graph to analyse.
  • spacegroup_number matches the number on the record, and for a chiral net that number is one of a pair. A quotient graph carries no handedness, so the computation fixes the group only up to its enantiomorphic pair: 202 records carry field_sources.spacegroup_handedness saying which pair, and a search for the other member of that pair will not return them. They are not counted in excluded_for_missing_data either — the field is not blank. Read the handedness note before treating the number as exact.
  • excluded_for_unknown_catenation — records dropped only because include_catenated=false was asked for and their catenation is unrecorded: they cannot be certified un-catenated and are not returned, but they were not judged catenated either. 0 whenever the query did not filter on catenation. Neither source states catenation — the RCSR archive has no such field and the MOF+ -c marker is a name, not a measurement — which is why the column is three-valued and why this count exists. It is 0 today because this project now computes the answer on every record that has a graph, and it stays in the envelope because a record can still arrive with none.
  • held_pending_licence — records that matched but are held. Never listed; each resolves at its own URI with 403. A client that reports total alone reports a smaller collection than exists.

held_pending_licence counts only what your filters could have judged, and a held record is judgeable on very little. It is counted against name, coordination and catenation, and not against spacegroup_number or pqrs: judging a held record by a value we decline to serve would serve it, one bit at a time — a count that moved with a spacegroup filter would give up the withheld number in 230 questions. The surfaces that report the count say so, rather than claiming a match on a field the count never applied.

Beyond that, a record held for want of a licence typically came from one source alone and has little to judge: no vertices, no coordination number, no recorded catenation. Any filter that touches those excludes it before the count is taken. So a zero means "no held record was ruled out by this query", not "nothing is being withheld"; only an unfiltered or name-filtered listing shows the real number. On this collection every row is published, so the numbers, run against the service as it stands, are:

Query total held_pending_licence
(none) 3077 0
name=ac 20 0
coordination=6 510 0
include_catenated=false 3023 0 (and excluded_for_unknown_catenation 0)
spacegroup_number=221 86 0 (and excluded_for_missing_data 271, of which 200 are 2-periodic)
plane_group_number=17 46 0 (and excluded_for_missing_data 2877)

order and the three counts are null on collections that have no such state (building blocks, frameworks). POST /api/v1/nets/search-cs answers a smaller envelope, whose results is a list of names — and which carries, beside them, what the match means:

{"total": 3, "results": ["pcu", "..."], "held_pending_licence": 0,
 "matched": [{"name": "pcu", "sequence_source": "stored", "basis": null}, "..."],
 "matched_on": "coordination sequences and vertex symbols",
 "establishes": "These nets carry the coordination sequences asked for. That is not an identification: …",
 "identity_endpoint": "/api/v1/nets/identify",
 "sequence_sources": {"stored": 2, "computed": 1},
 "excluded_unreadable_sequences": 0, "excluded_not_vertex_symbols": 0,
 "nets_without_sequences": 0, "computed_note": "…"}

Three things about that search are worth stating outright:

  • It is not an identification. Two different nets can carry the same coordination sequences, and the search compares only as many shells as were given. establishes says so and identity_endpoint points at the question that is answered yes or no.
  • A requested sequence gets a vertex class of its own. n requested sequences require n distinct classes, so asking for the same sequence twice asks for two kinds of vertex and is answered as such. It used to be a per-sequence existence test, which one vertex class could satisfy for every request at once. The same now holds for vertex symbols.
  • The sequences searched are not only the source's. A record that arrived with a MOF+ deposit is matched on the sequences the source supplied, as the legacy contract has always returned; the 634 records from the RCSR archive alone carry none — the archive has no such field — and are matched on the sequences computed from their own graph. Before this the form silently covered 2443 of 3077 records. sequence_sources counts which warrant each match rests on, and matched says it per record.

Provenance and ordering on every net

Every net representation — list row, full record, and held record — carries two blocks, always with all their keys present. A record with no tracked source says so with nulls rather than by omitting them.

"provenance": {"source": "rcsr+mofplus", "source_id": "aab",
               "retrieved": "2026-09-08T18:09:42.545747",
               "licence_source": "MOF++ project decision, CONTEXT §6 (Licensing): ...",
               "field_sources": {"cella": "mofplus", "systre_key": "rcsr",
                                 "catenated": "computed:mofpp.periodicity-v1 (mofplus-deposit)",
                                 "catenated_name_heuristic": "No; agrees with the computed value",
                                 "spacegroup": "computed:systre-19.6.0 (mofplus-deposit)",
                                 "p": "computed:systre-19.6.0 (mofplus-deposit)",
                                 "licence": "mofpp-project-decision (CONTEXT §6)",
                                 "retrieved_utc": "2026-09-08T18:09:42.545747+00:00",
                                 "name_match": "corroborated-by-systre-key",
                                 "cs_corroboration": "checked: ...",
                                 "cs_row_check": "corroborated: 96 of 96 vertex rows agree ...",
                                 "vs_corroboration": "corroborated: 96 of 96 vertex rows agree ...",
                                 "...": "..."}},
"ordering":   {"rcsr_index": 1,
               "rcsr_index_provenance": "derived",
               "rcsr_index_absence": null,
               "rcsr_index_note": "Derived by this site, not published by RCSR: ...",
               "complexity_rank": 2131}
Field Meaning
provenance.source rcsr, mofplus, rcsr+mofplus, or null for a seeded demonstration record
provenance.source_id That source's own identifier for the record
provenance.retrieved ISO 8601 timestamp of the retrieval, null if never. No offset: the column is naive UTC, and the offset is served separately in field_sources.retrieved_utc
provenance.licence_source The basis for the licence. The project decision on MOF+-sourced records; null on RCSR-only records, and null on the seeded ones
provenance.field_sources Per-field attribution — which source each stored value came from, plus the flags below. Full records only
ordering.rcsr_index Position in the derived RCSR ordering. null means only that this row carries no index — not that the net has no RCSR symbol; rcsr_index_absence says which
ordering.rcsr_index_provenance derived or authoritative. Every value here is derived
ordering.rcsr_index_absence null when an index is present; otherwise why it is blank: outside-archive (the name is not an RCSR symbol), unrecorded (it is a symbol, but no index is stored on this row), unknown (the local archive could not be read). Every blank in the collection today is outside-archive — 147 of them; no record is unrecorded
ordering.rcsr_index_note The sentence that travels with the field — the derivation rule when it holds a number, the reason for the blank when it does not — so neither can be quoted out of context
ordering.complexity_rank Ingest processing order, simplest first. Not a quality or difficulty ranking. 1–2443 are ranked on cell contents; 2444–3077 are unkeyed (the RCSR-only block, which supplies the ranking nothing)
ordering.complexity_basis The level, values and key the rank was computed from. Full records only
catenated true, false, or null. Three states, not two — see below
published Whether this record is served in full. false means held — see below
licence An SPDX-style identifier: CC-BY-4.0 on 2443 records, RCSR-2019 on the 634 RCSR-only ones. The second is deliberately not a Creative Commons code — it permits redistribution by MOF++ and grants a reader no onward licence, and carries open: false

List rows omit field_sources, complexity_basis, vertices, geometry and files; fetch the record for those.

catenated: null means unknown, never "no"

The column is nullable and all three states are real. Today: 54 true, 3023 false, 0 null — but do not hard-code that last figure. null is a supported state and means no answer could be given, not "no"; treating it as false overstates what is known.

Neither source states catenation. The RCSR archive has no catenation field at all, and MOF+ carries a -c marker in a name rather than a measurement. That is why the column is three-valued, and it is still true: what changed is that this project now computes the answer from the graph, so the state a source left unanswerable is answered here on all but five records.

The value is now a measurement, not a name heuristic. On 3071 records it is computed from the net's graph — the number of interpenetrating copies the quotient graph's cycle lattice unfolds into — and field_sources.catenated reads computed:mofpp.periodicity-v<VERSION> (<basis>) accordingly. The heuristic that used to supply it (the name contains -c) is preserved beside it as field_sources.catenated_name_heuristic on 2433 records, reading "<Yes|No>; agrees|disagrees with the computed value". It disagrees on eight, each a -c record whose deposited topology holds a single copy of the parent net.

Five records keep the heuristic's value — four stacks of layers (sql-c, sql-c6, hcb-c, hcb-c3) and one bundle of chains (lcw) — where the computation refuses to decide, because a net that is not made of 3-periodic copies has no copies to count and entanglement in space is not something a graph records. Their column is not blank: it still holds what the name heuristic read, field_sources.catenated still reads mofplus:name-heuristic, and catenated_note says in words that the value shown is that heuristic and that the measurement can neither confirm nor contradict it. So include_catenated=false returns lcw as recorded, and excluded_for_unknown_catenation counts it nowhere: the count is a fact about the column, and the note is where the strength of the value is stated.

catenated_note travels beside the value on records and on list rows, and qualifies whichever state a row is in — including a new one for a computed value and one for a computed non-answer. Show it wherever you show the value; the value alone reads as more certain than it is.

Flags a client will find in field_sources

Beyond the per-field source names, field_sources carries these. They are on the row, not only in an internal report, so a client can see what was checked:

Key On Meaning
retrieved_utc 2438 The retrieval timestamp with its +00:00 offset, which the naive retrieved column cannot hold
name_match 3076 Four values since the topology became computed. corroborated-by-systre-key (2292) — the canonical key computed from the MOF+ deposit equals the RCSR archive key, so the two sources hold the same net under that name; conflicting-systre-key (1, skl-a) — they do not, and neither source was edited to agree (the record carries a settled identity curation call saying what was decided about it); uncorroborable (3) — the check could not run, with name_match_reason saying why (all three are Systre timeouts); single-source (780) — only one source holds this net, so there is nothing to corroborate
cs_corroboration 2419 The class-to-sequence mapping was checked against the net's own topology. Its absence on a record with coordination sequences would mean no check was made — a different fact from "nothing disagreed"
cs_topology_mismatch 7 The stored sequences disagreed with the walk for the named classes
cs_class_mapping 7 The corrected mapping, class → the sequence the source stored positionally
cs_unresolved 0 A class whose sequence could not be attributed without guessing; its cs is null rather than wrong
mofplus_coverage 634 On RCSR-only records: MOF+ does not list this symbol at all, so no counterpart is expected
licence 3077 Which rights holder: mofpp-project-decision (CONTEXT §6) on 2438, rcsr-agreement (© 2019 RCSR.net) on 634, the seed's own sentence on 5
spacegroup, spacegroup_number 2806 each computed:systre-19.6.0 (<basis>) — the column was computed from the net's graph, not read from a source
plane_group, plane_group_number 200 each The same, for the 2-periodic nets. Decided by the same rule behind a gate of 2 instead of 3, so a record never carries both pairs
p, q 2806 each as above. r and s have no such key: nothing computes them
catenated 3077 computed:mofpp.periodicity-v<VERSION> (<basis>) on 3071; the older mofplus:name-heuristic on the rest
catenated_name_heuristic 2433 What the -c name rule said, kept beside the measurement, and whether the two agree
spacegroup_handedness 202 The written group is one of the eleven enantiomorphic pairs; the note names the pair and says which basis's lattice the symbol follows. Not a defect and not a conflict
rcsr_identification 116 Systre recognises this net's topology as an RCSR net under a different symbol than the record's own name. Recorded, never written into systre_key
vs_corroboration, vs_topology_mismatch 2437, 1 Whether the stored vertex symbols agree with the symbols recomputed from the graph, and which rows do not (dum, 6 rows). Two vocabularies live under this key. 2432 records carry the apply step's sentence; 5 seeded rows (pcu, pyr, qom, tbo, pcu-c) still carry the ingest's, because they store no vertex symbol to compare — four of them do have a usable rings analysis, and pcu-c has no graph at all. That retained sentence asserts that no ring perception is implemented in this project, which is no longer true; the record serves topology.checks.vs_corroboration_superseded beside it saying so, and a client rendering the key should render that too
cs_row_check 2423 The same for the stored coordination sequences, compared on the stored length
<column>_check 3 rows, 8 keys On a seeded row whose curated value was kept: whether it agrees with the computed one. pcu (spacegroup, spacegroup_number, p, q), pyr and qom (spacegroup, spacegroup_number). A value the apply step wrote itself is not a curation and gets no such key — it is identified by its computed: provenance, rewritten on every run and never frozen

cs itself is attributed mofplus:cs-class-mapping-resolved-by-topology instead of a flat mofplus on the seven records whose class mapping had to be corrected. In every case the sequences are the source's own values, byte for byte — only the question of which vertex class each one describes was settled here. The hazard and the method are explained in The two orderings a net carries.

systre — the canonical key

A full record carries a systre block. The key is on 2930 rows (every RCSR symbol) and is null on the 147 with no symbol.

"systre": {"key": "3 1 2 0 0 0 1 3 0 0 0 ...",
           "checksum": "d9542982e79b231ae96c7ed340001140",
           "provenance": "rcsr", "checksum_provenance": "rcsr",
           "absence": null, "computed_absence": null,
           "note": "The canonical Systre key, copied verbatim from ..."}
  • This field is archive text and nothing else is written into it. The key here is RCSR's, copied verbatim; the checksum is the archive's own, copied from the same block so that this copy can be audited against the upstream file — it is not computed or checked here either.
  • A key is computed, and it is served elsewhere. Systre 19.6.0 runs in this project, and the canonical key it computes from the MOF+ deposit is served under topology.symmetry.<basis>.systre_key, with key_matches_archive recording the comparison against the field above. It is deliberately not merged into this field: a computed key is our statement and this column is RCSR's, and merging them would destroy the comparison. On a MOF+-only record systre_key therefore stays null even where a computed key exists.
  • absence says why the key is blank when it is: outside-archive on all 147 today (the name is not an RCSR symbol, and MOF+ supplies no canonical key at all). A blank is an unknown key, never an empty one.
  • computed_absence says why topology…systre_key is null when it is, and it is the same discipline applied to this project's own work: not-run (no Systre analysis is stored), not-ok (every stored run refused, failed or hit the 900 s cap — runs names each basis, status and reason), or components (a run ended ok on a structure Systre resolved into several connected nets, each of which has a key, so the row has none). null when a key was computed. The third is a result, not a shortfall, and the note distinguishes all three: a refusal and a time limit are answers about the run, never the finding that this net has no canonical form.
  • The note is worded by what the run was given. A key computed from the deposited graph is an independent second statement of the canonical form and can disagree; a key computed from the archive text is a round trip through Systre, which checks that the copy is a well-formed canonical key and is not a second source. 636 records have only the second kind, and the note says so rather than claiming two independent statements agree.
  • List rows do not carry it. Fetch the record; a 500-row page of Systre keys is not a listing.

On a held record the key is the whole of the substance, and it is withheld. An RCSR-only row has no cell, no vertices and no coordination sequence — the archive carries none — so the canonical key and its checksum are the only real data it holds, and the terms that could not be verified are precisely that archive's. So a 403 body carries the block in a different shape, saying that much rather than pretending there is nothing behind the hold:

"systre": {"key": null, "checksum": null,
           "withheld": true, "key_on_record": true,
           "note": "This record carries a canonical Systre key and it is withheld ..."}

key_on_record distinguishes "held, and there is a key behind it" from "held, and there is no key either" — which for RCSR-only records is always the former. systre_key is also named in the withheld list.

coordination_sequences — how far the sequences can be trusted

A full record carries a block saying what is known about its coordination sequences, because a reassigned sequence and a corroborated one used to be presented identically. Sequence values are on the vertices; this block is their provenance.

"coordination_sequences": {"state": "reassigned",
                           "note": "The sequences below are the values the source supplied ...",
                           "provenance": "mofplus:cs-class-mapping-resolved-by-topology",
                           "corroboration": "checked: one walk ... (2 class(es) disagreed)",
                           "topology_mismatch": "class(es) [0, 1]: ...",
                           "class_mapping": "...", "unresolved": null}

state is the field to branch on. Its six values, and how many records are in each today:

state Records Meaning
corroborated 2412 The source's class-to-sequence mapping was checked against a walk over the net's own periodic topology, and agreed
reassigned 7 It disagreed. Each sequence was attached to the class whose walk it matches — the source's own values, in a different order. class_mapping gives the permutation
not-recorded 653 The source stores no sequence: the 634 RCSR-only records, and 19 deposited nets whose upstream record has none
unattributed 5 Not written by the provenance-tracked ingest (the seeded rows); sequences shown as stored, unchecked
uncorroborated 0 Sequences present, but the mapping was never checked
withheld 0 A class whose sequence could not be attributed without guessing. Its cs is null rather than wrong; unresolved names the classes

In every state the sequences are the source's own values byte for byte. Nothing here computes a sequence into a record, extends one, repairs one, or replaces one with a walked one. Why the check is needed at all — the mapping is invisible to the obvious cs[0] == coordination_number test whenever two classes share a coordination number, which is 44% of the deposited records — is in The two orderings a net carries.

topology — what was computed from the net itself

A full record carries a topology block: what this project computed from the net's own graph, as opposed to everything else on the record, which is what a source said. It is per basis and never merged, because a MOF+ deposit and an RCSR archive key are two different graphs that usually describe the same net and sometimes do not. The basis keys are mofplus-deposit and rcsr-archive-key.

Abridged, with the values dia actually serves (… marks keys left out of the excerpt, not keys that are absent):

"topology": {
  "note": "Computed here from the quotient graph of the net itself, not transcribed from either source: ...",
  "periodicity": {"mofplus-deposit": {
      "status": "ok", "reason": null, "dimension": 3, "periodicity": 3,
      "connected": true, "interpenetration": 1, "catenated": false,
      "undetermined_reason": null, "component_count": 1,
      "basis": "mofplus-deposit", "basis_note": "...",
      "tool": "mofpp.periodicity", "tool_version": "1", "seconds": 0.001,
      "input_checksum": "...", "computed": "2026-09-12T..."}},
  "symmetry":   {"mofplus-deposit": {
      "status": "ok", "space_group": "Fd-3m",
      "space_group_setting": "second origin choice", "space_group_number": 227,
      "plane_group_number": null, "point_group_order": 48,
      "node_kinds": 1, "edge_kinds": 1,
      "repeat_unit_accurate": false, "repeat_unit_factor": 4,
      "multiplicity": 1, "degrees_of_freedom": 1,
      "rcsr_identification": "dia",
      "systre_key": "3 1 2 0 0 0 1 2 0 0 1 1 2 0 1 0 1 2 1 0 0",
      "key_matches_archive": true, "key_matches_archive_reason": null,
      "td10": 981, "describes_one_net": true,
      "scope": "Systre found one connected net in the repeat unit, ...",
      "connected": true, "component_count": 1, "components": [ ... ],
      "warnings": [], "...": "..."}},
  "invariants": {"mofplus-deposit": {
      "status": "ok", "shells": 10, "td10": 981.0, "td10_reason": null,
      "genus": 3, "genus_reason": null,
      "repeat_unit_factor": 4, "repeat_unit_factor_source": "...", "...": "..."}},
  "rings":      {"mofplus-deposit": {
      "status": "ok", "max_ring": 40, "net_point_symbol": "(6^6)",
      "net_point_symbol_reason": null, "capped": false,
      "classes": [{"vertices": [0, 1, 2, 3, 4, 5, 6, 7],
                   "cycle_symbol": "6(2).6(2).6(2).6(2).6(2).6(2)",
                   "vertex_symbol": "6(2).6(2).6(2).6(2).6(2).6(2)",
                   "point_symbol": "6^6",
                   "coordination_sequence": [4, 12, 24, 42, 64, "..."],
                   "capped": false, "reason": null}], "...": "..."}},
  "checks":     {"name_match": "corroborated-by-systre-key", "name_match_reason": null,
                 "vs_corroboration": "corroborated: 8 of 8 vertex rows agree with mofpp.rings v2 (mofplus-deposit).",
                 "vs_corroboration_superseded": null,
                 "vs_topology_mismatch": null, "cs_row_check": "corroborated: ...",
                 "catenated_name_heuristic": "No; agrees with the computed value",
                 "spacegroup_handedness": null, "rcsr_identification": null,
                 "conflicts": {}}
}

Three of those values are worth reading twice, because they are easy to mistake for a defect. repeat_unit_factor 4 says the deposited cell holds four minimal repeat units — the deposit is a supercell of the net, which is ordinary, and it is why the one ring class lists eight vertices and the corroborations count eight rows: dia's deposit has eight vertex rows where its archive key has two, and the classes above are the deposit's own. And on dia-c, whose deposit is two interpenetrated copies, describes_one_net is false, genus is null with a reason ("genus is defined for one connected 3-periodic net; this one is not …"), and the space group served is that of one copy — which is exactly why the spacegroup column is left untouched for such a record.

Rules a client should hold to:

  • Read note before the values. It distinguishes three different empty states that would otherwise look identical: this representation was built with no database session to ask; the analysis has not been run for this record; or it has, in which case the note names the kinds, the bases and the tool versions actually below it. A kind with no row is {}, which never means the net has no such property.
  • Every entry carries status (ok, refused, failed, timed-out) with a reason where it is not ok. A refusal is Systre's answer about the net — 17 deposits are non-crystallographic ladders or have colliding nodes — and a timeout is a budget that ran out. Neither is a value of zero or a null to be read as "none".
  • input_checksum is the checksum of the graph actually analysed, so a result computed before a deposit changed can be told from one computed after. Nothing rebuilds a graph when serving, so no entry claims to be current.
  • r and s are absent by design. The note says so in every populated block: they count faces and tiles of a natural tiling, and none is derived here. The limit is measured, not pending — the Gavrog library this site runs Systre from has no route from a 3-periodic net to a tiling at all, and the 200 2-periodic nets for which it has one tile the plane, whose transitivity is a triple and deliberately does not go into an r/s pair (docs/reports/tiling-spike-2026-09-12.md).
  • Symmetry has a scope. describes_one_net and scope say whether Systre found one connected net, one net repeated n times, or several different nets sharing a repeat unit. A space group served from the second or third case describes one copy or one component, not the whole, and the column-level spacegroup is deliberately not written in those cases at all.
  • checks is quoted, not recomputed. These are the sentences the apply step recorded on the row; every key is null on a record the apply step has not reached, which differs from a check that ran and found nothing.
  • topology is named in the withheld list of a held record.

Computed fields on a vertex row

Vertex rows of a record with a deposit gain four computed keys beside the source's own, deposit basis only — Vertex.idx is the deposit's vertex numbering, and matching an archive key's vertices to these rows by position would be a guess:

Key Meaning
vertex_symbol_computed The MOF+ convention (shortest cycle per angle) — directly comparable with the stored vs
vertex_symbol_superseded Present (non-null) only where a settled curation decision ruled the stored symbol wrong. The row then presents the computed symbol under vertex_symbol, the stored string is served verbatim under vertex_symbol_as_stored, and this sentence names the decision, the role that took it and the date. 6 rows of one record (dum)
ring_vertex_symbol_computed The RCSR convention (shortest ring per angle, * where none within the 40-edge cap). A different quantity, hence its own key
point_symbol_computed The Wells point symbol, 4^12.6^3 style
coordination_sequence_computed The sequence walked from the net's own graph
computed_absence Why a computed value is missing, when one is: the analysis was never run, it did not end ok, or it covers no vertex of this index

These are served for truncated and placeholder rows as readily as for whole ones — that is the repair — while the stored string stays under its own key, unchanged and unrepaired. The two never merge, and a computed value never appears under a source's key.

POST /api/v1/nets/identify — which net is this?

The one direction the site could not be asked in. Every other search takes a name or a property and answers with records; this takes a file and answers with a name.

curl -F file=@mynet.mfpx https://mofplus.org/api/v1/nets/identify

file is a .mfpx topology or a .cgd holding a PERIODIC_GRAPH block; a .cgd with several graphs also needs block=<ID>. A .cif or a plain coordinate file is not read: it states no connectivity, and deriving one from interatomic distances means choosing a cutoff, which would make the answer depend on a decision the caller did not make.

Identity here is the canonical Systre key, an invariant of the quotient graph. The same net in another cell, with the vertices numbered differently, or with every coordinate nudged, gives the same key; two different nets do not. So this is an identification, not a similarity score — and it identifies the net, the abstract periodic graph, not the material the file came from.

Four outcomes, each with its own status, so a client can branch on the status line as well as on outcome:

Status outcome What it means
200 match The key is one this collection holds. holders names the records and, for each, whether it matched the RCSR archive's own key or a key this project computed, and on which basis
404 no-match The key was computed and matches nothing here — a statement about this collection, not about the net. neighbours offers the nearest coordination sequences, as suggestions: a sequence match is not an identity
422 refused Systre will not key this graph, with its own systre_status (refused, failed, timed-out, unavailable) and reason. Deliberately not a 404, which would say this collection does not hold it
400 unreadable The file could not be read, or the request is outside the bounds. An answer about the file, not about any net
429 — The rate limit, with Retry-After

A match can name more than one record, and they are not the same claim. A holder with key_origin: "archive" is the RCSR symbol the key is published under — the name the caller asked for. A holder with key_origin: "computed" is a record whose own graph keys to this net, which may be a second name for it or a deposit that is not the net its name claims; the archive holder is listed first, and each record's own page says what is known about it.

Limits, and why. The upload is bounded at 4 MB before it is parsed, and the graph at 4000 vertices and 20 000 edges after parsing and before any process starts — a few hundred bytes of .cgd can name a graph on a million vertices, and Systre's cost grows with the graph. Each call starts a Systre process under the same 900 s per-structure clock the analysis runner uses, so the endpoint is rate-limited per client. The file is never stored: it is read into memory, its connectivity is taken, and the bytes are dropped when the request ends.

site_symmetry — what kind of vertex each vertex is

Every net's vertex rows carry a symmetry, and the block that says what it rests on travels with the vertex table, under coordination_sequences.site_symmetry:

"site_symmetry": {"recorded": 8, "vertices": 8,
                  "provenance": "computed:mofpp.site_symmetry-v1 (spglib-2.7.0, mofplus-deposit)",
                  "note": "The site symmetry on each vertex row is the point group of …",
                  "absence": null}

A vertex's site symmetry is the point group of the operations of the net's ideal space group that leave that vertex where it is — its Wyckoff position in the maximum-symmetry embedding. It is what the coordination number cannot tell you. dia and nbo are both four-coordinated at every vertex; dia's sites are -43m (tetrahedral) and nbo's are 4/mmm (square-planar), and no building block fits both.

  • It is a property of the net, not of any material built on it. It is computed from the net's own graph and read from no source: neither source states one.
  • recorded and vertices are counts, not a ratio to round: nothing is written unless every row of a record can be given a site, so recorded is either vertices or 0.
  • absence carries the reason when it is 0, and the reason is about this project's evidence and never about the net — a vertex always has a site in its net's own group. 2794 of 3077 records carry one (146 243 vertex rows); the 283 without are 200 2-periodic nets (whose symmetry is a plane group, and the tables used are the 230 space groups), 52 that resolved into more than one connected net, 21 Systre refused or could not finish, 4 whose barycentric placement puts distinct vertices on one point, 1 with no graph, and 8 whose rows a computed site could not be put on.
  • The symbol is a bare Hermann–Mauguin point group (-43m, 4/mmm, m-3m, 1), not the axis-oriented site-symmetry spelling (4/mm.m): it is computed as the stabiliser in the net's own group and named by spglib's point-group classification.

curation — what a curator has to decide

Beside topology, a full record carries a curation list. A curation call is a disagreement inside the record between what a source said and what was computed from that same source's deposit. Each call carries a state:

state Meaning
open Stated, not settled. Deciding it needs somebody who can go back to the source, and nothing on the record has been changed
settled Ruled on. The call carries decision.ruling, decision.taken_by, decision.taken_on and decision.consequence, and the ruling changes what this site presents — never what is stored

An open call and a settled one are the same structure: a settled call is what an open one becomes, so branch on state and not on which keys are present. The four decision fields are null while a call is open.

The list is empty on all but 10 of the 3077 records.

8 open calls, kind: "catenation" — dia-c3s, eta-c, eta-c4, qtz-c, qtz-c3, qtz-c4, qtz-c6, twt-c. Each is named with the MOF+ -c marker for an interpenetrated net, and the topology those same records deposit is a single net: the periodicity analysis counts one copy, and Systre reads each as one connected net and identifies it as the parent.

1 settled call, kind: "vertex-symbol" — dum. Its six 3-coordinated rows store 3.8(4).8(4) where mofpp.rings computes 3.8(2).8(2); the other six rows of the same record, in the same convention from the same graph, are reproduced exactly. On 2026-09-12 the repository owner ruled: the stored symbol is wrong; the computed symbol stands. The consequence is on the vertex rows — those six serve the computed symbol as vertex_symbol, the stored string as vertex_symbol_as_stored and the decision as vertex_symbol_superseded, and vertex_symbols.superseded states it once for the record. Nothing was written to the database: the MOF+ value is untouched, and re-reading the source will still find the wrong symbol there.

1 settled call, kind: "identity" — skl-a, the collection's only name_match: conflicting-systre-key. The canonical key computed from the MOF+ deposit (56 vertices, 120 edges, degrees 24×3, 24×5, 8×6) is not the key the RCSR archive carries under that name (56 vertices, 124 edges, 24×3, 16×5, 16×6); TD10 is 1195.29 against 745.71 and genus 65 against 69. A canonical key names a net uniquely, so these are two nets under one symbol. On 2026-09-12 the repository owner ruled: the identity of the symbol stays with the RCSR archive key; the deposited topology is a different net and is not served as the topology of this symbol. Two follow-ups are part of the ruling and neither is a local edit — ask MOF+ to re-deposit or rename that record, and report the missing loop-derived edges to RCSR. Both graphs stay on the record, each under its own basis in topology; curation[].record reports which basis the computed columns are attributed to as it stands when the record is served.

"curation": [
  {"kind": "catenation", "slug": "catenated-marker-without-measured-interpenetration",
   "state": "open", "headline": "The name marks this record as catenated; ...",
   "note": "A curation call is a disagreement inside this record that this site can state but must not settle. ...",
   "claim":    {"field": "catenated", "value": true, "stated": "Yes", "basis": "The MOF+ name heuristic: ...",
                "recorded_as": "catenated_name_heuristic",
                "recorded_sentence": "Yes; disagrees with the computed value"},
   "finding":  {"field": "catenated", "value": false, "stated": "No", "interpenetration": 1,
                "periodicity": 3, "connected": true, "basis": "mofplus-deposit", "basis_note": "...",
                "tool": "mofpp.periodicity", "tool_version": "1", "statement": "..."},
   "evidence": [{"check": "interpenetration", "statement": "...", "interpenetration": 1, "…": "…"},
                {"check": "systre-identification", "rcsr_identification": "qtz",
                 "describes_one_net": true, "statement": "...", "…": "…"}],
   "record":   {"column": "catenated", "value": false, "stated": "No",
                "attribution": "computed:mofpp.periodicity-v1 (mofplus-deposit)", "statement": "..."},
   "decision": {"state": "open", "question": "...", "options": ["...", "..."], "note": "...",
                "ruling": null, "taken_by": null, "taken_on": null, "consequence": null},
   "changed":  "Nothing was changed in response to this call. ..."},

  {"kind": "vertex-symbol", "slug": "stored-vertex-symbol-contradicted-by-the-computed-one",
   "state": "settled", "headline": "The stored vertex symbol disagrees with ... and the stored one was ruled wrong.",
   "note": "A curation call is a disagreement inside this record, derived when the record is served ...",
   "claim":    {"field": "vs", "value": "3.8(4).8(4)", "stated": "3.8(4).8(4)", "basis": "The MOF+ vertex symbol ...",
                "recorded_as": "vs_topology_mismatch",
                "recorded_sentence": "vertex idx 0, 1, 2, 3, 4, 5; 6 of 12 rows compared.",
                "vertices": [0, 1, 2, 3, 4, 5]},
   "finding":  {"field": "vs", "value": "3.8(2).8(2)", "stated": "3.8(2).8(2)",
                "disagree": 6, "agree": 6, "compared": 12, "basis": "mofplus-deposit", "basis_note": "...",
                "tool": "mofpp.rings", "tool_version": "2", "statement": "...",
                "rows": [{"vertex": 0, "stored": "3.8(4).8(4)", "computed": "3.8(2).8(2)"}, "…"]},
   "evidence": [{"check": "comparison", "agree": 6, "disagree": 6, "compared": 12, "statement": "...", "…": "…"},
                {"check": "agreeing-rows", "agree": 6, "statement": "The convention is not the disagreement: ...", "…": "…"},
                {"check": "decision-evidence", "tool": null, "statement": "The evidence the ruling rests on: ..."}],
   "record":   {"column": "vertices.vs", "value": "3.8(4).8(4)", "stated": "3.8(4).8(4)",
                "attribution": "mofplus", "statement": "The 6 rows ruled on present the computed symbol ..."},
   "decision": {"state": "settled", "question": "...", "options": ["...", "..."], "note": "Decided. ...",
                "ruling": "The stored symbol is wrong; the computed symbol stands.",
                "taken_by": "the repository owner", "taken_on": "2026-09-12",
                "consequence": "What follows for a reader: ..."},
   "changed":  "Nothing was written to any record. The decision changes what this site presents ..."}
]
  • Derived at serve time, stored nowhere. A call is assembled from field_sources and the analysis rows the record already carries. No column is written for it, and no record has been renamed, re-deposited, merged or withdrawn on the strength of one. changed says so on every call.
  • The rule reads the record, not a list of names. A catenation call is raised when the -c marker was recorded for the record and read yes, a periodicity analysis ended ok and counted the copies, and that count is
    1. A genuinely interpenetrated -c net (dia-c, 2 copies) raises none; nor does a record whose analysis states it cannot count copies (the four layer deposits and lcw), nor one nobody has analysed. A vertex-symbol call is raised when the ring analysis of the deposit contradicts a stored symbol, row by row, comparing like with like. An identity call is raised when a Systre run computed a canonical key and it differs from the archive key the record carries. A record that stops meeting a rule stops carrying the call at its next request.
  • A ruling is matched to the question it answered, never to a record: the kind, the basis, the tool, the exact pair of strings in dispute and — where the ruling names one — the input_checksum of the graph that was analysed all have to be the ones ruled on. For the identity ruling the disputed pair is the two canonical keys themselves. A different record showing a different disagreement, or this record after a new deposit or a changed archive key, gets an open call rather than inheriting the decision.
  • Every evidence entry says whether this site measured it. computed_here is true for a comparison made from the record's own stored analyses and false for the decision-evidence entries, which are what a ruling rests on and were established elsewhere; those carry no tool and no basis.
  • record is not the claim and not the finding. It is what the columns hold today: on the eight, catenated is false, written by the apply step from the measurement, with the superseded marker kept under topology.checks.catenated_name_heuristic; on dum, vertices.vs still holds the string MOF+ supplied.
  • An empty list is not a clean bill of health. It says no rule fired, which on an unanalysed record is mostly a statement about the analysis.
  • curation is named in the withheld list of a held record, for the reason topology is: a call quotes the measured values in words.

403 — held pending licence verification

No record is in this state today — the RCSR terms were confirmed on 2026-09-09 and all 3077 are served. The state and its tests remain, because the next deposit whose provenance the licence decision does not recognise lands in it. A held record's URI resolves. It answers 403 with a body that states what it is, rather than 404 (which would deny that a minted, stable identifier exists) or 200 (which would present a withheld record as a served one).

{"name": "example", "uri": "https://.../net/example", "type": "net",
 "published": false,
 "status": "held-pending-licence-verification",
 "detail": "This record exists and is identified, but its licence could not be verified ...",
 "licence": "unverified",
 "provenance": {"source": "...", "field_sources": { ... },
                "field_sources_withheld": ["spacegroup_handedness", "vs_corroboration", "..."]},
 "ordering": { ... },
 "systre": {"key": null, "withheld": true, "key_on_record": true, "note": "..."},
 "withheld": ["spacegroup", "plane_group", "transitivity", "cell", "natoms",
              "catenated", "vertices", "geometry", "files", "systre_key", "topology",
              "curation"]}

Three things about that body are worth knowing.

detail says the licence "could not be verified against either source", which is the wording for a record in the state, not a claim about the collection. This is exactly why the rule below says branch on status and not on the prose.

provenance.field_sources is filtered on a held record, and field_sources_withheld names what was dropped. Only the entries that say where a value came from are served; an entry that says what a value is, or what comparing two values showed, is withheld with the value itself. The analysis writes such entries — spacegroup_handedness names the space group in words, catenated_name_heuristic names the catenation, vs_corroboration counts the vertex rows — and a hold that stopped at the columns would publish them in prose at the status code that declines to serve them.

How much of the rest of withheld is null anyway depends on the record: a row built from the RCSR archive alone has no cell, no coordinates and no coordination sequence, so the entries that cost a reader something are systre_key and topology. The systre block says so directly, and key_on_record tells you whether there is a key behind the hold at all.

What a client should do with it:

  • Branch on status, not on the prose in detail. held-pending-licence-verification is the machine-readable state; withheld names the fields a published record would have carried, so you can show the shape of the hole instead of an empty record.
  • Treat it as "exists, content withheld", not "not found". Keep the identifier. It is stable and will resolve to the full record when the RCSR archive's terms are confirmed.
  • Do not retry, and do not authenticate. This 403 is a standing state, not a rate limit and not an auth failure.
  • Use held_pending_licence from a listing rather than probing names one at a time — but read the catenation caveat above, which can zero that count.

GET /net/{name}.mfpx and .xyz on a held record answer 403 with {"detail": "net 'aca': ..."} — no deposit is served without a licence. The HTML record page renders the same state at the same status code.

The deprecated XML-RPC surface cannot express any of this: get_net, get_cs and get_vs raise fault 1 (not found) for a held net, and get_list_of_nets and search_cs simply omit them. That is a limitation of the old contract and one more reason to target the REST core.

Record URIs and content negotiation

Every record answers at one address in more than one representation:

GET /net/pcu                                 → HTML
GET /net/pcu     Accept: application/json    → JSON
GET /net/pcu.json                            → JSON
GET /net/pcu.mfpx                            → the topology file
GET /net/pcu.xyz                             → coordinates

The suffix form exists for callers that cannot set headers. Both are the same identifier.

.mfpx answers on 2876 of the 3077 records, and not all of them the same way:

Records What .mfpx is How to tell
2442 the file a contributor deposited, served verbatim provenance.field_sources.topofile is mofplus
434 generated on request from the net's computed maximum-symmetry embedding response header X-MOFpp-Geometry: computed-embedding, # lines in the file saying so, and geometry.computed in the JSON
200 nothing: 422 with the reason — the net is 2-periodic, the format's # cell line states six parameters and a layer has three geometry.dimension is 2
1 404 — the seeded pcu-c row, which has no geometry on any basis geometry is null

A generated file is not a measurement: every edge is the same length, the cell is the ideal cell of the net's symmetry group and the mean edge is scaled to 1. Do not read its cell as a lattice in ångström. What it states exactly is the topology — which vertices, which are joined, and across which cell boundary. docs/reference/file-formats.md sets out the three ways a client can tell a generated file from a deposit.

The 422 is deliberate and is not a 404: the geometry of those 200 records exists, is computed, and is served in full in the .json at the same URI. A 404 would deny it.

.xyz is available on no net: this loader deposits only the canonical .mfpx and writes no .xyz, so the suffix answers 404 on a published net (and 403 on a held one, which is judged before the file is looked for). Do not build a client that expects .xyz to appear for nets.

geometry in a net's JSON

The same object the viewer on the page draws, so the picture and the machine-readable answer cannot disagree. Full records only.

"geometry": {"dimension": 3,
             "cellparams": [2.3094, 2.3094, 2.3094, 90.0, 90.0, 90.0],
             "cell": [[2.3094, 0.0, 0.0], [0.0, 2.3094, 0.0], [0.0, 0.0, 2.3094]],
             "cell_note": null,
             "computed": "This geometry is computed, not deposited. …",
             "boundary_edges": 12,
             "vertices": [{"idx": 0, "coord": [0.125, 0.125, 0.625],
                           "coordination_number": 4}, "..."],
             "edges": [{"idx": 0, "i": 0, "j": 2, "image": [0, 0, 0],
                        "crosses_boundary": false}, "..."]}
  • dimension is 3 or 2 and everything else follows it. cell is that many Cartesian lattice vectors of that many components, cellparams the six or three parameters they were built from (a, b, c, alpha, beta, gamma or a, b, gamma), and each vertex coordinate and each edge image carries that many numbers. Read dimension first. A 2-periodic net is served as 2-periodic rather than padded into three with an axis nothing measured, and cell_note then carries the sentence saying so.
  • computed is the sentence saying this geometry was computed rather than deposited, and null on a deposit. It is the field to branch on.
  • image is the cell offset of the edge's far end, which is what makes a boundary-crossing edge drawable at all rather than a long line to the wrong vertex.
  • geometry is null where the record has no geometry on any basis — one row in this collection.

The /ff/ surface

Shapes are fixed by the installed client and will not change while it is supported.

GET  /ff/gen/check
GET  /ff/gen/{FF,FFrefs,FFfits,atypes,FFfrags,special_atypes}
GET  /ff/gen/params/{ric}/{fitid}
GET  /ff/gen/{refs4frags,refs4atypes}
GET  /ff/gen/{topoqeq_fit,topoqeq_par}
GET  /ff/frags/frags
GET  /ff/get_file/{fname}
POST /ff/gen/FFfits
POST /ff/gen/set_params/{ric}
POST /ff/gen/del_params/{ric}
POST /ff/frags/upload

Two quirks are preserved deliberately, because the client depends on them:

  • POST payloads travel in the query string. The client posts with its data as URL parameters rather than a body; only the fragment upload is multipart.
  • The response envelope is positional. A successful call answers {"status": "OK", "<first path segment>": …} — so /ff/gen/check returns a check key. Where the client instead reads id and errors off the whole document, those responses deliberately omit the positional key.

GET /ff/get_file/{fname} answers raw text, no envelope.

XML-RPC

Deprecated. Every response carries Deprecation: true and a Sunset date (RFC 8594). It exists so that installed code keeps working, and it will be deleted rather than maintained.

Adapted methods: add, get_net, get_list_of_nets, get_list_of_bbs, get_bb, get_mof_structure_by_id, get_cs, get_vs, search_cs, list_FFrefs, list_FFfrags, get_FFfrag, get_FFfrags, get_FFref, list_special_atypes, get_params, get_params_from_ref, create_fit.

Methods not yet adapted answer a fault naming their replacement rather than "method not found" — most of them depend on tables whose migration is still open. If your workflow needs one, say so; the fault text is the list of what to prioritise.

Errors

REST and /ff/ use HTTP status codes with a JSON body. XML-RPC returns faults: 1 not found, 2 not supported or insufficient tier, 3 bad arguments.