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

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.
| Feature | Search API | Fetch API |
|---|---|---|
| Main job | Discover relevant pages | Read one specific page |
| Input | A text query | One HTTP(S) URL |
| Output | A ranked list of results | Page 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) | 100 | 25 (plus a per-domain limit) |
↔️ Scroll horizontally to see all columns

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.
python -m venv .venvmacOS / Linux
source .venv/bin/activate
python -m pip install requests
export DEEPSEEK_API_KEY="your_api_key_here"Windows PowerShell
.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
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `query` | string | Yes | – | 1–128 Unicode characters, not all whitespace. Supports operators. |
| `count` | integer | No | `10` | Results to return, 1–50. |
| `need_content` | boolean | No | `false` | When `true`, `content` holds page text where available; when `false` it is always an empty string. |
| `ori_question` | string | No | – | The user's original question (1–128 characters); can help query understanding. |
| `localization` | string | No | `""` | 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:
{
"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.
"""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:
python deepseek_search_fetch.pyYour 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
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:
{ "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
Authorizationheader, 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
| API | Price per 1,000 successful requests | Per 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.
| Tasks | Searches | Fetches | Search + Fetch cost |
|---|---|---|---|
| 100 | 100 | 300 | $0.30 |
| 1,000 | 1,000 | 3,000 | $3.00 |
| 10,000 | 10,000 | 30,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": ...}}.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Invalid headers or body (bad JSON, bad field value) | Fix the request. |
| 401 | Missing or invalid API key | Check `DEEPSEEK_API_KEY`. |
| 402 | Insufficient balance | Top up, then retry. |
| 403 | Fetch rejected by security policy | Don't retry unchanged; check the URL. |
| 404 | Fetch couldn't get usable content (unavailable URL, blocked by site, target-site error, unsupported type, no extractable Markdown) | Verify the URL; try another source. |
| 408 | Reading the request body timed out | Wait and retry. |
| 413 | Request body too large | Shorten the request. |
| 429 | Search or Fetch QPS limit exceeded | Back off with jitter and lower concurrency. |
| 500 / 504 | Internal error / downstream timeout | Wait 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
- DeepSeek Search & Fetch guide
- Search API reference
- Fetch API reference
- Models & Pricing
- DeepSeek changelog
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 →
Related Articles
Tech NewsNov 18, 2025 • 2 min read
"Please unblock challenges.cloudflare.com" Message? Here's What's Really Happening
If you're getting that "Please unblock challenges.cloudflare.com" message today, take a deep breath. You're not being hacked, your device isn't infected, and you didn't do anything wrong.
AI & Machine LearningDec 30, 2025 • 16 min read
DeepSeek vs ChatGPT vs Gemini 2026: Which AI is Better? (Tested 30 Days)
Gemini topped India's AI searches in 2025. I tested GPT-5.2, Gemini 3 Flash, and DeepSeek V3.2 for 30 days. Real results, privacy warnings, honest winner.
AI & Machine LearningJan 4, 2026 • 19 min read
Zero Code? Indians Making ₹80K/Month With AI "Vibe Coding"
1.8 million developers worldwide pay for AI to write code. Indians earning ₹80K/month with vibe coding zero traditional programming needed. Complete 2026 guide inside.
Comments (0)
Leave a Comment
No comments yet
Be the first to share your thoughts!