DeepSeek Search & Fetch API Tutorial (2026): Python Setup, Examples, and Pricing

Aditya Kachhawa

•11 min read
Developer Tools
Diagram showing DeepSeek Search finding web pages and Fetch returning selected page content as Markdown
Click to expand

The DeepSeek Search API finds web pages for a query and returns ranked, structured results. The Fetch API retrieves one URL and returns the page as Markdown. Together they give AI agents and retrieval-augmented generation (RAG) apps — apps that pull outside documents into a model's prompt — access to live web content.

This guide covers the endpoints, a copy-paste Python example, what each request costs, rate limits, error handling, and the November 1, 2026 billing change.

Quick answer

  • Search: POST https://api.deepseek.com/v1/tools/search. Fetch: POST https://api.deepseek.com/v1/tools/fetch.
  • Price when you call them directly: $0.75 per 1,000 successful requests, for each API.
  • Temporary exemption: DeepSeek's guide says server-side Search/Fetch tool calls made when you use the DeepSeek API through DeepSeek Harness, Claude Code, or Codex are currently free. Per-request billing starts at 00:00 Beijing time on November 1, 2026 (October 31, 2026 at 16:00 UTC, which is 9:30 PM IST). As of October 27, 2026, the guide still describes that cutoff as upcoming.
  • Retries: DeepSeek says failed requests, empty Search results and timeouts are not billed, but a retry after a client-side timeout can still produce an extra billable request. See Retries and billing.
  • Last checked: the Search & Fetch guide and API references on October 27, 2026. Prices and promotions change, so re-check the guide and pricing page before you deploy.

What are the DeepSeek Search and Fetch APIs?

Both are raw JSON APIs that use the same bearer-token authentication as the rest of the DeepSeek API. They are separate from a chat-completion call: they retrieve source material, and a language model can then summarize or reason over it in a second step.

FeatureSearch APIFetch API
Main jobDiscover relevant pagesRead one specific page
InputA text queryOne HTTP(S) URL
OutputA ranked list of resultsPage title + Markdown content
Endpoint`/v1/tools/search``/v1/tools/fetch`
Direct price$0.75 / 1,000 successful requests$0.75 / 1,000 successful requests
QPS limit (per account)10025 (plus a per-domain limit)

↔️ Scroll horizontally to see all columns

Workflow diagram from search query to ranked URLs to fetched Markdown content
Click to expand
A typical workflow searches first, then fetches only the pages needed.

You don't need both for every task. If you only want titles and URLs, Search is enough. If you already know the page you need, call Fetch directly.

What you can build: research assistants that cite sources, RAG pipelines that add fresh web context, coding agents that look up current documentation, and single-page summarizers. These are patterns, not turnkey products — you still build the model orchestration, source attribution, error handling and safety controls.

Before you start

You need a DeepSeek API account and key (created on the DeepSeek platform), Python 3.10+, and the requests package. Keep the key out of source code, public repositories and browser JavaScript; use an environment variable or a secrets manager.

Bash / Terminal
python -m venv .venv

macOS / Linux

Bash / Terminal
source .venv/bin/activate
python -m pip install requests
export DEEPSEEK_API_KEY="your_api_key_here"

Windows PowerShell

Bash / Terminal
.venv\Scripts\Activate.ps1
python -m pip install requests
$env:DEEPSEEK_API_KEY = "your_api_key_here"

Replace the placeholder with your own key, and never share screenshots or logs that contain it.

Search API: request fields

FieldTypeRequiredDefaultNotes
`query`stringYes–1–128 Unicode characters, not all whitespace. Supports operators.
`count`integerNo`10`Results to return, 1–50.
`need_content`booleanNo`false`When `true`, `content` holds page text where available; when `false` it is always an empty string.
`ori_question`stringNo–The user's original question (1–128 characters); can help query understanding.
`localization`stringNo`""`An IP address used to localize results.

↔️ Scroll horizontally to see all columns

Unknown fields are ignored. The Python helper below sends only query, count and need_content; it does not send ori_question or localization.

Supported query operators include site:, -site:, exact phrases in quotes, -term, lang:, after: and before:. Example:

JSON
{
  "query": "DeepSeek site:github.com OR site:huggingface.co lang:en after:2025",
  "count": 10
}

A successful response is HTTP 200 with a results array (empty if nothing matches). Each result has rerank_score (0–3; 1.5+ is treated as high quality), title, url, snippet (short, for UIs), summary (longer, meant for models), content, site_name, site_icon, date and authority (0–4, -1 if unknown). site_name, site_icon and date can be null, so handle missing values.

