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=100 requested (best-effort at the high end — num is a ceiling, not a promise, and short deliveries carry meta.partialResults). Web billing is flat: one call, one charge, whatever the page count. Use /api/search Deep 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 num or pages to go deeper, and note that every page you ask for is charged/api/search bills 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/shopping answers 404 today.

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

  1. Create an accountSign up with your Google account (free)
  2. Generate an API key — Go to the API Keys page and create a key
  3. 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:

https://apiserpent.com

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
Keep your API key secret. Never expose it in client-side code or public repositories. Use the header. A key in the URL is still accepted, and always will be — but a URL is written down in plain text by everything it passes through: our request logs, any CDN or proxy in between, your own proxy or gateway logs, and your browser history. None of those are places you can clean up later, and every call you make adds another copy. Every worked example on this page uses the header for that reason. When a key does arrive in the query string, the response carries an X-API-Key-Warning header so an integration can spot it in testing.
Credit cost: Category pricing applies by endpoint. Web from $0.60/1K calls Default, $0.06/1K calls Growth, $0.03/1K calls Scale. New accounts get shared free calls on eligible endpoints. Growth pricing is locked in forever with a one-time $100 deposit (10× off); Scale with a one-time $500 deposit (20× off). See Pricing for full details.

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 any detail or limit
  • 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 any limit or field-group count
  • X (Twitter) (/api/x/tweet, /api/x/profile) — draws from the same shared pool. A profile call with hydrate=true costs 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 any limit, even the four-block limit=100
The 10-call free pool is shared across every endpoint — Web Search (Quick + Deep, any engine), News, Images, Videos, AI Ranking, LinkedIn, Maps (Quick, Deep, Place, Reviews), Social (YouTube + Instagram), Amazon, TikTok, X and Reddit. The one exclusion is TikTok's include_media, which is always paid because its cost is the file rather than the request. Shopping joins the pool when that endpoint launches.

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.

GET /api/search

Parameters

ParameterTypeDescription
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 →
Breaking change for 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.
Use 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.

GET /api/search?q=how+to+tie+a+tie&include_ai_mode=true
Three conditions have to hold, and if any one of them does not you get nothing back — with no error to tell you why. Check all three before you debug anything else.

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

ParameterTypeDescription
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.

With 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.

Async jobs take it too. A bulk / async item on the 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.

GET /api/news

Parameters

ParameterTypeDescription
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"
  }
}
Any field may be 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.

GET /api/images

Parameters

ParameterTypeDescription
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"
  }
}
Any field may be 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.

GET /api/videos

Parameters

ParameterTypeDescription
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): dayd, weekw, monthm, yeary. 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"
  }
}
Every one of the eleven fields is always present; any of them may be 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

Not launched yet — this endpoint is not callable today. 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.

GET /api/shopping

Parameters

ParameterTypeDescription
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.

Supported on Google, Yahoo, Bing and DuckDuckGo, on desktop, US locale. Not supported on 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.

GET /api/ai/rank

Parameters

ParameterTypeDescription
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

FieldTypeDescription
target_foundbooleantrue if the requested domain (or any of its subdomains) appears in this engine's citations
target_positioninteger | null1-indexed citation position where the domain was first matched. null if not found.
target_match_typestring | 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_domainstring | nullThe actual domain that matched. Equals domain for exact matches; equals the subdomain hostname for subdomain matches.
citationsarrayList of citations returned by this engine, each with position, url, title, domain.
total_citationsintegerNumber of citations parsed.
response_textstringThe 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.

Every AI Ranking engine costs the same: $20.00 per 1,000 calls on Default, $2.00 on Growth and $1.00 on Scale. Naming more than one engine in a single call bills the combined rate instead. AI Ranking is free-tier eligible — it draws on the same shared allowance as every other endpoint. See Pricing for the full ladder.

AI Ranking API — Single Engine

Query a specific AI engine for citation analysis. Available engines: claude, chatgpt, gemini, perplexity.

GET /api/ai/rank/:engine

URL Paths

PathEngineCost per 1K (Default)
/api/ai/rank/claudeAnthropic Claude$20.00
/api/ai/rank/chatgptOpenAI ChatGPT$20.00
/api/ai/rank/geminiGoogle Gemini$20.00
/api/ai/rank/perplexityPerplexity$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

ParameterTypeDescription
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 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.

GET /api/social/youtube/video

