REST API Reference

Base URL: https://signal-archive-api.fly.dev

Authentication

Most write endpoints, /search, and account routes require a Bearer JWT. Get one by exchanging your api_key for a token, or by completing the magic-link flow. JWTs are valid for 30 days. Magic links expire after 15 minutes; CLI sessions after 10.

POST /auth/request-login Send magic-link email

Public. Optionally bind to a CLI session for headless login.

Request body
{ "email": "string", "cli_session_id": "uuid | null" }
POST /auth/verify Consume a magic-link token

For new accounts a handle is required. Returns the JWT and the api_key (decrypted, shown once on the callback page).

Request body
{ "token": "string", "handle": "string?", "display_name": "string?" }
Response 200
{ "jwt": "string", "handle": "string", "email": "string", "is_new": false, "api_key": "string" }
POST /auth/cli-session Start a CLI login polling session

Public. Returns a login_url to open in a browser.

Response 200
{ "session_id": "uuid", "login_url": "string" }
GET /auth/cli-session/{session_id}/poll Poll until sign-in completes

Returns 410 if the 10-minute window expires.

Response 200
{ "ready": true, "api_key": "string" }   // when claimed
{ "ready": false }                       // still waiting
POST /auth/token Exchange api_key for JWT

No auth required.

Request body
{ "api_key": "string" }
Response 200
{ "jwt": "string", "handle": "string", "email": "string" }
GET /auth/me Authenticated caller's profile

Requires Authorization: Bearer <jwt>.

Response 200
{
  "handle": "string",
  "display_name": "string | null",
  "email": "string",
  "total_contributions": 0,
  "total_reuse_count": 0,
  "reputation_score": 0.0,
  "created_at": "2026-01-01T00:00:00Z"
}
PATCH /auth/me Update display name

Requires JWT.

Request body
{ "display_name": "string | null" }
Response shape matches GET /auth/me.
GET /auth/api-key Reveal decrypted api_key

Requires JWT. Used by the account page to copy the key into a shell profile.

Response 200
{ "api_key": "string" }

Canonical Questions

GET /canonical Browse all questions

Public. Paginated list sorted by recency, popularity, or activity.

Query params
limitint, 1–100default 20
offsetint, ≥0default 0
sortrecent | popular | activedefault recent
Response 200
[
  {
    "id": "uuid",
    "title": "string",
    "synthesized_summary": "string | null",
    "artifact_count": 0,
    "reuse_count": 0,
    "created_at": "2026-01-01T00:00:00Z",
    "last_updated_at": "2026-01-01T00:00:00Z"
  }
]
GET /canonical/{id} Single canonical question

Public. Returns 404 if not found.

GET /canonical/{id}/artifacts All artifacts for a question

Public. Returns list of ArtifactResponse objects.

Query params
include_supersededbooldefault false. When false, artifacts referenced by another's supersedes_id are hidden.
GET /canonical/{id}/related Semantically similar questions

Public. Returns list of SearchResult ordered by similarity.

POST /canonical/{id}/reuse Record a reuse event

Increments reuse count. Optional query param reused_by (contributor handle).

Search

GET /search Semantic vector search

Requires auth. Returns semantically similar canonical questions. Anonymous callers get up to 5 results.

Query params
qstring (required)search query
limitintdefault 10
sortrelevance | quality | reusedefault relevance. quality and reuse filter to ≥50% similarity candidates before re-ranking.

Artifacts

POST /artifacts Submit a research artifact

Requires auth. Submits a completed research result to the archive. quality_score is computed server-side and persisted on the artifact.

Request body (ArtifactSubmit)
{
  "cleaned_question": "string (≤2000)",
  "cleaned_prompt": "string (≤20000)",
  "clarifying_qa": [{ "question": "string", "answer": "string" }],
  "short_answer": "string (≤2000)",
  "full_body": "string (≤100000)",
  "citations": [{ "url": "string", "title": "string", "domain": "string" }],
  "run_date": "2026-01-01T00:00:00Z",
  "worker_type": "string",
  "model_info": "string | null",
  "source_domains": ["string"],
  "prompt_modified": false,
  "version": "string | null",
  "supersedes_id": "uuid | null"
}

supersedes_id is two-phase validated: the referenced artifact must exist, and after canonical assignment it must belong to the same canonical question (else 409).

Response 201
{ "id": "uuid", "canonical_question_id": "uuid" }
GET /artifacts/{id} Retrieve a single artifact

Public. Returns the full body, citations, flag counts, quality_score, and supersedes_id. Returns 404 if not found.

Flags

POST /flags Flag an artifact

Requires auth — Bearer JWT (web) or X-API-Key (agents/CLI). Per-contributor dedup: submitting the same flag twice returns 409. Flag counts feed the quality-weighted synthesis and contributor reputation.

Request body
{
  "artifact_id": "uuid",
  "flag_type": "useful | stale | weakly_sourced | wrong"
}
Status codes
201Flag recorded, count incremented
401Missing or invalid auth
404Artifact not found
409This contributor already submitted this flag for this artifact
422Invalid flag_type

Discovery

All public. Anonymous callers get up to 5 results; authenticated callers get up to 20.

GET /discovery/weekly Canonical questions with new artifacts this week
GET /discovery/top-reused Most reused canonical questions
GET /discovery/emerging Recent canonicals (≤14 days) with growth signals
GET /discovery/leaderboard Top contributors by reputation score

Contributors

POST /contributors Register handle-only (no email)

Public. Returns the api_key once.

Request body
{ "handle": "string", "display_name": "string?" }
Response 201
{ "handle": "string", "api_key": "string" }
GET /contributors/{handle} Public profile

Public.

Quick Start

1. Exchange your api_key for a JWT
curl -X POST https://signal-archive-api.fly.dev/auth/token \
  -H "Content-Type: application/json" \
  -d '{"api_key": "YOUR_API_KEY"}'
2. Use the JWT on subsequent requests
curl https://signal-archive-api.fly.dev/auth/me \
  -H "Authorization: Bearer YOUR_JWT"
3. Browse the archive
curl "https://signal-archive-api.fly.dev/canonical?sort=popular&limit=10"