Python example: search, then fetch

Create deepseek_search_fetch.py. It validates input locally, reads the key from the environment, sets timeouts, and retries only the errors DeepSeek says are transient (408, 429, 5xx). Malformed URLs, bad counts and unexpected response shapes fail with a clear error instead of a confusing one later.

deepseek_search_fetch.py
"""Minimal DeepSeek Search + Fetch example using only `requests`.

The helpers send only the fields they need: `query`, `count` and
`need_content` for Search, and `url` for Fetch. The API also accepts the
optional `ori_question` and `localization` Search fields, which this
example does not send.
"""

import os
import random
import time
from typing import Any
from urllib.parse import urlparse

import requests

BASE_URL = "https://api.deepseek.com"
RETRYABLE_STATUS = {408, 429}  # plus any 5xx, per DeepSeek's retry guidance
MAX_ATTEMPTS = 3
MAX_URL_LENGTH = 8192


class DeepSeekAPIError(RuntimeError):
    """Raised for API errors, exhausted retries or unexpected responses."""


def _headers() -> dict[str, str]:
    api_key = os.getenv("DEEPSEEK_API_KEY")
    if not api_key:
        raise DeepSeekAPIError("Set the DEEPSEEK_API_KEY environment variable first.")
    return {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }


def _error_message(response: requests.Response) -> str:
    """Build a short error message without assuming the body is valid JSON."""
    try:
        message = response.json()["error"]["message"]
    except (ValueError, KeyError, TypeError):
        message = response.text[:200]
    return f"HTTP {response.status_code}: {message}"


def _post(path: str, payload: dict[str, Any]) -> dict[str, Any]:
    """POST JSON and return the decoded object.

    Retries only transient failures (408, 429, 5xx, timeouts, connection
    errors). Note: if a request times out on your side, you cannot tell
    whether DeepSeek still completed it, so a retry may be billed again.
    """
    headers = _headers()
    last_error = "unknown error"
    for attempt in range(1, MAX_ATTEMPTS + 1):
        try:
            response = requests.post(
                f"{BASE_URL}{path}", headers=headers, json=payload, timeout=45
            )
        except (requests.Timeout, requests.ConnectionError) as exc:
            last_error = f"network error: {exc.__class__.__name__}"
        else:
            if response.ok:
                try:
                    data = response.json()
                except ValueError:
                    raise DeepSeekAPIError("Response was not valid JSON") from None
                if not isinstance(data, dict):
                    raise DeepSeekAPIError("Unexpected response shape (not a JSON object)")
                return data

            last_error = _error_message(response)
            transient = response.status_code in RETRYABLE_STATUS or response.status_code >= 500
            if not transient:
                # 400/401/402/403/404/413: change the request or account, don't retry.
                raise DeepSeekAPIError(last_error)

        if attempt < MAX_ATTEMPTS:
            time.sleep(min(2**attempt, 8) + random.random())  # backoff with jitter

    raise DeepSeekAPIError(f"Giving up after {MAX_ATTEMPTS} attempts ({last_error})")


def search_web(
    query: str, count: int = 5, need_content: bool = False
) -> list[dict[str, Any]]:
    """Return ranked search results (an empty list when nothing matches)."""
    if not isinstance(query, str):
        raise ValueError("query must be a string")
    query = query.strip()
    if not 1 <= len(query) <= 128:
        raise ValueError("query must be 1 to 128 characters")
    if isinstance(count, bool) or not isinstance(count, int) or not 1 <= count <= 50:
        raise ValueError("count must be an integer between 1 and 50")

    payload = {"query": query, "count": count, "need_content": need_content}
    results = _post("/v1/tools/search", payload).get("results")
    if not isinstance(results, list):
        raise DeepSeekAPIError("Unexpected response shape: 'results' is not a list")
    return [item for item in results if isinstance(item, dict)]