Parameters

ParameterTypeDescription
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"
When none of the ids names a video the call answers 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.

GET /api/social/youtube/channel

Parameters

ParameterTypeDescription
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"
A handle, id or URL that names no channel answers 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.

GET /api/social/youtube/playlist

Parameters

ParameterTypeDescription
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"
  }
}
A playlist that exists but holds no public videos answers 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.

FieldEmpty valueWhere 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.

These fields are best-effort. On a normal day nearly every one of them is filled on every row. During a service interruption all four YouTube endpoints keep answering, and the fields above can come back empty for the rest of that day. The key set, the key order and the types never change, so nothing about the response needs a second code path in your integration.

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.

GET /api/social/instagram/profile

Parameters

ParameterTypeDescription
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"
  }
}
Instagram profile data is what the public profile page shows. A private profile is still served: the call answers 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).

GET /api/linkedin/company

Parameters

ParameterTypeDescription
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"
Returns public company data only. Priced at $1.00/1K (Default) and is tier-discounted — $0.90/1K on Growth, $0.70/1K on Scale.

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).

GET /api/linkedin/jobs

Parameters

ParameterTypeDescription
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)
A filter value we do not recognise is ignored, not rejected. Send 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"
      }
    ]
  }
}
Priced at $0.50/1K (Default) and tier-discounted — $0.45/1K on Growth, $0.35/1K on Scale.

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).

GET /api/linkedin/job

Parameters

ParameterTypeDescription
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"
Priced at $0.50/1K (Default) and tier-discounted — $0.45/1K on Growth, $0.35/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).

GET /api/linkedin/profile

Parameters

ParameterTypeDescription
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"
Profile data is best-effort — the fields returned vary by profile and by what the person has made public. Priced at $0.50/1K (Default) and tier-discounted ($0.45/1K Growth, $0.35/1K Scale).

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).

GET /api/linkedin/profile/full

Parameters

ParameterTypeDescription
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"
Full Profile is best-effort and returns identity, summary, full experience history, education, photo, and follower/connection counts. Depth varies by profile — it is not a guaranteed complete résumé. Priced at $1.00/1K (Growth $0.90/1K, Scale $0.70/1K) — the standard tier discounts apply.

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).

GET /api/linkedin/posts

Parameters

ParameterTypeDescription
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, 150 (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"
Post retrieval is best-effort — only the person's publicly visible posts that can be resolved are returned, and depth varies by profile and by public post visibility. Priced at $1.00/1K (Growth $0.90/1K, Scale $0.70/1K). The 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.

GET/api/maps/search/quick

Parameters

ParameterTypeDescription
q requiredstringBusiness category or place query.
locationstringText location. Do not combine with lat/lng.
lat, lngnumberPaired coordinates — lat is −90 to 90, lng is −180 to 180. Both are required when either is supplied.
llstringShorthand 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.
countrystringTwo-letter country code (default: us).
languagestringTwo-letter language code (default: en).
zoomintegerMap zoom from 1 to 21 (default: 14). Lower numbers cover more ground per request; higher numbers concentrate the ranked list around the point.
startintegerOne of 0, 20, 40, 60, or 80.
timeoutintegerSeconds 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 }
}
Partial detail: a 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.

GET/api/maps/search

Parameters

Everything Quick accepts except start, plus two Deep-only parameters:

