webref/docs

API Reference

The webref REST API. Same API that powers the web app and the CLI. The MCP endpoint is separate at /mcp.

Base URL: https://webref.ai/api


Authentication

Include your API key in the Authorization header:

Authorization: Bearer wbrf_your_key_here

Get keys from your dashboard.


Pilot Auth Links

webref supports admin-created pilot invite links for early-access users.

  • Pilot links are created from the admin dashboard or the admin REST endpoints.
  • Each link is one-time use and expires after 7 days.
  • Redeeming a link only marks it used and attaches it to the account. It does not grant research credits.
  • The special auth UI lives on the normal /signup and /login routes with a pilotInviteToken query parameter.

Unavailable pilot links return PILOT_INVITE_UNAVAILABLE with this response:

json
{
  "error": {
    "code": "PILOT_INVITE_UNAVAILABLE",
    "message": "This pilot link is no longer available."
  }
}

POST /api/research

Research the web. Accepts questions, URLs, or both — the planner decomposes your query into search and read actions automatically.

Request:

bash
curl -X POST https://webref.ai/api/research \
  -H "Authorization: Bearer wbrf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "how to use React hooks"}'

You can also pass URLs directly:

bash
curl -X POST https://webref.ai/api/research \
  -H "Authorization: Bearer wbrf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "summarize https://example.com/article"}'

Or mix questions with URLs:

bash
curl -X POST https://webref.ai/api/research \
  -H "Authorization: Bearer wbrf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"query": "compare https://lib-a.dev/docs and https://lib-b.dev/docs for auth support"}'

Body:

Field Type Required Description
query string Yes Question, URL(s), or both (max 1000 chars)

Response (200):

json
{
  "content": "# React Hooks\n\nHooks let you use state...",
  "sources": [
    "https://react.dev/reference/react"
  ],
  "creditsUsed": 1,
  "duration": "14.2s"
}

For admin users, the response also includes a trace object with the full research execution breakdown.

Credits: 1 per call


POST /api/research/stream

Research the web with live progress over Server-Sent Events (SSE).

The stream surfaces the same user-facing stages used in the CLI and playground:

  • Searching...
  • Reading sources...
  • Thinking...

The server sends real events from the live pipeline, not synthetic delays. Event types currently include:

  • research.started
  • search.query_generated
  • source.discovered
  • source.completed
  • source.failed
  • thinking.started
  • answer.delta
  • research.completed
  • research.failed

Streams are resumable on the same running server instance. The initial response includes X-Research-Stream-Id. Every event also includes an SSE id: line and a streamId field in the JSON payload. To resume, reconnect with the same streamId in the POST body and send the last processed event ID in Last-Event-ID.

Request:

bash
curl -N -X POST https://webref.ai/api/research/stream \
  -H "Authorization: Bearer wbrf_your_key" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"query": "what are the best LLMs today"}'

Resume request:

bash
curl -N -X POST https://webref.ai/api/research/stream \
  -H "Authorization: Bearer wbrf_your_key" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: 12" \
  -d '{"streamId": "9a3a6db8f2f04b23a730f5d4b3a691d2"}'

Response headers:

  • X-Research-Stream-Id: stable ID for this in-flight or recently completed stream
  • Content-Type: text/event-stream

SSE example:

id: 7
event: search.query_generated
data: {"type":"search.query_generated","streamId":"9a3a6db8f2f04b23a730f5d4b3a691d2","stage":"searching","timestamp":"2026-03-06T12:00:00Z","queryText":"LLM leaderboard March 2026 ranking"}

id: 8
event: source.discovered
data: {"type":"source.discovered","streamId":"9a3a6db8f2f04b23a730f5d4b3a691d2","stage":"reading","timestamp":"2026-03-06T12:00:02Z","sourceTitle":"LLM Leaderboard","sourceUrl":"https://llm-stats.com/","sourceIndex":1,"sourceCount":5}

id: 21
event: answer.delta
data: {"type":"answer.delta","streamId":"9a3a6db8f2f04b23a730f5d4b3a691d2","stage":"thinking","timestamp":"2026-03-06T12:00:08Z","answerDelta":"The strongest general-purpose models right now are ..."}

id: 22
event: research.completed
data: {"type":"research.completed","streamId":"9a3a6db8f2f04b23a730f5d4b3a691d2","timestamp":"2026-03-06T12:00:14Z","content":"...","sources":["https://llm-stats.com/"],"creditsUsed":1,"duration":"14.2s"}

For admin users, the research.completed event also includes the trace object. The server also sends : keep-alive heartbeat comments every 15 seconds while the stream is idle.

Resume notes:

  • Resume is best-effort within the same running server process only.
  • If all clients disconnect, the research job stays alive for 60 seconds.
  • Completed or failed streams remain replayable for 2 minutes.
  • If the server has already forgotten the stream, you will receive STREAM_NOT_FOUND.

Streaming-specific errors:

  • INVALID_LAST_EVENT_ID (400): Last-Event-ID was malformed
  • STREAM_NOT_FOUND (404): unknown, expired, or unauthorized streamId

JavaScript example:

