API Endpoints

Every endpoint speaks plain JSON over HTTPS — no special client needed. Reading is always public and unauthenticated; writing requires the stated token, sent as Authorization: Token <token>. Every error uses the same shape: {"error": {"code": "...", "message": "..."}}.

Running locally? Every example below uses https://your-community.example as a stand-in for your actual deployed community. During development, betat runserver (or manage.py runserver) serves on http://127.0.0.1:8000 by default — swap the domain for that, and drop https:// for http://. For example, the /betat/info call further down becomes:

curl http://127.0.0.1:8000/betat/info

Same substitution applies to every endpoint on this page.

POST /betat/enroll — become a Provenancier

Public. method must be on the protocol list and enabled by this community (betat init --auth-method). Returns a token — save it, it’s how you submit.

curl -X POST https://your-community.example/betat/enroll \
  -H "Content-Type: application/json" \
  -d '{
    "method": "cryptographic_signature",
    "applicant": {
      "identity": "did:key:z6MkfExample",
      "public_key": "3b1c...",
      "signature": "a91f...",
      "display_name": "Ada Lovelace"
    }
  }'
{
  "identity": "did:key:z6MkfExample",
  "identity_type": "cryptographic_key",
  "authentication_method": "cryptographic_signature",
  "display_name": "Ada Lovelace",
  "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
}

applicant fields differ per method: community_peer_vouching needs just identity (+ optional display_name) — see “Two-phase peer-vouch enrollment” below, vouches are no longer supplied by the applicant; cryptographic_signature needs public_key + a self-signed signature (proof of possession — sign your own public key), or a passphrase instead of both (see below); institutional_endorsement needs institution_id + the institution’s signature over your identity.

Generating a keypair for cryptographic_signature

Option A — passphrase (no technical step, recommended for most applicants). The bundled UI’s enroll form accepts a passphrase field instead of a pasted public_key/signature: the server derives a deterministic Ed25519 keypair from it (scrypt, salted per-community — the same passphrase yields a different key on every community) and self-signs the proof itself. The private key is never persisted; the passphrase is never resent on later submissions. This is a deliberate trade-off — the server sees the passphrase transiently at enroll and at login — documented in BLUEPRINT.md §03 Decision Log (2026-09). A returning applicant re-authenticates with the same passphrase via POST /betat/login (below) rather than re-enrolling.

Option B — bring your own keypair (technical path, unchanged). The private key never touches the server. Generate it yourself from any Python shell with cryptography installed:

python -c "
from betat_community.communityauth import crypto
private_key, public_key = crypto.generate_keypair()
signature = crypto.sign(private_key, public_key)   # self-sign the public key as proof of possession
print('public_key:', public_key)
print('signature: ', signature)
print('private_key (keep this secret — needed again to authenticate later):', private_key)
"

Use public_key and signature as the enroll fields above. Keep private_keyauthenticate() calls (for future actions requiring re-proof) need it to sign a fresh message each time; it’s never sent during enrollment itself.

POST /betat/login — re-authenticate a passphrase-based identity

Public. Only for cryptographic_signature identities enrolled via Option A above (a passphrase) — identities enrolled by pasting a manual public_key/signature have nothing to re-derive and get no benefit from this endpoint. Re-derives the keypair from the passphrase and compares the public key against what was recorded at enrollment; returns the same token issued then.

curl -X POST https://your-community.example/betat/login \
  -H "Content-Type: application/json" \
  -d '{"identity": "did:key:z6MkfExample", "passphrase": "your passphrase"}'
{
  "identity": "did:key:z6MkfExample",
  "identity_type": "cryptographic_key",
  "authentication_method": "cryptographic_signature",
  "display_name": "Ada Lovelace",
  "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
}

401 with {"error": {"code": "invalid_credentials", ...}} for a wrong identity or passphrase — deliberately the same error either way, so a failed guess can’t reveal whether an identity exists.

Two-phase peer-vouch enrollment

community_peer_vouching no longer accepts a vouchers list from the applicant — that used to trust whoever the applicant named, with no confirmation those people had actually agreed. Enrolling now opens a pending request and returns 202, not 201:

{
  "status": "pending_vouches",
  "request_id": 4,
  "vouch_count": 0,
  "vouches_needed": 2,
  "message": "Enrollment request received. 2 existing enrolled members must vouch for you..."
}

