Back to Home

Fukurahub API v1

The wire API between a fukura client and a fukurahub server. Every endpoint is prefixed with /v1/. Breaking changes would move to /v2/.

Implemented in fukura v0.3 (client)Implemented in fukura-hub v0.1Read the full spec on GitHub →
Every endpoint on this page carries two badges: one for fukura (the Rust CLI / client library) and one for fukura-hub (the server). A ✅ on the client means the wire contract is emitted correctly today; a ✅ on the server means it answers as the spec describes. They move independently.

§2 Transport

client:Implemented in v0.3server:Implemented in v0.1

HTTPS + JSON + UTF-8. All routes sit under /v1/. Today the hub serves routes under /api/; the /v1/ path is introduced in P3 Slice 3 alongside the existing routes for a compat window.

§3 Authentication

client:Implemented in v0.3server:Implemented in v0.1 (JWT + org_id claim, switchable context)

Bearer tokens over Authorization: Bearer <token>. Tokens are opaque to the client; servers decide the format and scopes. In this implementation we embed org_id in the claim (ADR 0004), so users can operate as themselves (personal) or as any organisation they belong to. Context is flipped via POST /api/auth/switch-context, which reissues the token. Write endpoints always require auth; a hub MAY expose anonymous reads for public.

§4 Privacy tiers

client:Implemented in v0.3server:Implemented in v0.1 (team→org renamed)

private / org / public. private MUST stay client-local: clients reject it before the wire, servers respond 422 if one slips through. Today the hub’s enum is team; the rename to org is performed in Slice 4 as a 3-stage zero-downtime deploy.

§5.1 POST /v1/notes

client:Implemented in v0.3server:Implemented in v0.1

Upload a single note. Body is a NoteEnvelope carrying the EKP ontology. Response on create:

201 Created
{
  "object_id": "sha256:…",
  "url": "https://hub.example.com/v1/notes/sha256:…",
  "ontology": { "fingerprint": "sha256:…", "category": "cargo.compile.e0308" }
}

Reposting the same body MUST return 200 OK with the existing object_id. Privacy=private422 Unprocessable Entity.

§5.1 GET /v1/notes/{object_id}

client:Implemented in v0.3server:Implemented in v0.1 (410 on deleted_at)

Fetch a note by content-addressable object_id. 200 OK with the envelope. 404 Not Found for unknown ids; 410 Gone for ids that were once valid but have been deleted (alignment D4a).

§5.1 GET /v1/notes (search)

client:Implemented in v0.3server:Implemented in v0.1

Query parameters: q, fingerprint, category (prefix with .*), repeated tag, repeated privacy, cursor, limit (1..100, default 20). Responses include an optional effectiveness block per hit when attempt data exists.

§5.2 POST /v1/attempts

client:Implemented in v0.3server:Implemented in v0.1

Batch upload of SolutionAttempt records. Shell hooks can emit high volumes; one-at-a-time would waste round-trips.

202 Accepted
{ "accepted": 42, "rejected": 0, "errors": [] }

Reposting the same attempt_id MUST be a no-op and still count toward accepted.

§5.2 GET /v1/attempts/stats

client:Implemented in v0.3server:Implemented in v0.1

Aggregate success / failure / abandoned counts per fingerprint. fingerprint optional; omit to get top-N most-attempted.

§5.3 GET /v1/health

client:Implemented in v0.3server:Implemented in v0.1 (legacy /health kept)

Unauthenticated. Returns { "status": "ok", "version": "...", "hub_id": "..." }. Clients use it for connectivity checks and to pin a specific hub_id.

§5.3 GET /v1/info

client:Implemented in v0.4 (1h cache)server:Implemented in v0.1

Authenticated server policy advertisement: max_note_bytes, max_attempts_per_batch, rate_limit_per_minute, retained_privacy_tiers, server_time. Clients MUST honor these limits (SHOULD cache for up to 1 hour).

§6 Idempotency

client:Implemented in v0.4 (Idempotency-Key header)server:Implemented in v0.1 (sha256 object_id + UNIQUE)

Uploads are naturally idempotent via the EKP fingerprint / object_id: reposting the same note returns 200 OK with the pre-existing id. Attempts dedup by attempt_id (UUID). Clients SHOULD include an Idempotency-Key on retries.

§7 Pagination

client:Implemented in v0.3server:Implemented in v0.1 (opaque cursor)

Opaque cursors. Clients MUST NOT parse, decode, or construct them. A response with next_cursor indicates more results are available; pass it back verbatim.

§8 Rate limiting

client:Implemented in v0.4server:Implemented in v0.1 (600 req/min per user, token bucket)

Per token per minute. On exceed: 429 Too Many Requests with Retry-After. Clients MUST honor Retry-After and SHOULD add exponential backoff on top (base 2s, cap 60s).

§9 Size limits

client:Implemented in v0.4server:Implemented in v0.1

Single note body: default 256 KiB. Attempts batch: default 500. Total request body: ≥ 1 MiB accepted; > 8 MiB rejected with 413 Payload Too Large.

§10 Client MUSTs

client:Implemented in v0.4server:N/A

A conforming client: (1) redacts locally before sending; (2) rejects private pre-network; (3) parses info and honors limits; (4) retries 5xx / 429 with backoff. Item (5) is a SHOULD: include Idempotency-Key on retries.

Fukura v0.3 satisfies (1). Items (2)–(5) land in Task 3 for v0.4.

§11 Errors

client:Implemented in v0.3server:Implemented in v0.1 (/v1/* uses spec shape via v1_error helper)

All error responses share:

{ "error": { "code": "note_too_large", "message": "...", "retryable": false } }

Clients MUST switch on code, not message. Initial vocabulary: invalid_envelope, invalid_privacy, note_too_large, attempts_batch_too_large, unauthorized, forbidden, not_found, rate_limited, server_unavailable, internal_error.

§12 Forward compatibility

client:Implemented in v0.3server:Implemented in v0.1 (JSONB + serde-ignores-unknowns)

Both sides ignore unknown fields. Servers MUST round-trip unknown EKP ontology fields — they were added in a newer producer and other consumers may need them. Clients treat unknown enum values as unknown.

Authoritative text: docs/fukurahub-api.md. Cross-repo status matrix: docs/implementation-status.md.