ParameterTypeDescription
detailintegerHow 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.
limitintegerHow many places come back at all, 1100. 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"
On Maps Deep, 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 callPer callDefaultGrowth ($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
Both bands are the same rate per place, and that is the whole design. 20 places for $0.10 per 1,000 calls and 100 places for $0.50 per 1,000 calls both work out at $0.005 per 1,000 places returned. The premium band costs 5× because it returns 5× as much — the band changes what a call costs, never what a place costs.

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.

GET/api/maps/place
ParameterTypeDescription
place_idstringGoogle place ID.
data_idstringGoogle Maps data ID.
urlstringSupported 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

FieldTypeDescription
rankinteger | nullThe 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_idstring | nullGoogle place / data identifiers. Reuse either as place_id or data_id for the Place and Reviews endpoints.
maps_urlstring | nullCanonical HTTPS Google Maps URL for the place.
namestring | nullBusiness name.
descriptionstring | nullShort public description when Google provides one.
categoriesarrayBusiness category labels, e.g. ["Coffee shop", "Cafe"]. [] when none.
addressobjectAlways an object. Structured address with street, city, state, postal_code, borough, country, and a joined formatted string — each of those is string | null.
coordinatesobjectAlways an object: latitude and longitude, each number | null.
phonestring | nullPublic phone number.
websitestring | nullPublic website URL.
ratingnumber | nullStar rating from 0 to 5.
review_countinteger | nullNumber of public reviews Google reports.
review_distributionobject | nullReview counts by star band, keys 1 through 5. null when the place carries no breakdown.
opening_hoursobjectAlways an object, {} when Google exposes no hours. Weekly hours and current open/closed state.
popular_timesobjectAlways an object, {} when Google exposes none. Hourly popularity by day.
business_statusstring | nullCurrent state, e.g. Open, Closed, Temporarily closed, or Permanently closed.
price_rangestring | nullRelative price level when Google reports one.
timezonestring | nullIANA timezone, e.g. America/New_York.
plus_codestring | nullOpen Location Code when available.
amenities / service_optionsarrayAlways arrays, [] when Google exposes none. Each entry is { name, options: [{ name, enabled, values }] } — takeout, delivery, outdoor seating and so on.
linksobjectAlways an object with booking (array), order_online (array) and menu (object | null). The arrays are [] when the place has no such link.
ownershipobject | nullclaimed boolean, owner_name, and owner_id. null when the place carries no ownership block.
cover_imagestring | nullThe chosen cover image URL.
image_countinteger | nullNumber of public photos.
imagesarrayPublic photos as { url, label }; [] when none.
detail_statusstring | nullcomplete 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.

GET/api/maps/reviews
ParameterTypeDescription
place_id / data_id / urlstringExactly one place identifier.
sortstringrelevant, newest, highest, or lowest.
page_tokenstringOpaque 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

FieldTypeDescription
placeobjectThe 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.
reviewsarrayThis page of reviews — fields below.
countintegerHow many reviews are in this page (reviews.length), never a total for the place.
next_page_tokenstring | nullOpaque 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.
deliveryobjectPresent 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[])

FieldTypeDescription
review_id / review_urlstring | nullPublic review identifier and URL.
ratinginteger | nullReviewer star rating from 1 to 5.
textobjectAlways an object with original and translated, each string | null. The object is never null; the two fields inside it are.
published_at / updated_atstring | nullISO-8601 timestamps when Google exposes them.
relative_datestring | nullGoogle's relative date label, e.g. a month ago.
imagesarrayPublic review photo URLs; [] when none.
detailed_ratingsobject | nullPer-aspect scores when Google provides them.
owner_responseobject | nulloriginal / translated text plus published_at / updated_at when the owner replied; null when they did not.
reviewerobjectAlways 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).

GET /api/amazon/search

Parameters

ParameterTypeDescription
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 domaincountry=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, 120 (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

FieldTypeDescription
search_termstring | nullThe term the page was built for.
total_results / total_results_textinteger | null, string | nullAmazon'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_pageintegerThe page served.
results_countintegerHow many cards are in results. This is the count on this page, not a site total.
results[]arrayThe 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"
Priced at $0.020 per 1,000 requests (Default), tier-discounted to $0.018/1K on Growth and $0.014/1K on Scale. The unit is the request, not the row — one request is one page of 16–48 cards, so normalise before comparing this rate with a vendor that bills per product record.

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).

GET /api/amazon/product

Parameters

ParameterTypeDescription
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

FieldTypeDescription
asin / parent_asin / url / title / brandstring | nullIdentity. parent_asin is the variation parent when the listing has one.
price / list_price / sns_pricenumber | nullNumbers, 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.
currencystring | nullISO-4217 (USD, EUR, JPY), never a currency symbol.
coupon / availability / bought_past_month / deliverystring | integer | nullThe coupon label, the availability line, Amazon's "bought in past month" figure, and the delivery promise text.
seller_id / sold_by / ships_fromstring | nullThe buy-box merchant, as one name in one shape.
rating / ratings_totalnumber | integer | nullThe star average and the number of ratings the product has on Amazon.
rating_breakdown[]arrayThe star histogram as rows of { stars, percentage } and nothing else. Percentages only — see the note below.
reviews[] / reviews_countarray, integerThe 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_presentarray, string | null, booleanBullet points, the description text, and whether the listing carries an A+ content block.
main_image / image_urls[] / image_countstring | null, array, integerGallery imagery.
variants[] / variant_dimensions[] / variants_countarray, array, integerThe variation family and the axes it varies on (size, colour, style).
categories[] / bestsellers_rank[] / specsarray, array, objectBreadcrumb 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"
Priced at $0.020 per 1,000 requests (Default), $0.018/1K on Growth, $0.014/1K on Scale. The review sample and the rating breakdown are included in that rate — there is no separate reviews endpoint and no per-review charge.

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).

