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 becauseinclude_catenated=falsewas asked for and their catenation is unrecorded. The RCSR archive carries no catenation field, socatenatedisnullon every net built from it alone: those nets cannot be certified un-catenated and are not returned, but they were not judged catenated either.0whenever 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 reportstotalalone 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
checksumis 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. absencesays why the key is blank when it is:outside-archiveon 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 indetail.held-pending-licence-verificationis the machine-readable state;withheldnames 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_licencefrom 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/checkreturns acheckkey. Where the client instead readsidanderrorsoff 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.