POST /betat/vouch/{request_id} — vouch for a pending peer-vouch request

Requires a Provenancier token — the vouch is attributed to whoever is actually authenticated, not merely named by the applicant. Idempotent (vouching twice does nothing extra) and rejects vouching for your own request.

curl -X POST https://your-community.example/betat/vouch/4 \
  -H "Authorization: Token <voucher-token>"

Returns the same pending_vouches/200 shape as above while below threshold, or the enroll-style identity/token/201 shape once the last required vouch lands and the applicant is promoted to a full Provenancier.

POST /betat/submit — submit a contribution

Requires a Provenancier token. Content is never uploaded — only where it lives and its hash.

curl -X POST https://your-community.example/betat/submit \
  -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Field observation — Lake Oku, June 2026",
    "location": "https://archive.example/obs-4471",
    "content_hash": "sha256:e3b1c74d02a8f5e9b6d0c3a7f42e18d5b9c6a0f3e7d2b8c4a1f6e0d9b3c7a5f2",
    "language": "en",
    "declaration_accepted": true
  }'
{
  "id": 12,
  "provenancier_identity": "did:key:z6MkfExample",
  "title": "Field observation — Lake Oku, June 2026",
  "location": "https://archive.example/obs-4471",
  "content_hash": "sha256:e3b1c74d02a8f5e9b6d0c3a7f42e18d5b9c6a0f3e7d2b8c4a1f6e0d9b3c7a5f2",
  "language": "en",
  "declaration_accepted": true,
  "status": "pending_review",
  "submitted_at": "2026-09-01T12:00:00Z",
  "reviewed_at": null,
  "reviewed_by": "",
  "rejection_reason": "",
  "record_id": ""
}

declaration_accepted must be true or the submission is refused outright (400), not stored pending.

GET /betat/queue — pending submissions

Requires a verifier token (a staff account — see the framework’s admin panel). Returns every submission with status: "pending_review", same shape as above.

POST /betat/review/{id} — accept or reject

Requires a verifier token. Accept builds a full PROVENANCE_SPEC record and appends it to the store; reject closes the submission with no record.

curl -X POST https://your-community.example/betat/review/12 \
  -H "Authorization: Token <verifier-token>" \
  -H "Content-Type: application/json" \
  -d '{"decision": "accept"}'

The response is the submission again, now with status: "accepted" and record_id populated — fetch that id at /betat/records/{id} (below) to see the full record.

GET /betat/info — community identity

Public, no auth.

curl https://your-community.example/betat/info
{
  "id": "marinebiology-lagos.org",
  "name": "Marine Biology Lagos",
  "domain": "marine biology",
  "content_type": "scientific_observation",
  "hi_standard": "human-originated, community-verified",
  "auth_methods": ["community_peer_vouching", "cryptographic_signature"],
  "store_uri": "https://marinebiology-lagos.org/betat/records"
}

404 with {"error": {"code": "not_configured", ...}} before betat init has run.

GET /betat/records — paginated, newest first

Public. ?hi_only=true filters to hi_tag: true records (currently all of them — the store rejects anything else at write time); ?since=<timestamp> also works here, though /changes below is the dedicated endpoint for that.

curl "https://your-community.example/betat/records?page=1"
{
  "count": 42,
  "next": "https://your-community.example/betat/records?page=2",
  "previous": null,
  "results": [ /* full PROVENANCE_SPEC records  see PROVENANCE_SPEC.md */ ]
}

GET /betat/records/{id} — one record

Public. Returns the record exactly as stored (matches PROVENANCE_SPEC.md’s Record Format), or 404 not_found.

curl https://your-community.example/betat/records/7c4a1d29e8f3b6a5d0c2f47e91b8a3d6c5e2f0a9b7d4c1e8f6a3b0d7c4e1f8a2

A conforming client always recomputes this record’s hash and compares it to record_id before trusting it — see RENDERING.md’s integrity states; the bundled UI does exactly this on every record it renders.

GET /betat/changes?since= — incremental feed

Public. Same shape as /records, filtered to records after the given ISO 8601 timestamp — built for crawlers polling on a schedule.

curl "https://your-community.example/betat/changes?since=2026-08-01T00:00:00Z"

Comments & ideas

This site uses Just the Docs, a documentation theme for Jekyll.