GET /api/amazon/bestsellers

Parameters

ParameterTypeDescription
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

FieldTypeDescription
category / category_id / category_slug / category_urlstring | nullThe chart Amazon actually served, in its own words. Compare it with what you asked for.
child_categories[]arraySub-charts under this one, for walking the tree.
results_countintegerHow many entries are in results.
results[]arrayThe 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"
Priced at $0.020 per 1,000 requests (Default), $0.018/1K on Growth, $0.014/1K on Scale — one whole chart per request, however many entries it carries.

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).

GET /api/tiktok/profile

Parameters

ParameterTypeDescription
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 1180. 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"
  }
}
Two price bands, and the field groups are the switch. A profile or video call carrying none of the six field groups is basic: $0.10/1K Default, $0.09 Growth, $0.07 Scale. A call carrying any of them — 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 fabricatedregion 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.

GET /api/tiktok/video

Parameters

ParameterTypeDescription
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), 1180 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.hostedour 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

FieldTypeDescription
video_id / video_url / descriptionstring | nullIdentity and caption.
created_at / created_region / languagestring | nullISO-8601 creation time, the two-letter region the post was created in (distinct from a creator region), and the caption's language.
authorobjectIdentity onlyuser_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.
musicobjectmusic_id, title, author_name, original, duration, cover. Feed music_id straight into the music endpoint.
mediaobjectTechnical 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_urlstring | nullThe 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.hostedobjectOur 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.
statsobjectplay_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[]arrayParsed from the caption, plus TikTok's own content labels.
image_postobject | null{ image_count, title } for a photo post; null for an ordinary video.
is_ad / duet_enabled / stitch_enabled / share_enabledboolean | nullPost-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.10 per 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=180 is ($3.00 × 1.2 × 2.5) + $10.00 = $19.00 per 1,000 at Default — $17.10 on Growth, $13.30 on Scale. At the default limit=30 with one group it is $3.00 + $10.00 = $13.00.
It is opt-in and never free-tier eligible, because it is the one option here whose cost is the file rather than the request. A /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.

GET /api/tiktok/hashtag
GET /api/tiktok/search
GET /api/tiktok/music

Parameters

ParameterTypeDescription
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), 1180 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

FieldTypeDescription
items[]arrayFull 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_countintegerHow many items came back.
total_countinteger | nullTikTok'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_moreboolean | nullWhat TikTok said on the last page read.
next_cursorstring | nullWhere the next page would start.
truncatedbooleanTikTok 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.
restrictedbooleanThe 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.
completebooleanThe natural end of the list was reached — there is genuinely nothing more.
partialResultsobject | nullPresent 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"
One call, one charge, whole list. These three bill on the enriched band$3.00/1K Default at 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).

GET /api/x/tweet

Parameters

ParameterTypeDescription
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.

FieldTypeDescription
idstring | nullThe 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.
urlstring | nullCanonical permalink, built from the author's handle and the id.
text / lang / created_atstring | nullThe post body, its language code and its timestamp, as the record states them.
possibly_sensitive / is_editedbooleantrue only when the record says so; false otherwise.
authorobject | nullid, name, screen_name, verified, profile_image_url. verified is one boolean covering either verification programme — you get “is this account verified”, not which scheme.
countsobjectfavorites 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[]arraytype (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 stringHashtag text without the #, mentioned handles without the @, and links expanded to their destination where the record carries the expansion.
quoted_tweetobject | null{ id, url, text, author } on a quote post — one level only, never a chain. null otherwise.
reply_toobject | 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"
Priced at $0.10 per 1,000 requests (Default), $0.09/1K on Growth and $0.07/1K on Scale — the standard tier band. One request is one post, so there is no unit to normalise before you compare that rate with anyone.

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).

GET /api/x/profile

Parameters

ParameterTypeDescription
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

