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_hereGet 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
/signupand/loginroutes with apilotInviteTokenquery parameter.
Unavailable pilot links return PILOT_INVITE_UNAVAILABLE with this response:
{
"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:
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:
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:
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):
{
"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.startedsearch.query_generatedsource.discoveredsource.completedsource.failedthinking.startedanswer.deltaresearch.completedresearch.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:
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:
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 streamContent-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-IDwas malformedSTREAM_NOT_FOUND(404): unknown, expired, or unauthorizedstreamId
JavaScript example:
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:
curl https://webref.ai/api/me \
-H "Authorization: Bearer wbrf_your_key"Response (200):
{
"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:
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):
{
"success": true,
"searchHistoryEnabled": false
}GET /api/usage
Get dashboard usage totals, 30-day aggregate activity, onboarding state, and any retained detailed history rows.
Request:
curl https://webref.ai/api/usage \
-H "Authorization: Bearer wbrf_your_key"Response (200):
{
"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:
curl https://webref.ai/api/usage/history/123/result \
-H "Authorization: Bearer wbrf_your_key"Response (200):
{
"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:
curl https://webref.ai/api/credits \
-H "Authorization: Bearer wbrf_your_key"Response (200):
{
"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:
curl -X POST https://webref.ai/api/billing/purchase \
-H "Content-Type: application/json" \
-d '{"plan": "pro", "billingPeriod": "monthly"}'For extra usage:
curl -X POST https://webref.ai/api/billing/purchase \
-H "Content-Type: application/json" \
-d '{"extraCredits": 1000}'Response (200):
{
"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):
{
"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):
{
"url": "https://www.creem.io/portal/..."
}POST /api/admin/pilot-invites
Create a new one-time pilot invite link. Admin session required.
Request:
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):
{
"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:
curl https://webref.ai/api/admin/pilot-invites \
-H "Cookie: webref_session=..."Response (200):
{
"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:
curl "https://webref.ai/api/pilot-invites/preview?token=..."Response (200):
{
"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:
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:
curl https://webref.ai/api/keys \
-H "Authorization: Bearer wbrf_your_key"Response (200):
{
"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:
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):
{
"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:
curl -X DELETE https://webref.ai/api/keys/key_abc123 \
-H "Authorization: Bearer wbrf_your_key"Response (200):
{
"message": "Key revoked"
}Error responses
All errors follow this format:
{
"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:
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:
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:
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)