def _validate_fetch_url(url: str) -> None:
    """Raise ValueError unless `url` is a usable absolute HTTP(S) URL."""
    if not isinstance(url, str) or not url:
        raise ValueError("url must be a non-empty string")
    if len(url) > MAX_URL_LENGTH:
        raise ValueError(f"url must be at most {MAX_URL_LENGTH} characters")
    if any(ch.isspace() or ord(ch) < 32 for ch in url):
        raise ValueError("url must not contain spaces or control characters")

    try:
        parsed = urlparse(url)
        port = parsed.port  # raises ValueError for an invalid port
    except ValueError as exc:
        raise ValueError(f"malformed url: {exc}") from None

    if parsed.scheme.lower() not in {"http", "https"}:
        raise ValueError("url must use http or https")
    if not parsed.hostname:
        raise ValueError("url must include a hostname")
    if parsed.username is not None or parsed.password is not None:
        raise ValueError("URLs containing a username or password are not allowed")
    if port == 0:
        raise ValueError("malformed url: port 0 is not valid")


def fetch_page(url: str) -> dict[str, str]:
    """Return {"title": ..., "content": <Markdown>} for one HTTP(S) URL."""
    _validate_fetch_url(url)

    page = _post("/v1/tools/fetch", {"url": url})
    content = page.get("content")
    if not isinstance(content, str):
        raise DeepSeekAPIError("Unexpected response shape: 'content' is missing")
    title = page.get("title")
    return {"title": title if isinstance(title, str) else "", "content": content}


def main() -> None:
    results = search_web("DeepSeek Search and Fetch API documentation", count=5)
    if not results:
        raise SystemExit("No results. Empty searches are not billed; try another query.")

    for i, item in enumerate(results, start=1):
        print(f"{i}. {item.get('title') or '(untitled)'}")
        print(f"   {item.get('url', '')}")
        print(f"   {item.get('snippet', '')}\n")

    first_url = results[0].get("url")
    if not first_url:
        raise SystemExit("The first result had no URL to fetch.")

    page = fetch_page(first_url)
    print(f"Fetched: {page['title']}\n")
    print(page["content"][:1500])  # a sample, not the full page


if __name__ == "__main__":
    try:
        main()
    except (DeepSeekAPIError, ValueError) as error:
        raise SystemExit(f"Error: {error}")

Run it:

Bash / Terminal
python deepseek_search_fetch.py

Your results will differ with the query and the search index. The example has not been run against a live DeepSeek account by TechAffiliate. Its accompanying unit tests mock every HTTP call, so they check the helper's own logic (validation, retries, error handling), not how the live API behaves.

Retries and billing

DeepSeek's documentation says failed requests, empty Search results and timeouts are not billed. That is a statement about DeepSeek's billing rules. It doesn't tell your client what actually happened to a request.

  • The API returned an error (408, 429, 5xx): by the documented rules that attempt is not billed, so retrying it is reasonable.
  • Your client timed out or lost the connection: you can't tell whether DeepSeek finished the original request before the response was lost. If it did, that request may have been billable, and your retry is a second request that is billable too if it succeeds.

So don't assume every retry is free. Keep MAX_ATTEMPTS low, count the calls your application actually makes, and compare that with the usage and balance shown in your DeepSeek account.

need_content: Search content or Fetch?

With need_content: false (the default), Search returns snippet and summary text but an empty content field. Set it to true to get page content inline where it's available, or keep it false and call Fetch only for the pages you actually select. Which is cheaper or better depends on your workflow, so measure both. A Search request is billed once however many results it returns, and need_content is not listed as a separate charge.

cURL example

Bash / Terminal
curl https://api.deepseek.com/v1/tools/search \
  -H "Authorization: Bearer $DEEPSEEK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "DeepSeek Search API official documentation", "count": 5, "need_content": false}'

Fetch API: read a page as Markdown

Fetch takes one url (HTTP or HTTPS, with a host, no username or password, at most 8,192 characters) and returns:

JSON
{ "title": "Example article", "content": "# Example article\n\nArticle content." }

title is empty if unavailable. Fetching is subject to the target site's availability and access policies, so some pages fail with a 403 or 404 or produce no extractable Markdown.

Security notes for agent builders

  • Fetched pages are untrusted input. A page can contain prompt-injection text. Tell your model to treat retrieved content as reference material, not as instructions, and keep tools and credentials away from the text being summarized.
  • Protect your API key. Call these endpoints from a server, never from frontend code, never log the Authorization header, and rotate the key if it leaks.
  • Know whose server makes the request. When you call Fetch, DeepSeek's service requests the page, not your server. Server-side request forgery (SSRF) — tricking a server into requesting internal addresses — becomes your problem if your own code follows links or downloads user-supplied URLs. Validate those URLs and block private network ranges in that code.

Pricing

APIPrice per 1,000 successful requestsPer request
Search$0.75$0.00075
Fetch$0.75$0.00075

↔️ Scroll horizontally to see all columns

