Serpent API Documentation
Complete guide to integrating search, Maps business data, AI ranking and social data.
Getting Started
Serpent API provides programmatic access to search results from Google, Bing, Yahoo, DuckDuckGo, and Brave, plus AI ranking and social media data. It supports three categories of endpoints:
SERP Endpoints
- Quick Search — speed-optimized organic results, ~10 by default and up to
num=100requested (best-effort at the high end —numis a ceiling, not a promise, and short deliveries carrymeta.partialResults). Web billing is flat: one call, one charge, whatever the page count. Use/api/searchDeep Search when you need rich SERP features. "Google SERP" here means search results from Google · Bing · Yahoo · DuckDuckGo · Brave. - Deep Search — up to 10 pages (~100 results requested) with related searches, ads, PAA, featured snippets, and AI overviews. Defaults to 1 page — set
numorpagesto go deeper, and note that every page you ask for is charged —/api/searchbills one Web unit per requested page, at every tier and every balance. - News Search — News articles with sources, timestamps, and snippets
- Image Search — Images with thumbnails, originals, dimensions, and sources
- Video Search — Video results with titles, thumbnails, durations, and views
- Shopping Search coming soon — product results with titles, prices where available, merchant/store names, URLs, and thumbnails. Not callable yet —
/api/shoppinganswers404today.
AI Ranking Endpoints
- Combined AI Rank — Check how AI models (Claude, ChatGPT, Gemini, Perplexity) cite your domain
- Single Engine Rank — Query a specific LLM for citation analysis
Social Media Endpoints
- YouTube Search — Search for videos, channels, and playlists
- YouTube Video/Channel — Get detailed video and channel information
- Instagram Profile — Public profile data and recent posts
Google Maps Endpoints
- Quick Place Search — up to 20 ranked places; draws from the shared 10-call free pool for new accounts
- Deep Place Search — attempts up to 100 ranked places with truthful partial-detail status
- Place Details — one richest-available normalized place record
- Reviews — up to 20 public reviews per call with opaque pagination
Quick Start
- Create an account — Sign up with your Google account (free)
- Generate an API key — Go to the API Keys page and create a key
- Make your first request — Use your API key to search
curl "https://apiserpent.com/api/search?q=hello+world" \
-H "X-API-Key: YOUR_KEY"
You get Free to start, shared across every endpoint — Web Search, News, Images, Videos, AI Rank, LinkedIn, Maps, YouTube, and Instagram. (Shopping joins the pool when it launches.)
Base URL
All API requests use the following base URL:
All endpoints are relative to this base. For example, the web search endpoint is:
https://apiserpent.com/api/search
Authentication
All search endpoints require an API key. You can authenticate in two ways:
Option 1: HTTP Header (Recommended)
X-API-Key: sk_live_your_api_key_here
Option 2: Query Parameter (discouraged)
GET /api/search?q=keyword&api_key=sk_live_your_api_key_here
X-API-Key-Warning header so an integration can spot it in testing.Free Tier
Every new account receives one shared lifetime allowance of free calls (no credit card required), usable across:
- Quick Search (
/api/search/quick) — all engines (Google, Bing, Yahoo, DDG, Brave) — draws from the same shared pool - Deep Search (
/api/search) — all engines - Maps Quick (
/api/maps/search/quick) — draws from the same shared pool - Maps Deep and Maps List (
/api/maps/search) — draw from the same shared pool, and count as one free call at anydetailorlimit - Amazon (
/api/amazon/product,/search,/bestsellers) — draws from the same shared pool - TikTok (
/api/tiktok/profile,/video,/hashtag,/search,/music) — draws from the same shared pool, and counts as one free call at anylimitor field-group count - X (Twitter) (
/api/x/tweet,/api/x/profile) — draws from the same shared pool. A profile call withhydrate=truecosts one free call, not one and a half — the pool counts calls, not multipliers - Reddit (
/api/reddit/posts,/comments,/search,/user,/subreddit) — draws from the same shared pool, and counts as one free call at anylimit, even the four-blocklimit=100
include_media, which is always paid because its cost is the file rather than the request. Shopping joins the pool when that endpoint launches.Quick Search
Perform a fast organic web search. Returns ~10 organic results by default, and can be asked for up to 100 organic results in a single call via num=. num is a ceiling, not a promise: num=50 and below delivers in full, and above that a call is best-effort — a short answer carries meta.partialResults and the top-level delivery block saying exactly what arrived and why. Quick Search is organic-only on engine=google; on Yahoo, Bing, DuckDuckGo and Brave it still returns that engine’s supported feature blocks. Every feature key is always present in results, so the response shape never changes — a block this engine or this page does not carry arrives empty ([]) or null. Use Deep Search (/api/search) for the full Google feature set. "Google SERP" here means search results from Google · Bing · Yahoo · DuckDuckGo · Brave. Web billing is flat: one call, one charge, whatever the page count, balance or tier.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | The search query |
| num | integer | Desired total organic results, 1–100, on every engine (default: 10). Quick returns multiple pages in a single call: num=50 and below delivers in full, and anything above that is best-effort — a ceiling you request, not a count we guarantee. Short deliveries carry meta.partialResults. |
| pages | integer | Number of result pages to return, 1–10 (default: 1). Quick returns multiple pages in a single call on every engine. Use num or pages, not both. |
| engine | string | google, bing, yahoo, ddg, or brave (default: "google") |
| country | string | Country code for localized results (default: "us") |
| freshness | string | Time filter: h/1h (hour), d/1d (day), 7d (week), w, m/1m, y/1y (optional) |
| safe | string | SafeSearch: off, moderate, or strict (optional). moderate and strict can reduce how many results come back — see the note below this table. |
| language | string | 2-letter ISO language code (e.g., en, es, de, fr, ja). Restricts results to that language. Accepted on all engines; coverage is broadest on Yahoo, Bing and Brave, and narrower on DuckDuckGo. (optional) |
| format | string | full (default) or simple. Simple returns position, title, and url only. |
| timeout | integer | Seconds you are willing to wait for this call (optional). It can only lower this endpoint's time budget, never raise it, and it is clamped rather than rejected — ask for less than roughly a quarter of the budget and you get the floor. timeout_ms takes the same value in milliseconds. See Response Timing. |
| include_video_stats | boolean | include_video_stats (boolean, default false, paid plans): when true, every organic result carries views, publishedTime, duration, publisher — populated for video results, null otherwise. true, 1 and yes switch it on; any other value leaves it off, never a 400. Left off, the response is byte-for-byte what it was before the parameter existed. On a free-tier account the four keys are present on every row as null and metadata.videoStatsUnavailable is 'free_tier'. No price change. |
| include_ai_mode | boolean | Not read on Quick Search. The parameter is accepted and the call succeeds as usual, but no aiMode object is added and nothing extra is charged — exactly as include_aio behaves here. AI Mode is a Deep Search (/api/search) feature; send it there instead. Full spec → |
| pixel_position BETA | boolean | Not supported on Quick Search, on any engine. The parameter is still accepted — the call succeeds and returns organic results with no pixel_position or pixel_box fields, and the response carries metadata.pixelPositionUnavailable: 'not_supported_on_quick' plus a metadata.pixelPositionHint naming the endpoint that does support them. Use Deep Search (/api/search) for pixel positions. Full spec → |
safe=moderate or safe=strict, results we cannot filter to that standard are not returned at all — we would rather hand you a shorter list than an unfiltered one, and choosing an engine does not opt you out of the filter. So expect fewer results on a filtered request: the same query can come back shorter at strict than at off, and where the filter genuinely bites it can come back empty. That is the filter working, not a failure — a short response carries meta.partialResults and the top-level delivery block as usual. Want maximum coverage instead? Omit safe, or send safe=off. This applies to Deep Search in exactly the same way.Example Request
curl "https://apiserpent.com/api/search/quick?q=weather+today&engine=google&country=uk" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
{
"success": true,
"query": "weather today",
"engine": "google",
"country": "uk",
"results": {
"organic": [
{
"position": 1,
"title": "Weather Forecast - Today",
"url": "https://example.com/weather",
"snippet": "Today's weather forecast...",
"displayedUrl": "example.com"
}
],
"ads": { "top": [], "bottom": [], "totalCount": 0 },
"peopleAlsoAsk": [],
"relatedSearches": [],
"featuredSnippet": null,
"aiOverview": null,
"knowledgePanel": null,
"localPack": [],
"richSnippets": [],
"videos": [],
"shopping": []
},
"meta": {
"totalOrganic": 7,
"elapsed": "1200ms",
"timestamp": "2026-03-03T10:30:00.000Z"
}
}
num=100 requested in a single call (best-effort at the high end; num=50 and below delivers in full, and num is a ceiling rather than a guaranteed count). Quick is organic-only on engine=google; on Yahoo, Bing, DuckDuckGo and Brave it still returns that engine’s supported feature blocks. Use Deep Search (/api/search) for the full set of rich Google SERP features (People Also Ask, related searches, ads, featured snippets, knowledge panels, local packs, inline videos, and more). Add include_aio=true to Deep Search when you need AI Overview fields on eligible paid calls, or include_ai_mode=true for AI Mode — a written answer to your query, the pages it draws on, and suggested follow-up questions. Neither parameter is read on Quick Search.Deep Search
Perform a deep web search that can be asked for up to 100 organic results with related searches, ads, People Also Ask, featured snippets, AI overviews, and more. For more than 10 results, the API automatically retrieves multiple pages; num is the depth you request, not a count that is guaranteed, and a short answer carries meta.partialResults and the top-level delivery block. This endpoint is free-tier eligible.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | The search query (max 2,048 characters) |
| num | integer | Number of results: 10–100 (default: 10). Automatically determines pages to retrieve. Rounded up to the nearest 10 (e.g., num=15 returns up to 20). |
| engine | string | google, bing, yahoo, ddg, or brave (default: "google") |
| country | string | Country code for localized results (default: "us"). See Country Support. |
| format | string | Response format: full or simple (default: "full"). Simple returns just position, title, and URL. |
| pages | integer | Number of result pages to retrieve: 1–10 (default: 1). Alternative to num. |
| freshness | string | Time filter: h/1h (hour), d/1d (day), 7d (week), w, m/1m, y/1y (optional) |
| safe | string | SafeSearch: off, moderate, or strict (optional) |
| language | string | 2-letter ISO language code (e.g., en, es, de, fr, ja). Restricts results to that language. Accepted on all engines; coverage is broadest on Yahoo, Bing and Brave, and narrower on DuckDuckGo. (optional) |
| timeout | integer | Seconds you are willing to wait for this call (optional). It can only lower this endpoint's time budget, never raise it, and it is clamped rather than rejected. timeout_ms takes the same value in milliseconds. |
| include_aio | boolean | Deep Search on engine=google, engine=yahoo and engine=brave only. Set true to request AI Overview fields on eligible paid calls. If unavailable, aiOverview stays null and organic results still return. On engine=bing and engine=ddg the key is always null. Changed for engine=brave — see the note below this table. |
| include_ai_mode | boolean | Deep Search only, paid plans only. true, 1 or yes adds a top-level aiMode object — { answer, sources[], followUps[] } — beside results: a written answer to your query, the pages it draws on as { url, title }, and a short list of suggested next questions. false, 0 and no leave it off; any other value is refused with a 400, because a parameter that carries a charge is never silently misread. Left off, the key is absent entirely and the response is byte-for-byte what it was before the parameter existed. Works on every Deep engine. Billed as a flat $0.004 per call on top of the search call’s own rate, the same at every tier. On a free-tier account the key is still returned with empty fields and metadata.aiModeUnavailable is 'free_tier'. Full spec → |
| include_video_stats | boolean | include_video_stats (boolean, default false, paid plans): when true, every organic result carries views, publishedTime, duration, publisher — populated for video results, null otherwise. true, 1 and yes switch it on; any other value leaves it off, never a 400. Left off, the response is byte-for-byte what it was before the parameter existed. On a free-tier account the four keys are present on every row as null and metadata.videoStatsUnavailable is 'free_tier'. No price change. |
| pixel_position BETA | boolean | When true, every item in the response (organic, AI Overview, PAA, ads, featured snippet, knowledge panel, local pack, shopping, videos) gets two sibling fields: a pixel_position integer (y-coordinate in pixels from the top of the rendered desktop SERP) and a pixel_box object with the element's full bounding rectangle ({ x, y, w, h }). Supported on Google, Yahoo, Bing and DuckDuckGo, on desktop, US locale. Not supported on engine=brave — a Brave Deep response carries no pixel fields, so use one of the other four engines when you need them. Paid tiers only — free-tier responses include metadata.pixelPositionUnavailable: 'free_tier'. No price increase. Full spec → |
engine=brave — the AI Overview is now opt-in. Brave Deep Search used to return its AI block automatically to any paid caller. It now follows the same rule as google and yahoo: send include_aio=true on an eligible paid call, or results.aiOverview comes back null and metadata.hasAiOverview is false. If you read aiOverview on engine=brave, add the parameter to your request — nothing else about the Brave response changes, and adding it is safe to do at any time.num or pages, not both. If you pass num=50, the API will automatically retrieve 5 pages. Each page yields ~10 results. Deep Search (/api/search) is billed per requested page: one published Web category unit per page, up to 10 pages, at every tier and every balance. Growth, Scale and a large Default balance do not make a multi-page call cost one unit — num=50 is 5 pages and is charged as 5. Quick Search (/api/search/quick) is the flat one — one call, one charge at the same Web rate, whatever the page count, balance or tier.Example Request
curl "https://apiserpent.com/api/search?q=best+seo+tools&num=30&engine=google&country=us" \
-H "X-API-Key: sk_live_your_api_key"
The results object is the same on every engine
Deep Search returns one results shape, identical on google, yahoo, bing, ddg and brave: organic, ads, peopleAlsoAsk, relatedSearches, featuredSnippet, aiOverview, knowledgePanel, localPack, richSnippets, videos and shopping. Every one of those keys is always present. A block the result page did not contain — or that the engine you asked for does not carry — comes back as its own empty value (null for the object-shaped blocks, [] for the array-shaped ones), never as a missing key, so you can write one parser and point it at any engine.
metadata follows the same rule on every engine and always carries totalOrganicResults, hasAds, hasRelatedSearches, hasPeopleAlsoAsk, hasVideos, hasShopping, hasFeaturedSnippet, hasAiOverview, hasKnowledgePanel, hasLocalPack and source. Each has* boolean describes what this response actually published, so it is the cheapest way to test whether a block is worth reading. What varies by engine is how often a block is filled in, not which keys you get — see Search Engines.
Example Response
{
"success": true,
"query": "best seo tools",
"engine": "google",
"country": "us",
"pagesRequested": 3,
"pagesScraped": 3,
"results": {
"organic": [
{
"position": 1,
"title": "10 Best SEO Tools for 2026",
"url": "https://example.com/seo-tools",
"snippet": "Discover the top SEO tools...",
"displayedUrl": "example.com"
}
],
"relatedSearches": [{ "query": "free seo tools" }],
"ads": { "top": [], "bottom": [], "totalCount": 0 },
"peopleAlsoAsk": [],
"featuredSnippet": null,
"aiOverview": null,
"knowledgePanel": null,
"localPack": [],
"richSnippets": [],
"videos": [],
"shopping": []
},
"metadata": {
"totalOrganicResults": 30,
"hasAds": false,
"hasRelatedSearches": true,
"hasPeopleAlsoAsk": false,
"hasVideos": false,
"hasShopping": false,
"hasFeaturedSnippet": false,
"hasAiOverview": false,
"hasKnowledgePanel": false,
"hasLocalPack": false,
"source": "google"
},
"meta": {
"totalOrganic": 30,
"requestedNum": 30,
"elapsed": "8500ms",
"timestamp": "2026-03-10T10:30:00.000Z"
}
}
AI Mode
AI Mode is a parameter on Deep Search, not a separate endpoint. Add include_ai_mode=true to a /api/search call and the response carries one extra top-level key, aiMode, beside results: a written answer to your query, the pages that answer draws on, and a short list of suggested follow-up questions. Everything else in the response is exactly what it was.
1. Deep Search only. The parameter is read on
/api/search and is ignored on /api/search/quick, exactly as include_aio is. A Quick call that sends it still succeeds; it simply comes back with no aiMode key.2. Paid plans only. A paid plan, or any live balance, unlocks it. A free-tier key that asks for AI Mode still gets the
aiMode key — with answer empty and both arrays empty — plus metadata.aiModeUnavailable: 'free_tier' so you can tell the two apart, and is charged nothing for it.3. You have to ask for it. It is opt-in. Without
include_ai_mode=true (or 1, or yes) the aiMode key is absent from the response altogether — not empty, absent. Existing integrations therefore see no change at all.Parameter
| Parameter | Type | Description |
|---|---|---|
| include_ai_mode | boolean | Default false. true, 1 and yes switch it on; false, 0 and no leave it off. Any other value is refused with a 400 (Invalid include_ai_mode. Use true/false, 1/0, or yes/no.) rather than read as a “no” — a parameter that carries a charge is never guessed at. Accepted on every Deep Search engine: google, yahoo, bing, ddg and brave. |
Example Request
curl "https://apiserpent.com/api/search?q=how+to+tie+a+tie&engine=google&country=us&include_ai_mode=true" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
aiMode sits at the top level, beside results — it is not inside it. The results object, metadata and meta are untouched and are abbreviated here.
{
"success": true,
"query": "how to tie a tie",
"engine": "google",
"results": { "...": "unchanged" },
"aiMode": {
"answer": "The four-in-hand is the easiest knot to learn...",
"sources": [
{ "url": "https://example.com/guide", "title": "How to Tie a Tie" }
],
"followUps": [
"How do I tie a Windsor knot?",
"Which tie knot works best for a wide collar?"
]
},
"metadata": { "...": "unchanged" },
"meta": { "...": "unchanged" }
}
The shape never varies
When you ask for AI Mode you always get the same object: answer is a string, sources is an array of { url, title }, and followUps is an array of strings. It is never null and never a different type. A response that could not be filled carries an empty version of that same object — answer: "", sources: [], followUps: [] — and the search results ship with it as usual. So branch on the array lengths and on whether answer is empty, never on whether the key exists. The key exists whenever you asked for it and is absent whenever you did not.
format=simple you still get aiMode. It is a top-level key, so the simple projection — which turns results into a flat array — leaves it alone. The one difference is the free-tier marker: format=simple carries no metadata object, so aiModeUnavailable rides on meta instead.Pricing
A flat $0.004 per call, added to what the Deep Search call itself costs — never instead of it. It is charged once per request whether that request returns one page or ten, and it is the same on every plan: Growth and Scale do not discount it. See the rate card.
If the answer comes back empty, the $0.004 is refunded automatically and the search you asked for is still delivered and still billed at its own rate. You are never charged the surcharge for an aiMode object with nothing in it. A free-tier call that asks for AI Mode is never charged the surcharge at all.
On a paid account, asking for AI Mode takes the call out of your free allowance. A Deep Search carrying include_ai_mode=true is never served from your remaining free calls: it is billed in full — the Web rate for every page you asked for, plus the $0.004 — even when free calls are still left. Those free calls are not spent on it; they are simply not available to it, so leave the parameter off and the same search draws on the allowance exactly as before.
serp_web key accepts include_ai_mode with the same paid-plan rule and the same $0.004 surcharge as the synchronous call. Job items are stricter about the value than the synchronous call is: true, 1 or yes switch it on, false, 0 or no leave it off, and anything else rejects that item with Invalid include_ai_mode. Use true or false. serp_quick does not take it, for the same reason Quick Search does not.News Search API
Search for news articles from various sources. Returns article titles, sources, publication times, and snippets.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | The search query |
| num | integer | Number of articles: 1–50 (default: all available) |
| pages | integer | Number of result pages to retrieve: 1–5 (default: 1). Alternative to num. |
| engine | string | google, bing, yahoo, ddg, or brave (default: "google") |
| country | string | Country code for localized news (default: "us") |
| freshness | string | Time filter: h/1h (hour), d/1d (day), 7d (week), w, m/1m, y/1y (optional) |
| language | string | 2-letter ISO language code (e.g., en, es, de). Accepted on all engines; coverage is broadest on Yahoo, Bing and Brave, and narrower on DuckDuckGo. (optional) |
| sort | string | Sort order: relevance or date (optional, Yahoo and Bing only) |
| safe | string | SafeSearch: off, moderate, or strict (optional, Yahoo and Bing only) |
| timeout | integer | Seconds you are willing to wait for this call (optional). It can only lower this endpoint's time budget, never raise it. timeout_ms takes milliseconds. |
| format | string | full (default) or simple. Simple returns position, title, url, and source only. |
Example Request
curl "https://apiserpent.com/api/news?q=artificial+intelligence&num=20&country=us" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
{
"success": true,
"query": "artificial intelligence",
"type": "news",
"engine": "google",
"country": "us",
"pagesScraped": 2,
"results": {
"articles": [
{
"position": 1,
"title": "AI Breakthroughs Reshape Industry",
"url": "https://example.com/ai-news",
"source": "Tech News Daily",
"publishedTime": "2026-03-02",
"snippet": "Major advances in AI...",
"image": "https://example.com/ai-news-thumb.jpg"
}
],
"totalResults": 20
},
"meta": {
"totalArticles": 20,
"elapsed": "1800ms",
"timestamp": "2026-03-03T10:30:00.000Z"
}
}
null when the result does not include it.Image Search API
Search for images across the web. Returns thumbnails, original image URLs, and source websites. A single request can ask for up to 100 images via num=, but that is a ceiling, not a promise — no engine has been measured returning the full 100 in one call. Measured at num=100: about 60 on google, bing and yahoo, ~95 on ddg, ~50 on brave. A short answer carries meta.partialResults and the top-level delivery block.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | The search query |
| num | integer | Number of results to request: 1–100 (default: all available). A ceiling, not a promise — see the note above. |
| engine | string | google, bing, yahoo, ddg, or brave (default: "google") |
| country | string | Country code for localized results (default: "us") |
| size | string | Filter by image size: small, medium, large, wallpaper (optional) |
| type | string | Filter by image type: photo, clipart, lineart, animated, face (optional) |
| color | string | Filter by color: red, blue, green, yellow, etc. (optional) |
| aspect | string | Aspect ratio: square, wide, or tall (optional) |
| layout | string | Layout: square, tall, or wide (optional, DDG only) |
| license | string | License filter: cc, public, share, sharecommercial, modify, modifycommercial, any (optional) |
| people | string | People filter: face, portrait, or nonportrait (optional, Yahoo and Bing only) |
| time | string | Time filter: day, week, month, year (DDG/Yahoo only) |
| language | string | 2-letter ISO language code (e.g., en, es, de). Restricts image results by language. Accepted on all engines; coverage is broadest on Yahoo, Bing and Brave, and narrower on DuckDuckGo. (optional) |
| format | string | full (default) or simple. Simple returns position, title, original, and thumbnail only. |
| timeout | integer | Seconds you are willing to wait for this call (optional). It can only lower this endpoint's time budget, never raise it. timeout_ms takes milliseconds. |
Example Request
curl "https://apiserpent.com/api/images?q=mountain+landscape&num=20" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
{
"success": true,
"query": "mountain landscape",
"type": "images",
"engine": "google",
"country": "us",
"results": {
"images": [
{
"position": 1,
"title": "Beautiful Mountain View",
"thumbnail": "https://example.com/thumb/...",
"original": "https://example.com/image.jpg",
"width": 1920,
"height": 1080,
"source": "example.com",
"pageUrl": "https://example.com/gallery"
}
],
"totalResults": 20
},
"meta": {
"totalImages": 20,
"elapsed": "2100ms",
"timestamp": "2026-03-03T10:30:00.000Z"
}
}
null when the result does not include it.Video Search API
Search for videos on Google, DuckDuckGo, Yahoo, Bing or Brave. Returns video titles, thumbnails, durations, view counts, and source URLs. Available with engine=ddg (the default), engine=google, engine=yahoo, engine=bing or engine=brave. num asks for up to 100, but that is a ceiling, not a promise — measured at num=100: Google ~60, Yahoo ~60, Bing ~59, DDG ~59, Brave ~50. A short answer carries meta.partialResults and the top-level delivery block.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | The search query |
| num | integer | Number of results to request: 1–100 (default: all available). A ceiling, not a promise — see the note above. |
| engine | string | google, ddg, yahoo, bing, or brave (default: "ddg"). |
| country | string | Country code for localized results (default: "us") |
| duration | string | Filter by duration: short, medium, or long (optional, DDG, Yahoo and Bing) |
| resolution | string | Filter by resolution: high or standard (DDG); 360p, 480p, 720p, hd, 1080p (Yahoo and Bing) (optional) |
| time | string | Alias for freshness on every engine (optional): day → d, week → w, month → m, year → y. Any other value is rejected 400. If you send both, freshness wins — it is the finer-grained of the two. |
| freshness | string | Time filter: h/1h (hour), d/1d (day), 7d (week), w, m/1m, y/1y (optional) |
| safe | string | SafeSearch: off, moderate, or strict (optional) |
| format | string | full (default) or simple. Simple returns position, title, url, and duration only. |
| timeout | integer | Seconds you are willing to wait for this call (optional). It can only lower this endpoint's time budget, never raise it. timeout_ms takes milliseconds. |
Example Request
curl "https://apiserpent.com/api/videos?q=python+tutorial&num=20&engine=ddg" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
{
"success": true,
"query": "python tutorial",
"type": "videos",
"engine": "ddg",
"country": "us",
"results": {
"videos": [
{
"position": 1,
"title": "Python Tutorial for Beginners",
"url": "https://www.youtube.com/watch?v=...",
"duration": "6:14:07",
"source": "www.youtube.com",
"views": 45000000,
"thumbnail": "https://i.ytimg.com/vi/.../hqdefault.jpg",
"description": "A full beginner course covering syntax, data structures, and functions.",
"publisher": "Dev Tutorials",
"embedUrl": "https://www.youtube.com/embed/...",
"publishedTime": "2026-01-14"
}
],
"totalResults": 20
},
"meta": {
"totalVideos": 20,
"elapsed": "3800ms",
"timestamp": "2026-03-10T10:30:00.000Z"
}
}
null when the result does not include it.thumbnail for a YouTube video is always that video's own YouTube thumbnail (i.ytimg.com), on every engine; for any other video it is the engine's thumbnail when one is available, else null. publisher is the uploader's / channel's name, never the platform word: a result that names only the platform ("YouTube") returns null there, since the platform is already in source.Shopping Search API
GET /api/shopping currently answers 404 for every account, it is absent from the playground, and it draws no calls from the 10-call free pool. The rate card lists it as planned launch pricing; nothing here is billable until it ships. The contract below is published so you can build against it ahead of launch. Check GET /api/status — the endpoint appears in limits.endpoints only once it is live. This is the dedicated Shopping endpoint only. The shopping block inside a web SERP response is live today and always present.Search product results with engine=google, engine=yahoo (default), engine=ddg, or engine=brave. All four serve product listings with distinct, engine-specific field names: engine=google returns Google-style fields (link, source, product_rating, product_reviews); engine=yahoo, engine=ddg, and engine=brave return card-style fields (url, merchant, store, rating, reviews). Each item also includes price when available, a separate currency field, thumbnail, sponsored flag, and snippet.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | The product search query |
| engine | string | google, yahoo, ddg, or brave (default: "yahoo") |
| country | string | Country code for localized product results (default: "us") |
| num | integer | Number of products to return: 1–20 (default: 10) |
| language | string | Language code, for example en (optional) |
| format | string | full (default) or simple. Simple field names follow the engine — google returns position, title, price, source, link, and thumbnail; yahoo, ddg, and brave return position, title, price, merchant, url, and thumbnail. |
Example Request
curl "https://apiserpent.com/api/shopping?q=nike+running+shoes&engine=google&num=10" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
{
"success": true,
"query": "nike running shoes",
"type": "shopping",
"engine": "google",
"country": "us",
"results": {
"shopping": [
{
"position": 1,
"title": "Nike Vomero 18 Men's Road Running Shoes",
"link": "https://www.nike.com/...",
"source": "Nike",
"price": "$150.00",
"currency": "USD",
"thumbnail": "https://example.com/product.jpg",
"product_rating": 4.7,
"product_reviews": 128,
"sponsored": false,
"snippet": "A stable, responsive daily trainer for neutral runners."
}
],
"totalResults": 10
},
"meta": {
"totalShopping": 10,
"elapsed": "4200ms",
"timestamp": "2026-08-03T10:30:00.000Z"
}
}
Pixel Position BETA
Add pixel_position=true to /api/search on engine=google, yahoo, bing or ddg, and every item in the response — organic, AI Overview, PAA, ads, featured snippet, knowledge panel, local pack, shopping, videos — gets two extra fields: a pixel_position integer (y-coordinate in pixels from the top of the rendered desktop SERP) and a pixel_box object with the element's full bounding rectangle ({ x, y, w, h }). Useful for above-the-fold analysis, share-of-voice scoring, and SERP layout audits.
engine=brave — a Brave response carries no pixel_position or pixel_box fields, so pick one of the other four engines when you need them. Paid tiers only — free-tier responses include metadata.pixelPositionUnavailable: 'free_tier' instead of the pixel fields. No price increase over a normal call. Not supported on Quick Search (/api/search/quick) on any engine — that call still succeeds and carries metadata.pixelPositionUnavailable: 'not_supported_on_quick' with a metadata.pixelPositionHint naming /api/search. Not supported on News, Images, or Videos endpoints either. Full spec →AI Ranking API — Combined
Check how AI language models cite and rank your domain or brand. This endpoint queries multiple LLMs (Claude, ChatGPT, Gemini, Perplexity) simultaneously and returns citation analysis for each.
Parameters
| Parameter | Type | Description |
|---|---|---|
| keyword required | string | The search keyword/topic to query. Also accepts q as an alias. |
| domain | string | Your domain to track in citations (e.g., example.com) (optional) |
| engines | string | Comma-separated list of AI engines: claude, chatgpt, gemini, perplexity (default: all four). llm is an accepted alias and takes the same values — ?llm=gemini is ?engines=gemini. |
| prompt_type | string | Prompt style: standard, deep, or brand (default: "standard") |
| country | string | 2-letter country code. Asks the engines about that market — a query run for de returns the sources that market surfaces (default: "us"). Accepts the same codes as /api/search. |
| language | string | 2-letter ISO language code, e.g. en, de, es (optional) |
Example Request
curl "https://apiserpent.com/api/ai/rank?keyword=best+seo+tools&domain=ahrefs.com&engines=claude,chatgpt" \
-H "X-API-Key: sk_live_your_api_key"
Per-Engine Response Fields
| Field | Type | Description |
|---|---|---|
| target_found | boolean | true if the requested domain (or any of its subdomains) appears in this engine's citations |
| target_position | integer | null | 1-indexed citation position where the domain was first matched. null if not found. |
| target_match_type | string | null | "exact" (cited domain equals domain after stripping www.), "subdomain" (a subdomain of domain was cited — e.g. docs.example.com for example.com), or null if not found. |
| target_matched_domain | string | null | The actual domain that matched. Equals domain for exact matches; equals the subdomain hostname for subdomain matches. |
| citations | array | List of citations returned by this engine, each with position, url, title, domain. |
| total_citations | integer | Number of citations parsed. |
| response_text | string | The full LLM response text used to derive the citations. |
Subdomain matching is implicit — the API tries an exact match first, then falls back to a subdomain match. Use target_match_type on the client to differentiate the two when displaying ranking results.
AI Ranking API — Single Engine
Query a specific AI engine for citation analysis. Available engines: claude, chatgpt, gemini, perplexity.
URL Paths
| Path | Engine | Cost per 1K (Default) |
|---|---|---|
/api/ai/rank/claude | Anthropic Claude | $20.00 |
/api/ai/rank/chatgpt | OpenAI ChatGPT | $20.00 |
/api/ai/rank/gemini | Google Gemini | $20.00 |
/api/ai/rank/perplexity | Perplexity | $20.00 |
Prices shown are Default tier. Growth (10× off) locks in with a one-time $100 deposit and Scale (20× off) with a one-time $500 deposit.
Parameters
| Parameter | Type | Description |
|---|---|---|
| keyword required | string | The search keyword/topic. Also accepts q as an alias. |
| domain | string | Your domain to track in citations (optional) |
| prompt_type | string | Prompt style: standard, deep, or brand (default: "standard") |
| country | string | 2-letter country code. Asks the engines about that market — a query run for de returns the sources that market surfaces (default: "us"). Accepts the same codes as /api/search. |
| language | string | 2-letter ISO language code, e.g. en, de, es (optional) |
Example Request
curl "https://apiserpent.com/api/ai/rank/claude?keyword=best+crm+software&domain=hubspot.com" \
-H "X-API-Key: sk_live_your_api_key"
YouTube Search API
Search YouTube videos, channels, and playlists by keyword. Each result carries its title, channel, publish date and thumbnail, and the list can be filtered by type, order and duration. This endpoint is free-tier eligible.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | The search query |
| type | string | Result type: video, channel, or playlist (default: "video") |
| order | string | Sort order: relevance, date, viewCount, rating, title (default: "relevance") |
| num | integer | Number of results: 1–50 (default: 10) |
| duration | string | Video duration: any, short (<4 min), medium (4-20 min), long (>20 min) (optional) |
| country | string | Country code for regional results (default: "us") |
| details | boolean | Include detailed video statistics (true or 1) (default: false) |
| published_after | string | Filter to videos published after this point. ISO 8601 date or datetime (e.g. 2026-01-01 or 2026-01-01T00:00:00Z) (optional) |
| safe | string | SafeSearch level: moderate, strict, none (default: "moderate") |
| page_token | string | Pagination token from a previous response's nextPageToken (optional) |
Example Request
curl "https://apiserpent.com/api/social/youtube/search?q=machine+learning&type=video&num=10&order=viewCount" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
Captured live from GET /api/social/youtube/search?q=how+to+solve+a+rubiks+cube&num=3. Verbatim except the three description values, trimmed for print — on a real call they carry the video's full description text.
{
"success": true,
"query": "how to solve a rubiks cube",
"type": "video",
"results": [
{
"position": 1,
"id": "7Ron6MN45LY",
"type": "video",
"title": "Learn How to Solve a Rubik's Cube in 10 Minutes (Beginner Tutorial)",
"description": "Learning to solve a Rubik's Cube can be easy! Read the pinned comment for common questions.\n\nHow To Solve FASTER ► https://www.youtube.co...",
"channelId": "UCqTVfT9JQqhA6_Hi_h_h97Q",
"channelTitle": "J Perm",
"publishedAt": "2018-09-19T16:36:25Z",
"thumbnail": "https://i.ytimg.com/vi/7Ron6MN45LY/hqdefault.jpg",
"url": "https://www.youtube.com/watch?v=7Ron6MN45LY"
},
{
"position": 2,
"id": "PW2J8IblczM",
"type": "video",
"title": "How to Solve a Rubik's Cube [Best Method 2026]",
"description": "I have taught millions of people to solve their first 3x3 Rubik's Cube using this beginner's method. For this tutorial, I collaborated wi...",
"channelId": "UC9wvQsn9VGQsX9uFWEuQW4A",
"channelTitle": "CubeHead",
"publishedAt": "2024-02-14T14:00:16Z",
"thumbnail": "https://i.ytimg.com/vi/PW2J8IblczM/hqdefault.jpg",
"url": "https://www.youtube.com/watch?v=PW2J8IblczM"
},
{
"position": 3,
"id": "iAa7_mDgjTw",
"type": "video",
"title": "How to solve a Rubik's Cube In 2026",
"description": "If you have always wanted to learn how to solve a Rubik's Cube, today I am showing you the EASIEST way to understand Cubing! That does no...",
"channelId": "UCpp1GoykvTBuPiZt3FiFHjg",
"channelTitle": "SoupTimmy",
"publishedAt": "2026-01-24T00:01:02Z",
"thumbnail": "https://i.ytimg.com/vi/iAa7_mDgjTw/hqdefault.jpg",
"url": "https://www.youtube.com/watch?v=iAa7_mDgjTw"
}
],
"totalResults": 1000000,
"nextPageToken": "CAMQAA",
"prevPageToken": null,
"meta": {
"elapsed": "2535ms",
"timestamp": "2026-09-02T10:14:15.354Z"
}
}
title and description are now the full text. Both fields carry the complete text, unescaped: no entity codes (&, ') to decode and no shortened description on a basic search. If you were unescaping or re-fetching descriptions yourself, you can delete that step. Nothing was renamed and nothing else about the response changed.Stats fields (
viewCount, likeCount, duration…) appear only with details=true, or on YouTube Video Details. A basic search returns the snippet fields.Short answers. When you named a depth with
num and fewer results came back, the response carries a top-level delivery block — requested (the num you asked for), returned, a reason, a plain-English note and, where a job form is live, async_endpoint naming the job that can complete the request. A call that named no depth publishes no block: with no target, there is nothing to be short of. A full answer has no delivery key either.Empty fields. Several of these fields are best-effort and can carry their empty value — see Fields That Can Come Back Empty.
YouTube Video Details
Get detailed information about one or more YouTube videos, including statistics (views, likes, comments), descriptions, and tags. This endpoint is free-tier eligible.
Parameters
| Parameter | Type | Description |
|---|---|---|
| id | string | A single YouTube video ID (e.g., dQw4w9WgXcQ) |
| ids | string | Comma-separated list of video IDs (max 50). Use id or ids, not both. |
Example Request
curl "https://apiserpent.com/api/social/youtube/video?id=dQw4w9WgXcQ" \
-H "X-API-Key: sk_live_your_api_key"
404 with the body { "error": "Video not found" }. A resource that does not exist is an answer, and the call is charged. Every other non-2xx response is refunded automatically. When some ids resolve and others do not, the ones found are returned and totalResults counts them — a short list is a delivered answer and is charged.Short answers. When fewer videos come back than ids you sent, the response carries a top-level
delivery block — requested (how many ids you sent), returned, a reason, a plain-English note and, where the job form is live, async_endpoint naming the job that can complete the request. A response that resolved every id has no delivery key at all.Empty fields. Several of these fields are best-effort and can carry their empty value — see Fields That Can Come Back Empty.
YouTube Channel Details
Get channel information including subscriber count, video count, total views, and optionally recent videos. This endpoint is free-tier eligible.
Parameters
| Parameter | Type | Description |
|---|---|---|
| id | string | YouTube channel ID (e.g., UCBcRF18a7Qf58cCRy5xuWwQ) |
| handle | string | YouTube handle (e.g., @mkbhd). Use id, handle, or url. |
| url | string | Full YouTube channel URL, in any of the /@handle, /channel/UC…, /c/name or legacy /user/name forms |
| include_videos | boolean | Include recent videos (true or 1) (default: false) |
| video_count | integer | Number of recent videos to include: 1–50 (default: 10) |
Example Request
curl "https://apiserpent.com/api/social/youtube/channel?handle=@mkbhd&include_videos=true&video_count=5" \
-H "X-API-Key: sk_live_your_api_key"
404 with the body { "error": "Channel not found" }. A resource that does not exist is an answer, and the call is charged. Every other non-2xx response is refunded automatically.recentVideos, publishingFrequency, lastVideoDate, daysSinceLastVideo and isActive appear only with include_videos=true, and the last four are derived from the dates on those rows — a set of rows carrying no publishedAt publishes the same shape a channel with no uploads gets, and the rows themselves still ship.Empty fields. Several of these fields are best-effort and can carry their empty value — see Fields That Can Come Back Empty.
YouTube Playlist Videos
List the public videos in a YouTube playlist, in playlist order, each with the same statistics (views, likes, comments), description and tags that Video Details returns. The rows carry the same best-effort fields and empty values as Video Details. This endpoint is free-tier eligible.
Parameters
| Parameter | Type | Description |
|---|---|---|
| id | string | YouTube playlist ID (e.g., PLBCF2DAC6FFB574DE). Use id, playlist_id, or url — one of the three is required. |
| playlist_id | string | Alias of id |
| url | string | A full playlist or watch URL; the list= value is taken from it |
| video_count | integer | Number of videos to return, 1–50, clamped rather than rejected (default: 50) |
Example Request
curl "https://apiserpent.com/api/social/youtube/playlist?id=PLBCF2DAC6FFB574DE&video_count=2" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
Every row has the same fields as a Video Details result. Abbreviated to the first of the two videos:
{
"playlist_id": "PLBCF2DAC6FFB574DE",
"total_results": 2,
"videos": [
{
"id": "dQw4w9WgXcQ",
"title": "Example video",
"description": "The first line of the description.",
"channelId": "UCuAXFkgsw1L7xaCfnd5JJOw",
"channelTitle": "Example channel",
"publishedAt": "2009-10-25T06:57:33Z",
"thumbnail": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
"tags": ["music", "pop"],
"categoryId": "10",
"duration": "PT3M34S",
"durationFormatted": "3:34",
"durationSeconds": 214,
"isShort": false,
"viewCount": 1600000000,
"likeCount": 18000000,
"commentCount": 2300000,
"engagementRate": 1.27
}
],
"meta": {
"elapsed": "420ms",
"timestamp": "2026-03-12T10:00:00.000Z"
}
}
200 with total_results: 0 and an empty videos list — that is a delivered answer, and it is charged, exactly like an empty search. A playlist that does not exist, has been deleted, is private, or whose id is malformed answers 404 with the body { "error": "Playlist not found" }. A resource that does not exist is an answer, and the call is charged. Every other non-2xx response is refunded automatically. total_results is the number of rows returned, so a playlist with fewer public videos than video_count simply returns fewer rows.Short answers. When you named a depth with
video_count and fewer rows came back, the response carries a top-level delivery block — requested (the video_count you asked for), returned, a reason, a plain-English note and, where a job form is live, async_endpoint naming the job that can complete the request. A call that named no depth publishes no block: with no target, there is nothing to be short of. A full answer has no delivery key either.YouTube — Fields That Can Come Back Empty
Applies to all four YouTube endpoints. Every YouTube response carries the same keys, in the same order, with the same types on every call. A field we cannot state exactly is its documented empty value — never null, and never a guess.
| Field | Empty value | Where it appears |
|---|---|---|
| publishedAt | "" |
search rows, video rows, playlist rows, channel |
| description | "" |
search rows, video rows, playlist rows |
| tags | [] |
video rows, playlist rows, details=true search rows |
| categoryId | "" |
video rows, playlist rows, details=true search rows |
| duration / durationFormatted / durationSeconds | "" / "0:00" / 0 |
video rows, playlist rows, details=true search rows |
| viewCount / likeCount / commentCount | 0 |
video rows, playlist rows, details=true search rows |
| banner / country / keywords | "" |
channel |
engagementRate is 0 and isShort is false whenever the counts or the duration they are derived from are empty.
An empty value is never a rounded or an invented one.
"" and 0 mean "we do not have this", not "zero" — if you persist these fields, keep that distinction rather than writing a 0 into a statistics column. We would rather hand you a blank than a number we cannot stand behind.A call we cannot serve at all answers
503 with code: temporarily_unavailable and is refunded automatically; it is never a hollow row dressed up as an answer.Instagram Profile API
Retrieve public Instagram profile data including follower counts, bio, and recent posts. This endpoint is free-tier eligible.
Parameters
| Parameter | Type | Description |
|---|---|---|
| username required | string | Instagram username (e.g., natgeo). Also accepts handle (with @) or url. |
| include_posts | boolean | Set true to include recentPosts (best-effort). Defaults to false — the profile call stays lean. When enabled, recent posts are returned with shortcode, thumbnail, media type, permalink, and post date; like/comment counts are not available. |
| include_post_details | boolean | Set true for rich per-post details — full caption, like/comment counts, video view count, carousel children, music, location, and more (best-effort, up to ~12 recent posts). Implies include_posts. Billed at the higher $4.00/1K profile+post-details rate — but only for the tier actually delivered: if the profile's posts could not be opened for their details, the call is billed at the include_posts rate when the post list came back without details, or the basic-profile rate when no posts came back at all. |
| include_contact | boolean | Adds contactInfo — public email, phone, WhatsApp number, business contact method, and city. Adds +20% to the credit multiplier. |
| include_business | boolean | Adds businessInfo — professional/business account flags, business category, and shopping/commerce signals. Adds +20% to the credit multiplier. |
| include_reels_info | boolean | Adds reelsInfo — reels/clips counts, highlight reel counts, story, guides, and channel flags. Adds +20% to the credit multiplier. |
| include_profile_detail | boolean | Adds profileDetail — pronouns, joined-recently flag, hide-counts flag, structured bio links/entities, and high-res profile picture URLs. Adds +20% to the credit multiplier. |
Example Request
curl "https://apiserpent.com/api/social/instagram/profile?username=natgeo" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
Captured from that basic call (no include_*), showing the fields documented below. profilePicUrl is abbreviated for print — it is a long signed URL that expires:
{
"success": true,
"username": "natgeo",
"profileUrl": "https://www.instagram.com/natgeo/",
"fullName": "National Geographic",
"biography": "Step into wonder and find your inner explorer with National Geographic 🌎",
"externalUrl": "http://visitstore.bio/natgeo",
"profilePicUrl": "https://scontent-fra5-1.cdninstagram.com/v/t51.82787-19/…_n.jpg",
"followersCount": 268636947,
"followingCount": 194,
"postsCount": 32000,
"isVerified": true,
"isBusiness": false,
"isPrivate": false,
"accountType": "personal",
"engagementRate": null,
"avgLikesPerPost": null,
"avgCommentsPerPost": null,
"postsSampled": 0,
"followerFollowingRatio": 1384726.53,
"lastPostDate": null,
"daysSinceLastPost": null,
"isActive": null,
"recentPosts": [],
"accessLevel": "full",
"meta": {
"elapsed": "22040ms",
"timestamp": "2026-09-02T11:39:06.438Z"
}
}
200 with isPrivate: true and the public counts Instagram shows for it (followers, following, posts), and it is charged. A username that does not exist answers 404 with the body { "error": "Profile not found or not accessible" }. A resource that does not exist is an answer, and the call is charged. Every other non-2xx response is refunded automatically. externalUrl is the profile's link-in-bio, when the account has one; accounts without one return null. Recent posts and field groups are opt-in — pass include_posts=true or include_post_details=true for posts, and any of the four include_* field groups to expand profile coverage. Field groups are best-effort and vary by profile — absent values are returned as null, never fabricated. Each opted-in field group adds +20% to the call's credit multiplier (1 + 0.2×N).Engagement is only ever a measured value.
engagementRate, avgLikesPerPost and avgCommentsPerPost are null unless posts were actually sampled, and postsSampled is always present — 0 when nothing was averaged, so engagementRate null + postsSampled 0 reads as "nothing sampled", never a fabricated 0 and never a claim about the source. A genuine measured zero still returns 0. lastPostDate/daysSinceLastPost are null when recency could not be measured (never a stale date), and isActive is null when unknown — never a false "inactive".When a
include_post_details request would overrun the synchronous window, the API returns the profile plus however many post details fit, with a top-level delivery block (requested/returned/reason/note, plus max_supported when the depth was capped) — the same block, in the same place, as every other endpoint; the older top-level partial field is unchanged and still ships beside it — and an async_endpoint. delivery.async_endpoint names the job form to use; read the path from the response rather than hardcoding one, and submit the same request there — billed on the same curve, bounded only by the job deadline rather than the request one. A job form is only advertised when one is live for your account.LinkedIn Company API
Serpent's LinkedIn API returns public company firmographics — name, industry, size, headcount, headquarters, specialities, and more — from a company's LinkedIn URL or vanity slug. This endpoint is free-eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| url required | string | Full company LinkedIn URL (e.g., https://www.linkedin.com/company/microsoft). Provide url or slug. |
| slug | string | Company vanity slug (e.g., microsoft) (alternative to url) |
Example Request
curl "https://apiserpent.com/api/linkedin/company?url=https://www.linkedin.com/company/microsoft" \
-H "X-API-Key: sk_live_your_api_key"
LinkedIn Job Search API
Serpent's LinkedIn API returns an array of public job cards matching a keyword search, narrowed by any combination of location, geo ID, date posted, job type, experience level, workplace type, company, Easy Apply, early applicant and sort order. Every response carries a filters object echoing exactly which filters were applied. Use start to page through results. This endpoint is free-eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| keywords required | string | Job search keywords (e.g., software engineer) |
| location | string | Location filter (e.g., San Francisco Bay Area) (optional) |
| start | integer | Pagination offset into the result set — 0, 10, 25, and so on. A value that is not a number is read as 0. (default: 0) |
| geo_id | integer | Exact LinkedIn geo ID — for example 103644278 for the United States. More precise than location, which is free text. Digits only. (optional) |
| date_posted | enum | How recently the job was posted. Exactly one of past_hour, past_24h (alias: past_day), past_week, past_month. (optional) |
| job_type | enum(s) | One or more, comma-separated: full_time, part_time, contract, temporary, internship, volunteer, other. (optional) |
| experience_level | enum(s) | One or more, comma-separated: internship, entry, associate, mid_senior, director, executive. (optional) |
| remote | enum(s) | Workplace type — one or more, comma-separated: on_site, remote, hybrid. (optional) |
| company_id | integer(s) | One or more numeric LinkedIn company IDs, comma-separated (e.g., 1035,1441). A company name is not accepted here, and every non-digit character is removed before the ID is used — so company_id=google filters nothing, and company_id=abc123 is read as company 123. Take the ID from the company's own LinkedIn page, or from linkedin_internal_id on Company. (optional) |
| easy_apply | boolean | The single value true (any casing) limits results to Easy Apply postings. 1 and yes are not recognised here and leave the filter off. Best-effort — not every posting is labelled. (optional) |
| early_applicant | boolean | The single value true (any casing) limits results to postings that have had few applicants so far. 1 and yes are not recognised here. Best-effort. (optional) |
| sort | enum | relevance or recent. A preference rather than a guarantee — LinkedIn does not always re-order for it, so sort the jobs array yourself if strict recency matters. (optional) |
date_posted=last_week — not one of the values in the table — and the call still succeeds: that filter is dropped and the search runs without it, at the normal price. So check your spelling, because a typo buys you a broader result set than you asked for and is billed exactly the same as the narrow one you wanted. Multi-value filters are read value by value, so remote=remote,hybrid,bogus applies remote and hybrid and drops only bogus. Values are matched without regard to casing and surrounding spaces are trimmed, so Full_Time and full_time , contract both work.The
filters object in the response is your receipt. It lists exactly the filters and values that were applied. If a filter you sent is missing from it, that value was not recognised — check it against the table above before you read anything into the results.Example Request
curl "https://apiserpent.com/api/linkedin/jobs?keywords=software+engineer&location=San+Francisco+Bay+Area&start=0" \
-H "X-API-Key: sk_live_your_api_key"
Example Request (with filters)
curl "https://apiserpent.com/api/linkedin/jobs?keywords=software+engineer&location=United+States&date_posted=past_week&job_type=full_time,contract&experience_level=mid_senior,director&remote=remote,hybrid&easy_apply=true&sort=recent" \
-H "X-API-Key: sk_live_your_api_key"
Example Response (abridged — one of the 10 job cards shown)
{
"success": true,
"data": {
"keywords": "software engineer",
"location": "United States",
"start": 0,
"filters": {
"date_posted": "past_week",
"job_type": ["full_time", "contract"],
"experience_level": ["mid_senior", "director"],
"remote": ["remote", "hybrid"],
"easy_apply": true,
"sort": "recent"
},
"count": 10,
"jobs": [
{
"job_id": "3901234567",
"job_url": "https://www.linkedin.com/jobs/view/3901234567",
"job_title": "Senior Software Engineer",
"company_name": "Microsoft",
"company_linkedin_url": "https://www.linkedin.com/company/microsoft",
"company_logo": "https://media.licdn.com/dms/image/company-logo.png",
"location": "Redmond, WA",
"job_posted_date": "2026-06-24"
}
]
}
}
LinkedIn Job Details API
Serpent's LinkedIn API returns the full detail for a single public job posting by ID — description, seniority, employment type, applicant count, salary range, and the job poster. This endpoint is free-eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| job_id required | string | LinkedIn job ID (e.g., 3901234567). Also accepts id as an alias. |
Example Request
curl "https://apiserpent.com/api/linkedin/job?job_id=3901234567" \
-H "X-API-Key: sk_live_your_api_key"
LinkedIn Profile Search API BETA
Resolve a person's name to a short list of candidate public LinkedIn profiles, so you can pick the right one and then call Profile or Full Profile with its public_identifier. Narrow the match with the optional company, title and location parameters. Up to 10 candidates are returned. This endpoint is free-eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| name required | string | Person's full name (e.g., Bill Gates) |
| company | string | Company to narrow the match (e.g., Microsoft) (optional) |
| title | string | Job title to narrow the match (e.g., Co-chair) (optional) |
| location | string | Location to narrow the match (e.g., Seattle) (optional) |
Example Request
curl "https://apiserpent.com/api/linkedin/search?name=Bill+Gates&company=Microsoft" \
-H "X-API-Key: sk_live_your_api_key"
Example Response (abridged — one of the 7 candidates shown)
{
"success": true,
"data": {
"query_name": "Bill Gates",
"count": 7,
"results": [
{
"profile_url": "https://www.linkedin.com/in/williamhgates",
"public_identifier": "williamhgates",
"name": "Bill Gates",
"headline": "Chair, Gates Foundation and Founder, Breakthrough Energy"
}
]
}
}
null. A name that matches nobody answers 404 — that is a real answer, and it is charged. Priced at $1.00/1K (Default) and tier-discounted — $0.90/1K on Growth, $0.70/1K on Scale.LinkedIn Profile API
Serpent's LinkedIn API returns best-effort public profile basics — name, headline, occupation, location, follower/connection counts, profile flags, and current company — from a profile URL or vanity slug. This endpoint is free-eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| url required | string | Full profile URL (e.g., https://www.linkedin.com/in/williamhgates). Provide url or username. |
| username | string | Profile vanity slug (e.g., williamhgates) (alternative to url) |
| include_company_details | boolean | Set true to add company_details firmographics (name, industry, size, headcount, founded year, HQ) for the profile's current company. Adds +20% to the credit multiplier. If the company details could not be retrieved, the surcharge for that field group is refunded automatically; a company that genuinely publishes none of these firmographics is a delivered answer, and the surcharge stands. |
| include_public_web | boolean | Set true to cross-enrich the profile from public web search results — fills connections and people_also_viewed, and can refine headline and current company when the profile page leaves them out. Adds +20% to the credit multiplier. |
Example Request
curl "https://apiserpent.com/api/linkedin/profile?username=williamhgates" \
-H "X-API-Key: sk_live_your_api_key"
LinkedIn Full Profile API
Premium endpoint. Serpent's LinkedIn API returns richer public profile detail — identity, summary, full experience history, education, photo, and follower/connection counts. This endpoint is free-eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| url required | string | Full profile URL (e.g., https://www.linkedin.com/in/williamhgates). Provide url or username. |
| username | string | Profile vanity slug (e.g., williamhgates) (alternative to url) |
Example Request
curl "https://apiserpent.com/api/linkedin/profile/full?username=williamhgates" \
-H "X-API-Key: sk_live_your_api_key"
When the optional detail runs out of clock, the profile is returned as it stands with a top-level
delivery block — requested: 1, returned: 1, reason: "deadline_reached" and a sentence saying the profile came back before every optional detail could be added. What is short is the enrichment, not the profile. The identical legacy partial block ships beside it, unchanged. Where the job form is live, delivery.async_endpoint names it — the same request as a job, bounded by the job deadline rather than the request one.LinkedIn Posts API
Serpent's LinkedIn API returns a person's recent public posts — content, publish date, and engagement (reactions/comments/reposts) — resolved from the person's profile URL, username, or name. This endpoint is free-eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| username | string | Profile vanity slug (e.g., williamhgates). Provide username, url, or name. |
| url | string | Full profile URL (e.g., https://www.linkedin.com/in/williamhgates). |
| name | string | Person's full name (e.g., Bill Gates). |
| limit | integer | Max posts to return, 1–50 (default: 10). Priced by depth — see Pricing. |
| include_comments | boolean | Set true to include each post's public comment thread (text, author, avatar, likes) as comments. Adds +20% to the credit multiplier. |
| include_transcript | boolean | Set true to include a video post's transcript when LinkedIn publishes one for that video; null otherwise. Adds +20% to the credit multiplier — it applies to the call whether or not a page's posts carry one. |
Example Request
curl "https://apiserpent.com/api/linkedin/posts?username=williamhgates" \
-H "X-API-Key: sk_live_your_api_key"
include_comments and include_transcript field groups add +20% each, stacking on the depth price.engagement counts, image_url and author.followers are best-effort and vary by post; absent values come back as null, never fabricated. media_type is text, video or article; video_url, duration and embed_url are populated for video posts only. comments appears only with include_comments, and transcript only with include_transcript.Google Maps Quick Place Search
Returns up to 20 ranked place records. New accounts get Free to start, shared across every endpoint — Web Search, News, Images, Videos, AI Rank, LinkedIn, Maps, YouTube, and Instagram. The four Maps endpoints serve on https://api.apiserpent.com/api/maps/*; verify your exact query in the playground before production integration.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | Business category or place query. |
| location | string | Text location. Do not combine with lat/lng. |
| lat, lng | number | Paired coordinates — lat is −90 to 90, lng is −180 to 180. Both are required when either is supplied. |
| ll | string | Shorthand for the pair above: ll=40.7128,-74.0060 is the same request as lat=40.7128&lng=-74.0060. A leading @ is accepted so you can paste coordinates straight out of a Maps URL (@40.7580,-73.9855); a trailing zoom segment (,14z) is not — set zoom yourself. Send ll or lat/lng, never both, and do not combine either with location — sending two is a 400 even when they agree. |
| country | string | Two-letter country code (default: us). |
| language | string | Two-letter language code (default: en). |
| zoom | integer | Map zoom from 1 to 21 (default: 14). Lower numbers cover more ground per request; higher numbers concentrate the ranked list around the point. |
| start | integer | One of 0, 20, 40, 60, or 80. |
| timeout | integer | Seconds you are willing to wait for this call (optional). It can only lower this endpoint's time budget, never raise it. timeout_ms takes milliseconds. |
Tested cURL
BASE_URL="${SERPENT_BASE_URL:-http://localhost:3001}"
curl --get "$BASE_URL/api/maps/search/quick" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "q=coffee" \
--data-urlencode "location=New York, NY" \
--data-urlencode "country=us"Search response
{
"success": true,
"type": "maps",
"endpoint": "quick",
"places": [{
"rank": 1, "place_id": "...", "name": "Example Coffee",
"website": "https://example.com", "rating": 4.6,
"detail_status": "complete"
}],
"counts": { "requested": 20, "discovered": 20, "returned": 20, "fully_enriched": 19, "core_only": 1 },
"meta": { "elapsed_ms": 27498, "partial": true }
}core_only record keeps its discovered rank and available core fields. Treat its nullable detail fields as unknown, not as confirmed absence. counts and meta.partial make the state explicit, and the top-level delivery block gives the same fact with counts and a reason.Google Maps Deep Place Search
Attempts positions 1–100 using the same parameters and place schema as Quick, plus detail. Deep draws on the same shared 10-call free pool as every other endpoint; a free call counts as one call whatever detail you ask for.
Parameters
Everything Quick accepts except start, plus two Deep-only parameters:
| Parameter | Type | Description |
|---|---|---|
| detail | integer | How many of the returned places are opened and fully enriched, from 0 to 100 (default: 20). It does not set how long the list is — that is limit — and the places beyond detail arrive as core_only records (name, address, coordinates, rating, review count, phone, website). detail also decides which product you are buying. At detail of 1 or more this is Maps Deep, and the band is detail 1–20 at $0.0150 per call, 21–100 at a flat $0.0750 per call. detail=0 is a different, far cheaper product — it returns the ranked list alone and is billed as Maps List, not as Maps Deep. Synchronous requests are capped at detail=30 — a performance cap, not a pricing one, so 21–30 is available synchronously at the premium rate. Above the cap the API says so rather than silently trimming, and the same search is submitted as a job at GET /api/maps/search/async. GET /api/status is the authority on whether the job surface is live for your account — read limits.endpoints[…].async_available. |
| limit | integer | How many places come back at all, 1–100. The default depends on detail: 100 at any detail of 1 or more, and 20 when detail=0. A limit below a detail you named explicitly is refused with a 400 rather than quietly reconciled — asking to enrich more places than the answer holds is a contradiction, and either way of fixing it would charge you for something you did not ask for. Sent on its own, it lowers a defaulted detail to match. On a detail=0 call it also picks the price band — see Maps List below. |
curl --get "$BASE_URL/api/maps/search" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "q=coffee" \
--data-urlencode "location=New York, NY" \
--data-urlencode "detail=20"detail picks one of two flat price bands, and the step between them is a cliff. detail 1–20 is the standard band: $0.0150 per call ($15.00 per 1,000 calls). detail 21–100 is the premium band, flat: $0.0750 per call ($75.00 per 1,000 calls) — 21 and 99 cost exactly the same. detail=20 bills $0.0150 and detail=21 bills $0.0750, a 5× step for one extra enriched place. The default is detail=20, so a call that names no detail is always in the standard band. Neither band applies to detail=0 — that is Maps List, a different product at a different price. Per unit: at detail=20 that is $0.15 per 1,000 places returned and $0.75 per 1,000 fully-enriched places; at detail=100 it is $0.75 per 1,000 places returned and $0.75 per 1,000 fully-enriched places — the same enriched rate at both ends. In between, the premium band is worse per enriched record ($2.50 per 1,000 fully-enriched at detail=30), so buy at 20 or at 100 unless you need a specific depth. Depths above the synchronous cap of 30 are submitted as jobs at GET /api/maps/search/async at the same flat $0.0750 per call — call GET /api/status and read limits.endpoints[…].async_available to confirm the job surface is live for your account before you build against it.A local acceptance run on 2026-07-02 returned 97 places in 88.2 seconds: 85 complete and 12 core_only. That was a full-depth (detail=100) pass run directly against the worker, which is deeper than the synchronous cap allows and is not a production latency guarantee — real latency varies with detail, query, country, and platform load.
Maps List (detail=0)
detail=0 is a different product at a different price, on the same route. It returns the ranked business list and nothing else — nothing comes back enriched — and it is billed as Maps List, not as Maps Deep. There is no separate endpoint and no flag: send detail=0 and this is what you are buying.
curl --get "$BASE_URL/api/maps/search" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "q=dentist" \
--data-urlencode "location=Austin, TX" \
--data-urlencode "detail=0" \
--data-urlencode "limit=20"limit picks the band, and it counts places returned — not places enriched, which is what Deep’s band counts. Same shape, same edge, a different quantity:
limit on a detail=0 call | Per call | Default | Growth ($100+) | Scale ($500+) |
|---|---|---|---|---|
Maps List — standard band (limit 1–20; limit=20 is the default here) | $0.0001 | $0.10 | $0.09 | $0.07 |
Maps List — premium band (limit 21–100, flat — 21 and 99 cost the same) | $0.0005 | $0.50 | $0.45 | $0.35 |
Two things to know before you send
detail=0. First, the default limit here is 20, not 100 — a detail=0 call that names no limit returns 20 places. Ask for limit=100 if you want the full list, at the premium band. Second, the free pool counts this as one call at any list size, exactly as every other endpoint does.Google Maps Place Details
Returns one richest-available normalized place. Provide exactly one identifier.
| Parameter | Type | Description |
|---|---|---|
| place_id | string | Google place ID. |
| data_id | string | Google Maps data ID. |
| url | string | Supported HTTPS Google Maps URL. |
curl --get "$BASE_URL/api/maps/place" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "place_id=ChIJ..."Place response fields
| Field | Type | Description |
|---|---|---|
| rank | integer | null | The first key of every place record. 1-based position in the ranked list this place came back in. null only where the source gave no rank. |
| place_id / data_id | string | null | Google place / data identifiers. Reuse either as place_id or data_id for the Place and Reviews endpoints. |
| maps_url | string | null | Canonical HTTPS Google Maps URL for the place. |
| name | string | null | Business name. |
| description | string | null | Short public description when Google provides one. |
| categories | array | Business category labels, e.g. ["Coffee shop", "Cafe"]. [] when none. |
| address | object | Always an object. Structured address with street, city, state, postal_code, borough, country, and a joined formatted string — each of those is string | null. |
| coordinates | object | Always an object: latitude and longitude, each number | null. |
| phone | string | null | Public phone number. |
| website | string | null | Public website URL. |
| rating | number | null | Star rating from 0 to 5. |
| review_count | integer | null | Number of public reviews Google reports. |
| review_distribution | object | null | Review counts by star band, keys 1 through 5. null when the place carries no breakdown. |
| opening_hours | object | Always an object, {} when Google exposes no hours. Weekly hours and current open/closed state. |
| popular_times | object | Always an object, {} when Google exposes none. Hourly popularity by day. |
| business_status | string | null | Current state, e.g. Open, Closed, Temporarily closed, or Permanently closed. |
| price_range | string | null | Relative price level when Google reports one. |
| timezone | string | null | IANA timezone, e.g. America/New_York. |
| plus_code | string | null | Open Location Code when available. |
| amenities / service_options | array | Always arrays, [] when Google exposes none. Each entry is { name, options: [{ name, enabled, values }] } — takeout, delivery, outdoor seating and so on. |
| links | object | Always an object with booking (array), order_online (array) and menu (object | null). The arrays are [] when the place has no such link. |
| ownership | object | null | claimed boolean, owner_name, and owner_id. null when the place carries no ownership block. |
| cover_image | string | null | The chosen cover image URL. |
| image_count | integer | null | Number of public photos. |
| images | array | Public photos as { url, label }; [] when none. |
| detail_status | string | null | complete when enrichment finished, core_only when only the ranked core record was returned within the request deadline, null when the state cannot be named. |
Every one of those keys is always present, in that order, on every place record. The ones typed … | null carry null rather than disappearing; the ones marked always an object/array are never null. So 'phone' in place is always true — test the value, not the key. A null means Google does not expose that field for that place; absence is honest, never fabricated.
Google Maps Reviews
Returns up to 20 public reviews per call. Provide exactly one place identifier and reuse the returned opaque token for the next page.
| Parameter | Type | Description |
|---|---|---|
| place_id / data_id / url | string | Exactly one place identifier. |
| sort | string | relevant, newest, highest, or lowest. |
| page_token | string | Opaque token returned by the previous response. Do not decode or edit it. |
curl --get "$BASE_URL/api/maps/reviews" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "place_id=ChIJ..." \
--data-urlencode "sort=newest"Top-level response fields
| Field | Type | Description |
|---|---|---|
| place | object | The place the reviews belong to, in the same shape as Place Details. It identifies what was asked about; it is not the thing you bought. A page with zero reviews for a place we identified is a delivered result and is charged — a business with no reviews yet is a real answer to a real question. Credits are returned only when we could not complete the request, which is also the case in which this block comes back with no identity at all. |
| reviews | array | This page of reviews — fields below. |
| count | integer | How many reviews are in this page (reviews.length), never a total for the place. |
| next_page_token | string | null | Opaque cursor for the next page, null on the last one. Bound to the place and sort order, and it expires after 15 minutes — after that the request is rejected 400 and you start the listing again. |
| delivery | object | Present only when fewer than 20 reviews were returned — a full page omits it entirely. requested is 20, the page size, and returned is what this page holds. reason is deadline_reached when more reviews exist beyond this page and fewer_available when the place simply has no more — a four-review business is never blamed on our clock. Where the job form is live it also carries async_endpoint. Same block, same place, as every other endpoint. |
Review response fields (reviews[])
| Field | Type | Description |
|---|---|---|
| review_id / review_url | string | null | Public review identifier and URL. |
| rating | integer | null | Reviewer star rating from 1 to 5. |
| text | object | Always an object with original and translated, each string | null. The object is never null; the two fields inside it are. |
| published_at / updated_at | string | null | ISO-8601 timestamps when Google exposes them. |
| relative_date | string | null | Google's relative date label, e.g. a month ago. |
| images | array | Public review photo URLs; [] when none. |
| detailed_ratings | object | null | Per-aspect scores when Google provides them. |
| owner_response | object | null | original / translated text plus published_at / updated_at when the owner replied; null when they did not. |
| reviewer | object | Always an object: name, id, url, profile_photo, review_count and local_guide, each null when Google does not expose it. |
Review fields can be null when Google does not expose them for a given review. Absolute review dates are not always available; relative_date is the reliable label.
Amazon Search API
Serpent's Amazon API returns one page of ranked product cards for a keyword search on any supported marketplace — ASIN, title, image, price, currency, rating, ratings total and badges. This endpoint is free-tier eligible (shared 10-call pool). Amazon bills per 1,000 requests, never per 1,000 results: one request is one page, and a page carries 16–48 product cards depending on the marketplace (measured: 16 on amazon.com and amazon.co.uk, 48 on amazon.de and amazon.co.jp).
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | Search term, e.g. laptop stand. Up to 300 characters. |
| domain | string | Marketplace to search (default amazon.com). Accepts the full domain (amazon.co.uk), the suffix (co.uk) or the country (uk). 22 marketplaces serve search; amazon.eg is best-sellers only. A marketplace we do not serve is a 400 that lists the ones we do — never a silent guess. |
| country | string | Two-letter code, an alias for domain — country=uk and domain=amazon.co.uk name the same marketplace. Passing both is fine when they agree; naming two different marketplaces is a 400 rather than a guess. |
| language | string | A locale the marketplace actually serves, e.g. en, en-GB, ja. A bare code is expanded against that marketplace's own list; where a bare code is ambiguous (zh on amazon.com) the 400 names the choices instead of picking one. A locale the marketplace does not serve is refused. |
| page | integer | Results page, 1–20 (default 1). Only the first 5 pages are served synchronously; a deeper page returns 400 and names the job form when one is live for your account. The page is never silently clamped — you are billed for the page you asked for, so serving a different one would be both a wrong answer and a confusing invoice. |
Response fields
| Field | Type | Description |
|---|---|---|
| search_term | string | null | The term the page was built for. |
| total_results / total_results_text | integer | null, string | null | Amazon's own stated total, as a number and as the label it rendered. null when Amazon stated none — never back-filled from results_count. |
| current_page | integer | The page served. |
| results_count | integer | How many cards are in results. This is the count on this page, not a site total. |
| results[] | array | The ranked cards, each with position, asin, url, title, image, price, currency, list_price, rating, ratings_total, is_sponsored, is_amazons_choice, is_best_seller, badge, badge_type, badges, bought_past_month and delivery. |
Example Request
curl "https://apiserpent.com/api/amazon/search?q=laptop+stand&domain=amazon.com" \
-H "X-API-Key: sk_live_your_api_key"
Every price is read from the card that owns it, and
currency is an ISO-4217 code, never a glyph: a card priced in EUR on an amazon.com page is reported as EUR, because that is what Amazon rendered. A card Amazon rendered without a price returns price: null — it is never filled in from a neighbouring card.A page that genuinely lists nothing is an answer and is charged (
results_count: 0). A failure on our side is refunded automatically, as is a 400. This endpoint returns one page all at once, so there is no partial-page block on a 200; if a request runs out of time it answers with the standard timeout error, which names a job form when one is live for your account.Amazon Product API
Returns one full product record for an ASIN on any supported marketplace — pricing, availability, seller, ratings, feature bullets, images, variants, category ranks and specs, plus the review sample and the rating histogram Amazon renders on the product page. This endpoint is free-tier eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| asin required | string | A 10-character Amazon product id, e.g. B08N5WRWNW. Pass url instead if that is what you have — a full product URL (/dp/, /gp/product/) has its ASIN read out of it. |
| url | string | Full Amazon product URL (alternative to asin). A URL with no readable product id is a 400, never a fetch. |
| domain / country | string | Marketplace (default amazon.com), exactly as on Search. 22 marketplaces serve product pages; amazon.eg is best-sellers only and says so in its 400. |
| language | string | A locale the marketplace serves, as on Search. |
Response fields
| Field | Type | Description |
|---|---|---|
| asin / parent_asin / url / title / brand | string | null | Identity. parent_asin is the variation parent when the listing has one. |
| price / list_price / sns_price | number | null | Numbers, not strings. The current price, the strike-through reference price and the Subscribe & Save price. Each is null when the page did not carry it — never lifted from another block on the page. |
| currency | string | null | ISO-4217 (USD, EUR, JPY), never a currency symbol. |
| coupon / availability / bought_past_month / delivery | string | integer | null | The coupon label, the availability line, Amazon's "bought in past month" figure, and the delivery promise text. |
| seller_id / sold_by / ships_from | string | null | The buy-box merchant, as one name in one shape. |
| rating / ratings_total | number | integer | null | The star average and the number of ratings the product has on Amazon. |
| rating_breakdown[] | array | The star histogram as rows of { stars, percentage } and nothing else. Percentages only — see the note below. |
| reviews[] / reviews_count | array, integer | The review sample rendered into the product page — typically 8–13 reviews, each with id, title, rating, date, author, verified_purchase and the untruncated body. reviews_count is the size of this sample. It is not ratings_total and the two must not be reconciled. |
| feature_bullets[] / description / aplus_present | array, string | null, boolean | Bullet points, the description text, and whether the listing carries an A+ content block. |
| main_image / image_urls[] / image_count | string | null, array, integer | Gallery imagery. |
| variants[] / variant_dimensions[] / variants_count | array, array, integer | The variation family and the axes it varies on (size, colour, style). |
| categories[] / bestsellers_rank[] / specs | array, array, object | Breadcrumb categories, the product's Best Sellers Rank rows, and the specification table. |
Example Request
curl "https://apiserpent.com/api/amazon/product?asin=B08N5WRWNW&domain=amazon.com" \
-H "X-API-Key: sk_live_your_api_key"
Reviews are a sample, not a census.
reviews is what Amazon renders into the product document, typically 8–13 of them, against a ratings_total that can be in the hundreds of thousands. There is nothing to paginate: the product page is the only surface that carries them, and sort, star-filter and page variants return the same review ids. Do not read reviews_count as a total review count.The rating breakdown is percentages only. Each row is
{ stars, percentage }. A per-star count would have to be derived from percentage × ratings_total, which inherits Amazon's own rounding — on a 950,000-rating product that is thousands of ratings presented as a fact — so no such field is emitted.Absent values are
null, never invented. A field the listing does not carry comes back null or empty, and the response shape is identical for every product. An ASIN that does not exist on that marketplace answers 404; a resource that does not exist is an answer, and the call is charged. Every other non-2xx is refunded automatically.Amazon Bestsellers API
Returns a ranked Best Sellers chart — about 30 entries for a category chart, and somewhat more for the site-wide one — for a category on any supported marketplace, or for the site-wide chart when no category is given. This endpoint is free-tier eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| category | string | An Amazon category slug, e.g. electronics. Optional — omit it for the site-wide chart. Slugs are per-marketplace: electronics is a real chart on amazon.com, amazon.co.uk and amazon.co.jp but not on amazon.de, where computers is. A slug we know is wrong for that marketplace is refused with a 400 before you are charged — Amazon answers one of those with a full-size page carrying a couple of unrelated products, and being billed for that is worse than being told no. |
| domain / country | string | Marketplace (default amazon.com), exactly as on Search. All 23 marketplaces serve best sellers, including amazon.eg, which has no product surface. |
| language | string | A locale the marketplace serves, as on Search. |
Response fields
| Field | Type | Description |
|---|---|---|
| category / category_id / category_slug / category_url | string | null | The chart Amazon actually served, in its own words. Compare it with what you asked for. |
| child_categories[] | array | Sub-charts under this one, for walking the tree. |
| results_count | integer | How many entries are in results. |
| results[] | array | The chart entries, each with rank (Amazon's own displayed rank), position, asin, url, title, image, price, currency, rating, ratings_total, category and category_slug. |
Example Request
curl "https://apiserpent.com/api/amazon/bestsellers?category=electronics&domain=amazon.com" \
-H "X-API-Key: sk_live_your_api_key"
Read
category_slug back. It is the chart Amazon served, which is the only reliable check that it is the chart you meant. The site-wide chart is not a seed list for product ids — it is full of digital goods that have no ordinary product page, so feeding its ASINs into Product will produce genuine 404s. Use a category chart for that.A chart that genuinely lists nothing is an answer and is charged. A failure on our side is refunded automatically, as is a
400.TikTok Profile API
Returns a public TikTok creator profile — identity, bio, link-in-bio, verification, region, interaction settings and exact follower/like/video counts — and optionally the creator's posts, followers, following or liked lists. This endpoint is free-tier eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| username required | string | A TikTok handle (zachking), an @handle, or a full profile URL. Also accepted as url. |
| country | string | Optional two-letter country code for a localised request, e.g. us. |
| limit | integer | How many items each requested list group returns (default 30). Synchronous range 1–180. limit sets the price on an enriched call — see the pricing note below. Above the cap you get a 400, never a silent clamp: you are billed against the depth you asked for, so quietly serving 180 when 500 was requested would be both a wrong answer and a confusing invoice. |
| include_posts | boolean | Adds posts — the creator's own videos, each a full video record. Makes the call enriched. |
| include_followers | boolean | Adds followers — creator records with identity, bio, avatar and totals. Makes the call enriched. |
| include_following | boolean | Adds following, same record shape. Makes the call enriched. A private following list comes back as restricted: true with zero records — that is an answer, not a shortfall. |
| include_liked | boolean | Adds liked. Makes the call enriched. The liked tab is open on only about 12% of accounts; when it is closed the list is restricted: true. settings.liked_videos_public on the profile tells you in advance. |
Example Request
curl "https://apiserpent.com/api/tiktok/profile?username=zachking" \
-H "X-API-Key: sk_live_your_api_key"
Example Response
A basic call with no include_* group. Avatar URLs are abbreviated for print — they are signed and expiring:
{
"success": true,
"username": "zachking",
"user_id": "6612508400412",
"sec_uid": "MS4wLjABAAAA…",
"nickname": "Zach King",
"biography": "Make the impossible possible",
"bio_link": "https://zachkingmagic.com",
"profile_url": "https://www.tiktok.com/@zachking",
"verified": true,
"private_account": false,
"is_organization": false,
"commerce_user": false,
"tt_seller": false,
"is_ad_virtual": false,
"region": null,
"language": null,
"created_at": "2018-09-25T18:33:20.000Z",
"avatar": {
"thumb": "https://p16-sign.tiktokcdn-us.com/…~c5_100x100.jpeg?x-expires=…",
"medium": "https://p16-sign.tiktokcdn-us.com/…~c5_720x720.jpeg?x-expires=…",
"large": "https://p16-sign.tiktokcdn-us.com/…~c5_1080x1080.jpeg?x-expires=…"
},
"stats": {
"follower_count": 82100000,
"following_count": 67,
"like_count": 1000000000,
"video_count": 1024,
"friend_count": 42
},
"settings": {
"comment": 0,
"duet": 0,
"stitch": 0,
"download": 0,
"following_visibility": 1,
"liked_videos_public": false
},
"tabs": {
"music": false,
"question": false,
"playlist": true
},
"meta": {
"elapsed": "3184ms",
"timestamp": "2026-09-08T09:14:22.118Z"
}
}
include_posts, include_followers, include_following, include_liked, include_comments, include_related — and every hashtag, search and music call, is enriched: $3.00/1K Default, $2.70 Growth, $2.10 Scale at the default limit=30. include_media is not a field group — it never moves a call to the enriched band, it is charged separately on top of whatever band the call is already in. See Video.Enriched is priced by depth, so $3.00 is the
limit=30 price and not a flat rate. The formula is $3.00/1K × (0.7 + 0.01 × limit), printed here rather than discovered on an invoice. limit=30 is exactly 1.0× ($3.00); limit=100 is 1.7× ($5.10); limit=180, the synchronous cap, is 2.5× — $7.50 per 1,000 calls. Read it the other way too: per item that is $0.100 falling to $0.042 per 1,000 items, and $0.029 on Scale. Each requested group beyond the first adds +20% (1 + 0.2×N), multiplied by the depth factor — the first group is already in the base. Growth and Scale scale the whole ladder by 10% and 30%. On the free tier a call costs one free unit whatever the depth or group count.Counts are exact, never a rounded display value, and
null means unknown rather than zero. Absent values are null and are never fabricated — region and language come back null on a profile because the public profile does not carry them, and they are emitted explicitly rather than being allowed to vanish.⚠ Avatar, cover and media URLs are signed and short-lived. The host, the path and the signature all change on every fetch. Download what you need; never store one as a stable identifier or a cache key.
An empty list is an answer, and it says which kind.
restricted: true means the list is not public; truncated: true means TikTok itself cut it short; complete: true means you reached the natural end. When a requested group came back with fewer items than limit, the response carries a top-level partialResults block — on an enriched profile or video, one block per short group, keyed by the group's own response name (posts, followers, comments…) — each with requested, returned, an optional reason and a plain-English note. A list that is complete or truncated is reported but never pointed at a job form, because more time cannot produce more. A restricted list produces no block at all — it is a settled answer, not a shortfall.Refunds. A list that genuinely comes back empty, and a profile or video that does not exist (
404), are answers and are charged. A failure on our side is refunded automatically, and so is a 400. The response body never says whether a call was charged or refunded — read your usage, not the response.TikTok Video API
Returns a single public TikTok video record — caption, creation time and region, the true author, the sound, technical media metadata, exact engagement counts, hashtags, mentions and content labels — and optionally its comments, related videos, or a header-free hosted copy of the file. This endpoint is free-tier eligible (shared 10-call pool) except when include_media is set.
Parameters
| Parameter | Type | Description |
|---|---|---|
| video_id required | string | The numeric video id, or pass url with a full video URL instead. One of the two is required. |
| url | string | Full TikTok video URL (alternative to video_id). |
| username | string | Optional, and not an identity check. TikTok resolves a video by id alone, so a handle here is only a hint for building the URL. The author on the record is always the video's true creator, whatever handle you passed. |
| country | string | Optional two-letter country code for a localised request. |
| limit | integer | Items per requested list group (default 30), 1–180 synchronously. Sets the price on an enriched call — see the pricing note on Profile. |
| include_comments | boolean | Adds comments — each with comment_id, video_id, text, language, created_at, like_count, reply_count, reply_to_comment_id, pinned_by_author and an author identity block. Makes the call enriched. |
| include_related | boolean | Adds related — full video records for TikTok's related feed. Makes the call enriched. |
| include_media | boolean | Adds media.hosted — our own copy of the video file, on a link that needs no headers. This is not a field group: on its own it does not make the call enriched, and a video call with media and nothing else stays on the basic band. It is billed as a surcharge on top of the call and is never free-tier eligible — see the note below. Available on this endpoint only. |
Response fields
| Field | Type | Description |
|---|---|---|
| video_id / video_url / description | string | null | Identity and caption. |
| created_at / created_region / language | string | null | ISO-8601 creation time, the two-letter region the post was created in (distinct from a creator region), and the caption's language. |
| author | object | Identity only — user_id, username, sec_uid, nickname, verified, private_account, avatar_thumb. No creator totals: the follower and like figures a video payload carries are rounded and unreliable, so creator totals come from a profile call or not at all — the two sources are never blended. |
| music | object | music_id, title, author_name, original, duration, cover. Feed music_id straight into the music endpoint. |
| media | object | Technical metadata — duration, ratio, definition, width, height, format, codec, bitrate, size_bytes and the cover / origin_cover / dynamic_cover images. Plus the playback links, below. |
| media.play_url / media.download_url | string | null | The raw TikTok links, included at no extra charge on every video call. They are time-limited and they require the exact header set in media.video_link_headers — send it as returned, unchanged. media.link_expires_at is the expiry read from the link itself. When we cannot produce a link that will actually serve, all four come back null: we never publish a link we know will be refused. |
| media.hosted | object | Our own header-free copy, only with include_media. status is not_requested, available or unavailable, with url, expires_at and content_type. unavailable means a copy was requested and could not be produced — a real answer rather than a link that would fail later. |
| stats | object | play_count, digg_count, comment_count, share_count, collect_count, repost_count. Exact values, not rounded display labels; null where unknown, never a negative or a zero standing in for unknown. |
| hashtags[] / mentions[] / labels[] | array | Parsed from the caption, plus TikTok's own content labels. |
| image_post | object | null | { image_count, title } for a photo post; null for an ordinary video. |
| is_ad / duet_enabled / stitch_enabled / share_enabled | boolean | null | Post-level flags. |
Example Request
curl "https://apiserpent.com/api/tiktok/video?url=https://www.tiktok.com/@zachking/video/7137423965982686469" \
-H "X-API-Key: sk_live_your_api_key"
include_media is a surcharge, charged on top of the call — not a band that replaces it. Media is $10.00 per 1,000 on Default, $9.00 on Growth and $7.00 on Scale, and it is added to whatever the call itself costs:
- A basic video call with media — media and no field group, so the call is still basic:
$0.10 + $10.00 = $10.10per 1,000 at Default — not $10.00. That is $9.09 on Growth and $7.07 on Scale. - An enriched video call with media — media plus at least one field group — adds $10.00 to the enriched base including its depth and field-group multipliers. Two groups at
limit=180is($3.00 × 1.2 × 2.5) + $10.00 = $19.00per 1,000 at Default — $17.10 on Growth, $13.30 on Scale. At the defaultlimit=30with one group it is$3.00 + $10.00 = $13.00.
/video call without the flag stays free-tier eligible; adding the flag makes that call paid.TikTok Hashtag, Search & Music APIs
Three list endpoints that return full video records in one shared envelope: videos carrying a hashtag, videos matching a keyword search, and videos using a sound. All three are free-tier eligible (shared 10-call pool) and all three bill on the enriched band, because each returns a whole list rather than a single record.
Parameters
| Parameter | Type | Description |
|---|---|---|
| tag | string | Required on /api/tiktok/hashtag. The hashtag, with or without a leading #. Also accepted as q. |
| q | string | Required on /api/tiktok/search. The search term, up to 200 characters. |
| music_id | string | Required on /api/tiktok/music. The numeric sound id, or a full sound URL to read it from. A video record's music.music_id plugs straight in. |
| limit | integer | How many items to return (default 30), 1–180 synchronously. Sets the price — see the pricing note on Profile. Above the cap is a 400, never a silent clamp. |
| country | string | Optional two-letter country code for a localised request. |
The list envelope
| Field | Type | Description |
|---|---|---|
| items[] | array | Full video records — the same shape a single video call returns, so you never get two different records for one thing. Items fetched inside a list carry no playback links (media.play_url and friends are null, media.hosted.status is not_requested). |
| result_count | integer | How many items came back. |
| total_count | integer | null | TikTok's own stated total, and null when TikTok stated none. It is never back-filled from result_count, which would turn "we fetched 30" into "there are 30". |
| has_more | boolean | null | What TikTok said on the last page read. |
| next_cursor | string | null | Where the next page would start. |
| truncated | boolean | TikTok itself cut the list short. A public following list can answer 50 records against a stated total of 572 with no error at all — this flag is the difference between a documented platform limit and a bug report. |
| restricted | boolean | The list is not public. restricted: true with result_count: 0 is an answer; an empty list with no flag would be a lie by omission. |
| complete | boolean | The natural end of the list was reached — there is genuinely nothing more. |
| partialResults | object | null | Present only when fewer items came back than limit and that shortfall is not already settled by restricted. Carries requested, returned, an optional reason and a plain-English note. |
Example Request
curl "https://apiserpent.com/api/tiktok/hashtag?tag=cooking&limit=30" \
-H "X-API-Key: sk_live_your_api_key"
limit=30, scaled by 0.7 + 0.01 × limit, so limit=180 is $7.50/1K. There is no per-item charge: a 180-item list is one call. Growth takes 10% off the whole ladder and Scale 30%. include_media is not available on these endpoints./api/tiktok/search returns video cards only. TikTok mixes user and live cards into the same feed; those are dropped rather than reshaped into something they are not, so a page we read correctly can still return fewer than limit — that shows up as a partialResults block, not as an error.An empty list is an answer and is charged. Read
truncated, restricted and complete before treating one as a failure. A failure on our side is refunded automatically, as is a 400.X (Twitter) Post API
Returns one public post, looked up by its numeric id or its permalink — text, language, timestamp, author identity, engagement counts, media, hashtags, mentions, links, and the post it quotes or replies to. Billed per request, never per item: one call is one charge whatever the post carries. This endpoint is free-tier eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| id | string | The numeric post id — 1 to 20 digits and nothing else. Provide id or url; id wins when both are sent. Any non-digit character is a 400, including a leading @ or a pasted search phrase. |
| url | string | A post permalink to read the id from. x.com and twitter.com both work, as do the /i/web/status/…, /i/status/… and /statuses/… forms; a trailing ?s=…, query string or fragment is ignored. A bare numeric id sent in url is accepted too, so putting the id in the wrong field still works. |
One of the two is required — send neither and the call is a 400. Sending a list (?id=1&id=2) is also a 400: one post per call. Time limit: 20 seconds. Read the live value from limits.endpoints["/api/x/tweet"].max_seconds on GET /api/status.
Response fields
Every key below is always present, in this order. A value we could not read comes back as its own empty (null, [], false) — never as a missing key.
| Field | Type | Description |
|---|---|---|
| id | string | null | The post's own id. Identity-checked: if the record served back is not the post you asked for, the call answers 404 rather than returning a different post under your id. |
| url | string | null | Canonical permalink, built from the author's handle and the id. |
| text / lang / created_at | string | null | The post body, its language code and its timestamp, as the record states them. |
| possibly_sensitive / is_edited | boolean | true only when the record says so; false otherwise. |
| author | object | null | id, name, screen_name, verified, profile_image_url. verified is one boolean covering either verification programme — you get “is this account verified”, not which scheme. |
| counts | object | favorites and replies, and only those two — either is null when it could not be read. There is no retweet, quote, bookmark or view count here, and no key holding a permanent null for them: a field that is empty on every response reads as “we tried and failed”, so they are honestly absent rather than shipped hollow. |
| media[] | array | type (photo, video or animated_gif) and url are always there. width/height appear when the record states dimensions; poster, variants ({ url, bitrate }, highest first) and duration_ms exist on video and animated GIF only. This is the one place an X response varies in shape — branch on type, and test for a key before reading it. [] on a text-only post. |
| hashtags[] / mentions[] / urls[] | array of string | Hashtag text without the #, mentioned handles without the @, and links expanded to their destination where the record carries the expansion. |
| quoted_tweet | object | null | { id, url, text, author } on a quote post — one level only, never a chain. null otherwise. |
| reply_to | object | null | { screen_name, id } when the post is a reply. null otherwise. |
Example Request
curl "https://apiserpent.com/api/x/tweet?id=1234567890123456789" \
-H "X-API-Key: sk_live_your_api_key"
What this endpoint is not. It is a lookup by known id: there is no post search and no timeline pull. That is not a parameter we left undocumented; it is not part of the product.
A
404 is charged. Deleted, withheld, suspended, never existed, or a record that turns out not to be the post you asked for — we went and looked, and “it is not there” is a true and complete answer to the question you asked. A 400 is refunded before it is charged, and a 503 or 504 is refunded automatically.X (Twitter) Profile API
Returns a public profile by handle — display name, bio, avatar, follower/following/post counts, join date, website link, and the recent posts the profile itself lists. Billed per request, one charge whatever the profile holds. This endpoint is free-tier eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| handle | string | An X username: 1 to 15 characters, letters, digits and underscore. A leading @ is stripped for you. Provide handle or url; handle wins when both are sent. |
| url | string | A profile URL to read the handle from, e.g. https://x.com/<handle>. twitter.com works too. |
| hydrate | boolean | true, 1, yes or on turns it on; anything else, including omitting it, is off. Bills 1.5× — $0.15 per 1,000, flat at every tier. See the pricing note below. |
One of handle or url is required. X's own route names are not handles and are refused with a 400: i, home, explore, search, messages, notifications, settings, compose, login, logout, signup, about, tos, privacy, hashtag. Time limit: 45 seconds. Read the live value from limits.endpoints["/api/x/profile"].max_seconds on GET /api/status.
Response fields
| Field | Type | Description |
|---|---|---|
| handle | string | null | The handle as served. Identity-checked — a profile that turns out to belong to a different handle answers 404 rather than arriving under yours. |
| name / bio / avatar_url | string | null | Display name, profile description and profile image. |
| counts | object | followers, following, posts. Any of the three is null when it could not be read as a number. |
| joined / website | string | null | The join date as the profile states it, and the link on the profile where there is one. |
| recent_posts[] | array | The posts the profile lists. Each always carries id, url, text and created_at; counts and media appear only on posts that hydration filled in. [] when the profile lists none — a protected or brand-new account is an answer, not a failure. |
There is no verified field on a profile, and that is deliberate. The badge is not reliably readable from a public profile, and a wrong false on a verified account is worse than no field at all. Where you need it, the post endpoint carries author.verified on that account's posts.
Example Request
curl "https://apiserpent.com/api/x/profile?handle=example&hydrate=true" \
-H "X-API-Key: sk_live_your_api_key"
hydrate=true buys for the 1.5×. Without it, recent_posts carries what the profile itself lists per post: the id, its URL, a text snippet and a time. With it, each post additionally gets the exact post text, its counts (favorites and replies) and its media array — the same records the post endpoint returns.Hydration is per post and best-effort, with two consequences worth designing around.
counts and media are present on hydrated posts only — test for the key, do not assume it. And if you paid for hydration and not one post could be filled in, the premium comes back automatically: the 1.5× delta is returned and you effectively pay the $0.10 base. Your credit ledger — GET /api/billing — is the record of it; the response body never says anything about money.On the free pool a hydrated call costs one free call, not one and a half, because the pool counts calls rather than multipliers — and there is nothing to give back on a free call if hydration comes back empty.
What this endpoint is not. It returns a public profile and the recent posts it lists. There is no search, no complete timeline, and no followers or following list. Those are not undocumented parameters; they are not part of the product.
POST /api/bulk/jobs under the endpoint keys x_tweet and x_profile; there is no /async twin on either path, because the shared submit endpoint is the job form for this vertical. X has no depth parameter, so a job buys a bigger time budget and one submission for many handles — not more data per call. Billing is per item at submission, on exactly the same rates as the synchronous route: hydrate=true is charged at 1.5× on the job path too, and given back the same way when hydration enriches nothing. A failed item is refunded.Reddit Subreddit Listing API
Returns a community's post listing — up to 100 records synchronously, each with title, author, score, comment count, timestamps, flair, awards, media and body text. Five Reddit endpoints share one rate; all five are free-tier eligible (shared 10-call pool) and all five sit in the standard discount band. Three are flat per call; these two are priced by limit.
Parameters
| Parameter | Type | Description |
|---|---|---|
| subreddit required | string | Community name. python, r/python and /r/python are all accepted. 2–21 characters, letters, digits and underscore. |
| sort | string | hot (default), new, top or rising. Anything else is a 400. |
| t | string | Time window: hour, day, week, month, year, all. Only read when sort=top, where it defaults to day. On any other sort it is ignored entirely — not validated, not echoed back. |
| limit | integer | An integer 1–100 (default 25). This prices the call — see the block table below. The job form accepts up to 250. A non-integer, a zero, or anything above the ceiling is a 400, never a silent clamp. |
Time limit: 45 seconds.
Response fields
The envelope carries subreddit (echoed as served and identity-checked — a listing belonging to a different community answers 404 rather than arriving under the name you asked for), sort as used, count, and posts[]. t is not echoed. A delivery block is added only when the response is short of limit.
| Field | Type | Description |
|---|---|---|
| id / fullname | string | null | Base36 post id without the t3_ prefix, and the same id with it. id is what the comments endpoint takes as post_id. |
| title / selftext | string | null | Post title, and the post body on a text post. |
| author / author_id / author_avatar | string | null | Username without u/, a stable author id, and the author's avatar image. |
| subreddit / subreddit_id | string | null | Community name without r/, and the t5_… community id. |
| score / upvote_ratio / num_comments | number | null | Net score, upvote ratio and comment count, exactly as stated. |
| created_at | string | null | ISO 8601 timestamp, as the listing states it. |
| url / permalink / domain | string | null | What the post points at (the link target on a link post, the post itself otherwise), the path to the post on Reddit, and the host of url. |
| post_type / language | string | null | link, text, image, gallery or video; and the post language as stated. |
| award_count / top_award | integer / object | null | 0 when there are none; top_award is { id, icon_url } or null. |
| is_nsfw / stickied / locked / archived | boolean | Post state. |
| flair / media / thumbnail | object / string | null | flair is { text, background_color } or null. media is { type, url }, plus hls_url on a video post carrying a streaming playlist, and null whenever the post carries no media — link and text posts never do. thumbnail is a preview image on link posts only. |
Every post key is always present; a value the listing did not carry comes back as null, 0 or false, never as a missing key. The three nested objects — flair, top_award, media — work the other way round: they are null when there is nothing to carry, and when present they hold only the keys that were actually there. Check for a key inside them rather than assuming it.
Example Request
curl "https://apiserpent.com/api/reddit/posts?subreddit=python&sort=top&t=week&limit=50" \
-H "X-API-Key: sk_live_your_api_key"
/posts and /comments, limit is a price and not just a page size. The base rate buys 25 records, and every started block of 25 costs another unit. The block is charged as soon as it is started, so limit=26 costs exactly what limit=50 costs.limit | Blocks | Default, per 1,000 calls | Growth | Scale |
|---|---|---|---|---|
| 1–25 (the default is 25) | 1 | $0.12 | $0.108 | $0.084 |
| 26–50 | 2 | $0.24 | $0.216 | $0.168 |
| 51–75 | 3 | $0.36 | $0.324 | $0.252 |
| 76–100 (the synchronous ceiling) | 4 | $0.48 | $0.432 | $0.336 |
limit on a 25 boundary. Ask for 25, 50, 75 or 100 and the per-post price is a flat $0.0048 per 1,000 posts on Default; ask for 26 and you pay $0.0092 per 1,000 posts for the same data. It is the one parameter on this API that changes your invoice.A free-pool call consumes exactly one free unit whatever
limit you send — the pool counts calls, not blocks.There are no partial refunds on a short pull. A
limit=100 call that comes back with 60 records is charged the four blocks you asked for. What you get instead is an honest statement of the shortfall in the delivery block — fewer_available (the community genuinely holds fewer), deadline_reached (the time limit stopped the work and there was more to get) or engine_unavailable (we could not complete the whole pull; asking again can return more) — plus a pointer to the job form where that form is live for your account.Reddit Comments API
Returns a post's comment thread, flat or nested, up to 100 comments synchronously. Same rate and same block billing as the subreddit listing. Free-tier eligible (shared 10-call pool).
Parameters
| Parameter | Type | Description |
|---|---|---|
| subreddit required | string | Same rules as /posts. |
| post_id required | string | The post's base36 id, 4–10 characters. A t3_ prefix is accepted and stripped, and the id is lower-cased for you. It is the id field /posts and /search return. |
| limit | integer | An integer 1–100 (default 25). This prices the call, on the same block curve as /posts. The job form accepts up to 1000. |
| tree | boolean | true, 1, yes or on returns the nested shape; anything else, including omitting it, leaves the response flat. Same four spellings hydrate accepts on /api/x/profile. It changes the arrangement, never the price. |
Time limit: 45 seconds.
Response fields
The envelope carries post_id, subreddit, total_comments, count and comments[]. total_comments is the thread's own stated total, never our count of what we returned — it is null when no total was stated, and it is never back-filled from count, which would turn “we returned 100” into “there are 100”. Every comment served is identity-checked against post_id; one comment belonging to a different post makes the whole call a 404 rather than a mixed thread.
| Field | Type | Description |
|---|---|---|
| id / parent_id | string | null | Comment id without its t1_ prefix, and the parent comment's id with the prefix stripped. parent_id is null on a top-level comment. |
| author / body | string | null | Username without u/, and the comment text. |
| score / depth / award_count | integer | null | Score as stated, 0 depth on a top-level comment, and 0 awards when there are none. |
| created_at / permalink | string | null | ISO 8601 timestamp as the thread states it, and the path to the comment. |
| replies[] | array | tree=true only. Comments in exactly the same shape, nested, and [] at a leaf. The flat form does not carry this key at all. The set of comments is identical either way, and count is the total number of comments in both forms — not the number of top-level ones. |
Example Request
curl "https://apiserpent.com/api/reddit/comments?subreddit=python&post_id=1abcdef&limit=100&tree=true" \
-H "X-API-Key: sk_live_your_api_key"
Reddit Search, User & Community APIs
Three flat-rate lookups: post search, a user summary card and a community summary. Each is one charge at any depth — there is no block curve on these three, and on /search the limit is a page size and nothing more. All three are free-tier eligible (shared 10-call pool). Time limit: 45 seconds on each.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q required | string | Search only. Search terms, 1–512 characters; longer is a 400. |
| limit | integer | Search only. An integer 1–100 (default 25). A page size only — it does not change the price. |
| username required | string | User only. 3–20 characters: letters, digits, underscore and hyphen. u/name, /u/name, /user/name and @name are all accepted and normalised for you. |
| name required | string | Community only. Community name, with or without r/. Same 2–21 character rule as /posts. subreddit is an accepted alias — the spelling every other Reddit endpoint uses — and name wins when both are sent. |
Response fields
| Endpoint | Returns |
|---|---|
/api/reddit/search | query, count and results[]. Each result carries id (pass it to /comments as post_id), title, author, subreddit, subreddit_id, permalink, is_nsfw, spoiler, quarantined and position — the rank the results page states. Every key is present on every result; the last four are null when the results page did not state them. A search result is not a post record — it carries no score, comment count or body. Read those with /posts, or the thread with /comments. |
/api/reddit/user | username, post_karma, comment_karma, cake_day (ISO 8601) and avatar. All five keys are always present, each null when the card did not state it. username is identity-checked case-insensitively — Reddit renders its own canonical casing, so a card belonging to a different account answers 404 rather than being handed back under the name you asked for. This is the summary card, not a history: there is no post or comment list on this endpoint. |
/api/reddit/subreddit | name (identity-checked the same way), subreddit_id, description, weekly_visitors and weekly_contributions. Those last two are not member counts. They are the weekly activity figures the community's own summary states, published under their own honest names — there is no subscriber or member total on this endpoint, and neither figure is a stand-in for one. Every key other than name is null when the summary did not state it. |
Example Request
curl "https://apiserpent.com/api/reddit/search?q=async%20python&limit=50" \
-H "X-API-Key: sk_live_your_api_key"
curl "https://apiserpent.com/api/reddit/user?username=example_user" \
-H "X-API-Key: sk_live_your_api_key"
curl "https://apiserpent.com/api/reddit/subreddit?name=python" \
-H "X-API-Key: sk_live_your_api_key"
GET /api/reddit/posts/async accepts limit up to 250 and GET /api/reddit/comments/async up to 1000, billed on the same 25-block curve — a limit=250 posts job is 10 blocks and a limit=1000 comments job is 40. /search/async, /user/async and /subreddit/async stay one flat charge. There is no async surcharge and no async discount; the depth is what costs more. The same work can also be submitted as items on POST /api/bulk/jobs under the endpoint keys reddit_posts, reddit_comments, reddit_search, reddit_user and reddit_subreddit, billed per item at submission on the same curve; a failed item is refunded. Read limits.endpoints[…].async_available from GET /api/status before building against it.Async Jobs
Some pulls legitimately take longer than any synchronous request should hold a connection open — detail=100 on Maps, limit=50 on LinkedIn posts, or a few hundred searches you want run in one go. For those, submit the work as a job: you get a job id back immediately and collect the results when they are ready, by polling, by webhook, or from a results file.
limits.endpoints[…].async_available — while it is false, the endpoints below are not mounted for your account and answer 404. That field is resolved from what is actually running, so it is the authority: this page describes the contract, not today's rollout state.Jobs are billed on exactly the same price curve as the equivalent synchronous call — there is no async surcharge and no async discount.
Which form to use
| You want | Use |
|---|---|
| One Maps search deeper than the synchronous cap | GET /api/maps/search/async |
| One LinkedIn posts pull deeper than the synchronous cap | GET /api/linkedin/posts/async |
One Reddit listing or thread deeper than the synchronous cap (limit up to 250 on posts, 1000 on comments) | GET /api/reddit/posts/async, /comments/async — and /search/async, /user/async, /subreddit/async for the flat three |
X posts or profiles in the background — there is no /api/x/*/async twin, the shared submit endpoint is the job form for this vertical, and since X has no depth parameter a job buys a bigger time budget and one submission for many items rather than more data per call | POST /api/bulk/jobs, endpoint keys x_tweet and x_profile |
| Many calls in one submission (up to 200) | POST /api/bulk/jobs |
The first two are convenience wrappers: they take the same query parameters as their synchronous twin and submit a one-item job. Their 202 body is the same as POST /api/bulk/jobs plus two ready-made paths, poll and results, so you do not have to build the URLs yourself. Whichever form you use, you poll, collect and cancel through the same /api/bulk/jobs/… endpoints below.
1. Submit a job
curl -X POST "https://apiserpent.com/api/bulk/jobs" \
-H "X-API-Key: sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"clientRef": "nightly-run-2026-08-21",
"items": [
{ "endpoint": "serp_web", "params": { "q": "mesothelioma lawyer", "country": "us", "num": 100 } },
{ "endpoint": "serp_news", "params": { "q": "insurance rates", "country": "gb" } },
{ "endpoint": "maps_search", "params": { "q": "dentist", "location": "Austin, TX", "detail": 100 } }
],
"delivery": {
"webhook": { "url": "https://yourapp.example.com/hooks/serpent", "secret": "whsec_your_secret" },
"storage": { "enabled": true, "format": "jsonl" }
}
}'
Request body
| Field | Type | Description |
|---|---|---|
| items required | array | 1–200 items. Every item is validated before anything is charged — one bad item rejects the whole batch and reserves nothing. |
| items[].endpoint required | string | One of the endpoint keys in the table below. |
| items[].params | object | The parameters you would pass to the synchronous endpoint (default: {}) |
| delivery.poll | boolean | Keep results retrievable by polling (default: true) |
| delivery.webhook.url | string | HTTPS URL called once when the job finishes. Public hosts only. |
| delivery.webhook.secret | string | Signing secret. Strongly recommended — without it the callback is unsigned and you cannot verify it came from us. |
| delivery.storage.enabled | boolean | Also write all results to a single downloadable file. |
| delivery.storage.format | string | jsonl, json or csv (default: "jsonl") |
| clientRef | string | Your own reference, echoed back on the job. Anything longer than 256 characters is silently truncated to 256, not rejected — so keep references short enough to stay unique, or two different runs can come back with the same clientRef. |
Endpoint keys
endpoint | Equivalent synchronous call |
|---|---|
serp_web | GET /api/search |
serp_quick | GET /api/search/quick |
serp_news | GET /api/news |
serp_images | GET /api/images |
serp_videos | GET /api/videos |
shopping | GET /api/shopping — only while that endpoint is enabled; otherwise the item is rejected as Unsupported endpoint |
maps_search | GET /api/maps/search |
maps_quick | GET /api/maps/search/quick |
maps_place | GET /api/maps/place |
maps_reviews | GET /api/maps/reviews |
ai_rank | GET /api/ai/rank |
social_youtube_search | GET /api/social/youtube/search |
social_youtube_video | GET /api/social/youtube/video |
social_youtube_channel | GET /api/social/youtube/channel |
social_youtube_playlist | GET /api/social/youtube/playlist |
instagram_profile | GET /api/social/instagram/profile — only while Instagram jobs are enabled for your account; GET /api/status says whether they are |
linkedin_profile | GET /api/linkedin/profile |
linkedin_profile_full | GET /api/linkedin/profile/full |
linkedin_posts | GET /api/linkedin/posts |
linkedin_company | GET /api/linkedin/company |
linkedin_jobs | GET /api/linkedin/jobs |
linkedin_job | GET /api/linkedin/job |
linkedin_search | GET /api/linkedin/search |
x_tweet | GET /api/x/tweet |
x_profile | GET /api/x/profile — hydrate=true bills the same 1.5× here as it does synchronously, and is given back the same way when hydration enriches nothing |
reddit_posts | GET /api/reddit/posts — limit up to 250 on the job form, billed on the same block curve |
reddit_comments | GET /api/reddit/comments — limit up to 1000 on the job form, billed on the same block curve |
reddit_search | GET /api/reddit/search |
reddit_user | GET /api/reddit/user |
reddit_subreddit | GET /api/reddit/subreddit |
result is the same body that twin returns. GET /api/status lists the job twins that are live for your account under limits.endpoints[…].async_available — read that, not this table, before you build.The
serp_* endpoints honour num and pages, exactly as their synchronous equivalents do, and each item is billed at the same flat rate for its category — asking for more results per item does not cost more. serp_web accepts up to num=100; serp_quick returns the single page Quick Search does; serp_news up to num=50; serp_images and serp_videos take a single call and apply num as a limit on the list returned. The filters are forwarded too: serp_images takes size, type, color, aspect, people, time, license and layout; serp_videos takes freshness (or its time alias), duration and resolution; serp_web and serp_quick accept include_video_stats with the same paid-plan rule as the synchronous call, and serp_web also accepts include_ai_mode — same paid-plan rule, same flat $0.004 per-call surcharge, see AI Mode. serp_quick does not take include_ai_mode, for the same reason Quick Search does not. A job item that sends an unrecognised value is rejected with Invalid include_ai_mode. Use true or false. Unlike the synchronous call, a job item is strict about the value: true, 1 or yes switch it on, false, 0 or no leave it off, and anything else rejects the item with Invalid include_video_stats. Use true or false. The Maps, AI-rank, YouTube, Instagram and LinkedIn keys take their full parameter sets. maps_reviews is available only while the reviews endpoint itself is; when it is not, the item is rejected as Unsupported endpoint.Response — 202 Accepted
{
"success": true,
"job": {
"id": "job_9f2c41a8b7d05e6314ca7b90f2ee18d3",
"status": "queued",
"itemCount": 3,
"counts": { "pending": 3, "processing": 0, "succeeded": 0, "failed": 0 },
"billing": { "estimated": 0.0758, "settled": 0, "refunded": 0, "released": 0 },
"delivery": {
"poll": true,
"webhook": { "url": "https://yourapp.example.com/hooks/serpent", "hasSecret": true },
"storage": { "enabled": true, "format": "jsonl" }
},
"webhook": { "state": "pending" },
"clientRef": "nightly-run-2026-08-21",
"createdAt": "2026-08-21T09:14:22.518Z",
"updatedAt": "2026-08-21T09:14:22.518Z",
"completedAt": null,
"results": null
}
}
Keep job.id. Everything below is addressed by it.
2. Poll the job
curl "https://apiserpent.com/api/bulk/jobs/job_9f2c41a8b7d05e6314ca7b90f2ee18d3" \
-H "X-API-Key: sk_live_your_api_key"
The response is the same job object as the submit reply, with counts, status and billing updated. Poll it on your own schedule; there is no long-poll and no streaming.
Job status values
status | Meaning | Terminal |
|---|---|---|
queued | Accepted, nothing started yet. | No |
processing | At least one item is running. | No |
completed | Every item succeeded. | Yes |
partial | Some items succeeded, some failed. Collect the results and check each item. | Yes |
failed | Every item failed. Failed items are refunded. | Yes |
cancelled | You cancelled it. Items not yet dispatched were refunded. | Yes |
Per-item status is pending, processing, succeeded or failed.
3. Collect the results
curl -L "https://apiserpent.com/api/bulk/jobs/job_9f2c41a8b7d05e6314ca7b90f2ee18d3/results" \
-H "X-API-Key: sk_live_your_api_key"
If you asked for a results file and it is ready, this responds 302 with a signed download URL — use curl -L (or follow the redirect in your client) to fetch it. Otherwise the results come back inline. Each result is the same body the synchronous endpoint returns — the example is abbreviated to the envelope of a serp_web item that asked for num=100 and received 63 rows, its results.organic list left out:
{
"success": true,
"status": "partial",
"jobId": "job_9f2c41a8b7d05e6314ca7b90f2ee18d3",
"itemCount": 3,
"count": 3,
"offset": 0,
"limit": 100,
"items": [
{
"index": 0,
"endpoint": "serp_web",
"status": "succeeded",
"result": {
"success": true,
"query": "mesothelioma lawyer",
"type": "web",
"engine": "google",
"country": "us",
"pagesRequested": 10,
"requestedNum": 100,
"pagesScraped": 7,
"results": { "organic": [] },
"delivery": {
"requested": 100,
"returned": 63,
"note": "Delivered the results gathered within this request. Fewer than requested were returned."
}
}
},
{ "index": 1, "endpoint": "serp_news", "status": "succeeded", "result": { "success": true, "query": "insurance rates", "type": "news", "engine": "google", "country": "gb", "results": { "articles": [] } } },
{ "index": 2, "endpoint": "maps_search", "status": "failed", "error": { "code": "request_failed", "message": "We are undergoing maintenance. Please try again shortly." } }
]
}
| Query parameter | Type | Description |
|---|---|---|
| include | string | inline always returns items inline, even when a results file exists. |
| limit | integer | Items per page, 1–1000 (default: 100) |
| offset | integer | Items to skip. Page with offset + limit; itemCount is the total (default: 0) |
index matches the position of the item in the items array you submitted, so results line up with your input regardless of the order they finished in.
delivery block as a synchronous response — requested, returned, reason (only when the answer is short by more than 40%) and note — in the same place, so whatever check you run on a short synchronous response works unchanged on a job item. One key never appears on a job result: async_endpoint. A job is the asynchronous path, so there is nothing further to point you at. The refund rule is the synchronous one too: an item that completed and genuinely found nothing is charged; an item we could not complete is refunded. The item’s note never says which — it explains what happened to the results and nothing about money, on every kind. job.billing and your credit ledger are the authority: read settled and refunded on the job, or the ledger, never the item body.A failed item carries error as an object, never a string:
error.code | Meaning |
|---|---|
invalid_params | The item's parameters were rejected. error.message says which. |
not_found | The requested resource does not exist. This one is charged — see below. |
unavailable | The item could not be served. Resubmit it. |
request_failed | Any other failure. Resubmit it. |
cancelled | The job was cancelled before this item was dispatched. |
A failed item is refunded in full — except not_found, which is charged. An item whose target genuinely does not exist — an Instagram username, a YouTube video, channel or playlist id — fails with { "code": "not_found", "message": "The requested resource was not found." }, and that is charged: a resource that does not exist is an answer, exactly as it is on the synchronous call. Every other failure code returns the item's credits in full.
4. List your jobs
curl "https://apiserpent.com/api/bulk/jobs?limit=25" \
-H "X-API-Key: sk_live_your_api_key"
Returns { "success": true, "count": n, "jobs": [ … ] }, newest first. limit is 1–100 (default: 25).
5. Cancel a job
curl -X POST "https://apiserpent.com/api/bulk/jobs/job_9f2c41a8b7d05e6314ca7b90f2ee18d3/cancel" \
-H "X-API-Key: sk_live_your_api_key"
Best effort: items already running are allowed to finish and are charged; items not yet dispatched are cancelled and refunded. Results for whatever completed stay retrievable.
6. Webhook callback
If you supplied delivery.webhook.url, we POST a JSON body to it once, when the job reaches a terminal state.
{
"type": "bulk.job.completed",
"jobId": "job_9f2c41a8b7d05e6314ca7b90f2ee18d3",
"status": "partial",
"counts": { "pending": 0, "processing": 0, "succeeded": 2, "failed": 1 },
"results": {
"downloadUrl": "https://…signed-download-url…",
"expiresAt": "2026-08-21T10:14:22.518Z"
}
}
results is present only when you asked for a results file. The callback carries the summary, never the result payloads — fetch those from the download URL or the results endpoint.
Webhook headers
| Header | Description |
|---|---|
webhook-id | Unique id for this delivery. Use it to make your handler idempotent. |
webhook-timestamp | Unix seconds at signing time. Reject anything far outside your clock skew. |
webhook-signature | v1,<base64 HMAC-SHA256>. Sent only when you supplied a secret. |
Verifying the signature
Standard Webhooks scheme. Sign the string {webhook-id}.{webhook-timestamp}.{raw body} with your secret and compare in constant time. A secret beginning whsec_ is base64-decoded after the prefix; any other secret is used as raw UTF-8.
const crypto = require('crypto');
function verify(headers, rawBody, secret) {
const id = headers['webhook-id'];
const ts = headers['webhook-timestamp'];
const sent = headers['webhook-signature']; // "v1,<base64>"
if (!id || !ts || !sent) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5-minute replay window
const key = secret.startsWith('whsec_')
? Buffer.from(secret.slice(6), 'base64')
: Buffer.from(secret, 'utf8');
const expected = 'v1,' + crypto.createHmac('sha256', key)
.update(`${id}.${ts}.${rawBody}`).digest('base64');
const a = Buffer.from(sent), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Delivery rules
- HTTPS only, and the URL must resolve to a public address.
- Up to 3 attempts, 10 seconds each. Any
2xxcounts as delivered. - Redirects are not followed — a
3xxcounts as not delivered. - Delivery never blocks polling. If the callback fails every attempt the results are still there, and
job.webhook.statereadsfailed.
7. How long jobs and results live
| Thing | Lifetime |
|---|---|
| The job and its per-item results | 7 days from submission. |
| The signed results-file download URL | About an hour — the exact moment is in results.expiresAt. |
GET /api/bulk/jobs/:jobId/results at the moment you actually want the file and you get a fresh URL, for as long as the job still exists. Download anything you need to keep within the 7 days.8. Billing
Credits are reserved per item when the job is submitted, at the price tier locked at submission time — so a long-running job cannot change price partway through. Every item is settled on its own, and the rules are the synchronous ones:
| What happened to the item | What it costs |
|---|---|
| Succeeded | Charged. An item that completes and genuinely finds nothing is a delivered answer and is charged too — an empty list is not, by itself, a refund. |
| Succeeded, but a field group or depth band you paid extra for was not delivered | Charged, minus that band — the item itself was delivered, so only the part you did not receive comes back (Instagram post details billed at the tier actually delivered; a Maps deep call that opened no place pages). |
Failed with not_found | Charged. A resource that genuinely does not exist is an answer, exactly as on the synchronous call. |
| Failed with any other code | Refunded in full. |
| Came back empty because we could not complete it | Refunded in full. The item body does not say so — job.billing.refunded and your ledger do. |
| Cancelled before dispatch | Refunded in full. |
job.billing reports it as it happens: estimated at submit, then settled (charged, including the charged not_found items and the delivered part of any partly-refunded item), refunded (refunded items and refunded bands) and released (cancelled items). settled + refunded always adds up to estimated once a job reaches a terminal state, so the ledger reconciles against your own estimate without inspecting a single item. That ledger, not the item body, is the record of what you paid: a failed item's error body is identical whether it was charged or refunded.
9. Errors on submission
| Status | code | Meaning |
|---|---|---|
400 | invalid_parameter | An item's endpoint or parameters are invalid, or delivery is malformed. The message says which. Nothing was charged. |
402 | insufficient_credits | Your balance does not cover the batch estimate. Nothing was charged. |
413 | batch_too_large | More than 200 items. Split the batch. |
503 | temporarily_unavailable | Submissions are paused. Nothing was charged; retry later. |
On poll, results and cancel, a 404 with code: "not_found" means no job with that id belongs to this account — check the id returned by the submit call, and remember jobs are deleted after 7 days. These three management calls consume no credits at all, so this 404 costs nothing; the charged not_found above is an item's result, not a job-lookup miss.
Search Engines
Serpent API supports five search engines: Google (default), Bing, Yahoo, DuckDuckGo and Brave. Use the engine parameter on any SERP endpoint to choose.
On Deep Search the results object carries the same keys on every engine — the notes below describe which blocks each engine tends to fill in, not which keys you get back. On Quick Search the answer is the same for all five: organic results only. AI Mode (include_ai_mode=true) is engine-independent — unlike include_aio, it is returned on all five Deep Search engines.
| Engine | Value | Supported Endpoints |
|---|---|---|
google |
Web, News, Images, Videos, Shopping (soon). Includes ads, PAA, featured snippets, optional AI overviews, and inline videos where present. | |
| Bing | bing |
Web, News, Images, Videos. Includes ads, PAA, and inline videos where present. aiOverview is always null. |
| Yahoo | yahoo |
Web, News, Images, Videos, Shopping (soon). Includes ads, PAA, inline videos, and optional AI Overview fields on Deep Search with include_aio=true, where present. |
| DuckDuckGo | ddg |
Web, News, Images, Videos. Includes ads where present. relatedSearches is always [] and aiOverview is always null. |
| Brave | brave |
Web, News, Images, Videos. Includes ads, PAA, related searches, inline videos, and optional AI Overview fields on Deep Search with include_aio=true, where present. No pixel positions. |
Country/Region Support
Get localized search results by specifying a country code. This affects the language, regional rankings, and availability of results.
Returns a list of all supported country codes programmatically. This endpoint does not require an API key.
Example Response
Abbreviated — the real list carries every supported code, and count is its length:
{
"success": true,
"count": 112,
"countries": [
{ "code": "us", "region": "us" },
{ "code": "uk", "region": "uk" }
]
}
Supported Countries
We support 112 countries for localized search results (gb is an alias for uk).
gb is accepted as an alias for uk — both work and return United Kingdom results.Popular Countries
| Code | Country | Code | Country |
|---|---|---|---|
us | United States | uk | United Kingdom |
ca | Canada | au | Australia |
de | Germany | fr | France |
in | India | jp | Japan |
View All Supported Countries
English-Speaking (12)
| Code | Country | Code | Country |
|---|---|---|---|
us | United States | uk | United Kingdom |
gb | United Kingdom (alias) | ca | Canada |
au | Australia | nz | New Zealand |
ie | Ireland | sg | Singapore |
ph | Philippines | my | Malaysia |
in | India | za | South Africa |
Western Europe (10)
| Code | Country | Code | Country |
|---|---|---|---|
de | Germany | fr | France |
es | Spain | it | Italy |
nl | Netherlands | be | Belgium |
at | Austria | ch | Switzerland |
pt | Portugal | lu | Luxembourg |
Nordics (5)
| Code | Country | Code | Country |
|---|---|---|---|
se | Sweden | no | Norway |
dk | Denmark | fi | Finland |
is | Iceland |
Eastern Europe (18)
| Code | Country | Code | Country |
|---|---|---|---|
pl | Poland | ro | Romania |
cz | Czech Republic | sk | Slovakia |
hu | Hungary | bg | Bulgaria |
hr | Croatia | si | Slovenia |
ee | Estonia | lv | Latvia |
lt | Lithuania | ua | Ukraine |
ru | Russia | gr | Greece |
tr | Turkey | ba | Bosnia and Herzegovina |
mk | North Macedonia | ge | Georgia |
Asia (14)
| Code | Country | Code | Country |
|---|---|---|---|
jp | Japan | tw | Taiwan |
hk | Hong Kong | kr | South Korea |
cn | China | id | Indonesia |
th | Thailand | vn | Vietnam |
bd | Bangladesh | lk | Sri Lanka |
mm | Myanmar | kh | Cambodia |
la | Laos | np | Nepal |
Middle East (14)
| Code | Country | Code | Country |
|---|---|---|---|
il | Israel | sa | Saudi Arabia |
pk | Pakistan | ae | United Arab Emirates |
qa | Qatar | kw | Kuwait |
om | Oman | bh | Bahrain |
jo | Jordan | lb | Lebanon |
iq | Iraq | ir | Iran |
sy | Syria | ye | Yemen |
North Africa (5)
| Code | Country | Code | Country |
|---|---|---|---|
eg | Egypt | ma | Morocco |
tn | Tunisia | ly | Libya |
dz | Algeria |
Sub-Saharan Africa (10)
| Code | Country | Code | Country |
|---|---|---|---|
ng | Nigeria | ke | Kenya |
gh | Ghana | et | Ethiopia |
tz | Tanzania | ug | Uganda |
zw | Zimbabwe | bw | Botswana |
na | Namibia | sn | Senegal |
Latin America (18)
| Code | Country | Code | Country |
|---|---|---|---|
mx | Mexico | br | Brazil |
ar | Argentina | cl | Chile |
co | Colombia | pe | Peru |
ve | Venezuela | ec | Ecuador |
bo | Bolivia | py | Paraguay |
cr | Costa Rica | pa | Panama |
do | Dominican Republic | gt | Guatemala |
hn | Honduras | ni | Nicaragua |
sv | El Salvador | pr | Puerto Rico |
Caribbean (3)
| Code | Country | Code | Country |
|---|---|---|---|
cu | Cuba | jm | Jamaica |
tt | Trinidad and Tobago |
Other (3)
| Code | Country | Code | Country |
|---|---|---|---|
mt | Malta | am | Armenia |
az | Azerbaijan |
All 112 codes shown above. Use GET /api/countries for the programmatic list.
Example: Search in Germany
curl "https://apiserpent.com/api/search?q=beste+seo+tools&country=de" \
-H "X-API-Key: sk_live_your_api_key"
Language Support
Restrict search results to a specific language by passing a 2-letter ISO 639-1 code to the language parameter. Accepted on all engines across web, news and image search — and on shopping search when that endpoint ships; language coverage is broadest on Yahoo, Bing and Brave, and narrower on DuckDuckGo. It is not accepted on video search.
language filters by content language; country sets the geographic search context. Combine both for the best localization (e.g., country=de&language=de for German searches in Germany).Common Languages
| Code | Language | Code | Language |
|---|---|---|---|
en | English | es | Spanish |
fr | French | de | German |
pt | Portuguese | it | Italian |
ja | Japanese | zh | Chinese |
ar | Arabic | hi | Hindi |
View All Supported Languages
Germanic
| Code | Language | Code | Language |
|---|---|---|---|
en | English | de | German |
nl | Dutch | sv | Swedish |
da | Danish | no | Norwegian |
is | Icelandic | af | Afrikaans |
Romance
| Code | Language | Code | Language |
|---|---|---|---|
es | Spanish | fr | French |
it | Italian | pt | Portuguese |
ro | Romanian | ca | Catalan |
gl | Galician |
Slavic
| Code | Language | Code | Language |
|---|---|---|---|
ru | Russian | pl | Polish |
cs | Czech | sk | Slovak |
uk | Ukrainian | bg | Bulgarian |
sr | Serbian | hr | Croatian |
bs | Bosnian | mk | Macedonian |
sl | Slovenian |
Baltic / Celtic / Other European
| Code | Language | Code | Language |
|---|---|---|---|
lt | Lithuanian | lv | Latvian |
el | Greek | sq | Albanian |
cy | Welsh | ga | Irish |
eu | Basque | mt | Maltese |
Uralic / Turkic
| Code | Language | Code | Language |
|---|---|---|---|
fi | Finnish | et | Estonian |
hu | Hungarian | tr | Turkish |
az | Azerbaijani | kk | Kazakh |
uz | Uzbek |
Indic / Iranian
| Code | Language | Code | Language |
|---|---|---|---|
hi | Hindi | bn | Bengali |
ur | Urdu | pa | Punjabi |
mr | Marathi | gu | Gujarati |
ne | Nepali | si | Sinhala |
fa | Persian | ps | Pashto |
ku | Kurdish |
Dravidian
| Code | Language | Code | Language |
|---|---|---|---|
ta | Tamil | te | Telugu |
kn | Kannada | ml | Malayalam |
East & Southeast Asian
| Code | Language | Code | Language |
|---|---|---|---|
zh | Chinese | ja | Japanese |
ko | Korean | my | Burmese |
vi | Vietnamese | th | Thai |
km | Khmer | lo | Lao |
id | Indonesian | ms | Malay |
tl | Filipino |
Semitic / African
| Code | Language | Code | Language |
|---|---|---|---|
ar | Arabic | he | Hebrew |
am | Amharic | sw | Swahili |
ha | Hausa | yo | Yoruba |
ig | Igbo | zu | Zulu |
xh | Xhosa | so | Somali |
Caucasian
| Code | Language | Code | Language |
|---|---|---|---|
ka | Georgian | hy | Armenian |
Pass any valid ISO 639-1 code as language. Engine support varies — Yahoo, Bing and Brave have the broadest coverage.
Example: French-Language Search in France
curl "https://apiserpent.com/api/search?q=meilleurs+outils+seo&country=fr&language=fr" \
-H "X-API-Key: sk_live_your_api_key"
Check Status
Check your API key status, credit balance, shared free-call allowance, and pricing tier. This endpoint reports on your account, so it requires your API key — unlike /api/countries and /api/pricing, which are public.
Example Response
{
"success": true,
"data": {
"plan": "paid",
"credits": 45.50,
"costPerSearch": 0.0006,
"priceTier": "default",
"freeSearches": {
"used": 4,
"limit": 10,
"remaining": 6,
"scope": "all endpoints"
},
"limits": {
"behavior_on_timeout": "partial_results_with_counts",
"timeout_param": {
"name": "timeout",
"unit": "seconds",
"min": 5,
"note": "Optional. Lowers this request's budget; it can never raise it above the endpoint maximum."
},
"endpoints": {
"/api/search": { "max_seconds": 45, "async_available": true },
"/api/search/quick": { "max_seconds": 20, "async_available": false }
}
}
}
}
limits block is always present, and it is the authority — read it rather than assuming from this page. The endpoints map above is abridged; the live response lists every endpoint mounted for your account, with max_seconds (how long that endpoint may spend before it stops and answers with what it has) and async_available (whether the same work can be submitted as a job). async_available is resolved from what is actually mounted, so it never points at a path that would 404. timeout_param names the per-request timeout parameter, its unit and its floor. Accounts below the $100 balance bracket also receive a notice string.Read the row for the endpoint you actually call. Some rows are wildcards (
/api/social/*, /api/linkedin/*) and some are a single path, and where an endpoint has its own row that row is the answer for it. /api/social/instagram/profile is one of those: it publishes its own max_seconds and its own async_available, not the /api/social/* wildcard's — the profile endpoint is allowed more time than the wildcard states. Match the longest, most specific path first, and never assume a wildcard covers an endpoint listed separately.Pricing
Pay-as-you-go pricing with three tiers locked in by your largest single deposit. The tier never downgrades as you spend down, and top-ups do not add up — it is the biggest deposit you have ever made in one go that sets it. Core categories (Web, News, YouTube, AI Ranking) use 10×/20× Growth/Scale discounts; the standard verticals — Images, Videos, Shopping, Instagram, Maps, LinkedIn, Amazon, TikTok, Reddit and the X post call — use the smaller standard 10%/30% discount. One endpoint takes no discount at all: /api/x/profile is the same rate on all three tiers.
Pricing Tiers
| Tier | Largest single deposit | Discount |
|---|---|---|
| Default | $0 | Full price |
| Growth | $100 | 10× off core categories; 10% off standard categories (Images, Videos, Shopping, Instagram, Maps, LinkedIn, Amazon, TikTok, Reddit, X post). /api/x/profile is flat and takes no discount. |
| Scale | $500 | 20× off core categories; 30% off standard categories (Images, Videos, Shopping, Instagram, Maps, LinkedIn, Amazon, TikTok, Reddit, X post). /api/x/profile is flat and takes no discount. |
Per-Category Pricing (per 1,000 calls — Default / Growth / Scale)
| Category | Default | Growth ($100+) | Scale ($500+) |
|---|---|---|---|
| Google SERP (Quick + Deep, all engines, any pages) | $0.60 | $0.06 | $0.03 |
AI Mode surcharge — include_ai_mode=true on Deep Search, added to that call’s own rate rather than replacing it (paid plans only, $0.004 per call, charged once whether the call returns 1 page or 10); flat at every tier | +$4.00 | +$4.00 | +$4.00 |
| News (all engines) | $0.20 | $0.02 | $0.01 |
| Images (all engines) | $0.35 | $0.32 | $0.25 |
| Videos (all engines) | $0.10 | $0.09 | $0.07 |
| Shopping (Google, Yahoo, DDG, Brave) — planned launch pricing; not billable yet | $0.35 | $0.32 | $0.25 |
| YouTube (search/video/channel/playlist) | $0.20 | $0.02 | $0.01 |
| Instagram profile | $0.40 | $0.36 | $0.28 |
Instagram profile + recent posts (include_posts=true) | $0.40 | $0.36 | $0.28 |
Instagram profile + post details (include_post_details=true) | $4.00 | $3.60 | $2.80 |
| LinkedIn company | $1.00 | $0.90 | $0.70 |
| LinkedIn jobs / job / profile | $0.50 | $0.45 | $0.35 |
| LinkedIn full profile | $1.00 | $0.90 | $0.70 |
| LinkedIn search / posts | $1.00 | $0.90 | $0.70 |
Amazon product (/api/amazon/product) | $0.020 | $0.018 | $0.014 |
Amazon search (/api/amazon/search) — one page of 16–48 cards | $0.020 | $0.018 | $0.014 |
Amazon bestsellers (/api/amazon/bestsellers) — one whole chart, ~30 entries | $0.020 | $0.018 | $0.014 |
TikTok basic — /profile or /video with no field group (include_media alone does not change the band) | $0.10 | $0.09 | $0.07 |
TikTok enriched — any of the six field groups, plus /hashtag, /search, /music (at limit=30; scales with depth) | $3.00 | $2.70 | $2.10 |
TikTok media — include_media, a surcharge added to the call, never free-tier | +$10.00 | +$9.00 | +$7.00 |
X post (/api/x/tweet) — one post per request | $0.10 | $0.09 | $0.07 |
X profile (/api/x/profile) — flat: no tier discount | $0.10 | $0.10 | $0.10 |
X profile with hydrate=true — 1.5× the profile rate, also flat | $0.15 | $0.15 | $0.15 |
Reddit search / user / community (/search, /user, /subreddit) — flat per call at any depth | $0.12 | $0.108 | $0.084 |
Reddit posts / comments (/posts, /comments) — per started block of 25 limit, so limit=100 is 4 blocks | $0.12 | $0.108 | $0.084 |
Maps List — standard band (detail=0; up to 20 places returned, none enriched) | $0.10 | $0.09 | $0.07 |
Maps List — premium band (detail=0 with limit 21–100, flat) | $0.50 | $0.45 | $0.35 |
| Maps Quick (up to 20 places) | $15.00 | $13.50 | $10.50 |
Maps Deep — standard band (detail 1–20, and 20 is the default; up to 100 places returned, up to 20 fully enriched) | $15.00 | $13.50 | $10.50 |
Maps Deep — premium band (detail 21–100, flat; up to 100 places returned, up to 100 fully enriched) | $75.00 | $67.50 | $52.50 |
| Maps Place (one place) | $1.50 | $1.35 | $1.05 |
| Maps Reviews (up to 20 reviews) | $3.00 | $2.70 | $2.10 |
| AI Ranking — single LLM | $20.00 | $2.00 | $1.00 |
| AI Ranking — all 4 combined | $40.00 | $4.00 | $2.00 |
detail decides how many of them are opened and fully enriched (the rest arrive as core_only records). detail=0 is not on this ladder at all — it is Maps List, from $0.10 per 1,000 calls. detail 1–20 bills $0.0150 per call; detail 21–100 bills a flat $0.0750 per call, so 21 and 99 cost the same. detail=20 is $0.0150 and detail=21 is $0.0750 — 5× for one extra enriched place. The default is detail=20. Per unit: detail=20 is $0.15 per 1,000 places returned and $0.75 per 1,000 fully-enriched places; detail=100 is $0.75 per 1,000 places returned and $0.75 per 1,000 fully-enriched places. Same enriched rate at both ends, worse in the middle ($2.50 per 1,000 fully-enriched at detail=30). Synchronous Deep is capped at detail=30 for performance, not price — 21–30 works synchronously at the premium rate; deeper pulls are submitted as jobs at GET /api/maps/search/async at the same flat $0.0750 per call, and GET /api/status → limits.endpoints[…].async_available is the authority on whether that surface is live for your account./api/search/quick, whatever the page count, balance or tier; and on /api/search one published Web category unit is charged per page requested, up to 10 pages, at every tier and every balance. Both endpoints bill at the same Web rate. Engine choice (Google/Yahoo/Bing/DDG) doesn't change the category rate. Failed requests (non-2xx) are auto-refunded — except a 404 for a resource that does not exist, which is an answer and is charged.GET /api/pricing (no API key required). New accounts get Free to start, shared across every endpoint — Web Search, News, Images, Videos, AI Rank, LinkedIn, Maps, YouTube, Instagram, Amazon, TikTok, X and Reddit (TikTok's include_media is the one exclusion and is always paid). Shopping joins the pool when that endpoint launches.Response Format
All responses are JSON. Every response includes a success boolean. Search responses include engine, country, and a meta object with timing info.
Full Response (Web Search)
The default format=full response includes all available data. The example is abbreviated to two organic rows and one related search; aiOverview is filled only on Deep Search with include_aio=true on google, yahoo or brave. The two organic rows are real — captured from q=how to tie a tie, engine=google, country=us with include_video_stats=true (positions 2–4 left out). The last four keys on each row — views, publishedTime, duration, publisher — appear only when the request sent that parameter; they are populated for video results and null on every other row. A YouTube playlist link (/playlist?list=…) is not a video and carries the four nulls too — only /watch videos carry values. Without the parameter every row ends at position, byte-for-byte what it was before. A Deep Search that sent include_ai_mode=true also carries a top-level aiMode object — { answer, sources[], followUps[] } — beside results; see AI Mode.
{
"success": true,
"query": "how to tie a tie",
"type": "web",
"engine": "google",
"country": "us",
"pagesRequested": 5,
"pagesScraped": 5,
"results": {
"organic": [
{
"title": "4 Ways to Tie a Tie - wikiHow",
"url": "https://www.wikihow.com/Tie-a-Tie",
"snippet": "Have you graduated beyond the clip-on tie? ...",
"displayedUrl": "www.wikihow.com/Tie-a-Tie",
"position": 1,
"views": null,
"publishedTime": null,
"duration": null,
"publisher": null
},
{
"title": "How to Tie a Tie Perfectly (Beginner Tutorial) | Easiest Step-by-Step ...",
"url": "https://www.youtube.com/watch?v=mS6lX5QVUu0",
"snippet": "In this video, you'll learn: The easiest method to tie a tie ...",
"displayedUrl": "www.youtube.com/watch?v=mS6lX5QVUu0",
"position": 5,
"views": 5897,
"publishedTime": "2025-11-20",
"duration": "2:14",
"publisher": "How Can I Tie a Tie"
}
],
"relatedSearches": [{ "query": "how to tie a tie easy" }],
"ads": { "top": [], "bottom": [], "totalCount": 0 },
"peopleAlsoAsk": [],
"featuredSnippet": null,
"aiOverview": null,
"knowledgePanel": null,
"localPack": [],
"richSnippets": [],
"videos": [],
"shopping": []
},
"metadata": {
"totalOrganicResults": 50,
"hasAds": false,
"hasRelatedSearches": true,
"hasPeopleAlsoAsk": false,
"hasVideos": false,
"hasShopping": false,
"hasFeaturedSnippet": false,
"hasAiOverview": false,
"hasKnowledgePanel": false,
"hasLocalPack": false,
"source": "google"
},
"meta": {
"totalOrganic": 50,
"requestedNum": 50,
"elapsed": "8100ms",
"timestamp": "2026-03-12T10:00:00.000Z"
}
}
peopleAlsoAsk on Deep Search when available; Google may also include featuredSnippet, optional aiOverview, and videos data. Yahoo includes videos and optional aiOverview on Deep Search; Bing includes peopleAlsoAsk and videos. DDG includes ads, and always returns [] for relatedSearches. Brave includes ads, peopleAlsoAsk, relatedSearches, videos, and optional aiOverview fields on Deep Search. All fields are always present in the response (empty arrays/null when not available).metadata fields appear only in specific situations, and none of them changes anything else in the response. metadata.aiOverviewUnavailable is 'free_tier' on a call that asked for the AI Overview and could not be served one, because the AI Overview is a paid feature. Organic results and related searches are unaffected; on a free-tier call the remaining paid blocks (ads, People also ask, featured snippet, knowledge panel, local pack, inline videos, shopping and rich snippets) are present as empty values, so the shape of the response never varies. metadata.videoStatsUnavailable is 'free_tier' on a free-tier call that sent include_video_stats=true: the four keys (views, publishedTime, duration, publisher) are still present on every organic row, all null, and nothing else changes. metadata.aiModeUnavailable is 'free_tier' on a free-tier call that sent include_ai_mode=true: the top-level aiMode object is still returned, with answer empty and both arrays empty, nothing else changes, and no surcharge is charged — see AI Mode. With format=simple all three ride on meta instead, because that shape carries no metadata object.Simple Response
Use format=simple for a lightweight response. Available on all five live search endpoints (quick search, deep web search, news, images, videos) — and on shopping when that endpoint ships.
format=simple, the results field is a flat array of lightweight endpoint-specific items — not an object. With format=full (the default), results is an object with endpoint-specific arrays such as organic, articles, images, videos, or shopping. A safe web-search accessor is: const organic = Array.isArray(r.results) ? r.results : (r.results?.organic || []);
format only changes the response shape; it does not change endpoint pricing. /api/search/quick and /api/search bill at the same Web rate — the difference is payload, not price. Choose Quick for a fast organic list, Deep when you need the rich SERP features.
Web Search (/api/search)
Returns position, title, url. With pixel_position=true, each item also carries a pixel_position integer and a pixel_box object (see Pixel Position); with include_video_stats=true (paid plans) each item also ends with views, publishedTime, duration and publisher, populated for video results and null otherwise. The example below is a pixel_position=true call without video stats, abbreviated to two items. AI Mode is unaffected by format: aiMode is a top-level key, so it is returned here too when the request asked for it.
{
"success": true,
"query": "best seo tools",
"engine": "google",
"country": "us",
"results": [
{
"position": 1,
"title": "The 10 Best SEO Tools in 2026 (Tested)",
"url": "https://example.com/best-seo-tools",
"pixel_position": 412,
"pixel_box": { "x": 180, "y": 412, "w": 600, "h": 110 }
},
{
"position": 2,
"title": "SEO Tools Compared",
"url": "https://example.org/seo-tools",
"pixel_position": 847,
"pixel_box": { "x": 180, "y": 847, "w": 600, "h": 96 }
}
],
"meta": { "total": 50, "requestedNum": 50, "elapsed": "2450ms" }
}
With include_video_stats=true the four keys come last on each item. This item is real — captured from q=how to tie a tie, engine=google, country=us; an ordinary page in the same list carries the four keys as null:
{
"position": 5,
"title": "How to Tie a Tie Perfectly (Beginner Tutorial) | Easiest Step-by-Step ...",
"url": "https://www.youtube.com/watch?v=mS6lX5QVUu0",
"views": 5897,
"publishedTime": "2025-11-20",
"duration": "2:14",
"publisher": "How Can I Tie a Tie"
}
News Search (/api/news)
Returns position, title, url, source:
{
"success": true,
"query": "artificial intelligence",
"type": "news",
"engine": "google",
"country": "us",
"results": [
{ "position": 1, "title": "...", "url": "...", "source": "..." }
],
"meta": { "total": 20, "elapsed": "1800ms" }
}
Image Search (/api/images)
Returns position, title, original, thumbnail:
{
"success": true,
"query": "mountain landscape",
"type": "images",
"engine": "google",
"country": "us",
"results": [
{ "position": 1, "title": "...", "original": "...", "thumbnail": "..." }
],
"meta": { "total": 50, "elapsed": "1200ms" }
}
Short responses — the delivery block
When a response carries fewer results than you asked for, it says so in one place, in the same shape, on every endpoint: a top-level delivery block. A complete response has no delivery field at all, so 'delivery' in response is a sufficient test for “did I get everything”. On the endpoints where a depth is a parameter you choose (num, pages, video_count, limit), the block also needs you to have named that depth: a call that asked for no particular number has no target to be short of, and publishes no block rather than apologising for a perfectly good default-sized answer.
| Field | Always present | Meaning |
|---|---|---|
requested | yes | What you asked for. On endpoints that return a single entity (a profile, a company) this is 1. |
returned | yes | What you got. Compare the two — that is the whole check. |
reason | no | The cause. On SERP endpoints it appears only when the answer fell more than 40% short; the entity verticals (LinkedIn, Instagram, Maps) always carry one. |
note | yes | The same thing in a sentence, safe to show a human. |
max_supported | no | On depth-capped endpoints, the largest depth this request size could serve. |
async_endpoint | on every endpoint that has a job form | Where the same work can be submitted as a job. Present only when that path is live for your account. |
delivery.async_endpoint names the job form that can complete the request. It is there on every endpoint that has one, whatever cut the response short — including a call that ran out of time, which is exactly when you want the pointer. An endpoint with no job form simply omits the key, and a job result never carries it (a job already is the asynchronous path). The older per-endpoint fields — meta.partialResults, data.partial, the top-level partial, meta.partial on Maps — are unchanged and still ship beside it.Error Codes
Every error response carries the same four fields, in this order — success (always false), error (a short, stable title you can skim), code (a stable snake_case token) and message (a sentence telling you what was wrong and what to do next). Any extras — param, retryAfter, window, limit, credits, costPerSearch, errorId, retryable — ride after them depending on the code.
code is the field to branch on. It is never renamed and never repurposed; new codes are only ever added. Do not match on the text of error or message — wording can be improved, codes cannot move.
| Code | Meaning | Description |
|---|---|---|
| 400 | Bad Request | Missing or invalid parameters (e.g., no q param, invalid engine) |
| 400 | num must be a positive integer | The num parameter must be a positive integer (1 or greater). Values like 0, -1, or non-numeric strings are rejected. |
| 401 | Unauthorized | Invalid or missing API key |
| 402 | Payment Required | Insufficient credits. Free searches exhausted and no credit balance. |
| 404 | Not Found | Endpoint does not exist, or the requested resource does not exist. A resource that does not exist is an answer, so that call is charged; an unknown route is never billed at all. |
| 403 | Forbidden | forbidden — your account does not have access to that resource. |
| 409 | Conflict | conflict — the resource already exists. Choose a different name and try again. |
| 413 | Batch too large | batch_too_large — an async job carries more items than one batch allows. Split it into smaller batches. |
| 429 | Too Many Requests | Automated overuse or server load. rate_limit_exceeded carries retryAfter, window and limit; too_many_concurrent_requests means more calls in flight at once than your account allows and carries activeRequests. Both send a Retry-After header. See Rate Limits. |
| 500 | Server Error | Internal error — try again or contact support, quoting errorId when the response carries one. Credits are refunded automatically. |
| 502 | Bad Gateway | The search could not be completed — credits are refunded automatically. Not returned when a search simply finds nothing. SERP responses carry retryable: true. |
| 503 | Temporarily unavailable | temporarily_unavailable — we are not serving this right now. Retryable, and credits are refunded automatically. |
| 504 | Request timed out | request_timeout — the clock ran out before anything was gathered. When any results were gathered you get a 200 carrying them plus delivery and meta.partialResults with reason: "deadline_reached" instead. |
Error codes
code is the stable machine token. The table below is the complete published set; not every code can occur on every endpoint (cursor_expired is Maps-only, batch_too_large is async-jobs-only, and forbidden / conflict come from the account and job-management surfaces), so a client that switches on code needs no other list.
| code | Status | error | When |
|---|---|---|---|
missing_parameter | 400 | Missing parameter | A required parameter was not sent. Carries param. |
invalid_parameter | 400 | Invalid parameter | A parameter value is not accepted (bad freshness, safe, num, filter value, …). Carries param. |
unsupported_engine | 400 | Invalid engine | engine is not one of google, yahoo, bing, ddg, brave. |
unsupported_country | 400 | Invalid country code | country is not a supported 2-letter code — call GET /api/countries. |
unsupported_language | 400 | Invalid language | language is not a 2-letter ISO code. |
query_too_long | 400 | Query too long | q is over 2,048 characters. |
cursor_expired | 400 | Cursor expired | A Maps Reviews page_token has expired (they live 15 minutes). Start the listing again. |
missing_api_key | 401 | Missing API key | No key was sent. Send it in the X-API-Key header. |
invalid_api_key | 401 | Invalid API key | The key is not valid. Check it in your dashboard. |
insufficient_credits | 402 | Payment Required | No credits and no free calls left. Carries credits and costPerSearch. |
forbidden | 403 | Forbidden | The account does not have access to that resource. |
not_found | 404 | Not found | Unknown route, or the requested resource does not exist. A resource that does not exist is an answer, so that call is charged; an unknown route is never billed at all. |
conflict | 409 | Conflict | The resource already exists. Choose a different name. |
batch_too_large | 413 | Batch too large | Too many items in one async job. Split the batch. |
rate_limit_exceeded | 429 | Rate limit exceeded | Automated overuse or server load. Carries retryAfter, window, limit. |
too_many_concurrent_requests | 429 | Too many concurrent requests | More calls in flight at once than your account allows. Run them sequentially or reduce parallelism. |
internal_error | 500 | Internal error | Something broke on our side. Carries errorId — quote it to support. |
temporarily_unavailable | 500 / 502 / 503 | Temporarily unavailable | We are not serving this right now. Retryable, credits auto-refunded, and SERP responses also carry retryable: true. The three statuses are the same condition reached from different points in the request — branch on the code, not the status. |
request_timeout | 504 | Request timed out | The clock ran out before anything was gathered. |
200, not an error. A query with no matches is a successful search, so it answers 200 with an empty result array, the counts, and a top-level delivery block explaining what happened. Check results (or meta.totalOrganic) to tell “no matches” apart from a failure, rather than the status code. A 5xx means the search genuinely could not be completed, which is the only case worth retrying.What it costs: empty responses are refunded automatically when the search could not be completed — our time limit ran out, or we could not complete it for another reason on our side. A search that completes and genuinely has no matches is a delivered result and is charged — “there is nothing here” is a real answer to a real question.
Where to read what you were charged — the response never says. No field in any response tells you whether a call was charged or refunded. The
delivery note explains what happened to the results and nothing about money, so a charged empty answer and a refunded empty answer look the same. Your credit ledger is the authority — GET /api/billing and the billing page list every charge and every refund, and that record is what a question about a charge is answered from. Reconcile there, not per response.Error Response Example
// 400 Bad Request — validation. `param` names the parameter at fault.
{
"success": false,
"error": "Missing parameter",
"code": "missing_parameter",
"message": "Query parameter \"q\" is required",
"param": "q"
}
// 401 Unauthorized — two states only: no key sent, or the key is not valid.
{
"success": false,
"error": "Missing API key",
"code": "missing_api_key",
"message": "Send your key in the X-API-Key header."
}
// 402 Payment Required
{
"success": false,
"error": "Payment Required",
"code": "insufficient_credits",
"message": "Insufficient credits. Please add funds to continue.",
"credits": 0,
"costPerSearch": 0.0006
}
// 429 Too Many Requests — also sends a Retry-After header.
{
"success": false,
"error": "Rate limit exceeded",
"code": "rate_limit_exceeded",
"message": "You have reached your hourly request limit (200 per hour). Top up to $100+ to raise your rate limits instantly, or contact support for custom limits.",
"retryAfter": 1843,
"window": "hour",
"limit": 200
}
// 503 Service Unavailable
{
"success": false,
"error": "Temporarily unavailable",
"code": "temporarily_unavailable",
"message": "We are undergoing maintenance. Please try again shortly."
}
// 504 Gateway Timeout — only when nothing at all was gathered.
{
"success": false,
"error": "Request timed out",
"code": "request_timeout",
"message": "The request exceeded its time limit. Lower num, or raise timeout up to the endpoint ceiling shown on GET /api/status."
}
Rate Limits
Rate limits are per account (shared across all your API keys) and scale with your current credit balance. Every account gets four ceilings — max concurrent in-flight requests, and max requests per minute, per hour, and per day. Higher brackets also get higher priority in the processing queue under load.
| Bracket | Concurrent | Per minute | Per hour | Per day | Queue priority |
|---|---|---|---|---|---|
| Free — no deposit | 1 | 10 | 100 | 500 | Lowest |
| Default — balance < $100 | 3–20 | 20–100 | 200–1,000 | 1,000–unlimited* | Standard |
| Growth — balance $100–$499 | 10–100 | 100–1,000 | 1,000–10,000 | 8,000–unlimited* | High |
| Scale — balance $500+ | 100–1,000 | 500–5,000 | 5,000–50,000 | 30,000–unlimited* | Highest |
429 response names which window you hit and includes a Retry-After header — back off and retry. Your bracket follows your live credit balance — topping up raises your limits immediately; spending down can lower them. Pricing tiers are a separate dimension: they're locked in by a one-time deposit and don't move with your balance.| Other limits | Value |
|---|---|
| Demo API key | 10/hour per IP |
Response Timing
Response time varies by endpoint, engine, requested depth, target country, and current platform load. The Playground shows live elapsed time for individual requests.
Every endpoint has a maximum time it will spend on a request. When that budget runs out we return the results already gathered plus a block saying why the response is short — we do not throw the work away and answer an error. GET /api/status publishes the live number for every endpoint mounted for your account (limits.endpoints[…].max_seconds), so you never have to guess.
Setting your own limit — timeout
Pass timeout (in seconds) on any search request to lower the budget for that one call — useful when your own SLA is tighter than ours. timeout_ms takes the same value in milliseconds. It is accepted on Quick Search, Deep Search, News, Images, Videos and the Maps endpoints.
curl "https://apiserpent.com/api/search?q=mesothelioma+lawyer&num=50&timeout=12" \
-H "X-API-Key: sk_live_your_api_key"
timeout can only lower the budget, never raise it above the endpoint maximum, and it is clamped rather than rejected — an out-of-range value means you wanted a bound, not a 400. It also has a per-endpoint floor: below roughly a quarter of the endpoint's budget we cannot stop cleanly mid-page, so asking for less returns the floor instead of a promise we would break. GET /api/status publishes the floor as limits.timeout_param.min. When the clock runs out with results in hand you get a 200 carrying them plus meta.partialResults and the top-level delivery block with reason: "deadline_reached"; only a call that gathered nothing at all answers 504.Need higher rate limits?
Published limits are standard defaults. If your workload needs more — higher concurrency, /min, /hour, or /day allocations — contact us for a reviewed custom-limit allocation.
Code Examples
JavaScript / Node.js
const API_KEY = 'sk_live_your_api_key';
const BASE = 'https://apiserpent.com';
const headers = { 'X-API-Key': API_KEY };
// Quick web search (free-tier eligible)
async function quickSearch(query, engine = 'google') {
const res = await fetch(
`${BASE}/api/search/quick?q=${encodeURIComponent(query)}&engine=${engine}`,
{ headers }
);
return (await res.json()).results.organic;
}
// Deep web search (num is the depth requested, not a guaranteed count)
async function webSearch(query, options = {}) {
const params = new URLSearchParams({
q: query,
num: options.num || 10,
engine: options.engine || 'google',
country: options.country || 'us',
});
const res = await fetch(`${BASE}/api/search?${params}`, { headers });
const data = await res.json();
if (!data.success) throw new Error(data.error);
return data.results.organic;
}
// News search
async function newsSearch(query, num = 20) {
const res = await fetch(
`${BASE}/api/news?q=${encodeURIComponent(query)}&num=${num}`,
{ headers }
);
return (await res.json()).results.articles;
}
// Video search
async function videoSearch(query, engine = 'ddg') {
const res = await fetch(
`${BASE}/api/videos?q=${encodeURIComponent(query)}&engine=${engine}`,
{ headers }
);
return (await res.json()).results.videos;
}
// AI Ranking check
async function aiRank(keyword, domain) {
const res = await fetch(
`${BASE}/api/ai/rank?keyword=${encodeURIComponent(keyword)}&domain=${domain}`,
{ headers }
);
return (await res.json());
}
// YouTube search (free-tier eligible)
async function ytSearch(query, num = 10) {
const res = await fetch(
`${BASE}/api/social/youtube/search?q=${encodeURIComponent(query)}&num=${num}`,
{ headers }
);
return (await res.json());
}
// Google Maps quick place search (free-tier eligible)
async function mapsQuickSearch(query, options = {}) {
const params = new URLSearchParams({
q: query,
location: options.location || '',
country: options.country || 'us',
});
const res = await fetch(`${BASE}/api/maps/search/quick?${params}`, { headers });
const data = await res.json();
return data.places;
}
// Google Maps place details
async function mapsPlace(placeId) {
const res = await fetch(`${BASE}/api/maps/place?place_id=${encodeURIComponent(placeId)}`, { headers });
return (await res.json()).place;
}
// Google Maps reviews (paginate with next_page_token)
async function mapsReviews(placeId, { sort = 'relevant', pageToken } = {}) {
const params = new URLSearchParams({ place_id: placeId, sort });
if (pageToken) params.set('page_token', pageToken);
const res = await fetch(`${BASE}/api/maps/reviews?${params}`, { headers });
return await res.json();
}
// Usage
const quick = await quickSearch('weather today');
const deep = await webSearch('best seo tools', { num: 50 });
const news = await newsSearch('artificial intelligence');
const videos = await videoSearch('python tutorial');
const ranking = await aiRank('best crm software', 'hubspot.com');
const yt = await ytSearch('machine learning');
const places = await mapsQuickSearch('coffee shops', { location: 'New York, NY' });
const place = await mapsPlace(places[0].place_id);
const reviews = await mapsReviews(places[0].place_id, { sort: 'newest' });
Python
import requests
API_KEY = 'sk_live_your_api_key'
BASE = 'https://apiserpent.com'
HEADERS = {'X-API-Key': API_KEY}
# Quick search (free-tier eligible)
def quick_search(query, engine='google'):
resp = requests.get(f'{BASE}/api/search/quick', headers=HEADERS,
params={'q': query, 'engine': engine})
return resp.json()['results']['organic']
# Deep web search (num is the depth requested, not a guaranteed count)
def web_search(query, num=10, engine='google', country='us'):
resp = requests.get(f'{BASE}/api/search', headers=HEADERS,
params={'q': query, 'num': num, 'engine': engine, 'country': country})
data = resp.json()
if not data.get('success'):
raise Exception(data.get('error', 'Search failed'))
return data['results']['organic']
# News search
def news_search(query, num=20):
resp = requests.get(f'{BASE}/api/news', headers=HEADERS,
params={'q': query, 'num': num})
return resp.json()['results']['articles']
# Image search
def image_search(query, num=50):
resp = requests.get(f'{BASE}/api/images', headers=HEADERS,
params={'q': query, 'num': num})
return resp.json()['results']['images']
# Video search
def video_search(query, engine='ddg'):
resp = requests.get(f'{BASE}/api/videos', headers=HEADERS,
params={'q': query, 'engine': engine})
return resp.json()['results']['videos']
# AI Ranking
def ai_rank(keyword, domain=None, engines='claude,chatgpt'):
params = {'keyword': keyword, 'engines': engines}
if domain:
params['domain'] = domain
resp = requests.get(f'{BASE}/api/ai/rank', headers=HEADERS, params=params)
return resp.json()
# YouTube search (free-tier eligible)
def yt_search(query, num=10):
resp = requests.get(f'{BASE}/api/social/youtube/search', headers=HEADERS,
params={'q': query, 'num': num})
return resp.json()
# Instagram profile (free-tier eligible)
def ig_profile(username):
resp = requests.get(f'{BASE}/api/social/instagram/profile', headers=HEADERS,
params={'username': username})
return resp.json()
# Google Maps quick place search (free-tier eligible)
def maps_quick_search(query, location=None, country='us'):
params = {'q': query, 'country': country}
if location:
params['location'] = location
resp = requests.get(f'{BASE}/api/maps/search/quick', headers=HEADERS, params=params)
return resp.json()['places']
# Google Maps place details
def maps_place(place_id):
resp = requests.get(f'{BASE}/api/maps/place', headers=HEADERS,
params={'place_id': place_id})
return resp.json()['place']
# Google Maps reviews (paginate with next_page_token)
def maps_reviews(place_id, sort='relevant', page_token=None):
params = {'place_id': place_id, 'sort': sort}
if page_token:
params['page_token'] = page_token
resp = requests.get(f'{BASE}/api/maps/reviews', headers=HEADERS, params=params)
return resp.json()
# Usage
results = web_search('best seo tools', num=50, engine='google')
news = news_search('AI breakthroughs')
images = image_search('sunset beach')
videos = video_search('python tutorial')
ranking = ai_rank('best crm software', domain='hubspot.com')
yt = yt_search('machine learning')
ig = ig_profile('natgeo')
places = maps_quick_search('coffee shops', location='New York, NY')
place = maps_place(places[0]['place_id'])
reviews = maps_reviews(places[0]['place_id'], sort='newest')
cURL
# Quick search (free-tier eligible)
curl "https://apiserpent.com/api/search/quick?q=weather+today&engine=google" \
-H "X-API-Key: sk_live_your_api_key"
# Deep web search (30 results from Google in Germany)
curl "https://apiserpent.com/api/search?q=beste+seo+tools&num=30&engine=google&country=de" \
-H "X-API-Key: sk_live_your_api_key"
# News search with freshness filter
curl "https://apiserpent.com/api/news?q=technology&num=20&freshness=w" \
-H "X-API-Key: sk_live_your_api_key"
# Image search with filters
curl "https://apiserpent.com/api/images?q=mountain+landscape&num=50&size=large" \
-H "X-API-Key: sk_live_your_api_key"
# Video search
curl "https://apiserpent.com/api/videos?q=python+tutorial&engine=ddg&duration=medium" \
-H "X-API-Key: sk_live_your_api_key"
# AI Ranking (Claude + ChatGPT)
curl "https://apiserpent.com/api/ai/rank?keyword=best+crm+software&domain=hubspot.com&engines=claude,chatgpt" \
-H "X-API-Key: sk_live_your_api_key"
# Single AI engine ranking
curl "https://apiserpent.com/api/ai/rank/gemini?keyword=best+seo+tools" \
-H "X-API-Key: sk_live_your_api_key"
# YouTube search (free-tier eligible)
curl "https://apiserpent.com/api/social/youtube/search?q=machine+learning&num=10" \
-H "X-API-Key: sk_live_your_api_key"
# YouTube video details
curl "https://apiserpent.com/api/social/youtube/video?id=dQw4w9WgXcQ" \
-H "X-API-Key: sk_live_your_api_key"
# YouTube channel details
curl "https://apiserpent.com/api/social/youtube/channel?handle=@mkbhd&include_videos=true" \
-H "X-API-Key: sk_live_your_api_key"
# Instagram profile (free-tier eligible)
curl "https://apiserpent.com/api/social/instagram/profile?username=natgeo" \
-H "X-API-Key: sk_live_your_api_key"
# Check credits and usage
curl "https://apiserpent.com/api/status" \
-H "X-API-Key: sk_live_your_api_key"
# Get pricing (no API key required)
curl "https://apiserpent.com/api/pricing"