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, pqrs, include_catenated, order
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.

Most nets are published; 634 are held and answer 403

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, and the licence position across it 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 unverified null false 403
5 null (seeded) CC-BY-4.0 null true 200

So an unfiltered GET /api/v1/nets answers "total": 2443 and "held_pending_licence": 634.

Published records came, at least in part, from MOF+. 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. That is the project's own decision, 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").

Held records came only from the RCSR archive. rcsr.anu.edu.au is a third party, the archive file states no terms, and nobody has confirmed those terms in writing — so there is no licence to record and the record is not published. This is an open blocker, and the only one left. It is resolved by one thing: a written confirmation of the RCSR archive's terms that can be cited. Until then no default is applied and nothing is inferred. When it arrives, those 634 URIs resolve to full records without changing — the identifiers are already minted and stable.

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": 2443, "limit": 50, "offset": 0, "order": "rcsr",
 "excluded_for_missing_data": 0, "excluded_for_unknown_catenation": 0,
 "held_pending_licence": 634, "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, pqrs) is blank on them. Neither matched nor ruled out; not the same as "did not match".
  • excluded_for_unknown_catenation — records dropped only because include_catenated=false was asked for and their catenation is unrecorded. The RCSR archive carries no catenation field, so catenated is null on every net built from it alone: those nets 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.
  • 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; but a held record came from the RCSR archive alone, so it has no vertices, no coordination number and no recorded catenation. Any filter that touches those excludes it before the count is taken. Measured against the collection as it stands:

Query total held_pending_licence
(none) 2443 634
name=ac 18 2
coordination=6 511 0
include_catenated=false 2377 0 (and excluded_for_unknown_catenation 4)
POST /nets/search-cs 13 0

So a zero here 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. include_catenated=false is the sharpest case: it drops all 634 by the catenation filter, and excluded_for_unknown_catenation is itself counted over published rows, so they appear in none of the three counts.

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: {"total", "results", "held_pending_licence"}, where results is a list of names.

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": "mofplus:name-heuristic",
                                 "licence": "mofpp-project-decision (CONTEXT §6)",
                                 "retrieved_utc": "2026-09-08T18:09:42.545747+00:00",
                                 "name_match": "uncorroborable",
                                 "cs_corroboration": "checked: ...", "...": "..."}},
"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), or the string unverified (634), which is deliberately not a licence

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: 62 records true, 2377 false, 638 null. The RCSR archive carries no catenation field, so a net built from it alone has no recorded catenation and null says so; treating null as false overstates what any source said.

The true/false values are not a measurement either. They come from the legacy client's own rule — the name contains -c — which is why field_sources.catenated reads mofplus:name-heuristic rather than mofplus.

catenated_note travels beside the value on records and on list rows, and qualifies all three states in words: "No, on the MOF+ name heuristic … A name heuristic, not a computed property" for a recorded value, and "Not recorded: no source we read states whether this net is interpenetrated" for a blank. Show it wherever you show the value; the value alone reads as a measurement.

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 3072 uncorroborable (2292) — the two sources agree on the name but share no field to check it by, so the match stands unchecked; single-source (780) — only one source holds this net
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 mofpp-project-decision (CONTEXT §6) where the decision applied, unverified where it did not

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,
           "note": "The canonical Systre key, copied verbatim from ..."}
  • The key is copied, never computed. No Gavrog/Systre toolchain runs on this machine, so nothing here derives, extends, repairs or verifies a key. 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.
  • 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.
  • 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.

403 — held pending licence verification

634 records are in this state: those sourced only from the RCSR archive. 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": "aca", "uri": "https://.../net/aca", "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": { ... }, "ordering": { ... },
 "systre": {"key": null, "withheld": true, "key_on_record": true, "note": "..."},
 "withheld": ["spacegroup", "transitivity", "cell", "natoms",
              "catenated", "vertices", "geometry", "files", "systre_key"]}

Two things about that body are worth knowing.

detail still says the licence "could not be verified against either source", which was written when nothing was published and is now imprecise: it is the RCSR archive alone whose terms are unconfirmed, and the MOF+ side of the collection is published. This is exactly why the rule below says branch on status and not on the prose.

Most of the withheld list is null on a held row in any case — the archive supplies no cell, no coordinates and no coordination sequence — so the entry that actually costs you something is systre_key. 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 is available on 2442 records — every one with a deposit, which means every MOF+-backed net. .xyz is available on none of them: 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.

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.