Billing is per successful request, not per token or per result. According to the official guide, a request is billable only when it returns HTTP 200 and produces a result: Search must return a non-empty results array, and Fetch must return page content. The guide also says failed requests, empty search results and timeouts are not billed, and that count does not change the price. Model tokens are billed separately under the model pricing.

Worked example

Assume each research task makes 1 Search and 3 Fetch requests, and each of those four requests succeeds exactly once: 4 requests × $0.00075 = $0.003 per task.

TasksSearchesFetchesSearch + Fetch cost
100100300$0.30
1,0001,0003,000$3.00
10,00010,00030,000$30.00

↔️ Scroll horizontally to see all columns

This excludes model tokens, hosting, storage and other services, and it excludes extra billable calls caused by retries (see Retries and billing).

Is the DeepSeek Search API free?

Direct calls are not free. They are billed at the rates above when they succeed.

The exception is narrow. When you use the DeepSeek API through DeepSeek Harness, Claude Code, or Codex, the server-side Search/Fetch tool calls they trigger are currently free of charge. DeepSeek's guide says per-request billing for those calls starts at 00:00 Beijing time on November 1, 2026, which is October 31, 2026 at 16:00 UTC (9:30 PM IST). As of October 27, 2026 that cutoff is still ahead. If you are reading this after it has passed, check the official pricing page for the current status rather than assuming either outcome.

  • Direct Search/Fetch API call: billed at $0.75 per 1,000 successful requests.
  • Server-side tool call via Harness, Claude Code or Codex: free until the cutoff, then billed per request, per the current notice.

The exemption does not make DeepSeek model usage, or anything else in those tools, free.

Rate limits and errors

Limits are per account, regardless of API key, and Search and Fetch are limited independently: 100 QPS (queries per second) for Search and 25 QPS for Fetch. Fetch also has a per-domain QPS limit, which the guide does not give a number for. Going over either limit returns HTTP 429. Limits can change, so check the guide.

Errors use an OpenAI-compatible JSON shape: {"error": {"message": ..., "type": ..., "param": null, "code": ...}}.

StatusMeaningWhat to do
400Invalid headers or body (bad JSON, bad field value)Fix the request.
401Missing or invalid API keyCheck `DEEPSEEK_API_KEY`.
402Insufficient balanceTop up, then retry.
403Fetch rejected by security policyDon't retry unchanged; check the URL.
404Fetch couldn't get usable content (unavailable URL, blocked by site, target-site error, unsupported type, no extractable Markdown)Verify the URL; try another source.
408Reading the request body timed outWait and retry.
413Request body too largeShorten the request.
429Search or Fetch QPS limit exceededBack off with jitter and lower concurrency.
500 / 504Internal error / downstream timeoutWait and retry, with a cap.

↔️ Scroll horizontally to see all columns

Search and Fetch are read-only, so repeating a request doesn't change any data, but see Retries and billing for the cost caveat. Don't retry other 4xx errors without changing the request.

Limitations

  • Fetch can't read every site: pages may be blocked, unsupported or impossible to convert.
  • Search results are ranked, not exhaustive. Try several queries and check important claims against primary sources.
  • Snippets and fetched pages can be outdated or wrong; keep each source URL and date.
  • Model inference is a separate cost if you summarize results with an LLM.

FAQ

How much does the DeepSeek Search API cost? $0.75 per 1,000 successful requests; Fetch is the same, billed separately. By DeepSeek's rules, failed requests, empty results and timeouts aren't billed.

Does asking for 50 results cost more than 5? No. count changes the result size, not the per-request price.

Is it free? Not for direct calls. Server-side tool calls through DeepSeek Harness, Claude Code and Codex are free until the November 1, 2026 (Beijing time) cutoff described above.

Can retries add to my bill? They can. A retry after an error response isn't billed by the documented rules, but a client-side timeout may hide a request DeepSeek did complete, so a retry can add a second billable call.

Does Fetch always return the whole page? No. It returns extractable content when the site allows retrieval.

Next steps

Start with one Search call, fetch only the pages you need, and track your successful-request counts before scaling. If you're also exploring newer open models for agent work, see our GLM-5.2 access and benchmark guide.

Official sources

Found this helpful? Share it with others!

Affiliate Disclosure

TechAffiliate may earn a commission if you purchase through our links. This helps support our work but does not influence our reviews. We provide our honest assessments of products. See my testing methodology →

Comments (0)

Leave a Comment

No comments yet

Be the first to share your thoughts!