Pith records as JSON
Each paper's Pith record can be fetched with a plain GET and no key: the record, its signed bundle, the automatic checks on it, its claims. This page lists each address with what it answers, for one real record.
A record's signature is checked with Pith's public key and nothing else. The recipe is on this page.
The record, and what an address takes for it
The examples below use one real record · arXiv:1706.03762
pith:2017:CXPXXMQRMHRHYZ2C7KEJ4A4ORY
- A Pith number's 26 characters
CXPXXMQRMHRHYZ2C7KEJ4A4ORY
Taken by every address below.
- Its first eight characters
CXPXXMQR
Taken by every address below.
- An arXiv identifier
1706.03762
Taken by integrity.json, claims.json and the badge. For the rest, ask /api/pith-number/resolve for the record first.
The addresses
-
GET /pith/{id}.json
The whole record: what the number was made from, the signed receipt, a snapshot of the paper, and what has been added since.
curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY.json'
Answers 200, JSON with these fields: record_type, schema_url, pith_number, short_pith_number, schema_version, canonical_sha256, source, attestation_state, paper, verification_status, canonical_record, receipt, graph_snapshot, aliases, events, event_summary, paper_claims, inbound_citations, links, agent_actions, created_at, updated_at.
GET /pith/{id} with the header Accept: application/json answers the same record as application/ld+json.
-
GET /pith/{id}/bundle.json
The signed bundle: the record, its other names, every signed addition, the timestamp proofs and the public keys to check them with.
curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/bundle.json'
Answers 200, JSON with these fields: bundle_type, bundle_version, pith_number, short_pith_number, canonical_record, canonical_sha256, source, source_aliases, events, timestamp_proofs, mirror_hints, public_keys, merge_version, built_at, links, state, bundle_signature.
The same bundle is at /.well-known/pith/{id}/bundle.json and at /api/v1/bundles/{id}.
-
GET /pith/{id}/state.json
The record as it stands, worked out from the bundle's additions by a fixed rule. Work it out again from the bundle and the answers must match.
curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/state.json'
Answers 200, JSON with these fields: state_type, state_version, pith_number, merge_version, event_count, valid_event_count, invalid_event_count, equivocation_count, current, equivocations, invalid_events, applied_event_ids, state_sha256.
-
GET /pith/{id}/integrity.json
The automatic checks on the paper: which ran, what each found, and the newest signed record of each finding.
curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/integrity.json'
Answers 200, JSON with these fields: schema, pith_number, arxiv_id, integrity, events, endpoint_self, protocol_url.
Also takes an arXiv identifier in place of the number.
-
GET /pith/{id}/claims.json
The claims Pith recorded for the paper, each with what it rests on.
curl -s 'https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/claims.json'
Answers 200, JSON with these fields: schema, pith_number, arxiv_id, summary, claims, endpoint_self, integrity_endpoint, bundle_endpoint.
Also takes an arXiv identifier in place of the number.
-
GET /api/pith-number/{id}/graph.json
The snapshot of the paper alone: its details, its references and its checks, without the rest of the record.
curl -s 'https://pith.science/api/pith-number/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/graph.json'
Answers 200, JSON with these fields: paper, claims, source, verdict, integrity, references, author_claims, builder_version.
-
GET /api/pith-number/{id}/events.json
What has been added to the record, alone: a list, empty for a record nothing has been added to.
curl -s 'https://pith.science/api/pith-number/CXPXXMQRMHRHYZ2C7KEJ4A4ORY/events.json'
Answers 200, a JSON list.
-
GET /api/pith-number/resolve?arxiv={arxiv_id}&json=true
The record of a paper you know only by its arXiv identifier (or ?doi= for a DOI).
curl -s 'https://pith.science/api/pith-number/resolve?arxiv=1706.03762&json=true'
Answers 200, JSON with these fields: record_type, schema_url, pith_number, short_pith_number, schema_version, canonical_sha256, source, attestation_state, paper, verification_status, canonical_record, receipt, graph_snapshot, aliases, events, event_summary, paper_claims, inbound_citations, links, agent_actions, created_at, updated_at.
Without json=true it answers 302 to the record's page.
-
GET /badge/{id}.svg
A small picture that reads "pith:<number> reviewed", for an author's or a journal's page.
curl -s 'https://pith.science/badge/CXPXXMQRMHRHYZ2C7KEJ4A4ORY.svg'
Answers 200, an SVG picture.
Also takes an arXiv identifier in place of the number.
-
GET /pith-signing-key.json
Pith's public key.
curl -s 'https://pith.science/pith-signing-key.json'
Answers 200, JSON with these fields: key_id, algorithm, format, public_key_b64, public_key_hex, fingerprint_sha256_b32_first128bits, fingerprint_sha256_hex, rotates_at, url, notes.
-
GET /pith-mirrors.json
The places that serve bundles. Today the list has one entry, Pith itself.
curl -s 'https://pith.science/pith-mirrors.json'
Answers 200, JSON with these fields: manifest_type, manifest_version, generated_at, merge_version, mirrors, notes.
Check a signature
Pith signs with one Ed25519 key. What is signed is always the 32 bytes of a SHA-256: for a record, its fingerprint (canonical_sha256). This raises an error when the signature is bad and prints nothing when it is good:
import base64, json, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
key = json.load(urllib.request.urlopen("https://pith.science/pith-signing-key.json"))
rec = json.load(urllib.request.urlopen("https://pith.science/pith/CXPXXMQRMHRHYZ2C7KEJ4A4ORY.json"))
pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(key["public_key_b64"]))
pub.verify(base64.b64decode(rec["receipt"]["signature_b64"]),
bytes.fromhex(rec["canonical_sha256"]))
The signed record of a finding follows the same rule: its payload_sha256 is the SHA-256 of the payload written as JSON with sorted keys and no spaces, and its signature is over those 32 bytes.
The badge
The badge says the paper was reviewed by Pith. It does not say the paper is right.
<a href="https://pith.science/paper/CXPXXMQR">
<img src="https://pith.science/badge/CXPXXMQR.svg"
alt="pith:CXPXXMQRMHRHYZ2C7KEJ4A4ORY reviewed" height="24">
</a>
The schemas
- /schemas/pith-number/v1.json: the record at /pith/{id}.json
- /schemas/pith-open-graph-bundle/v1.json: the bundle
- /schemas/pith-open-graph-event/v1.json: one signed addition in a bundle
- /schemas/pith-open-graph-state/v1.json: the record as it stands
- /schemas/pith-integrity-event/v1.json: the signed record of one finding
What to expect
- No key and no account are needed for any address on this page: each is a plain GET.
- integrity.json and claims.json send Cache-Control: public, max-age=300. The other addresses on this page send no caching header.
- None of the addresses on this page sends Access-Control-Allow-Origin, so fetch them from a server, not from a page on another site. The same bundle at /api/v1/bundles/{id} does send it.
- An identifier that is not a record: /pith/{id}.json, bundle.json, state.json, integrity.json, claims.json, graph.json, events.json and resolve answer 404 with {"detail": "Pith Number not found"}. The badge answers 200 all the same.
- The version is in each schema's address; v1 is the only one. A change that would break a reader gets a new address.