javascript
const response = await fetch("https://webref.ai/api/research/stream", {
  method: "POST",
  headers: {
    "Authorization": "Bearer wbrf_your_key",
    "Content-Type": "application/json",
    "Accept": "text/event-stream"
  },
  body: JSON.stringify({ query: "what changed in the latest Next.js release?" })
});

const streamId = response.headers.get("X-Research-Stream-Id");
const streamReader = response.body.getReader();
const textDecoder = new TextDecoder();

while (true) {
  const streamReadResult = await streamReader.read();
  if (streamReadResult.done) break;
  console.log(textDecoder.decode(streamReadResult.value, { stream: true }));
}

GET /api/me

Get current user info.

Request:

bash
curl https://webref.ai/api/me \
  -H "Authorization: Bearer wbrf_your_key"

Response (200):

json
{
  "id": 1,
  "email": "[email protected]",
  "name": "Your Name",
  "credits": 1234,
  "subscriptionPlan": "pro",
  "subscriptionStatus": "active",
  "subscriptionAllowance": 900,
  "extraUsageCredits": 334,
  "emailVerified": true,
  "searchHistoryEnabled": true,
  "role": "user",
  "createdAt": "2026-03-11T12:34:56Z"
}

searchHistoryEnabled controls whether detailed search history and saved search results are stored for the user, and whether the full trace is shipped to Axiom. When it is false, Privacy Mode is effectively on: aggregate usage counts still update, but query-bearing history is not saved.


PUT /api/me/settings

Update privacy-related account settings.

Request:

bash
curl -X PUT https://webref.ai/api/me/settings \
  -H "Authorization: Bearer wbrf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"search_history_enabled": false}'

Body:

Field Type Required Description
search_history_enabled boolean No Whether to persist detailed query-bearing history and saved search results (and ship traces to Axiom)

Response (200):

json
{
  "success": true,
  "searchHistoryEnabled": false
}

GET /api/usage

Get dashboard usage totals, 30-day aggregate activity, onboarding state, and any retained detailed history rows.

Request:

bash
curl https://webref.ai/api/usage \
  -H "Authorization: Bearer wbrf_your_key"

Response (200):

json
{
  "totalSearches": 42,
  "searchesThisMonth": 9,
  "dailyActivity": [
    {
      "date": "2026-03-25",
      "researchCount": 1,
      "webSearchCount": 0,
      "readCount": 0
    },
    {
      "date": "2026-03-26",
      "researchCount": 2,
      "webSearchCount": 1,
      "readCount": 1
    }
  ],
  "searchHistory": [
    {
      "id": 123,
      "type": "search",
      "query": "how to enable privacy mode",
      "caller": "CLI: Personal Key",
      "creditsUsed": 1,
      "createdAt": "2026-03-26T12:00:00Z",
      "hasSavedResult": true
    }
  ]
}

dailyActivity is always populated from privacy-safe aggregate counts. When Privacy Mode is enabled, totals and charts continue updating, but searchHistory only includes previously retained rows because new detailed history is no longer stored. searchHistory is research-only and hasSavedResult tells the dashboard whether a persisted markdown answer is available for preview.


GET /api/usage/history/{id}/result

Get the saved markdown answer plus the slim trace slices the dashboard renders for a history row: the final source URLs and the planned sub-queries from the planning step. Full pipeline traces are not stored in the database — they are shipped to Axiom for debugging.

Request:

bash
curl https://webref.ai/api/usage/history/123/result \
  -H "Authorization: Bearer wbrf_your_key"

Response (200):

json
{
  "content": "## Privacy mode\n\nWhen enabled, webref stops saving...",
  "sourceUrls": ["https://webref.ai/privacy"],
  "plannedQueries": ["privacy mode webref docs", "disable search history"]
}

Returns 404 NOT_FOUND when the history row does not exist, is not a research row, does not belong to the authenticated user, or has no persisted saved result.


GET /api/credits

Check credit balance.

Request:

bash
curl https://webref.ai/api/credits \
  -H "Authorization: Bearer wbrf_your_key"

Response (200):

json
{
  "balance": 1234,
  "subscriptionAllowance": 900,
  "extraUsageCredits": 334,
  "subscriptionPlan": "pro",
  "subscriptionStatus": "active"
}

POST /api/billing/purchase

Create a Creem checkout session for a self-serve subscription or prepaid extra usage. Session or API key auth required.

Request:

bash
curl -X POST https://webref.ai/api/billing/purchase \
  -H "Content-Type: application/json" \
  -d '{"plan": "pro", "billingPeriod": "monthly"}'

For extra usage:

bash
curl -X POST https://webref.ai/api/billing/purchase \
  -H "Content-Type: application/json" \
  -d '{"extraCredits": 1000}'

Response (200):

json
{
  "checkoutUrl": "https://www.creem.io/checkout/...",
  "checkoutId": "chk_..."
}

The checkout success URL returns to /dashboard/billing?success=true&checkout_id={CHECKOUT_ID}. The dashboard polls until extra-usage purchases show as completed in purchase history, or until subscription credits reflect an active paid plan.


GET /api/billing/history

