API Documentation

Real-time web scraping and search for AI agents, researchers, and developers. One key, two calls, structured output — /scrape and /search.

Introduction

The Indo Data Works API turns any public web page into clean, structured data. Send a URL to /scrape and get back Markdown, HTML, or JSON. Send a query to /search and get back search results with the full content of each page already scraped — one call instead of two.

  • Zero restrictions — no domain whitelists. If it's on the public web, we can fetch it.
  • Anti-bot bypass — handles Cloudflare challenges, JavaScript rendering, and rate limiting on your behalf.
  • AI-agent first — JSON/Markdown output designed to be piped straight into LLM context windows.
  • Pay per request — $0.01 per scrape, $0.03 per search. No subscriptions.
Base URL https://api.indodataworks.asia Status Operational — /health

All endpoints accept and return JSON with Content-Type: application/json. Requests over HTTPS only.

Getting Started

From zero to your first scrape in under a minute. You need nothing but a terminal or a few lines of code — no signup form, no credit card.

1

Generate an API key

Call POST /api-keys with your email and a name for the key. The key is returned once — store it securely, like a password.

curl -X POST https://api.indodataworks.asia/api-keys \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "name": "my-first-key"}'

# => {"api_key": "d01a726c-85b9-...", "created_at": "2026-10-18T16:00:00Z", "user_id": "..."}
2

Test with /scrape

Export your key as an environment variable, then scrape a page. Set it and forget it for every example below.

export IDW_API_KEY="paste_the_api_key_you_saved_above"

curl -X POST https://api.indodataworks.asia/scrape \
  -H "X-API-Key: $IDW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "format": "markdown"}'
3

Check your usage

See how many requests you've burned and where you stand against the hourly limit.

curl "https://api.indodataworks.asia/usage?api_key=$IDW_API_KEY"

# => {"total_requests": 3, "requests_today": 3, "requests_this_hour": 1, "rate_limit": {...}}
4

Integrate into your app

Drop the key into your app's environment (INDODATAWORKS_API_KEY is a good convention), add a timeout and a retry on 429/5xx, and you're production-ready. Code examples for every endpoint are below — switch tabs for cURL, Python, and JavaScript.

Tip for AI agents: you can bootstrap entirely by machine — /api-keys → /scrape → /usage are three plain HTTP calls with no browser or captcha step.

Authentication

Authenticated endpoints (/scrape and /search) require a single header:

X-API-Key: YOUR_API_KEY
EndpointAuth required?How
POST /scrapeRequiredX-API-Key header
POST /searchRequiredX-API-Key header
POST /api-keysNoneOpen — this is how you get a key
GET /usageQuery param?api_key=...
GET /healthNonePublic

A missing or invalid key returns 401 Unauthorized:

{
  "detail": "Missing API key. Provide X-API-Key header."
}
Keep keys secret. Send the key only in the X-API-Key header of server-to-server calls. Never embed it in client-side JavaScript bundles, mobile app binaries, or public repositories. Rotate a key immediately if it leaks (delete it via /api-keys management and generate a new one).

Rate Limits

Every API key is limited to 100 requests per hour, tracked per key on a sliding window. The limit applies to /scrape and /search combined.

  • Exceeding the limit returns 429 Too Many Requests with a JSON detail message.
  • Check your current window at any time with GET /usage → requests_this_hour.
  • On 429, back off and retry when the window resets — exponential backoff (e.g. 30s → 60s → 120s) works well.
  • Need more than 100/hour? Contact us for an enterprise plan.
{
  "detail": "Rate limit exceeded: 100 requests/hour. Try again later."
}

Pricing

Pay-per-request, no subscription, no commitment. /api-keys, /usage, and /health are free.

POST /scrape

$0.01/request

Scrape any URL into Markdown, HTML, or JSON.

Enterprise

Custom

Higher limits, priority support, SLA.