FieldTypeDescription
handlestring | nullThe 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_urlstring | nullDisplay name, profile description and profile image.
countsobjectfollowers, following, posts. Any of the three is null when it could not be read as a number.
joined / websitestring | nullThe join date as the profile states it, and the link on the profile where there is one.
recent_posts[]arrayThe 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"
This is the one rate on the site with no tier discount at all. $0.10 per 1,000 requests on Default, Growth and Scale, and $0.15 per 1,000 hydrated on all three. Every other standard-band vertical takes 10% off at Growth and 30% at Scale; this one takes neither, and we would rather print the same number three times than a discount that does not exist. The post endpoint does take the standard band — so the two cost the same at Default, and the profile costs more than the post at Growth and Scale. Budget from the tier you are actually on.
What 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.
Both X endpoints can also run as background jobs. Submit them as items on 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.

GET /api/reddit/posts

Parameters

ParameterTypeDescription
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 1100 (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.

FieldTypeDescription
id / fullnamestring | nullBase36 post id without the t3_ prefix, and the same id with it. id is what the comments endpoint takes as post_id.
title / selftextstring | nullPost title, and the post body on a text post.
author / author_id / author_avatarstring | nullUsername without u/, a stable author id, and the author's avatar image.
subreddit / subreddit_idstring | nullCommunity name without r/, and the t5_… community id.
score / upvote_ratio / num_commentsnumber | nullNet score, upvote ratio and comment count, exactly as stated.
created_atstring | nullISO 8601 timestamp, as the listing states it.
url / permalink / domainstring | nullWhat 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 / languagestring | nulllink, text, image, gallery or video; and the post language as stated.
award_count / top_awardinteger / object | null0 when there are none; top_award is { id, icon_url } or null.
is_nsfw / stickied / locked / archivedbooleanPost state.
flair / media / thumbnailobject / string | nullflair 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"
On /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.
limitBlocksDefault, per 1,000 callsGrowthScale
1–25 (the default is 25)1$0.12$0.108$0.084
26–502$0.24$0.216$0.168
51–753$0.36$0.324$0.252
76–100 (the synchronous ceiling)4$0.48$0.432$0.336
Set 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).

GET /api/reddit/comments

Parameters