List recent extra-usage purchases for the current user. Session or API key auth required.

Response (200):

json
{
  "purchases": [
    {
      "id": 1,
      "checkoutId": "chk_...",
      "tier": "extra_usage",
      "amountCents": 5000,
      "credits": 1000,
      "status": "completed",
      "createdAt": "2026-05-12T12:00:00Z"
    }
  ]
}

POST /api/billing/portal

Create a Creem customer portal session for payment methods, receipts, invoices, cancellation, and payment recovery. Session or API key auth required.

Response (200):

json
{
  "url": "https://www.creem.io/portal/..."
}

POST /api/admin/pilot-invites

Create a new one-time pilot invite link. Admin session required.

Request:

bash
curl -X POST https://webref.ai/api/admin/pilot-invites \
  -H "Content-Type: application/json" \
  -H "Cookie: webref_session=..." \
  -d '{"firstName": "Maya"}'

Body:

Field Type Required Description
firstName string Yes Recipient first name shown in the special auth copy

Response (201):

json
{
  "inviteId": 12,
  "firstName": "Maya",
  "credits": 0,
  "expiresAt": "2026-03-31T15:04:05Z",
  "signupUrl": "https://webref.ai/signup?pilotInviteToken=..."
}

The raw invite token is only exposed through signupUrl in this creation response.


GET /api/admin/pilot-invites

List recent pilot invites. Admin session required.

Request:

bash
curl https://webref.ai/api/admin/pilot-invites \
  -H "Cookie: webref_session=..."

Response (200):

json
{
  "invites": [
    {
      "id": 12,
      "firstName": "Maya",
      "credits": 0,
      "status": "active",
      "createdAt": "2026-03-24 15:04:05",
      "expiresAt": "2026-03-31T15:04:05Z"
    }
  ]
}

status is derived server-side and is one of active, redeemed, or expired.


GET /api/pilot-invites/preview

Load the public metadata used by the special auth UI.

Request:

bash
curl "https://webref.ai/api/pilot-invites/preview?token=..."

Response (200):

json
{
  "firstName": "Maya",
  "credits": 0,
  "expiresAt": "2026-03-31T15:04:05Z"
}

If the link is missing, expired, or already redeemed, the endpoint returns PILOT_INVITE_UNAVAILABLE.


POST /api/pilot-invites/redeem

Redeem a pilot invite for the currently signed-in account. Session auth required.

Request:

bash
curl -X POST https://webref.ai/api/pilot-invites/redeem \
  -H "Content-Type: application/json" \
  -H "Cookie: webref_session=..." \
  -d '{"token": "..."}'

Response (204):

No body.

On success, the invite is marked redeemed for the current account. It does not grant subscription allowance or extra usage credits.


GET /api/keys

List API keys (metadata only).

Request:

bash
curl https://webref.ai/api/keys \
  -H "Authorization: Bearer wbrf_your_key"

Response (200):

json
{
  "keys": [
    {
      "id": "key_abc123",
      "name": "Production",
      "prefix": "wbrf_...x7k2",
      "createdAt": "2024-01-15T10:30:00Z",
      "lastUsedAt": "2024-01-20T14:22:00Z"
    }
  ]
}

POST /api/keys

Create a new API key.

Request:

bash
curl -X POST https://webref.ai/api/keys \
  -H "Authorization: Bearer wbrf_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "CI/CD Pipeline"}'

Response (201):

json
{
  "key": {
    "id": "key_ghi789",
    "name": "CI/CD Pipeline",
    "key": "wbrf_live_abc123xyz789...",
    "createdAt": "2024-01-21T09:00:00Z"
  }
}

The full key is only shown once. Store it immediately.


DELETE /api/keys/:id

Revoke an API key.

Request:

bash
curl -X DELETE https://webref.ai/api/keys/key_abc123 \
  -H "Authorization: Bearer wbrf_your_key"

Response (200):

json
{
  "message": "Key revoked"
}

Error responses

All errors follow this format:

json
{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Insufficient credits. Open billing to buy extra usage or upgrade your plan."
  }
}
Status Code When
400 BAD_REQUEST Missing/malformed request
401 UNAUTHORIZED Invalid API key
402 INSUFFICIENT_CREDITS Balance is zero
404 NOT_FOUND Resource doesn't exist
422 VALIDATION_ERROR Validation failed
429 RATE_LIMITED Too many requests
500 INTERNAL_ERROR Server error

SDK examples

Python:

python
import requests

response = requests.post(
    "https://webref.ai/api/research",
    headers={"Authorization": "Bearer wbrf_your_key"},
    json={"query": "Python async patterns"}
)
result = response.json()
print(result["content"])

JavaScript:

javascript
const response = await fetch("https://webref.ai/api/research", {
  method: "POST",
  headers: {
    "Authorization": "Bearer wbrf_your_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ query: "JavaScript promises" })
});
const result = await response.json();
console.log(result.content);

Go:

go
req, _ := http.NewRequest("POST", "https://webref.ai/api/research",
    strings.NewReader(`{"query": "Go error handling"}`))
req.Header.Set("Authorization", "Bearer wbrf_your_key")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)