x402 native. Indo Data Works supports the x402 micropayment protocol — billing settles per request. API-key usage is metered and billed against your balance; check spend with GET /usage.

Endpoints

Five endpoints, one base URL: https://api.indodataworks.asia. Each section below documents the request schema, response schema, errors, and runnable examples in cURL, Python, and JavaScript.

POST /scrape X-API-Key required $0.01 / request

Scrape a webpage

Fetches the target URL, defeats bot protection and renders JavaScript when needed, then converts the page to your chosen format. The fastest way to get clean, LLM-ready text out of any public page.

Request body

FieldTypeRequiredDescription
urlstringrequiredFull page URL to scrape, including https://.
format"markdown" | "html" | "json"optionalOutput format. Default "markdown" — best for feeding LLMs. "html" returns cleaned HTML, "json" returns structured extraction.
timeoutintegeroptionalServer-side fetch timeout in seconds. Default 30. Increase for slow/heavy pages.

Headers

HeaderRequiredValue
X-API-KeyyesYour API key
Content-Typeyesapplication/json

Response body 200 OK

FieldTypeDescription
successbooleantrue when content was captured.
contentstringScraped page content in the requested format.
metadata.titlestringPage title.
metadata.urlstringFinal URL after redirects.
metadata.status_codeintegerHTTP status the target page returned.
metadata.content_lengthintegerLength of content in characters.
strategystringWhich fetching strategy succeeded (e.g. httpx, browser, bypass) — useful for debugging difficult sites.
scrape_timenumberFetch + render time in seconds.
errorstring | nullnull on success; reason string on failure.

Examples

curl -X POST https://api.indodataworks.asia/scrape \
  -H "X-API-Key: $IDW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "format": "markdown",
    "timeout": 30
  }'
import os
import requests

resp = requests.post(
    "https://api.indodataworks.asia/scrape",
    headers={"X-API-Key": os.environ["IDW_API_KEY"]},
    json={"url": "https://example.com", "format": "markdown", "timeout": 30},
    timeout=60,
)
resp.raise_for_status()
data = resp.json()

if data["success"]:
    print(data["metadata"]["title"])
    print(data["content"][:500])
else:
    print("Scrape failed:", data["error"])
const res = await fetch("https://api.indodataworks.asia/scrape", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.IDW_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com",
    format: "markdown",
    timeout: 30,
  }),
});

if (!res.ok) throw new Error(`API error: ${res.status}`);
const data = await res.json();
console.log(data.metadata.title);
console.log(data.content.slice(0, 500));

Example response

{
  "success": true,
  "content": "# Example Domain\n\nThis domain is for use in illustrative examples...",
  "metadata": {
    "title": "Example Domain",
    "url": "https://example.com",
    "status_code": 200,
    "content_length": 1120
  },
  "error": null,
  "strategy": "httpx",
  "scrape_time": 0.4748
}

Error codes

StatusWhen
401Missing or invalid X-API-Key.
422Validation error — bad/missing url or unknown format.
429Rate limit exceeded (100 requests/hour).
500Unexpected server error — safe to retry with backoff.
POST /search X-API-Key required $0.03 / request

Search the web & scrape results

One call does both halves of a research loop: it runs a web search, then scrapes each top result so you get full page content — not just snippets. Ideal for AI agents answering questions that need fresh sources.

Request body

FieldTypeRequiredDescription
querystringrequiredSearch query.
max_resultsintegeroptionalNumber of results to return and scrape. Default 5.
scrape_contentbooleanoptionalWhen true (default), each result includes full scraped content. Set false for titles/URLs/snippets only.

Response body 200 OK

FieldTypeDescription
successbooleantrue when the search completed.
resultsarrayOne object per result.
results[].titlestringResult page title.
results[].urlstringResult URL.
results[].snippetstringShort description from the search engine.
results[].contentstringFull scraped content (null if scrape_content=false or scraping that page failed).
errorstring | nullnull on success; reason string on failure.