ParameterTypeDescription
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 1100 (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.

FieldTypeDescription
id / parent_idstring | nullComment 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 / bodystring | nullUsername without u/, and the comment text.
score / depth / award_countinteger | nullScore as stated, 0 depth on a top-level comment, and 0 awards when there are none.
created_at / permalinkstring | nullISO 8601 timestamp as the thread states it, and the path to the comment.
replies[]arraytree=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.

GET /api/reddit/search
GET /api/reddit/user
GET /api/reddit/subreddit

Parameters

ParameterTypeDescription
q required string Search only. Search terms, 1–512 characters; longer is a 400.
limit integer Search only. An integer 1100 (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

EndpointReturns
/api/reddit/searchquery, 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/userusername, 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/subredditname (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"
Every Reddit operation has a job form, and it goes deeper at the same block price. 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.

Check availability before you build against this. The async job surface is enabled per account rollout. Call GET /api/status and read 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 wantUse
One Maps search deeper than the synchronous capGET /api/maps/search/async
One LinkedIn posts pull deeper than the synchronous capGET /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 callPOST /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

POST /api/bulk/jobs
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

FieldTypeDescription
items requiredarray1–200 items. Every item is validated before anything is charged — one bad item rejects the whole batch and reserves nothing.
items[].endpoint requiredstringOne of the endpoint keys in the table below.
items[].paramsobjectThe parameters you would pass to the synchronous endpoint (default: {})
delivery.pollbooleanKeep results retrievable by polling (default: true)
delivery.webhook.urlstringHTTPS URL called once when the job finishes. Public hosts only.
delivery.webhook.secretstringSigning secret. Strongly recommended — without it the callback is unsigned and you cannot verify it came from us.
delivery.storage.enabledbooleanAlso write all results to a single downloadable file.
delivery.storage.formatstringjsonl, json or csv (default: "jsonl")
clientRefstringYour 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

endpointEquivalent synchronous call
serp_webGET /api/search
serp_quickGET /api/search/quick
serp_newsGET /api/news
serp_imagesGET /api/images
serp_videosGET /api/videos
shoppingGET /api/shopping — only while that endpoint is enabled; otherwise the item is rejected as Unsupported endpoint
maps_searchGET /api/maps/search
maps_quickGET /api/maps/search/quick
maps_placeGET /api/maps/place
maps_reviewsGET /api/maps/reviews
ai_rankGET /api/ai/rank
social_youtube_searchGET /api/social/youtube/search
social_youtube_videoGET /api/social/youtube/video
social_youtube_channelGET /api/social/youtube/channel
social_youtube_playlistGET /api/social/youtube/playlist
instagram_profileGET /api/social/instagram/profile — only while Instagram jobs are enabled for your account; GET /api/status says whether they are
linkedin_profileGET /api/linkedin/profile
linkedin_profile_fullGET /api/linkedin/profile/full
linkedin_postsGET /api/linkedin/posts
linkedin_companyGET /api/linkedin/company
linkedin_jobsGET /api/linkedin/jobs
linkedin_jobGET /api/linkedin/job
linkedin_searchGET /api/linkedin/search
x_tweetGET /api/x/tweet
x_profileGET /api/x/profilehydrate=true bills the same 1.5× here as it does synchronously, and is given back the same way when hydration enriches nothing
reddit_postsGET /api/reddit/postslimit up to 250 on the job form, billed on the same block curve
reddit_commentsGET /api/reddit/commentslimit up to 1000 on the job form, billed on the same block curve
reddit_searchGET /api/reddit/search
reddit_userGET /api/reddit/user
reddit_subredditGET /api/reddit/subreddit
Every key takes the parameters of its synchronous twin, validated by the same rules, and its item 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

GET /api/bulk/jobs/:jobId
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

statusMeaningTerminal
queuedAccepted, nothing started yet.No
processingAt least one item is running.No
completedEvery item succeeded.Yes
partialSome items succeeded, some failed. Collect the results and check each item.Yes
failedEvery item failed. Failed items are refunded.Yes
cancelledYou cancelled it. Items not yet dispatched were refunded.Yes

Per-item status is pending, processing, succeeded or failed.

3. Collect the results

GET /api/bulk/jobs/:jobId/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 parameterTypeDescription
includestringinline always returns items inline, even when a results file exists.
limitintegerItems per page, 1–1000 (default: 100)
offsetintegerItems 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.

A job result carries the same delivery block as a synchronous responserequested, 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.codeMeaning
invalid_paramsThe item's parameters were rejected. error.message says which.
not_foundThe requested resource does not exist. This one is charged — see below.
unavailableThe item could not be served. Resubmit it.
request_failedAny other failure. Resubmit it.
cancelledThe 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

GET /api/bulk/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

POST /api/bulk/jobs/:jobId/cancel
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

HeaderDescription
webhook-idUnique id for this delivery. Use it to make your handler idempotent.
webhook-timestampUnix seconds at signing time. Reject anything far outside your clock skew.
webhook-signaturev1,<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 2xx counts as delivered.
  • Redirects are not followed — a 3xx counts as not delivered.
  • Delivery never blocks polling. If the callback fails every attempt the results are still there, and job.webhook.state reads failed.

7. How long jobs and results live

ThingLifetime
The job and its per-item results7 days from submission.
The signed results-file download URLAbout an hour — the exact moment is in results.expiresAt.
These two are very different, and the short one catches people out. The download URL in a webhook callback expires long before the job does: if you queue that URL for a nightly batch, it will be dead by the time you use it. Re-request 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 itemWhat it costs
SucceededCharged. 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 deliveredCharged, 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_foundCharged. A resource that genuinely does not exist is an answer, exactly as on the synchronous call.
Failed with any other codeRefunded in full.
Came back empty because we could not complete itRefunded in full. The item body does not say so — job.billing.refunded and your ledger do.
Cancelled before dispatchRefunded 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

StatuscodeMeaning
400invalid_parameterAn item's endpoint or parameters are invalid, or delivery is malformed. The message says which. Nothing was charged.
402insufficient_creditsYour balance does not cover the batch estimate. Nothing was charged.
413batch_too_largeMore than 200 items. Split the batch.
503temporarily_unavailableSubmissions 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.

EngineValueSupported Endpoints
Google 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.
Pricing is category-based. Engine choice does not change the public rate within Web, News, Images, Videos, or Shopping. Dedicated Video Search supports Google, DDG, Yahoo, Bing, and Brave. Dedicated Shopping Search — coming soon, not callable today — is planned for Google, Yahoo, DDG and Brave.

Country/Region Support

Get localized search results by specifying a country code. This affects the language, regional rankings, and availability of results.

GET /api/countries

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

CodeCountryCodeCountry
usUnited StatesukUnited Kingdom
caCanadaauAustralia
deGermanyfrFrance
inIndiajpJapan
View All Supported Countries

English-Speaking (12)

CodeCountryCodeCountry
usUnited StatesukUnited Kingdom
gbUnited Kingdom (alias)caCanada
auAustralianzNew Zealand
ieIrelandsgSingapore
phPhilippinesmyMalaysia
inIndiazaSouth Africa

Western Europe (10)

CodeCountryCodeCountry
deGermanyfrFrance
esSpainitItaly
nlNetherlandsbeBelgium
atAustriachSwitzerland
ptPortugalluLuxembourg

Nordics (5)

CodeCountryCodeCountry
seSwedennoNorway
dkDenmarkfiFinland
isIceland

Eastern Europe (18)

CodeCountryCodeCountry
plPolandroRomania
czCzech RepublicskSlovakia
huHungarybgBulgaria
hrCroatiasiSlovenia
eeEstonialvLatvia
ltLithuaniauaUkraine
ruRussiagrGreece
trTurkeybaBosnia and Herzegovina
mkNorth MacedoniageGeorgia

Asia (14)

CodeCountryCodeCountry
jpJapantwTaiwan
hkHong KongkrSouth Korea
cnChinaidIndonesia
thThailandvnVietnam
bdBangladeshlkSri Lanka
mmMyanmarkhCambodia
laLaosnpNepal

Middle East (14)

CodeCountryCodeCountry
ilIsraelsaSaudi Arabia
pkPakistanaeUnited Arab Emirates
qaQatarkwKuwait
omOmanbhBahrain
joJordanlbLebanon
iqIraqirIran
sySyriayeYemen

North Africa (5)

CodeCountryCodeCountry
egEgyptmaMorocco
tnTunisialyLibya
dzAlgeria

Sub-Saharan Africa (10)

CodeCountryCodeCountry
ngNigeriakeKenya
ghGhanaetEthiopia
tzTanzaniaugUganda
zwZimbabwebwBotswana
naNamibiasnSenegal

Latin America (18)

CodeCountryCodeCountry
mxMexicobrBrazil
arArgentinaclChile
coColombiapePeru
veVenezuelaecEcuador
boBoliviapyParaguay
crCosta RicapaPanama
doDominican RepublicgtGuatemala
hnHondurasniNicaragua
svEl SalvadorprPuerto Rico

Caribbean (3)

CodeCountryCodeCountry
cuCubajmJamaica
ttTrinidad and Tobago

Other (3)

CodeCountryCodeCountry
mtMaltaamArmenia
azAzerbaijan

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.

Tip: 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

CodeLanguageCodeLanguage
enEnglishesSpanish
frFrenchdeGerman
ptPortugueseitItalian
jaJapanesezhChinese
arArabichiHindi
View All Supported Languages

Germanic

CodeLanguageCodeLanguage
enEnglishdeGerman
nlDutchsvSwedish
daDanishnoNorwegian
isIcelandicafAfrikaans

Romance

CodeLanguageCodeLanguage
esSpanishfrFrench
itItalianptPortuguese
roRomaniancaCatalan
glGalician

Slavic

CodeLanguageCodeLanguage
ruRussianplPolish
csCzechskSlovak
ukUkrainianbgBulgarian
srSerbianhrCroatian
bsBosnianmkMacedonian
slSlovenian

Baltic / Celtic / Other European

CodeLanguageCodeLanguage
ltLithuanianlvLatvian
elGreeksqAlbanian
cyWelshgaIrish
euBasquemtMaltese

Uralic / Turkic

CodeLanguageCodeLanguage
fiFinnishetEstonian
huHungariantrTurkish
azAzerbaijanikkKazakh
uzUzbek

Indic / Iranian

CodeLanguageCodeLanguage
hiHindibnBengali
urUrdupaPunjabi
mrMarathiguGujarati
neNepalisiSinhala
faPersianpsPashto
kuKurdish

Dravidian

CodeLanguageCodeLanguage
taTamilteTelugu
knKannadamlMalayalam

East & Southeast Asian

CodeLanguageCodeLanguage
zhChinesejaJapanese
koKoreanmyBurmese
viVietnamesethThai
kmKhmerloLao
idIndonesianmsMalay
tlFilipino

Semitic / African

CodeLanguageCodeLanguage
arArabicheHebrew
amAmharicswSwahili
haHausayoYoruba
igIgbozuZulu
xhXhosasoSomali

Caucasian

CodeLanguageCodeLanguage
kaGeorgianhyArmenian

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.

GET /api/status

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 }
      }
    }
  }
}
The 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

