Base URL: https://signal-archive-api.fly.dev
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.
Send a magic-link email. Optionally bind it to a CLI session for the headless login flow.
POST /auth/request-login
Content-Type: application/json
{ "email": "you@example.com", "cli_session_id": "uuid | null" }
Response 200: { "message": "Magic link sent" }
Consume a magic-link token. For new accounts a handle is required.
POST /auth/verify
Content-Type: application/json
{ "token": "string", "handle": "string?", "display_name": "string?" }
Response 200:
{
"jwt": "string",
"handle": "string",
"email": "string",
"is_new": false,
"api_key": "string"
}The api_key is returned every verify (decrypted from the contributor row), so the website's callback page can show it once.
Create a polling session for the CLI login flow. Returns a login_url to open in a browser.
Response 200: { "session_id": "uuid", "login_url": "string" }
Poll until the user completes the magic-link sign-in. Returns 410 if expired.
Response 200: { "ready": true, "api_key": "string" } (when claimed) or { "ready": false }.
Exchange an api_key for a JWT.
POST /auth/token
Content-Type: application/json
{ "api_key": "string" }
Response 200: { "jwt": "string", "handle": "string", "email": "string" }
Return the authenticated caller's profile.
GET /auth/me
Authorization: Bearer <jwt>
Response 200:
{
"handle": "alice",
"display_name": "Alice",
"email": "alice@example.com",
"total_contributions": 12,
"total_reuse_count": 47,
"reputation_score": 3.9,
"created_at": "2026-01-01T00:00:00Z"
}Update the caller's display name. Requires JWT.
Body: { "display_name": "string | null" }. Response: same shape as GET /auth/me.
Return the caller's decrypted api_key. Requires JWT. Returns 500 if the row predates encryption (re-register via magic link).
Response 200: { "api_key": "string" }
Browse all canonical questions with pagination. Public.
Query params:
| param | type | default |
|---|---|---|
| limit | int, 1–100 | 20 |
| offset | int, ≥0 | 0 |
| sort | recent | popular | active |
recent |
recent— newest first (bylast_updated_at)popular— highestreuse_countfirstactive— highestartifact_countfirst
Response 200:
[
{
"id": "uuid",
"title": "How do I configure pgvector on Supabase?",
"synthesized_summary": "...",
"artifact_count": 3,
"reuse_count": 11,
"created_at": "2026-01-01T00:00:00Z",
"last_updated_at": "2026-04-01T00:00:00Z"
}
]Retrieve a single canonical question. Public. Returns 404 if not found.
All research artifacts attached to a canonical question. Public.
Query param: include_superseded (bool, default false). When false, artifacts referenced by another artifact's supersedes_id are hidden.
Semantically similar canonical questions ordered by similarity. Public.
Record a reuse event. Public. Optional query param: reused_by (contributor handle).
Semantic vector search over the archive. Auth optional.
Query params: q (required, 3–1000 chars), limit (1–20, default 5), sort (relevance | quality | reuse, default relevance).
relevance— pure vector similarity.quality— re-rank top-50 candidates (similarity ≥ 0.5) by average artifactquality_score.reuse— re-rank top-50 candidates (similarity ≥ 0.5) byreuse_count.
Anonymous callers get up to 5 results and synthesized_summary is returned as null. Authenticated callers get up to limit results with summaries.
Response 200 — list of SearchResult:
[
{
"canonical_question_id": "uuid",
"title": "string",
"synthesized_summary": "string | null",
"similarity": 0.92,
"artifact_count": 3,
"reuse_count": 7,
"last_updated_at": "2026-04-01T00:00:00Z"
}
]Submit a completed research artifact. Requires auth.
{
"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).
quality_score is computed server-side after submission and persisted on the artifact.
Response 201: { "id": "uuid", "canonical_question_id": "uuid" }
Retrieve a single artifact. Public. Returns the full body, citations, flag counts, quality_score, and supersedes_id. Returns 404 if not found.
Flag an artifact. Requires auth — Bearer JWT (web users) or X-API-Key header (agents/CLI). Anonymous callers get 401.
{ "artifact_id": "uuid", "flag_type": "useful | stale | weakly_sourced | wrong" }Per-contributor deduplication is enforced by a partial unique index (artifact_id, flag_type, contributor_id) — submitting the same flag twice from the same account returns 409. The flag count on the artifact is incremented on the first successful submission; the same contributor can still submit other flag types.
| Status | Meaning |
|---|---|
| 201 | Flag recorded, count incremented |
| 401 | Missing or invalid auth |
| 404 | Artifact not found |
| 409 | This contributor already submitted this flag for this artifact |
| 422 | Invalid flag_type |
All public. Anonymous callers get up to 5 results; authenticated callers get up to 20.
| Endpoint | Description |
|---|---|
| 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 |
Register a handle-only contributor (no email). Returns the api_key once.
Body: { "handle": "string", "display_name": "string?" }. Response 201: { "handle": "string", "api_key": "string" }.
Public profile lookup.
# 1. Get a JWT
TOKEN=$(curl -s -X POST https://signal-archive-api.fly.dev/auth/token \
-H "Content-Type: application/json" \
-d '{"api_key": "YOUR_API_KEY"}' | jq -r .jwt)
# 2. Check your profile
curl https://signal-archive-api.fly.dev/auth/me \
-H "Authorization: Bearer $TOKEN"
# 3. Browse the archive
curl "https://signal-archive-api.fly.dev/canonical?sort=popular&limit=10"
# 4. Search (auth optional, but auth = more results + summaries)
curl "https://signal-archive-api.fly.dev/search?q=how+to+use+pgvector&sort=quality" \
-H "Authorization: Bearer $TOKEN"