Examples

curl -X POST https://api.indodataworks.asia/search \
  -H "X-API-Key: $IDW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "AI trends 2026",
    "max_results": 5,
    "scrape_content": true
  }'
import os
import requests

resp = requests.post(
    "https://api.indodataworks.asia/search",
    headers={"X-API-Key": os.environ["IDW_API_KEY"]},
    json={"query": "AI trends 2026", "max_results": 5, "scrape_content": True},
    timeout=120,
)
resp.raise_for_status()
data = resp.json()

for r in data["results"]:
    print(f"- {r['title']}  ({r['url']})")
    if r.get("content"):
        print(f"  {r['content'][:200]}...")
const res = await fetch("https://api.indodataworks.asia/search", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.IDW_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: "AI trends 2026",
    max_results: 5,
    scrape_content: true,
  }),
});

if (!res.ok) throw new Error(`API error: ${res.status}`);
const { results } = await res.json();
for (const r of results) {
  console.log(`- ${r.title} (${r.url})`);
}

Example response

{
  "success": true,
  "results": [
    {
      "title": "The State of AI in 2026",
      "url": "https://example.com/ai-2026",
      "snippet": "Agents, inference costs, and open models...",
      "content": "# The State of AI in 2026\n\n..."
    }
  ],
  "error": null
}

Error codes

StatusWhen
401Missing or invalid X-API-Key.
422Validation error — missing query.
429Rate limit exceeded (100 requests/hour).
500Unexpected server error — safe to retry with backoff.
POST /api-keys No auth required Free

Generate an API key

Creates a new API key tied to your email. This is the entry point of the whole system — you call it once, store the key, and use it on every subsequent request.

Request body

FieldTypeRequiredDescription
emailstring (email)requiredContact email the key is bound to.
namestringrequiredHuman-readable label, e.g. "prod-bot".

Response body 200 OK

FieldTypeDescription
api_keystringYour new secret key (UUID format). Shown only in this response — save it immediately.
user_idstringIdentifier of the key holder.
created_atstring (ISO 8601)Creation timestamp.

Examples

curl -X POST https://api.indodataworks.asia/api-keys \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "name": "my-first-key"}'
import requests

resp = requests.post(
    "https://api.indodataworks.asia/api-keys",
    json={"email": "you@example.com", "name": "my-first-key"},
    timeout=30,
)
resp.raise_for_status()
key = resp.json()["api_key"]
print("Save this key now:", key)
const res = await fetch("https://api.indodataworks.asia/api-keys", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "you@example.com", name: "my-first-key" }),
});

if (!res.ok) throw new Error(`API error: ${res.status}`);
const { api_key } = await res.json();
console.log("Save this key now:", api_key);

Example response

{
  "api_key": "d01a726c-85b9-4736-89d6-ded094865658",
  "user_id": "usr_9f2c41",
  "created_at": "2026-10-18T16:00:00Z"
}

Error codes

StatusWhen
422Validation error — missing email or name, or invalid email format.
500Unexpected server error — safe to retry with backoff.
GET /usage?api_key=... Key via query param Free

Check usage statistics

Returns request counts for your API key — total, today, and inside the current rate-limit hour, plus a live view of your remaining quota. Use it in dashboards, cron jobs, or budget alerts.

Query parameters

ParamTypeRequiredDescription
api_keystringrequiredThe API key to report usage for.

Response body 200 OK

FieldTypeDescription
api_keystringThe key you queried.
total_requestsintegerAll-time requests with this key.
requests_todayintegerRequests since 00:00 UTC today.
requests_this_hourintegerRequests inside the current rolling hour — compare against the 100/hour limit.
rate_limitobjectLive quota view: {limit, remaining, used} for the current hour.

Examples

curl "https://api.indodataworks.asia/usage?api_key=$IDW_API_KEY"
import os
import requests

