# Serpex documentation > The full text of every Serpex docs page (https://serpex.dev/docs) in one file. Site index: https://serpex.dev/llms.txt # Serpex Docs Source: https://serpex.dev/docs > Search API and web extraction API for developers — structured results, clean markdown, full-browser rendering. > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. ## What is Serpex? Serpex is a developer API for two things: * **Search** — send a query, get structured web-search results (title, URL, snippet, position) in JSON, optionally with each page's content as Markdown. No browser required. 1 credit per fresh search. * **Extract / Crawl** — send URLs, get clean page content in Markdown or HTML. Up to 10 URLs per request. 1 credit per URL, or 5 credits per URL with stealth mode for JavaScript-heavy pages. Base URL: `https://api.serpex.dev`. Auth: `Authorization: Bearer sk_...`. - [Quickstart](https://serpex.dev/docs/getting-started/quickstart): Make your first search and extract call in under 5 minutes. - [Authentication](https://serpex.dev/docs/getting-started/authentication): Bearer API keys — how to get one and keep it safe. - [Search API](https://serpex.dev/docs/api/search): Full parameter reference for GET and POST /api/search. - [TypeScript SDK](https://serpex.dev/docs/sdks/typescript): Official SDK for Node.js — npm install serpex. ## APIs at a glance | Endpoint | What it does | Cost | | ---------------------------------------- | --------------------------------------- | ---------------------------------------- | | `GET` or `POST /api/search` | Web search: returns ranked results | 1 credit (3 or 6 with `include_content`) | | `POST /api/crawl` | Extract page content → Markdown or HTML | 1 credit/URL | | `POST /api/crawl` with `"stealth": true` | Same, for JavaScript-heavy pages | 5 credits/URL | | `GET /api/usage` | Credit balance and request counts | Free | ## Quick example ```bash curl -X POST https://api.serpex.dev/api/search \ -H "Authorization: Bearer sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{"q": "openai gpt-4o"}' ``` Response: ```json { "results": [ { "title": "GPT-4o — OpenAI", "url": "https://openai.com/gpt-4o", "snippet": "GPT-4o is our most capable multimodal model...", "position": 1, "engine": "auto" } ], "metadata": { "credits_used": 1, "from_cache": false, "status": "success", "response_time": 1105 } } ``` --- # Getting Started Source: https://serpex.dev/docs/getting-started > Set up Serpex and make your first search and extract calls. > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. Everything you need to start using the Serpex API — get a key, authenticate, understand limits, and pricing. - [Quickstart](https://serpex.dev/docs/getting-started/quickstart): Make your first search and extract call in under 5 minutes. - [Authentication](https://serpex.dev/docs/getting-started/authentication): Bearer API keys — how to get one and keep it safe. - [Rate Limits](https://serpex.dev/docs/getting-started/rate-limits): Per-plan request rates, stealth queueing, and retry guidance. - [Pricing](https://serpex.dev/docs/getting-started/pricing): Credits, per-request cost, and tier pricing. --- # Quickstart Source: https://serpex.dev/docs/getting-started/quickstart > Make your first Serpex search and extract request in under 5 minutes. > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. ## Get your API key Sign in at [app.serpex.dev](https://app.serpex.dev), then go to **API Keys** in the sidebar and create a key. It starts with `sk_`. You start with **200 free credits** — no credit card required. ## Run your first search `POST /api/search` with your query. Only `q` is required. ```bash curl -X POST https://api.serpex.dev/api/search \ -H "Authorization: Bearer sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{"q": "best practices for REST API design"}' ``` ```typescript import { SerpexClient } from "serpex"; const client = new SerpexClient("sk_your_api_key"); const result = await client.search({ q: "best practices for REST API design", }); console.log(result.results[0].title); console.log(result.metadata.credits_used); // 1 for a fresh result, 0 for your own repeat within 5 minutes ``` ```python from serpex import SerpexClient client = SerpexClient("sk_your_api_key") result = client.search({"q": "best practices for REST API design"}) print(result.results[0].title) print(result.metadata.credits_used) # 1 for a fresh result, 0 for your own repeat within 5 minutes ``` **Response:** ```json { "query": "best practices for REST API design", "results": [ { "title": "RESTful API Design Best Practices", "url": "https://example.com/rest-api-best-practices", "snippet": "A comprehensive guide to designing clean, scalable REST APIs...", "position": 1, "engine": "auto" }, { "title": "Top 10 REST API Design Tips", "url": "https://example.com/api-design-tips", "snippet": "Follow these guidelines to build APIs your consumers will love...", "position": 2, "engine": "auto" } ], "metadata": { "number_of_results": 10, "credits_used": 1, "from_cache": false, "status": "success", "response_time": 1420 } } ``` ## Extract content from a URL `POST /api/crawl` fetches one or more pages and returns clean content. Up to 10 URLs per request. ```bash curl -X POST https://api.serpex.dev/api/crawl \ -H "Authorization: Bearer sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "urls": ["https://en.wikipedia.org/wiki/Representational_state_transfer"], "format": "markdown" }' ``` ```typescript const extracted = await client.extract({ urls: ["https://en.wikipedia.org/wiki/Representational_state_transfer"], format: "markdown", }); const page = extracted.results[0]; if (page.success) { console.log(page.markdown); // clean markdown ready for your LLM } console.log(extracted.metadata.credits_used); // 1 per URL crawled ``` ```python extracted = client.extract({ "urls": ["https://en.wikipedia.org/wiki/Representational_state_transfer"], "format": "markdown", }) page = extracted.results[0] if page.success: print(page.markdown) # clean markdown ready for your LLM print(extracted.metadata.credits_used) # 1 per URL crawled ``` **Response:** ```json { "results": [ { "url": "https://en.wikipedia.org/wiki/Representational_state_transfer", "success": true, "markdown": "# Representational state transfer\n\nRepresentational state transfer (REST) is a software architectural style...", "status_code": 200 } ], "metadata": { "successful_crawls": 1, "failed_crawls": 0, "credits_used": 1 } } ``` > For JavaScript-heavy pages that need full rendering to load completely, add `"stealth": true` to your request. Stealth mode costs **5 credits per URL** instead of 1. ## Check your credit usage Always read `metadata.credits_used` in the response — it tells you exactly what was charged for that request. * `credits_used: 0`: your organization already paid for the same request within the last 5 minutes, so this repeat is free. * `credits_used: 1` — fresh search or normal crawl URL. * `credits_used: 3` or `6` — search with `include_content: true` (`content_results: 5` or `10`). See [Search API Reference](https://serpex.dev/docs/api/search#credits) for details — page content is best-effort, so a request that delivers no content at all is only charged the plain-search rate of 1. * `credits_used: 5` — stealth crawl URL. * Errors are **never charged**. An empty search (`metadata.status: "no_results"`) is charged 1 credit **only** when `metadata.no_results_verified` is `true` (the query genuinely has no results, confirmed across multiple search sources); otherwise it is not charged. `metadata.charged` and `metadata.message` explain the outcome — see [When are empty results charged?](https://serpex.dev/docs/api/search#when-are-empty-results-charged). ## Next steps - [Authentication](https://serpex.dev/docs/getting-started/authentication): How API keys work and how to keep them safe. - [Rate Limits](https://serpex.dev/docs/getting-started/rate-limits): Per-plan request limits and how to handle 429s. - [Search API Reference](https://serpex.dev/docs/api/search): Full parameter reference for `/api/search`. - [TypeScript SDK](https://serpex.dev/docs/sdks/typescript): Install and use the official TypeScript SDK. --- # Authentication Source: https://serpex.dev/docs/getting-started/authentication > How to authenticate with the Serpex API using Bearer API keys. All Serpex API requests are authenticated with a **Bearer API key** sent in the `Authorization` header. ## Get your API key 1. Sign in at [app.serpex.dev](https://app.serpex.dev). 2. Navigate to **API Keys** in the left sidebar. 3. Click **Create API Key**, give it a name, and copy the key. API keys start with `sk_` and are shown once — copy it immediately and store it somewhere safe. ## Sending the header Include the key on every request: ```http Authorization: Bearer sk_your_api_key ``` **cURL example:** ```bash curl -X POST https://api.serpex.dev/api/search \ -H "Authorization: Bearer sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{"q": "openai gpt-4"}' ``` **SDK — key is set once on the client:** ```typescript import { SerpexClient } from "serpex"; // Pass the key at construction time — all subsequent calls use it. const client = new SerpexClient(process.env.SERPEX_API_KEY!); ``` ```python import os from serpex import SerpexClient client = SerpexClient(os.environ["SERPEX_API_KEY"]) ``` ## Error responses | Status | Meaning | | ---------------------- | --------------------------------------------------- | | `401 Unauthorized` | Key missing, malformed, or revoked. | | `402 Payment Required` | Key is valid but the organization has zero credits. | ## Keep your key secret > Your API key grants full access to your organization's credits. Never expose it in client-side JavaScript, public repositories, or build logs. Best practices: * Store the key in an environment variable (`SERPEX_API_KEY`). * Call the API from a backend service, not directly from a browser. * Rotate the key immediately in the dashboard if you suspect it was leaked — old keys are invalidated the moment you delete them. ## Multiple keys You can create multiple API keys per organization — useful for separating environments (development, staging, production) or per-service access without sharing a single credential. --- # Rate Limits Source: https://serpex.dev/docs/getting-started/rate-limits > Per-plan request-per-second limits, stealth throttling, and retry guidance. > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. Serpex enforces **per-organization** rate limits based on your highest completed purchase. Limits are exact — there is no burst allowance. ## Limits by tier | Field | Type | Default | Description | | --- | --- | --- | --- | | Starter | 5 req/sec | | Largest purchase under $50 | | Standard | 25 req/sec | | Largest purchase $50–$499 | | Scale | 50 req/sec | | Largest purchase $500+ | | Enterprise | Custom | | Contact sales | The tier is set by your **largest single completed purchase**, not your current balance or the sum of purchases. A search with page content uses more of the limit: it counts as 1, plus 1 for every 5 pages of content you ask for (`content_results`). Content for 5 results counts as 2 requests toward your per-second limit; content for 10 results counts as 3. Usage calls (`/api/usage`) count as 1. ## Stealth mode cap Stealth extract requests (`"stealth": true` on `/api/crawl`) also share a global budget of **10 URLs per second**. A request over the budget waits up to 15 seconds for capacity. If capacity isn't free by then, it gets a `503` with `"retryable": true`; it isn't charged and is safe to retry. Stealth mode ends on 1 November 2026. > Stealth requests can take up to 15 seconds longer when the budget is busy. Factor this into your timeout settings. No extra credits are charged for the wait. ## HTTP response codes | Code | Meaning | Action | | ------------- | -------------------------- | -------------------------------------------------- | | `200` | Success | — | | `401` | Invalid or missing API key | Fix the key; don't retry | | `402` | Insufficient credits | Top up at [app.serpex.dev](https://app.serpex.dev) | | `429` | Rate limit exceeded | Back off and retry (see below) | | `500` / `503` | Transient server error | Retry with exponential backoff | The `429` response includes `Retry-After` and `X-RateLimit-Limit` headers: ```http HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 50 X-RateLimit-Remaining: 0 Retry-After: 1 ``` ## Retry strategy Implement exponential backoff for `429` and `5xx` responses: ```typescript async function searchWithRetry(client: SerpexClient, query: string, maxRetries = 4) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await client.search({ q: query }); } catch (err: any) { const status = err?.status ?? err?.response?.status; if (status === 429 || status === 503) { const waitMs = Math.min(1000 * 2 ** attempt, 16000); await new Promise((r) => setTimeout(r, waitMs)); continue; } throw err; // non-retryable (401, 402, 400, …) } } throw new Error("Max retries exceeded"); } ``` ```python import time def search_with_retry(client, query: str, max_retries: int = 4): for attempt in range(max_retries): try: return client.search({"q": query}) except Exception as e: status = getattr(e, "status_code", None) if status in (429, 503): wait = min(1 * (2 ** attempt), 16) time.sleep(wait) continue raise # non-retryable raise RuntimeError("Max retries exceeded") ``` > Do **not** retry `401` (bad key) or `402` (no credits). Those errors won't resolve on their own. ## High-volume usage If your workload requires sustained throughput above the Scale tier, [contact us](https://serpex.dev/support) to discuss an Enterprise plan with a custom limit. --- # Pricing Source: https://serpex.dev/docs/getting-started/pricing > Credits model, per-request costs, tier table, and per-org caching. > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. Serpex uses a **pay-as-you-go credits** model. You buy a block of credits; each API request consumes a small number of credits. ## Per-request credit cost | Request type | Credits | | ------------------------------------------------------------ | --------------------- | | Search (`POST /api/search`) | **1 credit** | | Extract — normal (`POST /api/crawl`) | **1 credit per URL** | | Extract — stealth (`POST /api/crawl` with `"stealth": true`) | **5 credits per URL** | Errors are **never charged**. A search that returns no results is charged **only** when the query genuinely has no results — confirmed across multiple search sources — and is never charged when we could not confirm that (in practice, mainly `site:` queries can be confirmed today). See [When are empty results charged?](https://serpex.dev/docs/api/search#when-are-empty-results-charged). Check `metadata.credits_used` in every response to see exactly what was deducted. ## Pricing tiers Larger top-ups unlock a lower effective price per 1,000 requests: | Tier | Top-up amount | Price per 1k requests | | ---------- | ------------- | ------------------------------------------- | | Starter | $5 – $49 | \~$0.80 | | Standard | $50 – $499 | \~$0.65 | | Scale | $500 – $1,999 | \~$0.50 | | Enterprise | $2,000+ | [Contact sales](https://serpex.dev/support) | Each top-up's rate is set by the size of that top-up: a $500 top-up buys credits at \~$0.50 per 1,000, and a later $20 top-up at \~$0.80. Credits never expire, whatever rate they were bought at. Your [rate limit](https://serpex.dev/docs/getting-started/rate-limits) tier is set separately, by your largest single completed purchase. At 10 million searches a month and up, volume pricing starts from $0.20 per 1,000, prepaid. [Contact sales](https://serpex.dev/support) for a quote. New accounts receive **200 free credits** to explore the API without a credit card. ## Per-org caching Repeats are free per organization. If your code (or any key in the same organization) repeats a request within **5 minutes** of the request that was charged, **no credits are charged** (`credits_used: 0`). A repeat means: * **Search:** the same query text with the same content options (plain, `content_results: 5` or `content_results: 10`). * **Extract:** the same URL with the same `format` and the same mode (normal or stealth). Free URLs are counted in `metadata.cached_free`. `from_cache` on a search response only tells you whether the results were served from our cache. It is not a billing flag: read `credits_used` to see what you paid. > Free repeats are per organization: a different organization making the same request still pays. Only your own repeats within the 5 minute window are free. This makes development iteration and idempotent retries cheap: ```json { "metadata": { "credits_used": 0 } } ``` ## Managing credits * **Buy credits:** go to **Billing** in the [dashboard](https://app.serpex.dev) and select a top-up amount. * **Check balance:** the dashboard shows your current balance and usage history. * **Low-credit alerts:** Serpex sends an email and Slack notification when your balance falls below 10% of your last top-up amount (or below 200 credits on the free tier). ## See also Visit [serpex.dev/pricing](https://serpex.dev/pricing) for the full pricing page, including an interactive credit calculator. --- # API Reference Source: https://serpex.dev/docs/api > Full reference for the Serpex Search, Extract and Usage endpoints. > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. The Serpex API has two core endpoints, search the web and extract page content, plus stealth mode for JavaScript-heavy pages and a free endpoint to check your credits and usage. - [Search](https://serpex.dev/docs/api/search): `GET` or `POST /api/search`: structured web-search results in JSON. - [Extract](https://serpex.dev/docs/api/extract): `POST /api/crawl`: clean page content as Markdown or HTML. - [Usage](https://serpex.dev/docs/api/usage): `GET /api/usage`: credit balance and request counts. Free. - [Stealth Mode](https://serpex.dev/docs/api/stealth): Full-browser rendering for JavaScript-heavy pages. - [Errors](https://serpex.dev/docs/api/errors): Status codes, retry guidance, and what is never charged. --- # Search API Source: https://serpex.dev/docs/api/search > Search the web with a single POST request ## Endpoint ``` POST https://api.serpex.dev/api/search GET https://api.serpex.dev/api/search ``` Both `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 | Field | Type | Default | Description | | --- | --- | --- | --- | | q | string | | Search query. Maximum 500 characters. Required. | | include_content | boolean | false | Fetch each result's page content and return it as markdown. Best-effort — see Credits below. | | content_results | 5 \| 10 | 5 | How many top results to fetch page content for. Only used when include_content is true. Must be exactly 5 or 10 — any other value is rejected. | > The legacy `engine` and `engines` parameters are deprecated and ignored. Every search is automatically routed to the best available source, with automatic fallback. Requests that still send either parameter receive a `Deprecation` response header — you can safely remove them. ## Search operators Operators go inside `q`, exactly as you would type them into a search box. The response always echoes your original `q`. | Operator | Example | Behaviour | | -------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `site:` | `acme hotel site:linkedin.com/company` | Only results from that domain or path. | | `-site:` | `acme hotel -site:booking.com` | Removes results from that domain. | | `after:` / `before:` | `acme hotel news after:2026-09-15 before:2026-10-15` | Restricts results to that date range. Dates must be `YYYY-MM-DD`; `after:` alone means "from that date until today". | > `after:` / `before:` are applied as a real date filter by the sources that support one. If the search has to fall back to a source without date filtering, results can fall outside the range. Empty results for queries with date operators are never charged. ## Code Examples **cURL** ```bash 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"}' ``` **TypeScript** ```typescript 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); ``` **Python** ```python 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 ```json { "id": "3f6c2a9e-8b1d-4c57-9f0a-2d4e6b8c1a73", "query": "best JavaScript frameworks 2025", "engines": ["auto"], "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": "auto", "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": "auto", "content_error": "blocked" } ], "metadata": { "number_of_results": 10, "credits_used": 3, "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`. Four of the five requested pages came back with content, so it cost 3 credits (see [Credits](https://serpex.dev/docs/api/search#credits)). 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 | Field | Type | Default | Description | | --- | --- | --- | --- | | id | string | | Unique ID for this search response (a UUID). | | query | string | | The query text you sent. | | message | string | | Present ONLY when metadata.status is 'no_results'. Same text as metadata.message. | | engines | string[] | | Deprecated. Always ["auto"]: search is automatically routed. Still returned so existing clients keep working. | | results[].title | string | | Page title from the search result. | | results[].url | string | | Full URL of the result. | | results[].snippet | string | | Short excerpt or description from the result. | | results[].position | number | | Rank position in the result set (1-indexed). | | results[].engine | string | | Deprecated. Always "auto": search is automatically routed. Still returned so existing clients keep working. | | results[].img_src | string | | Image URL for the result. Present only when one is available. | | results[].duration | string | | Media duration for the result (for example a video). Present only when one is available. | | results[].score | number | | Relevance score for the result. Present only when one is available. | | results[].content | string | | The result page's content as markdown. Present ONLY when the request had include_content: true and the fetch succeeded. Never present together with content_error. | | results[].content_error | string | | Why page-content fetching failed for this result (e.g. blocked, timeout, robots-disallowed). Present ONLY when include_content: true and the fetch failed. Not billed. Never present together with content. | | metadata.credits_used | number | | Credits deducted for this request: 0, 1, 3 or 6. 0 for your organization's own repeats within 5 minutes and for empty results that were not confirmed as genuinely empty. | | metadata.from_cache | boolean | false | true when the response was served from the content cache. | | metadata.status | string | | Request status: 'success' (results returned) or 'no_results' (HTTP 200, empty results). 'error' appears only in the HTTP 503 body (see below). | | metadata.no_results_verified | boolean | | Present ONLY when status is 'no_results'. true when multiple search engines confirmed the query genuinely has no results (charged as a normal search); false when it could not be confirmed as genuinely empty (not charged). | | metadata.charged | boolean | | Present ONLY when status is 'no_results'. true when this request deducted credits (see credits_used). | | metadata.message | string | | Present ONLY when status is 'no_results'. Human-readable explanation of why the results are empty and whether you were charged. | | metadata.response_time | number | | Total server-side response time in milliseconds. | | metadata.timestamp | string | | ISO 8601 timestamp of when the request was processed. | | metadata.number_of_results | number | | Number of results returned in this response. | | metadata.content_requested | number | | The content_results tier that was requested (5 or 10). Present ONLY when include_content: true. | | metadata.content_delivered | number | | How many of the requested results actually came back with page content. Present ONLY when include_content: true. This is also the number billing is based on. | ### Error response (HTTP 503) When the search cannot be completed after automatic retries, the API returns HTTP `503`. It is never charged and is safe to retry. ```json { "status": "error", "error": "Search temporarily unavailable, please retry.", "retryable": true, "metadata": { "status": "error", "response_time": 30412, "timestamp": "2025-06-21T10:30:00.000Z" } } ``` Other errors (`400`, `401`, `402`, `429`) are described in [Errors](https://serpex.dev/docs/api/errors). ## Credits * **1 credit** per successful search. * **0 credits** when your organization repeats the same query with the same content options within 5 minutes of the request that was charged (see [Per-org caching](https://serpex.dev/docs/getting-started/pricing#per-org-caching)). Another organization sending the same query is charged. * **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`**: charged by how many results actually came back with page content (`metadata.content_delivered`), not by how many you asked for: | Pages delivered | Credits | | --------------- | ------------------------- | | 0 | 1 (the plain-search rate) | | 1 to 5 | 3 | | 6 to 10 | 6 | So `content_results: 5` costs at most 3 credits, and `content_results: 10` costs at most 6. Page-content fetching is best-effort: in practice content is delivered for roughly **70%** of requested results. Never expect 100%: some pages are blocked or disallowed by `robots.txt` and come back with `content_error` instead of `content`. Those results still return their normal title, url and snippet. ## 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 5 minutes of the charged request | `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](https://serpex.dev/docs/getting-started/pricing#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 and queries with `after:` / `before:` date operators 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` — `true` when the query was confirmed empty as described above. * `charged` — `true` when this request deducted credits (`credits_used` holds the amount). * `message` — a human-readable explanation of both. The same text is also returned as the top-level `message`. **Confirmed empty (charged):** ```json { "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):** ```json { "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. --- # Extract API Source: https://serpex.dev/docs/api/extract > Extract content from any URL as Markdown or HTML > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. ## Endpoint ``` POST https://api.serpex.dev/api/crawl ``` ## Authentication ``` Authorization: Bearer sk_... ``` ## Request Parameters | Field | Type | Default | Description | | --- | --- | --- | --- | | urls | string[] | | Array of URLs to extract content from. Maximum 10 URLs per request. Required. | | stealth | boolean | false | Render the page in a full browser for JavaScript-heavy pages that standard extraction can't fully load. Costs 5 credits per URL instead of 1. | | format | "markdown" \| "html" | "markdown" | Output format. 'markdown' is ideal for LLM consumption; 'html' returns the raw HTML for structured data extraction. | > The maximum is 10 URLs per request. To process more URLs, batch them into multiple requests. ## What Gets Extracted Extraction handles **text-based content**. Binary documents are not extracted. **Supported:** HTML and XHTML, plain text, Markdown, CSV, JSON (including `+json` types such as `application/ld+json`), and XML — which covers **RSS and Atom feeds** and `sitemap.xml`. **Not supported:** PDFs, images (including SVG), video, audio, archives, office documents, fonts and other binaries. | Limit | Behaviour | `error_type` | | --------------------------- | --------------------------------------------------------------------- | -------------------------- | | **Text-based content only** | Binary documents are rejected | `unsupported_content_type` | | **10 MB maximum** | Pages larger than 10 MB are rejected without being downloaded in full | `too_large` | Rejections are **per URL, not per request** — the remaining URLs in the same call are still extracted normally — and a rejected URL is **never charged**. A URL is rejected on its file extension before any network request is made where possible, and otherwise on the response's `content-type` and size. A binary served under a text `content-type` is still detected and rejected. > PDF extraction is not supported. Search results may legitimately include `.pdf` links — that is expected, and those results are returned to you normally. Passing such a URL to `/api/crawl` returns `success: false` with `error_type: "unsupported_content_type"` and costs no credits. We also honour each site's `robots.txt`. A URL disallowed by the target site returns `error_type: "robots_disallowed"` and is not charged. ## Code Examples **cURL** ```bash curl -X POST https://api.serpex.dev/api/crawl \ -H "Authorization: Bearer sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "urls": ["https://example.com", "https://httpbin.org"], "format": "markdown" }' ``` **TypeScript** ```typescript import { SerpexClient } from "serpex"; const client = new SerpexClient("sk_your_api_key"); // Markdown output (default) — ready for LLM processing const result = await client.extract({ urls: ["https://example.com", "https://httpbin.org"], }); console.log(result.results[0].markdown); console.log(`Credits used: ${result.metadata.credits_used}`); // HTML output — useful for structured data extraction const htmlResult = await client.extract({ urls: ["https://example.com"], format: "html", }); console.log(htmlResult.results[0].html); ``` **Python** ```python from serpex import SerpexClient client = SerpexClient("sk_your_api_key") # Markdown output (default) — ready for LLM processing result = client.extract({ "urls": ["https://example.com", "https://httpbin.org"], }) print(result.results[0].markdown) print(f"Credits used: {result.metadata.credits_used}") # HTML output — useful for structured data extraction html_result = client.extract({ "urls": ["https://example.com"], "format": "html", }) print(html_result.results[0].html) ``` ## Markdown vs HTML Output **Markdown** (`format: "markdown"`) strips navigation, ads, and boilerplate — leaving only the main content in a clean format that LLMs can read directly. **HTML** (`format: "html"`) returns the full rendered HTML of the page. Use this when you need to parse structured data with CSS selectors or XPath. ## Response ```json { "success": true, "results": [ { "url": "https://example.com", "success": true, "markdown": "# Example Domain\n\nThis domain is for use in illustrative examples...", "status_code": 200 }, { "url": "https://example.com/missing", "success": false, "markdown": "", "status_code": 404, "error": "HTTP 404: Not Found", "error_type": "http" } ], "metadata": { "total_urls": 2, "processed_urls": 2, "successful_crawls": 1, "failed_crawls": 1, "credits_used": 1, "response_time": 1850, "timestamp": "2025-06-21T10:30:00.000Z" } } ``` ## Response Fields `metadata.stealth` and `results[].stealth` appear only on `stealth: true` requests. `metadata.cached_free` appears only when at least one URL was a free repeat (see [Credits](https://serpex.dev/docs/api/extract#credits)); it is omitted when the count is 0. | Field | Type | Default | Description | | --- | --- | --- | --- | | success | boolean | | true when the request was processed. Check results[].success for each URL. | | results[].url | string | | The URL that was extracted. | | results[].success | boolean | | Whether extraction succeeded for this URL. | | results[].markdown | string | | Extracted content as Markdown. Present when format is 'markdown' (default). | | results[].html | string | | Extracted content as HTML. Present when format is 'html'. | | results[].status_code | number | | HTTP status code from the target URL. Present on success and, when available, on failures (e.g. 404, 403). | | results[].error | string | | Human-readable error message when success is false. | | results[].error_code | string | | Stable machine-readable failure code, currently emitted for stealth extraction only. Lets you branch without parsing the human-readable 'error' string. Values: 'stealth_target_unreachable' (the domain did not resolve or refused the connection — the target is dead), 'stealth_target_status' (the target answered with an error status — see status_code), 'stealth_target_empty' (the target answered 200 with no usable body — typically an empty or placeholder page), 'stealth_timeout' (the target did not finish rendering in time), 'stealth_network', 'stealth_unconfigured'. When our rendering service itself is unavailable, the result has no error_code and error_type is 'server_error' (retry; not a problem with your URL). Absent on the non-stealth path. | | results[].stealth | boolean | | Always true. Present ONLY on stealth: true requests. | | results[].error_type | string | | Failure category — the SAME set for normal and stealth extraction: 'timeout' (too slow), 'connection' (network), 'dns_unresolved' (the domain does not resolve: the site no longer exists or the domain expired; not retryable), 'ssl_error' (the site's TLS certificate is expired, self-signed, or does not match the hostname — not retryable), 'http' (target returned an error status — see status_code), 'blocked' (the target refused the request), 'robots_disallowed' (robots.txt), 'unsupported_content_type' (not text-based content — e.g. a PDF or image), 'too_large' (over the 10 MB limit), 'server_error' (our side / rendering service — retryable), or 'unknown'. Branch on this; pair with status_code for HTTP errors. | | metadata.total_urls | number | | Total number of URLs submitted in the request. | | metadata.processed_urls | number | | Number of URLs processed. Equal to total_urls. | | metadata.successful_crawls | number | | Number of URLs successfully extracted. | | metadata.failed_crawls | number | | Number of URLs that failed extraction. | | metadata.credits_used | number | | Total credits deducted. 1 credit per successful URL in normal mode; 5 per successful URL in stealth mode. Failed URLs and free repeats are not charged. | | metadata.stealth | boolean | | Always true. Present ONLY on stealth: true requests. | | metadata.cached_free | number | | Number of successful URLs not charged because your organization already paid for the same URL, format and mode within the last 5 minutes. Omitted when 0. | | metadata.response_time | number | | Total server-side response time in milliseconds. | | metadata.timestamp | string | | ISO 8601 timestamp of when the request was processed. | ## Credits * **1 credit** per successfully extracted URL (normal mode). * **5 credits** per successfully extracted URL with `stealth: true`. * **0 credits** when your organization repeats the same URL with the same `format` and the same mode (normal or stealth) within 5 minutes of the request that was charged. These URLs are counted in `metadata.cached_free`. Another organization requesting the same URL is charged. * Failed extractions are not charged. * A request that times out returns HTTP `504` and is not charged (see [Errors](https://serpex.dev/docs/api/errors#504-gateway-timeout)). --- # Usage API Source: https://serpex.dev/docs/api/usage > Check your credit balance and request counts ## Endpoint ``` GET https://api.serpex.dev/api/usage ``` Returns your organization's credit balance, request counts for a recent period, and the 10 most recent requests. Counts cover every API key in the organization, not only the key you call with. Calls to this endpoint are free: they do not use credits. They do count toward your plan's per-second rate limit (1 request each), so poll it at a modest interval. ## 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. ## Query Parameters | Field | Type | Default | Description | | --- | --- | --- | --- | | days | integer | 30 | How many days back to count, from 1 to 90. Values above 90 are treated as 90. A value that is not a whole number, or is less than 1, returns HTTP 400. | Request history is kept for 30 days, so any `days` value above 30 returns the same data as `days=30`. The credit balance is updated about once a minute, so it can trail your most recent requests by up to a minute. ## Code Examples **cURL** ```bash curl "https://api.serpex.dev/api/usage?days=7" \ -H "Authorization: Bearer sk_your_api_key" ``` **TypeScript** ```typescript import { SerpexClient } from "serpex"; const client = new SerpexClient("sk_your_api_key"); const usage = await client.usage({ days: 7 }); console.log(usage.credits.balance); console.log(usage.statistics.totalRequests); ``` **Python** ```python from serpex import SerpexClient client = SerpexClient("sk_your_api_key") usage = client.usage({"days": 7}) print(usage.credits.balance) print(usage.statistics.totalRequests) ``` Both SDKs default to 30 days when called with no arguments (`client.usage()`). ## Response ```json { "api_key": "Production key", "organization_id": "org_2b7c9d", "period_days": 7, "effective_days": 7, "statistics": { "totalRequests": 1240, "successfulRequests": 1228, "failedRequests": 12, "noResultsRequests": 31, "engineStats": { "search": 1100, "crawl": 140 } }, "credits": { "balance": 18450, "totalUsed": 1550 }, "recent_requests": [ { "id": 981234, "organizationId": "org_2b7c9d", "searchEngine": "search", "requestedEngine": "auto", "resultsCount": 10, "status": "success", "responseTime": 1240, "fromCache": false, "contentRequested": false, "contentPagesRequested": null, "contentPagesDelivered": null, "createdAt": "2026-09-30T10:30:00.000Z" } ] } ``` ## Response Fields | Field | Type | Default | Description | | --- | --- | --- | --- | | api_key | string | | The name you gave the API key used for this call. It is the key's label, never the secret key itself. | | organization_id | string | | Your organization ID. | | period_days | number | | The number of days counted (your days value, capped at 90). | | effective_days | number | | The number of days the counts actually cover. Request history is kept for 30 days, so it is the smaller of period_days and 30 (for example, days=90 returns effective_days of 30). | | statistics.totalRequests | number | | All logged Search and Extract requests in the period. | | statistics.successfulRequests | number | | Requests that were served, including searches that returned no results. | | statistics.failedRequests | number | | Requests that ended in an error. | | statistics.noResultsRequests | number | | Searches that returned no results. Also counted in successfulRequests. | | statistics.engineStats | object | | Requests per product: keys are 'search', 'crawl' (Extract) and 'stealth'. A key is present only when its count is above 0. | | credits.balance | number | | Credits remaining for your organization. | | credits.totalUsed | number | | Credits used to date. | | recent_requests | object[] | | Up to 10 of the most recent requests in the period, newest first. | | recent_requests[].id | number | | Request log ID. | | recent_requests[].organizationId | string | | Your organization ID. | | recent_requests[].searchEngine | string | | Product that served the request: 'search', 'crawl' or 'stealth'. | | recent_requests[].requestedEngine | string \| null | | 'auto' for searches, 'crawl' or 'stealth' for Extract requests. May be null on older records. | | recent_requests[].resultsCount | number | | Results returned for a search, or URLs submitted for an Extract request. | | recent_requests[].status | string | | 'success', 'no_results' or 'error'. | | recent_requests[].responseTime | number | | Response time in milliseconds. | | recent_requests[].fromCache | boolean | | Whether a search was served from our cache. | | recent_requests[].contentRequested | boolean | | Whether the search set include_content: true. | | recent_requests[].contentPagesRequested | number \| null | | content_results value (5 or 10). null when page content was not requested. | | recent_requests[].contentPagesDelivered | number \| null | | Pages that came back with content. null when page content was not requested. | | recent_requests[].createdAt | string | | ISO 8601 timestamp of the request. | ## Errors | Status | Body | | ------ | ----------------------------------------------------------------------------- | | 400 | `{"error": "days must be a whole number from 1 to 90"}` | | 401 | `{"error": "API key required"}` or `{"error": "Invalid or inactive API key"}` | | 500 | `{"error": "Failed to fetch usage statistics"}` | --- # Stealth Mode Source: https://serpex.dev/docs/api/stealth > Full-browser rendering for JavaScript-heavy pages > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. ## What Stealth Mode Solves Some pages need a real browser to render fully. Common cases: * **JavaScript rendering requirements** — pages that return blank HTML until scripts run * **Heavy client-side frameworks** — content that's assembled dynamically after the initial page load * **Complex page behavior** — pages that need real browser events to finish loading Standard extraction (`stealth: false`) sends a direct HTTP request. This works well for the vast majority of public websites and is both fast and cost-efficient. Stealth mode renders the page in a full browser and executes its JavaScript before returning the content. Use it only when standard extraction returns incomplete content or fails. ## When to Use Stealth vs Normal | Situation | Recommendation | | -------------------------------------------- | --------------------------------------------------------- | | Public articles, blogs, documentation | Normal extraction (1 credit/URL) | | E-commerce product pages | Try normal first; use stealth if the result is incomplete | | Sites requiring JavaScript to render content | Stealth mode | | Sites with heavy client-side rendering | Stealth mode | | High-volume batch extraction | Normal mode; fall back to stealth on failure | ## Example **cURL** ```bash curl -X POST https://api.serpex.dev/api/crawl \ -H "Authorization: Bearer sk_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "urls": ["https://example-protected-site.com/product/123"], "stealth": true, "format": "html" }' ``` **TypeScript** ```typescript import { SerpexClient } from "serpex"; const client = new SerpexClient("sk_your_api_key"); const result = await client.extract({ urls: ["https://example-protected-site.com/product/123"], stealth: true, format: "html", }); if (result.results[0].success) { // Parse structured data from HTML console.log(result.results[0].html); } else { console.error(result.results[0].error); } ``` **Python** ```python from serpex import SerpexClient client = SerpexClient("sk_your_api_key") result = client.extract({ "urls": ["https://example-protected-site.com/product/123"], "stealth": True, "format": "html", }) if result.results[0].success: # Parse structured data from HTML print(result.results[0].html) else: print(result.results[0].error) ``` ## Response The response shape is the same as normal extraction. `metadata.stealth` and `results[].stealth` (both `true`) show stealth mode was active. ```json { "success": true, "results": [ { "url": "https://example-protected-site.com/product/123", "success": true, "html": "...", "stealth": true, "status_code": 200 } ], "metadata": { "total_urls": 1, "processed_urls": 1, "successful_crawls": 1, "failed_crawls": 0, "credits_used": 5, "stealth": true, "response_time": 11000, "timestamp": "2025-06-21T10:30:00.000Z" } } ``` ## Performance Stealth requests are significantly slower than normal extraction — each one loads the page in a full browser and waits for its JavaScript to finish executing. | Mode | Typical time per URL | | ----------------- | ------------------------------------------------ | | Normal extraction | \~1–5s | | Stealth | \~7–15s (up to \~25s for JavaScript-heavy sites) | > Set your client timeout to at least **60 seconds** for stealth requests. Batch requests (up to 10 URLs) are processed concurrently, so total time is bounded by the slowest URL — not the sum. ## Cost > Stealth mode costs **5 credits per URL**, compared to 1 credit per URL for normal extraction. Only use it when standard extraction is insufficient. * Normal extraction: **1 credit/URL** * Stealth mode: **5 credits/URL** * Repeats: **0 credits** when your organization requests the same URL with the same `format` in stealth mode again within 5 minutes of the charged request Failed stealth requests are not charged. --- # Errors Source: https://serpex.dev/docs/api/errors > Error codes and retry guidance > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. > Errors are never charged. Credits are deducted for searches that return results, and for searches whose query genuinely has no results (confirmed across multiple search sources). An empty response we could not confirm as genuinely empty is never charged; see [When are empty results charged?](https://serpex.dev/docs/api/search#when-are-empty-results-charged). Your organization's own repeats of the same request within 5 minutes are free. ## HTTP Status Codes | Status | Meaning | Retryable | | ------ | --------------------------------------------- | --------- | | 400 | Bad request | No | | 401 | Invalid or missing API key | No | | 402 | Insufficient credits | No | | 403 | Stealth mode is not included in your plan | No | | 410 | Stealth mode has ended (from 1 November 2026) | No | | 429 | Rate limit exceeded | Yes | | 500 | Internal server error | Yes | | 502 | Extraction service error (not charged) | Yes | | 503 | Temporarily unavailable | Yes | | 504 | Extract timed out (not charged) | Yes | ## 400 Bad Request Returned when the request is malformed or missing required parameters. Common causes: * Missing `q` parameter on the Search endpoint * Query exceeds 500 characters * `include_content` is not `true` or `false`, or `content_results` is not `5` or `10` * Missing `urls`, more than 10 URLs, or a URL that cannot be parsed on the Extract endpoint * `days` on the Usage endpoint is not a whole number of at least 1 ```json { "error": "Query parameter (q or query) is required", "example": "/api/search?q=javascript" } ``` An Extract request with URLs that cannot be parsed lists them: ```json { "error": "Invalid URLs provided", "invalid_urls": ["not a url"] } ``` ## 401 Unauthorized Returned when the API key is missing, invalid, or inactive. ```json { "error": "Invalid or inactive API key" } ``` When no key is sent at all, the message is `"API key required"`. Check that you are sending `Authorization: Bearer sk_...` and that the key is active in your [dashboard](https://app.serpex.dev). ## 402 Insufficient Credits Returned when your account does not have enough credits to cover the request cost. ```json { "error": "Insufficient credits. Please purchase more credits to continue.", "remainingCredits": 0 } ``` On the Extract endpoint the message is `"Insufficient credits"`. Extract checks your balance against the maximum cost of the request (every URL charged) before it runs, and then charges only the successful URLs. The `remainingCredits` field shows your current balance. Top up credits from the [billing page](https://app.serpex.dev/dashboard/billing). ## 403 Forbidden Returned by the Extract endpoint when a request sets `"stealth": true` and your plan does not include stealth mode. Nothing is charged and nothing is fetched. ```json { "error": "Stealth mode is not included in your plan. Contact support@serpex.dev to add it." } ``` ## 410 Gone Returned by the Extract endpoint when a request sets `"stealth": true` on or after 1 November 2026 (00:00 UTC). Nothing is charged and nothing is fetched. Send the request without `stealth`. ```json { "error": "Stealth mode is no longer available (ended 1 November 2026). Send the request without stealth.", "retryable": false } ``` ## 429 Rate Limited Returned when you exceed your plan's request-per-second limit. ```json { "error": "Rate limit exceeded", "limit": 50, "retryAfterMs": 800 } ``` Response headers include: * `X-RateLimit-Limit` — your plan's per-second ceiling * `X-RateLimit-Remaining` — requests remaining in the current window * `Retry-After` — seconds to wait before retrying **Retry guidance:** Wait for `retryAfterMs` milliseconds, then retry the request. Use exponential backoff if you are consistently hitting the limit. | Plan tier | Rate limit | | ------------------------------------ | ---------- | | Starter (largest purchase under $50) | 5 req/sec | | Standard ($50–$499) | 25 req/sec | | Scale ($500+) | 50 req/sec | | Enterprise (contact sales) | Custom | ## 500 Internal Server Error Returned by Search and Extract when something unexpected fails on our side. The message is always the same and never includes internal details. The request is not charged. ```json { "error": "Internal server error", "request_id": "3f6c1a52-8d0e-4b7a-9c41-0e2d5a7b9f13" } ``` Quote the `request_id` when you contact [support@serpex.dev](mailto:support@serpex.dev) so we can find the exact request. ## 502 Bad Gateway Returned by the Extract endpoint when the extraction service fails. Nothing is charged. ```json { "error": "Extraction service error. Not charged. Please retry.", "retryable": true, "request_id": "3f6c1a52-8d0e-4b7a-9c41-0e2d5a7b9f13" } ``` **Retry guidance:** Retry the request. If it keeps failing, contact support with the `request_id`. ## Request ID Error responses from Search and Extract that point to a problem on our side (`500`, `502`) include a `request_id` field. Quote it to support. If an Extract request is rejected because of its `urls`, the `400` body says which rule it broke: ```json { "error": "URLs array is required" } ``` ```json { "error": "Maximum 10 URLs allowed per request" } ``` ```json { "error": "Invalid URLs provided", "invalid_urls": ["not-a-url"] } ``` ## 503 Service Unavailable Returned when the request cannot be completed right now. It is never charged and is safe to retry. **Search** returns this when the search could not be completed after automatic retries: ```json { "status": "error", "error": "Search temporarily unavailable, please retry.", "retryable": true, "metadata": { "status": "error", "response_time": 30412, "timestamp": "2025-06-21T10:30:00.000Z" } } ``` **Extract** with `"stealth": true` returns this when stealth capacity is busy: ```json { "error": "Stealth capacity is busy, please retry shortly.", "retryable": true } ``` **Retry guidance:** Wait at least 1 second, then retry. Use exponential backoff with a maximum of 3 retries. If the issue persists beyond a few minutes, check the [status page](https://serpex.dev). ## 504 Gateway Timeout Returned by the Extract endpoint when extraction does not finish in time. Nothing is charged. ```json { "error": "Extraction timed out upstream and was not charged. Retry, or split the request into fewer URLs.", "retryable": true } ``` **Retry guidance:** Retry the request, or send fewer URLs per request. ## Error Handling Example **TypeScript** ```typescript import { SerpexClient, SerpApiException } from "serpex"; const client = new SerpexClient("sk_your_api_key"); try { const results = await client.search({ q: "test query" }); } catch (error) { if (error instanceof SerpApiException) { switch (error.statusCode) { case 429: console.log(`Rate limited. Retry after ${error.details?.retryAfterMs}ms`); break; case 402: console.log(`Out of credits. Balance: ${error.details?.remainingCredits}`); break; default: console.log(`API error ${error.statusCode}: ${error.message}`); } } } ``` **Python** ```python from serpex import SerpexClient, SerpApiException client = SerpexClient("sk_your_api_key") try: results = client.search({"q": "test query"}) except SerpApiException as e: if e.status_code == 429: print(f"Rate limited. Retry after {e.details.get('retryAfterMs')}ms") elif e.status_code == 402: print(f"Out of credits. Balance: {e.details.get('remainingCredits')}") else: print(f"API error {e.status_code}: {e}") ``` --- # SDKs Source: https://serpex.dev/docs/sdks > Official Serpex SDKs and the MCP server. Use Serpex from your language of choice, or plug it into an AI agent via the MCP server. - [TypeScript](https://serpex.dev/docs/sdks/typescript): `npm install serpex` — search and extract from Node.js. - [Python](https://serpex.dev/docs/sdks/python): `pip install serpex` — search and extract from Python. - [MCP Server](https://serpex.dev/docs/sdks/mcp): Expose Serpex as a tool to AI agents via MCP. --- # TypeScript SDK Source: https://serpex.dev/docs/sdks/typescript > Official TypeScript/JavaScript SDK for Serpex > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. The `serpex` package is the official TypeScript SDK for the Serpex API. It works in Node.js and any TypeScript or JavaScript project. **Package:** [serpex on npm](https://www.npmjs.com/package/serpex) — v2.6.0 ## Installation ```bash npm install serpex # or yarn add serpex # or pnpm add serpex ``` ## Initialize the Client ```typescript import { SerpexClient } from "serpex"; const client = new SerpexClient("sk_your_api_key"); ``` The constructor accepts an optional second argument `baseUrl` if you need to point at a different host (defaults to `https://api.serpex.dev`). ## Search ```typescript import { SerpexClient } from "serpex"; const client = new SerpexClient("sk_your_api_key"); const results = await client.search({ q: "typescript tutorial", }); console.log(results.results[0].title); console.log(results.results[0].url); console.log(results.metadata.credits_used); ``` ### Search with page content Set `include_content` to fetch each top result's page and get it back as markdown — handy for feeding results straight into an LLM without a separate crawl call. It's best-effort (roughly 70% of requested results come back with content; blocked or `robots.txt`-disallowed pages return `content_error` instead) and costs more than a plain search — see [pricing](https://serpex.dev/docs/api/search#credits). ```typescript const results = await client.search({ q: "latest AI news", include_content: true, content_results: 5, // 5 (default, 3 credits) or 10 (6 credits) }); for (const r of results.results) { if (r.content) { console.log(`${r.title}: ${r.content.length} chars of markdown`); } else if (r.content_error) { console.log(`${r.title}: content unavailable — ${r.content_error}`); } } console.log(`Content delivered: ${results.metadata.content_delivered}/${results.metadata.content_requested}`); ``` ### Search Parameters ```typescript interface SearchParams { q: string; // Required. Max 500 characters. include_content?: boolean; // Default: false. Fetch page content (markdown) for top results. content_results?: 5 | 10; // Default: 5. Only used when include_content is true. } ``` ## Extract ```typescript import { SerpexClient } from "serpex"; const client = new SerpexClient("sk_your_api_key"); // Extract as Markdown — ideal for LLM processing const result = await client.extract({ urls: ["https://example.com", "https://httpbin.org"], }); result.results.forEach((r) => { if (r.success) { console.log(`${r.url}: ${r.markdown?.length} characters`); } else { console.log(`${r.url}: failed — ${r.error}`); } }); console.log(`Credits used: ${result.metadata.credits_used}`); ``` ### Extract with Stealth Mode ```typescript // Stealth mode for JavaScript-heavy pages — costs 5 credits per URL const stealthResult = await client.extract({ urls: ["https://protected-site.com/page"], stealth: true, format: "html", }); console.log(stealthResult.results[0].html); ``` ### Extract Parameters ```typescript interface ExtractParams { urls: string[]; // Required. Max 10 URLs per request. stealth?: boolean; // Default: false. 5 credits/URL when true. format?: "markdown" | "html"; // Default: "markdown" } ``` ## Usage Check your credit balance and request counts. Free: it does not use credits. See the [Usage API reference](https://serpex.dev/docs/api/usage) for every field. ```typescript const usage = await client.usage(); // last 30 days const week = await client.usage({ days: 7 }); // whole number, 1 or more console.log(usage.credits.balance); console.log(usage.statistics.engineStats); // e.g. { search: 1100, crawl: 140 } ``` ## Error Handling ```typescript import { SerpexClient, SerpApiException } from "serpex"; const client = new SerpexClient("sk_your_api_key"); try { const results = await client.search({ q: "test query" }); } catch (error) { if (error instanceof SerpApiException) { console.log("API Error:", error.message); console.log("Status Code:", error.statusCode); console.log("Details:", error.details); } } ``` ## Links * [npm package](https://www.npmjs.com/package/serpex) * [API Reference](https://serpex.dev/docs/api/search) --- # Python SDK Source: https://serpex.dev/docs/sdks/python > Official Python SDK for Serpex > Stealth mode will no longer be available from 1 November 2026. Search and Extract are unchanged. The `serpex` package is the official Python SDK for the Serpex API. Requires Python 3.8+. **Package:** [serpex on PyPI](https://pypi.org/project/serpex/) — v2.6.0 ## Installation ```bash pip install serpex ``` Or with Poetry: ```bash poetry add serpex ``` ## Initialize the Client ```python from serpex import SerpexClient client = SerpexClient("sk_your_api_key") ``` The constructor accepts an optional `base_url` argument (defaults to `https://api.serpex.dev`). ## Search ```python from serpex import SerpexClient client = SerpexClient("sk_your_api_key") results = client.search({ "q": "python tutorial", }) print(results.results[0].title) print(results.results[0].url) print(results.metadata.credits_used) ``` ### Using SearchParams for Type Safety ```python from serpex import SerpexClient, SearchParams client = SerpexClient("sk_your_api_key") params = SearchParams(q="machine learning") results = client.search(params) print(results.results[0].title) ``` ### Search with page content Set `include_content` to fetch each top result's page and get it back as markdown — handy for feeding results straight into an LLM without a separate crawl call. It's best-effort (roughly 70% of requested results come back with content; blocked or `robots.txt`-disallowed pages return `content_error` instead) and costs more than a plain search — see [pricing](https://serpex.dev/docs/api/search#credits). ```python params = SearchParams( q="latest AI news", include_content=True, content_results=5, # 5 (default, 3 credits) or 10 (6 credits) ) results = client.search(params) for r in results.results: if r.content: print(f"{r.title}: {len(r.content)} chars of markdown") elif r.content_error: print(f"{r.title}: content unavailable — {r.content_error}") print(f"Content delivered: {results.metadata.content_delivered}/{results.metadata.content_requested}") ``` ### Search Parameters ```python from dataclasses import dataclass from typing import Literal, Optional @dataclass class SearchParams: q: str # Required. Max 500 characters. include_content: bool = False # Fetch page content (markdown) for top results. content_results: Literal[5, 10] = 5 # Only used when include_content is True. ``` ## Extract ```python from serpex import SerpexClient client = SerpexClient("sk_your_api_key") # Extract as Markdown — ideal for LLM processing result = client.extract({ "urls": ["https://example.com", "https://httpbin.org"], }) for r in result.results: if r.success: print(f"{r.url}: {len(r.markdown)} characters") else: print(f"{r.url}: failed — {r.error}") print(f"Credits used: {result.metadata.credits_used}") ``` ### Extract with Stealth Mode ```python # Stealth mode for JavaScript-heavy pages — costs 5 credits per URL stealth_result = client.extract({ "urls": ["https://protected-site.com/page"], "stealth": True, "format": "html", }) print(stealth_result.results[0].html) ``` ### Using ExtractParams for Type Safety ```python from serpex import SerpexClient, ExtractParams client = SerpexClient("sk_your_api_key") params = ExtractParams( urls=["https://example.com", "https://httpbin.org"], stealth=False, format="markdown", ) result = client.extract(params) ``` ### Extract Parameters ```python @dataclass class ExtractParams: urls: list[str] # Required. Max 10 URLs per request. stealth: bool = False # 5 credits/URL when True. format: str = "markdown" # "markdown" | "html" ``` ## Usage Check your credit balance and request counts. Free: it does not use credits. See the [Usage API reference](https://serpex.dev/docs/api/usage) for every field. ```python usage = client.usage() # last 30 days usage = client.usage({"days": 7}) # whole number, 1 or more print(usage.credits.balance) print(usage.statistics.engineStats) # e.g. {"search": 1100, "crawl": 140} ``` ## Error Handling ```python from serpex import SerpexClient, SerpApiException client = SerpexClient("sk_your_api_key") try: results = client.search({"q": "test query"}) except SerpApiException as e: print(f"API error: {e}") print(f"Status code: {e.status_code}") print(f"Details: {e.details}") ``` ## Requirements * Python 3.8+ * `requests` ## Links * [PyPI package](https://pypi.org/project/serpex/) * [API Reference](https://serpex.dev/docs/api/search) --- # MCP Server Source: https://serpex.dev/docs/sdks/mcp > Use Serpex as AI agent tools via the Model Context Protocol `serpex-mcp` is an MCP (Model Context Protocol) server that exposes Serpex search and extract as tools for AI agents. Once configured, agents like Claude can call `serpex_search` and `serpex_extract` directly to perform live web research without leaving their context window. **Package:** [serpex-mcp on npm](https://www.npmjs.com/package/serpex-mcp) — v1.0.2 ## What the MCP Server Provides The server exposes two tools: * **`serpex_search`** — search the web and get structured JSON results with titles, URLs, and snippets, optionally with each page's content as markdown * **`serpex_extract`** — get the content of up to 10 known URLs as clean markdown or HTML AI agents call these tools the same way they call any function. The results come back as structured JSON that the agent can reason over directly. ## Setup with Claude Desktop Add the following to your Claude Desktop configuration file. **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "serpex": { "command": "npx", "args": ["-y", "serpex-mcp"], "env": { "SERPEX_API_KEY": "sk_your_api_key" } } } } ``` Restart Claude Desktop after saving. The `serpex_search` tool will appear in the tool list automatically. ## Setup with Other MCP Clients Any MCP-compatible client (Cursor, Windsurf, Continue, etc.) can use the same configuration pattern: * **Command:** `npx` * **Arguments:** `-y serpex-mcp` * **Environment variable:** `SERPEX_API_KEY=sk_your_api_key` ## Available Tool: `serpex_search` ### Input Schema | Field | Type | Default | Description | | --- | --- | --- | --- | | q | string | | Search query. Maximum 500 characters. Required. | | include_content | boolean | false | Fetch each result's page content and return it as markdown — useful for research without a separate crawl step. Best-effort: roughly 70% of requested results come back with content; blocked or robots.txt-disallowed pages return content_error instead. Costs 3 credits (5 results) or 6 credits (10 results) instead of the usual 1. | | content_results | 5 \| 10 | 5 | How many top results to fetch page content for. Only used when include_content is true. Must be exactly 5 or 10. | ### Example Tool Call ```json { "q": "artificial intelligence trends 2025", "include_content": true, "content_results": 5 } ``` ### Example Response The tool returns a JSON string with the search results: ```json { "query": "artificial intelligence trends 2025", "status": "success", "total_results": 10, "credits_used": 3, "content_requested": 5, "content_delivered": 4, "results": [ { "title": "AI Trends to Watch in 2025", "url": "https://example.com/ai-trends", "snippet": "The most significant AI developments of 2025 include...", "position": 1, "content": "# AI Trends to Watch in 2025\n\nThe most significant AI developments of 2025 include..." } ] } ``` `content` (page markdown) or `content_error` (why the fetch failed) is only present per-result when the call included `include_content: true`; a plain call omits both keys, and `content_requested` / `content_delivered`, entirely. When nothing is found, `status` is `"no_results"` and a `message` explains whether the search was charged. ## Available Tool: `serpex_extract` Extract the content of known pages as clean Markdown (or HTML). Up to 10 URLs per call. Pages that can't be extracted come back with `success: false`, an `error` and an `error_type`, and aren't charged. | Field | Type | Default | Description | | --- | --- | --- | --- | | urls | string[] | | 1 to 10 absolute http(s) URLs. Required. | | format | "markdown" \| "html" | "markdown" | Output format for each page. | The tool returns `successful`, `failed`, `credits_used` and one entry per URL: `url`, `success`, then `markdown` (or `html`) and `status_code` on success, or `error`, `error_type` and `status_code` (when the site answered) on failure. ## Standalone Usage You can also run the MCP server directly from the command line for testing or custom integrations: ```bash export SERPEX_API_KEY="sk_your_api_key" npx serpex-mcp ``` The server communicates over stdio using the MCP protocol. ## Getting an API Key 1. Sign up at [serpex.dev](https://serpex.dev) 2. Go to your [dashboard](https://app.serpex.dev) 3. Create an API key from the API Keys section 4. Set it as `SERPEX_API_KEY` in the MCP server config ## Links * [npm package](https://www.npmjs.com/package/serpex-mcp) * [Model Context Protocol](https://modelcontextprotocol.io) * [Search API Reference](https://serpex.dev/docs/api/search)