Search API
Search the web with a single POST request
Endpoint
POST https://api.serpex.dev/api/search
GET https://api.serpex.dev/api/searchBoth GET (query params) and POST (JSON body) are supported. POST is recommended for production.
Authentication
Pass your API key as a Bearer token in the Authorization header.
Authorization: Bearer sk_...The x-api-key header is also accepted as a fallback.
Request Parameters
Prop
Type
Code Examples
curl -X POST https://api.serpex.dev/api/search \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"q": "best JavaScript frameworks 2025"}'import { SerpexClient } from "serpex";
const client = new SerpexClient("sk_your_api_key");
const results = await client.search({
q: "best JavaScript frameworks 2025",
});
console.log(results.results[0].title);
console.log(results.metadata.credits_used);from serpex import SerpexClient
client = SerpexClient("sk_your_api_key")
results = client.search({
"q": "best JavaScript frameworks 2025",
})
print(results.results[0].title)
print(results.metadata.credits_used)Response
{
"id": "srp_01j9abc...",
"query": "best JavaScript frameworks 2025",
"engines": ["duckduckgo"],
"results": [
{
"title": "Top JavaScript Frameworks in 2025",
"url": "https://example.com/js-frameworks",
"snippet": "React, Vue, and Svelte remain the top choices for JavaScript developers...",
"position": 1,
"engine": "duckduckgo",
"content": "# Top JavaScript Frameworks in 2025\n\nReact, Vue, and Svelte remain the top choices for JavaScript developers heading into 2025..."
},
{
"title": "2025 Frontend Framework Comparison",
"url": "https://example.com/frontend-comparison",
"snippet": "A deep dive into performance and DX across the major frameworks...",
"position": 2,
"engine": "duckduckgo",
"content_error": "blocked"
}
],
"metadata": {
"number_of_results": 10,
"credits_used": 2,
"from_cache": false,
"status": "success",
"response_time": 1240,
"timestamp": "2025-06-21T10:30:00.000Z",
"content_requested": 5,
"content_delivered": 4
}
}The example above was requested with include_content: true and content_results: 5. The first result's page fetch succeeded (content holds the markdown); the second failed (content_error explains why) — a result only ever carries one of the two keys, never both, and both keys are omitted entirely on a plain search.
Response Fields
Prop
Type
Credits
- 1 credit per successful search.
- 0 credits when your organization repeats the same request within about 5 minutes (see Per-org caching).
- 1 credit when the query genuinely has no results, confirmed by multiple search engines (
metadata.no_results_verified: true). - 0 credits when a search comes back empty but could not be confirmed as genuinely empty (
metadata.no_results_verified: false) — see below. - Errors are never charged.
- With
include_content: true: 2 credits forcontent_results: 5, 4 credits forcontent_results: 10— instead of the usual 1. Page-content fetching is best-effort: in practice content is delivered for roughly 79% of requested results. Never expect 100% — some pages are blocked or disallowed byrobots.txtand come back withcontent_errorinstead ofcontent(results that fail content fetch still return their normal title/url/snippet; only the content fetch itself is best-effort, and a failed fetch is never billed on its own).- Charged by how much content was actually delivered, not just requested: 1-5 delivered → 2 credits, 6-10 delivered → 4 credits.
- If content delivery fails for every requested result, you're only charged the plain-search rate of 1 credit — you're never charged the content rate for content you didn't get.
When are empty results charged?
An empty search always returns HTTP 200 with results: [] and metadata.status: "no_results". Whether it costs a credit depends on whether we could confirm it is genuinely empty:
| Situation | no_results_verified | Charged |
|---|---|---|
| The query genuinely has no results — confirmed by multiple search engines | true | 1 credit (a normal search) |
| The same confirmed-empty query repeated by your organization within about 5 minutes | true | Free (charged once per 5 minutes, like any repeated search) |
| The same confirmed-empty query repeated after that, or by another organization (served from cache) | true | 1 credit |
| Empty, but we could not confirm it is genuinely empty (for example, a search engine was unavailable) | false | Never |
Error response (HTTP 503 etc.) | — | Never |
| Successful results served from cache | — | Unchanged (see Per-org caching) |
How a query is confirmed empty. All of these must hold: every search engine we route that query to completed without an error or timeout, none of them returned a result, every one that came back empty explicitly said the query has no results (an empty page without that notice prevents confirmation), and at least two of them made that explicit statement. A confirmed-empty answer is cached for 15 minutes, so identical repeats return instantly.
Which queries can be confirmed. Both plain queries and queries with a positive site: operator (for example site:example.com "exact address") can be confirmed when every engine we route them to shows its own "no results" page. If any engine errors, times out, or returns an empty page without that notice, the empty response is not charged. Some query shapes are rarely confirmable: with two or more quoted phrases, or filetype: plus a quoted phrase, one of the engines is known to return false empties, so its answer does not count. Negative-only -site: queries are never confirmed. With include_content: true, a confirmed-empty search is charged the plain-search rate of 1 credit.
Three fields on metadata explain the outcome (present only when status is "no_results"):
no_results_verified—truewhen the query was confirmed empty as described above.charged—truewhen this request deducted credits (credits_usedholds the amount).message— a human-readable explanation of both. The same text is also returned as the top-levelmessage.
Confirmed empty (charged):
{
"query": "site:example.com \"4821 Nonexistent Harbor Lane\"",
"results": [],
"message": "No results found: this query genuinely has no results (confirmed by multiple search engines). Charged as a normal search.",
"metadata": {
"number_of_results": 0,
"credits_used": 1,
"from_cache": false,
"status": "no_results",
"no_results_verified": true,
"charged": true,
"message": "No results found: this query genuinely has no results (confirmed by multiple search engines). Charged as a normal search."
}
}Not confirmed (not charged):
{
"query": "\"4821 Nonexistent Harbor Lane\" Springfield",
"results": [],
"message": "No results found. We could not confirm this query is genuinely empty (for example, a search engine was unavailable), so you were not charged.",
"metadata": {
"number_of_results": 0,
"credits_used": 0,
"from_cache": false,
"status": "no_results",
"no_results_verified": false,
"charged": false,
"message": "No results found. We could not confirm this query is genuinely empty (for example, a search engine was unavailable), so you were not charged."
}
}Unconfirmed empty responses are never cached, so repeating the request runs a fresh search.