resp = requests.get(
    "https://api.indodataworks.asia/usage",
    params={"api_key": os.environ["IDW_API_KEY"]},
    timeout=15,
)
resp.raise_for_status()
usage = resp.json()

rl = usage["rate_limit"]
print(f"Total: {usage['total_requests']}, today: {usage['requests_today']}, "
      f"this hour: {usage['requests_this_hour']} ({rl['remaining']}/{rl['limit']} left)")
const url = new URL("https://api.indodataworks.asia/usage");
url.searchParams.set("api_key", process.env.IDW_API_KEY);

const res = await fetch(url);
if (!res.ok) throw new Error(`API error: ${res.status}`);

const usage = await res.json();
console.log(`This hour: ${usage.requests_this_hour} used, ` +
  `${usage.rate_limit.remaining}/${usage.rate_limit.limit} remaining`);

Example response

{
  "api_key": "d01a726c-85b9-4736-89d6-ded094865658",
  "total_requests": 142,
  "requests_today": 37,
  "requests_this_hour": 8,
  "rate_limit": {
    "limit": 100,
    "remaining": 92,
    "used": 8
  }
}

Error codes

StatusWhen
422Missing api_key query parameter.
500Unexpected server error — safe to retry with backoff.
GET /health No auth required Free

Health check

Public liveness probe. No authentication, no billing. Use it for uptime monitors and to distinguish API problems from your own network problems.

Response body 200 OK

FieldTypeDescription
statusstring"healthy" when the API is up.
timestampstring (ISO 8601)Server time of the check.
scraper_initializedbooleanWhether the scraping engine is warm.

Examples

curl https://api.indodataworks.asia/health
import requests

resp = requests.get("https://api.indodataworks.asia/health", timeout=10)
print(resp.json()["status"])  # "healthy"
const res = await fetch("https://api.indodataworks.asia/health");
const { status } = await res.json();
console.log(status); // "healthy"

Example response

{
  "status": "healthy",
  "timestamp": "2026-10-18T16:00:00.000000",
  "scraper_initialized": true
}

Error Codes

All errors return JSON with a human-readable detail message. HTTP status is the contract — branch on it, log detail for debugging.

StatusMeaningExample bodyWhat to do
401 Missing/invalid API key {"detail": "Missing API key. Provide X-API-Key header."} Fix the header or regenerate your key via POST /api-keys.
422 Validation error {"detail": [{"type": "missing", "loc": ["body", "url"], "msg": "Field required"}]} Read loc — it names the exact bad field. Fix your request body.
429 Rate limit exceeded {"detail": "Rate limit exceeded: 100 requests/hour. Try again later."} Back off (30s+) and retry. Watch requests_this_hour via GET /usage to stay under the limit.
500 Server error {"detail": "Internal server error"} Retry with exponential backoff. If it persists, check GET /health, then contact support.
Note on scraping failures: a page that loads but can't be converted may still return 200 OK with "success": false and an error string inside the body. Always check success, not just the HTTP status.

Best Practices

  • Prefer format: "markdown" for LLM pipelines — it strips navigation/boilerplate and tokenizes efficiently.
  • Set a client timeout above the server timeout. If you pass timeout: 30, wait at least 45s on the client so the server's structured error beats your socket abort.
  • Retry idempotently on 429/5xx with exponential backoff + jitter; never retry 401/422 blindly.
  • Cache results keyed by URL. Scraping the same page twice in one run costs double for zero new information.
  • Use scrape_content: false on /search when the agent only needs links, then scrape the 1–2 pages it actually reads.
  • Monitor with GET /usage before the window matters: alert at 80 requests/hour to avoid mid-run 429s.
  • Store keys in environment variables or a secrets manager — never in source control.

Try It Live

Send a real request without leaving this page. Enter your API key and a URL — the request runs from your browser against the live API, and the JSON response appears below. Your key goes only to api.indodataworks.asia, never anywhere else, and is never stored.