TierLargest single depositDiscount
Default$0Full price
Growth$10010× 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$50020× 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)

CategoryDefaultGrowth ($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=true1.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
Maps Deep billing — two flat bands, and a cliff between them: a Deep call returns up to 100 ranked places by default, and 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/statuslimits.endpoints[…].async_available is the authority on whether that surface is live for your account.
Web category billing: one call, one charge — always on /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.
Full pricing details are available at 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"
  }
}
SERP features: Quick Search returns organic results only, on every engine — the feature blocks below are part of Deep Search. Google and Yahoo include 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).
Three 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.

Important: with 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.

FieldAlways presentMeaning
requestedyesWhat you asked for. On endpoints that return a single entity (a profile, a company) this is 1.
returnedyesWhat you got. Compare the two — that is the whole check.
reasonnoThe cause. On SERP endpoints it appears only when the answer fell more than 40% short; the entity verticals (LinkedIn, Instagram, Maps) always carry one.
noteyesThe same thing in a sentence, safe to show a human.
max_supportednoOn depth-capped endpoints, the largest depth this request size could serve.
async_endpointon every endpoint that has a job formWhere the same work can be submitted as a job. Present only when that path is live for your account.
Whenever fewer results than requested are delivered, 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 ordersuccess (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.

CodeMeaningDescription
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.

codeStatuserrorWhen
missing_parameter400Missing parameterA required parameter was not sent. Carries param.
invalid_parameter400Invalid parameterA parameter value is not accepted (bad freshness, safe, num, filter value, …). Carries param.
unsupported_engine400Invalid engineengine is not one of google, yahoo, bing, ddg, brave.
unsupported_country400Invalid country codecountry is not a supported 2-letter code — call GET /api/countries.
unsupported_language400Invalid languagelanguage is not a 2-letter ISO code.
query_too_long400Query too longq is over 2,048 characters.
cursor_expired400Cursor expiredA Maps Reviews page_token has expired (they live 15 minutes). Start the listing again.
missing_api_key401Missing API keyNo key was sent. Send it in the X-API-Key header.
invalid_api_key401Invalid API keyThe key is not valid. Check it in your dashboard.
insufficient_credits402Payment RequiredNo credits and no free calls left. Carries credits and costPerSearch.
forbidden403ForbiddenThe account does not have access to that resource.
not_found404Not foundUnknown 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.
conflict409ConflictThe resource already exists. Choose a different name.
batch_too_large413Batch too largeToo many items in one async job. Split the batch.
rate_limit_exceeded429Rate limit exceededAutomated overuse or server load. Carries retryAfter, window, limit.
too_many_concurrent_requests429Too many concurrent requestsMore calls in flight at once than your account allows. Run them sequentially or reduce parallelism.
internal_error500Internal errorSomething broke on our side. Carries errorId — quote it to support.
temporarily_unavailable500 / 502 / 503Temporarily unavailableWe 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_timeout504Request timed outThe clock ran out before anything was gathered.
A search that finds nothing returns 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 authorityGET /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.

BracketConcurrentPer minutePer hourPer dayQueue priority
Free — no deposit110100500Lowest
Default — balance < $1003–2020–100200–1,0001,000–unlimited*Standard
Growth — balance $100–$49910–100100–1,0001,000–10,0008,000–unlimited*High
Scale — balance $500+100–1,000500–5,0005,000–50,00030,000–unlimited*Highest
Published figures are standard "up to" allocations, not guaranteed floors — actual throughput may be lower when the platform is under heavy load. Each range shows the standard allocation at your balance to the high end available on request: * higher custom allocations are provided on requestcontact us for custom limits. For Default accounts, requests above 1,000 calls/day are served on a global shared pool (best effort, not a guaranteed floor). The per-minute, per-hour and per-day windows reset on fixed UTC boundaries (top of each minute/hour, and 00:00 UTC for the day). A 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 limitsValue
Demo API key10/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"