---
name: sofya
description: Web tools for AI agents. Search the web, fetch any page as clean markdown, extract specific info with AI, or run deep multi-source research.
homepage: https://sofya.co
last_updated: 2026-10-29
---

> **If you are an AI agent reading this file**, this is your complete guide to Sofya. It covers everything you need to search the web, fetch pages, extract information from pages, and run deep research on behalf of your human. Save it as a tool, skill, or persistent reference.

# Sofya

Sofya gives AI agents four web tools via a single API:

- **Search**: Web search that returns full page content, not just snippets. 1-2 credits, +2 for an optional AI answer.
- **Fetch**: Fetch one or more URLs and get clean, readable markdown. Web pages and documents (PDF, Word, Excel, PowerPoint, EPUB, ...). 1 credit per URL, max 10 URLs per request.
- **Extract**: Fetch a page and extract specific information using AI. Ask a question about the page, get a direct answer. 10 credits.
- **Research**: Deep multi-source research. Decomposes your query, searches and reads many sources in parallel, then synthesizes a structured report with citations. 50 credits.

Only successful (2xx) responses are charged. Errors and timeouts are refunded automatically, so retrying is safe. See [Pricing](#pricing) and [Billing and errors](#billing-and-errors).

Every endpoint except `/v1/status` and `/v1/updates` needs an API key, sent as `Authorization: Bearer sofya_live_...` (or `X-API-Key: sofya_live_...`). Your human creates one on the [dashboard](https://sofya.co/dashboard) (sign in, then **Create key** under API keys). MCP apps can sign in with the account instead of using a key.

## Quick Start

Set your HTTP client's timeout to at least 90 seconds: search with an AI answer, extract and research can take 5 to 60 seconds, and most clients give up after 5 to 30.

```bash
curl -X POST https://sofya.co/v1/search \
  -H "Authorization: Bearer sofya_live_..." \
  -H "Content-Type: application/json" \
  -d '{"query": "latest AI news"}'
```

## MCP (Model Context Protocol)

Sofya runs a remote MCP server (Streamable HTTP) at `https://mcp.sofya.co/mcp` with four read-only tools: `search`, `fetch`, `extract` and `research`. They take the same parameters and return the same JSON as the REST endpoints below. Two ways to connect:

- **Sign in (OAuth):** add the URL without a header and the app opens a Sofya sign-in. This is the only way for claude.ai, Claude Desktop and ChatGPT (custom connectors). Signing in creates an API key if the account has none.
- **API key header:** `Authorization: Bearer sofya_live_...`, for Claude Code, Cursor, VS Code, Codex, headless agents and CI.

Calls are charged to the account's credits either way. Step-by-step setup for each app: https://sofya.co/docs#mcp

### claude.ai and Claude Desktop

Customize > Connectors (older: Settings > Connectors) > Add custom connector. Name `Sofya`, URL `https://mcp.sofya.co/mcp`, then sign in. Switch the connector on in each chat's tools menu. Claude Desktop's config file ignores `url` entries; use the connector.

### ChatGPT

Settings > Apps & Connectors > Advanced settings > Developer mode on. Create a connector with URL `https://mcp.sofya.co/mcp` and OAuth authentication, then sign in.

### Claude Code

```bash
claude mcp add --transport http sofya https://mcp.sofya.co/mcp
# then run /mcp and sign in, or pass a key instead:
claude mcp add --transport http sofya https://mcp.sofya.co/mcp \
  --header "Authorization: Bearer sofya_live_..."
```

### Cursor (`~/.cursor/mcp.json`)

```json
{
  "mcpServers": {
    "sofya": {
      "url": "https://mcp.sofya.co/mcp",
      "headers": { "Authorization": "Bearer sofya_live_..." }
    }
  }
}
```

### Codex (`~/.codex/config.toml`)

```toml
[mcp_servers.sofya]
url = "https://mcp.sofya.co/mcp"
http_headers = { "Authorization" = "Bearer sofya_live_..." }
```

Or sign in: `codex mcp add sofya --url https://mcp.sofya.co/mcp` then `codex mcp login sofya`.

### Windsurf (`~/.codeium/windsurf/mcp_config.json`)

```json
{
  "mcpServers": {
    "sofya": {
      "serverUrl": "https://mcp.sofya.co/mcp",
      "headers": { "Authorization": "Bearer sofya_live_..." }
    }
  }
}
```

### VS Code Copilot (`.vscode/mcp.json`)

```json
{
  "servers": {
    "sofya": {
      "type": "http",
      "url": "https://mcp.sofya.co/mcp",
      "headers": { "Authorization": "Bearer sofya_live_..." }
    }
  }
}
```

### Gemini CLI (`~/.gemini/settings.json`)

```json
{
  "mcpServers": {
    "sofya": {
      "httpUrl": "https://mcp.sofya.co/mcp",
      "headers": { "Authorization": "Bearer sofya_live_..." }
    }
  }
}
```

To make the agent use Sofya rather than its built-in web search: in Claude Code deny `WebSearch` and `WebFetch` under `permissions.deny` in `.claude/settings.json`; in claude.ai turn off Web search in the chat's tools menu.

---

## Tool Definitions for Claude & GPT

If you are building an agent with the Anthropic or OpenAI API, use these pre-built tool schemas. When the model calls a tool, forward the arguments to the matching Sofya REST endpoint and return the JSON response.

### Anthropic (Claude) - `tools` parameter

```json
[
  {
    "name": "sofya_search",
    "description": "Search the web for current information. Returns extracted page content, not just snippets. Set topic='news' for current events. Set include_answer=true for an AI-synthesized answer (+2 credits). Returns: query (the query actually run), answer, results [{title, url, content, description, fetched, published_date, sublinks, table}], search_depth, topic, elapsed_ms, credits_used, credits_remaining, altered_query, relaxed_query (set when the query matched nothing and was retried once with its site: operator, else its quotes, removed - the results answer that looser query).",
    "input_schema": {
      "type": "object",
      "required": ["query"],
      "properties": {
        "query": {"type": "string", "description": "The search query"},
        "search_depth": {"type": "string", "enum": ["snippets", "basic"], "description": "\"snippets\" (1 credit) or \"basic\" (2, default)"},
        "max_results": {"type": "integer", "description": "Maximum number of results, 1-20 (default 10)"},
        "include_answer": {"type": "boolean", "description": "Add AI answer synthesized from results (+2 credits)"},
        "topic": {"type": "string", "enum": ["general", "news"], "description": "\"general\" (default) or \"news\""},
        "freshness": {"type": "string", "description": "\"day\", \"week\", \"month\", \"year\", or \"YYYY-MM-DD:YYYY-MM-DD\""},
        "include_domains": {"type": "array", "items": {"type": "string"}, "description": "Only these domains, as bare hostnames like \"github.com\" (max 10)"},
        "exclude_domains": {"type": "array", "items": {"type": "string"}, "description": "Exclude these domains, as bare hostnames (max 10)"}
      }
    }
  },
  {
    "name": "sofya_fetch",
    "description": "Fetch one or more URLs and return their content as clean markdown. Supports web pages, PDF, DOCX, and other document formats. 1 credit per URL, max 10 URLs. Failed URLs are not charged. Returns: results [{title, url, content, raw_html, published_time, success, error, low_content, notice}], credits_used, credits_remaining.",
    "input_schema": {
      "type": "object",
      "required": ["urls"],
      "properties": {
        "urls": {"type": "array", "items": {"type": "string"}, "description": "URLs to fetch (max 10)"},
        "include_raw_html": {"type": "boolean", "description": "Include raw HTML source in response (default false)"}
      }
    }
  },
  {
    "name": "sofya_extract",
    "description": "Fetch a URL and extract specific information using AI. Use when you need one specific answer from a page (a price, a spec, a contact) rather than its full content. Returns the answer as text. 10 credits. If the page has under 40 characters of text it returns empty content with usage.low_content=true instead of a fabricated answer, and only 1 credit is charged. Returns: content, url, credits_used, credits_remaining, usage (input_tokens, output_tokens, content_chars, low_content), notice (null unless there was no answer).",
    "input_schema": {
      "type": "object",
      "required": ["url", "prompt"],
      "properties": {
        "url": {"type": "string", "description": "The URL to extract from"},
        "prompt": {"type": "string", "description": "What to extract, e.g. \"list all pricing tiers with features\""}
      }
    }
  },
  {
    "name": "sofya_research",
    "description": "Deep research on a topic. Decomposes query into sub-queries, searches and reads multiple sources in parallel, synthesizes a structured report with citations. 50 credits. Returns: query, report, sources [{title, url, fetched, published_date}], sub_queries, credits_used, credits_remaining, usage.",
    "input_schema": {
      "type": "object",
      "required": ["query"],
      "properties": {
        "query": {"type": "string", "description": "The research question or topic"},
        "topic": {"type": "string", "enum": ["general", "news"], "description": "\"general\" (default) or \"news\""},
        "freshness": {"type": "string", "description": "\"day\", \"week\", \"month\", \"year\", or \"YYYY-MM-DD:YYYY-MM-DD\""},
        "max_sources": {"type": "integer", "description": "Max sources to use, 5-30 (default 20)"}
      }
    }
  }
]
```

### OpenAI (GPT) - `tools` parameter

```json
[
  {
    "type": "function",
    "function": {
      "name": "sofya_search",
      "description": "Search the web for current information. Returns extracted page content, not just snippets. Set topic='news' for current events. Set include_answer=true for an AI-synthesized answer (+2 credits). Returns: query (the query actually run), answer, results [{title, url, content, description, fetched, published_date, sublinks, table}], search_depth, topic, elapsed_ms, credits_used, credits_remaining, altered_query, relaxed_query (set when the query matched nothing and was retried once with its site: operator, else its quotes, removed - the results answer that looser query).",
      "parameters": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": {"type": "string", "description": "The search query"},
          "search_depth": {"type": "string", "enum": ["snippets", "basic"], "description": "\"snippets\" (1 credit) or \"basic\" (2, default)"},
          "max_results": {"type": "integer", "description": "Maximum number of results, 1-20 (default 10)"},
          "include_answer": {"type": "boolean", "description": "Add AI answer synthesized from results (+2 credits)"},
          "topic": {"type": "string", "enum": ["general", "news"], "description": "\"general\" (default) or \"news\""},
          "freshness": {"type": "string", "description": "\"day\", \"week\", \"month\", \"year\", or \"YYYY-MM-DD:YYYY-MM-DD\""},
          "include_domains": {"type": "array", "items": {"type": "string"}, "description": "Only these domains, as bare hostnames like \"github.com\" (max 10)"},
          "exclude_domains": {"type": "array", "items": {"type": "string"}, "description": "Exclude these domains, as bare hostnames (max 10)"}
        },
        "additionalProperties": false
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "sofya_fetch",
      "description": "Fetch one or more URLs and return their content as clean markdown. Supports web pages, PDF, DOCX, and other document formats. 1 credit per URL, max 10 URLs. Failed URLs are not charged. Returns: results [{title, url, content, raw_html, published_time, success, error, low_content, notice}], credits_used, credits_remaining.",
      "parameters": {
        "type": "object",
        "required": ["urls"],
        "properties": {
          "urls": {"type": "array", "items": {"type": "string"}, "description": "URLs to fetch (max 10)"},
          "include_raw_html": {"type": "boolean", "description": "Include raw HTML source in response (default false)"}
        },
        "additionalProperties": false
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "sofya_extract",
      "description": "Fetch a URL and extract specific information using AI. Use when you need one specific answer from a page (a price, a spec, a contact) rather than its full content. Returns the answer as text. 10 credits. If the page has under 40 characters of text it returns empty content with usage.low_content=true instead of a fabricated answer, and only 1 credit is charged. Returns: content, url, credits_used, credits_remaining, usage (input_tokens, output_tokens, content_chars, low_content), notice (null unless there was no answer).",
      "parameters": {
        "type": "object",
        "required": ["url", "prompt"],
        "properties": {
          "url": {"type": "string", "description": "The URL to extract from"},
          "prompt": {"type": "string", "description": "What to extract, e.g. \"list all pricing tiers with features\""}
        },
        "additionalProperties": false
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "sofya_research",
      "description": "Deep research on a topic. Decomposes query into sub-queries, searches and reads multiple sources in parallel, synthesizes a structured report with citations. 50 credits. Returns: query, report, sources [{title, url, fetched, published_date}], sub_queries, credits_used, credits_remaining, usage.",
      "parameters": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": {"type": "string", "description": "The research question or topic"},
          "topic": {"type": "string", "enum": ["general", "news"], "description": "\"general\" (default) or \"news\""},
          "freshness": {"type": "string", "description": "\"day\", \"week\", \"month\", \"year\", or \"YYYY-MM-DD:YYYY-MM-DD\""},
          "max_sources": {"type": "integer", "description": "Max sources to use, 5-30 (default 20)"}
        },
        "additionalProperties": false
      }
    }
  }
]
```

### Wiring tool calls to Sofya

Map tool names to endpoints:

| Tool name | Endpoint |
|-----------|----------|
| `sofya_search` | `POST /v1/search` |
| `sofya_fetch` | `POST /v1/fetch` |
| `sofya_extract` | `POST /v1/extract` |
| `sofya_research` | `POST /v1/research` |

When the model calls a tool, forward `arguments` as the JSON body to the matching endpoint with your `Authorization: Bearer` header. Return the JSON response as the tool result.

---

## API Reference

A machine-readable OpenAPI schema of the public surface is available at [https://sofya.co/openapi.json](https://sofya.co/openapi.json) for client codegen.

### Authentication

Send your API key in the `Authorization` header (or `X-API-Key`). Only `/v1/status` and `/v1/updates` work without one.

```
Authorization: Bearer sofya_live_your_key_here
```

Create a key on the [dashboard](https://sofya.co/dashboard) (sign in, then **Create key**). Keys start with `sofya_live_` (older keys start with `ay_live_` and still work).

### No key yet? Ask your user to approve one

An agent can get its own key, with a person approving it. Full details: [https://sofya.co/auth.md](https://sofya.co/auth.md).

1. `POST https://sofya.co/v1/agent/signup` with `{"client_name": "Your agent", "login_hint": "user@example.com"}` (both optional). The response has `claim_token`, `user_code`, `verification_uri_complete`, `expires_in` (900) and `interval` (5).
2. Ask your user to open `verification_uri_complete`, sign in or create an account, check the code and approve.
3. Poll `POST https://sofya.co/v1/agent/token` with `{"claim_token": "..."}` every `interval` seconds. It returns `400 {"error": "authorization_pending"}` until they approve, then `200` with `api_key` exactly once. Other errors: `slow_down`, `access_denied`, `expired_token` (start again).

The key works on REST and MCP and doesn't expire. Your user can set a monthly limit on it or revoke it in the dashboard.

---

### POST /v1/search

Search the web. Returns page content, not just snippets. Choose a depth to control quality vs. cost.

| Depth | Credits | What you get |
|-------|---------|--------------|
| `snippets` | 1 | Search-result snippets only, no page content. Fastest. |
| `basic` | 2 | Fetches pages, extracted content (~12,000 chars per result). Default. |

Add `include_answer: true` to any depth for an AI-synthesized answer (+2 credits). For example, basic + answer = 4 credits, a lightweight alternative to the 50-credit research endpoint when you need a quick synthesis without multi-query decomposition.

**Request:**

```json
{
  "query": "string",              // required
  "search_depth": "basic",       // "snippets" or "basic"
  "max_results": 10,              // 1-20
  "include_answer": false,        // AI answer from results (+2 credits). Combine with any depth for search + synthesis (e.g. basic = 4 credits)
  "include_domains": [],          // e.g. ["reddit.com", "github.com"]
  "exclude_domains": [],          // e.g. ["pinterest.com"]
  "topic": "general",            // "general" or "news"
  "freshness": null              // "day", "week", "month", "year", or "YYYY-MM-DD:YYYY-MM-DD"
}
```

**Response:**

```json
{
  "query": "latest AI news",
  "answer": null,
  "results": [
    {
      "title": "...",
      "url": "...",
      "content": "Extracted page content...",
      "description": "The result's snippet",
      "fetched": true,
      "published_date": "2026-03-08",
      "sublinks": [],
      "table": {}
    }
  ],
  "search_depth": "basic",
  "topic": "general",
  "elapsed_ms": 4200,
  "credits_used": 2,
  "credits_remaining": 1998,
  "altered_query": null,
  "relaxed_query": null
}
```

**When to use each depth:**

- `snippets`: You just need links and snippets. Good for finding URLs to fetch later.
- `basic`: Default. You need actual page content for analysis. For deeper/multi-source analysis with synthesis, use `/v1/research` instead.

**When to use `topic: "news"`:**

Use `topic: "news"` whenever the query is about current events, breaking news, politics, world events, or anything time-sensitive. This searches news sources specifically and returns articles with publication dates (`published_date`, as YYYY-MM-DD). Examples of queries that should use news:
- "Trump news", "Ukraine war", "stock market today"
- "latest AI announcements", "election results", "earthquake"
- Any query where recency matters more than comprehensive coverage

**Tips:**

- Use `include_answer: true` when your human asks a question and wants a direct answer, not a list of links. This is a cheaper alternative to `/v1/research` when you don't need multi-query decomposition (4 credits vs 50).
- Use `include_domains` to restrict results to specific sites (e.g. `["reddit.com"]` for discussions). Bare hostnames only: `https://` or a path is rejected with 422.
- `max_results` is a maximum, not a guaranteed count. Values outside 1-20 are clamped.
- The response `query` is the query actually run, including any `site:` operators added from your domain filters.
- Use `exclude_domains` to filter out low-quality sites.
- Use `freshness` to filter by date: `"day"` (last 24h), `"week"`, `"month"`, `"year"`, or a custom range `"2026-01-01:2026-03-01"`.

---

### POST /v1/fetch

Fetch one or more URLs and get their content as clean markdown. Max 10 URLs per request, fetched in parallel. Failed URLs are not charged, and a URL listed twice is fetched and charged once.

**Cost:** 1 credit per URL

**Request:**

```json
{
  "urls": ["https://example.com"],
  "include_raw_html": false
}
```

**Response:**

```json
{
  "results": [
    {
      "title": "Example Page",
      "url": "https://example.com",
      "content": "# Markdown content...",
      "raw_html": null,
      "published_time": null,
      "success": true,
      "error": null,
      "low_content": false,
      "notice": null
    }
  ],
  "credits_used": 1,
  "credits_remaining": 1999
}
```

**Tips:**

- Works with web pages and documents: PDF, Word (DOCX, DOC), Excel (XLSX, XLS), PowerPoint (PPTX, PPT), OpenDocument, EPUB, RTF, CSV, XML and plain text.
- The output is clean markdown, ready to feed into an LLM context window.
- Multiple URLs are fetched in parallel for maximum speed.
- Failed URLs return `success: false` with an error message and are not charged. The rest of the batch still succeeds.
- `low_content: true` means the page came back with under 300 characters of readable text (it may need JavaScript, sit behind a wall, or be image-only), and `notice` says so. It is still `success: true` and still charged.
- A single URL gives up after 50s and comes back as `success: false`, so one slow site never fails the whole request.
- Set `include_raw_html: true` to inspect embedded URLs, data attributes, iframes, or script tags that are stripped during markdown conversion. May be null for non-HTML content (PDF, DOCX, etc.) and for some sites. Same cost.

---

### POST /v1/extract

Fetch a webpage and extract specific information using AI. You provide a URL and a natural-language prompt, and get back the answer as text (not JSON). Only the first 50,000 characters of the page are read.

**Cost:** 10 credits.

**Request:**

```json
{
  "url": "string",    // required
  "prompt": "string"  // required: what to extract
}
```

**Response:**

```json
{
  "content": "Extracted information...",
  "url": "https://example.com",
  "credits_used": 10,
  "credits_remaining": 1990,
  "usage": { "input_tokens": 1430, "output_tokens": 24, "content_chars": 4820, "low_content": false },
  "notice": null
}
```

`usage.content_chars` is how many characters of page text the model received. `usage.low_content` is `true` when the page had under 40 characters of text (empty or JavaScript-rendered body) - in that case `content` comes back empty and the model is **not** called, so it never fabricates an answer from the prompt alone. Gate on `low_content` to detect pages you cannot ground on. (That is a 200, but it costs only 1 credit, the cost of fetching the page; the rest is refunded. If the page cannot be fetched at all you get a 422 and are not charged.)

**When to use extract vs. fetch:**

- Use **fetch** when you need the full page content.
- Use **extract** when you need a specific piece of information from a page (e.g. "What is the price?", "List all the authors", "Summarize the methodology section").

---

### POST /v1/research

Deep research on any topic. Decomposes your query into sub-queries, searches and reads multiple sources in parallel, then synthesizes a structured report with citations.

**Cost:** 50 credits.

**Request:**

```json
{
  "query": "string",              // required
  "topic": "general",            // "general" or "news"
  "freshness": null,             // "day", "week", "month", "year", or "YYYY-MM-DD:YYYY-MM-DD"
  "max_sources": 20              // 5-30
}
```

**Response:**

```json
{
  "query": "How do modern LLMs handle long context?",
  "report": "## Key Findings\n\n- ...",
  "sources": [
    {
      "title": "Scaling Transformer Context Windows",
      "url": "https://arxiv.org/abs/...",
      "fetched": true,
      "published_date": "2026-09-14"
    }
  ],
  "sub_queries": [
    "transformer context window scaling techniques",
    "RoPE positional encoding extensions"
  ],
  "credits_used": 50,
  "credits_remaining": 1950,
  "usage": { "input_tokens": 12400, "output_tokens": 1850 }
}
```

**When to use research vs. search:**

- Use **search** for quick lookups or when you need specific results and can process them yourself.
- Use **research** for broad questions that need coverage from many angles. The AI reads and synthesizes everything for you.

Each Key Findings bullet in `report` ends with an evidence tag: `[Confirmed]` (several sources agree), `[Reported]` (one source) or `[Contested]` (sources disagree). An identical research request sent while the same key's first one is still running waits for it and returns the same report with `credits_used: 0`.

---

## Pricing

One credit costs **$0.0025**. Pay as you go, no subscription. Buy credits from the [dashboard](https://sofya.co/dashboard), minimum $10 (4,000 credits). Purchased credits never expire. Accounts created with a GitHub account at least a year old also get 2,000 free credits, reset every 30 days and spent before purchased credits. Email sign-ups start at 0 credits (a 402 until they buy).

| Action | Credits | USD |
|--------|---------|-----|
| Search (snippets) | 1 | $0.0025 |
| Search (basic) | 2 | $0.005 |
| AI answer add-on | +2 | +$0.005 |
| Fetch | 1 per URL (failed URLs free) | $0.0025 per URL |
| Extract | 10 | $0.025 |
| Research | 50 | $0.125 |

Only 2xx responses are charged. See [Billing and errors](#billing-and-errors).

### Buying credits as an agent

You can't pay, but you can make a payment link for your user:

- `POST https://sofya.co/v1/billing/topup` with `{"amount_usd": 10}` (whole dollars, 10 to 25,000) returns `checkout_url`, `amount_usd`, `credits`, `expires_in` (86400) and `message`. Give `checkout_url` to your user. Nothing is charged until they pay on that Stripe page, and the credits are added as soon as the payment goes through. At most 20 links per account per hour.
- `GET https://sofya.co/v1/billing/balance` returns `credits`, `plan_credits`, `purchased_credits`, `credits_reset_at`, `key_monthly_cap`, `key_credits_used_this_month`, `credits_per_usd`, `min_topup_usd`, `max_topup_usd`, `top_up_url`.
- Over MCP these are the `buy_credits` (`amount_usd`) and `get_balance` tools. Both are free.

---

## Account Endpoints

### GET /v1/auth/me

Returns your balance (`credits` total, `plan_credits`, `purchased_credits`), `is_free_tier`, `credits_reset_at`, `total_requests`, `email`, `github_username` and alert settings (`new_ip_alerts_enabled`, and the low-balance email: `low_balance_alerts_enabled`, `low_balance_threshold`, null for automatic, and `low_balance_threshold_auto`). `api_key` is always null, unless the signed-in dashboard asks for it.

### GET /v1/auth/transactions

Returns your last 50 top-ups and account adjustments, newest first: `id`, `type` (`credit` or `debit`), `amount`, `endpoint`, `balance_after`, `created_at`, `has_receipt` (true for paid top-ups) and `amount_usd_cents` (what was actually charged, negative for refunds and disputes, null for adjustments; don't derive dollars from credits, older top-ups had a different rate). Per-request charges and refunds are not listed here; see usage.

### GET /v1/auth/usage

Returns daily request and credit counts per endpoint. Query params: `days` (1-90, default 7; out-of-range values are clamped), `offset_days` (default 0), `api_key_id` (optional, one key only). Rows are per day, endpoint and API key, each with `date`, `endpoint`, `request_count`, `total_credits`, `api_key_id` and `api_key_name` (both null for rows not tied to a key). An `api_key_id` that is not one of your keys returns 404.

---

## Service Status & Updates

### GET /v1/status

Public endpoint health. No auth required. Returns `overall` (operational / degraded / partial_outage / major_outage, or checking right after a restart), per-endpoint `status` (operational / degraded / down / unknown) and `latency_ms` (may be null), and a list of recent `incidents` ({status, message, timestamp}). Poll this to check if Sofya is up before running expensive workflows.

### GET /v1/updates

Public product changelog. No auth required. Returns `updates[]` newest first, each with `date`, `title`, `slug`, `body` (markdown), and `body_html` (pre-rendered). Use this to track user-visible API and product changes without scraping a page.

---

## Example Workflows

### News and current events

```
1. Search with topic: "news" to get recent news articles with dates
2. Optionally use freshness: "day" to limit to the last 24 hours
```

### Quick lookup

```
1. Search with search_depth: "snippets" to find relevant URLs
2. Fetch the most relevant result for full content
```

### Research a topic

```
1. Call /v1/research with your question
2. Get a structured report with citations, done in one request
```

### Pull specific facts from pages

```
1. Search to find relevant pages
2. Extract the fact you need from each result URL
3. Return the answers to your human
```

### Monitor a page for changes

```
1. Fetch the URL periodically
2. Compare content to previous fetch
3. Notify your human of changes
```

---

## Billing and errors

**The billing rule:** a 2xx response is charged exactly its `credits_used`. Anything else costs nothing. Credits are reserved when a request starts and refunded automatically if it fails or times out, so retrying after an error never double-charges. For fetch, a URL that fails inside a 200 (`success: false`) is refunded too, and `credits_used` already excludes it. If `include_answer` produces no answer, `answer` is null and the add-on is refunded. An extract that returns `low_content: true` is a successful 200 charged only 1 credit, and `credits_used` says so.

| Code | Meaning | What to do |
|------|---------|------------|
| 400 | A request-level problem, e.g. an empty `urls` list | Fix the request. Don't retry as-is. |
| 401 | Invalid or missing API key, or the key's IP allowlist doesn't include your IP | Check the key. Don't retry. No key: see [https://sofya.co/auth.md](https://sofya.co/auth.md). |
| 402 | Insufficient credits (`"error": "insufficient_credits"`, with `credits_required`, `credits_available` and `top_up_url`), or the key hit its owner's monthly limit (`"error": "key_cap_reached"`) | Out of credits: give your user `top_up_url`, or make a payment link with `POST /v1/billing/topup` (MCP: `buy_credits`). Key limit: ask the owner to raise it in the dashboard; buying credits won't help. |
| 422 | An invalid parameter (unknown `search_depth` or `topic`, bad `freshness`, a domain that isn't a bare hostname, more than 10 URLs, a query or prompt too long), or on extract, the target page couldn't be fetched (404, blocked, no DNS, timed out) | Fix the parameter, or check the URL. Retrying as-is won't help. |
| 429 | Rate limited (30 requests/second per API key, counted separately for REST and MCP). Fetch and extract also return it when one account has over 100 URLs in progress at once, or over 600 failed (refunded) URLs in 10 minutes. | Wait the `Retry-After` seconds, then retry. |
| 502 | Internal error | Retry, ideally with backoff. |
| 503 | Search capacity is temporarily unavailable | Retry shortly. |
| 504 | Hit the time limit (60s; 55s for research) | Retry. For research, narrow the query or lower `max_sources`. |

Over MCP the same errors come back as a tool result with `isError: true` and the message, not as HTTP status codes.

None of these are charged. There are no idempotency keys: rely on the rule above, and if you reserve funds on your side, release them on any non-2xx. One exception covers retries of the most expensive call: a research request identical to one the same key still has running (same query, ignoring case and spacing, same options) waits for that run and returns its report with `credits_used: 0`, instead of starting and charging a second run.

Every response carries an `X-Request-Id` header (send your own, up to 64 letters, digits or `. _ : -`, and it is echoed back). Quote it when reporting a problem. Responses to keyed requests also carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until the per-second window frees up).

---

## Checking if Sofya is up

If a Sofya API call fails or hangs and you want to know whether the service itself is degraded, check `GET https://sofya.co/v1/status` for per-endpoint health, or the off-host status feed (no auth, no API key needed), which watches the website from a separate server:

```
curl https://status.sofya.co/api/v2/status.json
```

Returns `{"status": {"indicator": "none", "description": "All Systems Operational"}}` when healthy. Any non-`none` indicator (`minor` = degraded, `major` = down) means you should back off retries or surface a notice. The status page runs on a separate server, so it stays online even when https://sofya.co is unreachable - which is exactly when you need it.

---

## Links

| Resource | URL |
|----------|-----|
| Dashboard | https://sofya.co/dashboard |
| API Docs | https://sofya.co/docs |
| Playground | https://sofya.co/playground |
| MCP Endpoint | https://mcp.sofya.co/mcp |
| Status page (human) | https://status.sofya.co |
| Status JSON (Statuspage v2) | https://status.sofya.co/api/v2/summary.json |
| Status JSON (overall only) | https://status.sofya.co/api/v2/status.json |
| Status JSON (Sofya-native, richer) | https://status.sofya.co/api/status.json |