AiContent Services — Integration Guide
Canonical reference for apps and AI agents that need to consume our services. Every service below uses the same standardized section template:
- Overview — what it does, base URL
- Auth — how to authenticate
- Pricing & quotas — credits, limits, billing
- Endpoints — request/response shapes
- Errors — HTTP status meanings + retry guidance
- TypeScript client — drop-in sample
- Python client — drop-in sample
- Admin SQL — managing clients / inspecting usage
For agents: when integrating one of our services into another project, you only need the section for that service. Copy the env vars, drop in the client sample, done.
Services index
| Service | Base URL | Status |
|---|---|---|
| AiContent Scraper | API: https://scraperbox.aicontent.engineering · Marketing: https://scraper.aicontent.engineering |
Live (test billing) |
AiContent Scraper
Overview
Stealth headless-browser web scraping. Executes JavaScript, evades bot detection (Cloudflare, Datadome, PerimeterX), returns the full rendered HTML plus extracted text.
- API base URL:
https://scraperbox.aicontent.engineering - What it returns: raw HTML (post-JS), extracted text, HTTP status, title, timing
- Stealth: every request runs through a fingerprint-correct, anti-detection browser stack — bot signatures, navigator quirks, and TLS fingerprints look like a real visitor.
Auth
Send your client key in the X-API-Key header. One key per consuming app. Keys are
issued in test mode now; we mirror to live mode when ready (no contract change).
Future keys use the sc_ prefix. Existing keys with other prefixes keep working.
curl -X POST https://scraperbox.aicontent.engineering/scrape \
-H "X-API-Key: sc_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
Pricing & quotas
| Line | Cost | Notes |
|---|---|---|
| Base subscription | $99 / month | Per client. Required. Covers platform access + support. |
| Stealth JS scrape (default) | 5 credits | $0.125 per scrape · $125 / 1k |
| Auto-escalation to residential | +20 credits | $0.50 extra per escalated scrape. Only fires when the default path is blocked, thin, or challenged. Billed only for the residential attempt. |
Credits price at $0.025 each. Usage is reported as Stripe meter events and rolls up monthly into the same invoice as the base subscription.
Local quota. Each client also has a monthly_credit_limit (default 100,000). When
the month's usage would exceed it, the service returns
429 {"error":"monthly credit limit reached","limit":N,"used":N}. Reset is the 1st
of each calendar month (UTC). The limit is enforced regardless of Stripe billing
status.
Endpoints
POST /scrape
Scrape a single URL. (Path is /scrape for backward compatibility; will alias to
/scrape on the new subdomain.)
Request:
{ "url": "https://example.com", "wait_ms": 1500 }
| Field | Type | Required | Notes |
|---|---|---|---|
url |
string | yes | Full URL with scheme |
wait_ms |
int | no | Extra wait after networkidle. Default 1500. Auto-polls up to 6s for >200 chars text already. |
Response (200):
{
"url": "https://example.com",
"status": 200,
"title": "Example Domain",
"html": "<!DOCTYPE html>...",
"text": "Example Domain\n\nThis domain is for...",
"render_ms": 9404,
"stealth": true,
"credits_charged": 5
}
Response (502):
{ "url": "...", "error": "navigation timeout", "render_ms": 30000, "stealth": true, "credits_charged": 5 }
Failures still charge — we spent the compute.
POST /scrape-batch
Up to 50 URLs per request. Workers process serially with bounded concurrency.
Request:
{ "urls": ["https://a.com", "https://b.com"] }
Response:
{
"results": [
{ "url": "https://a.com", "status": 200, "html": "...", "text": "...", "render_ms": 4200, "stealth": true, "credits_charged": 5 },
{ "url": "https://b.com", "error": "Navigation timeout", "render_ms": 30000, "stealth": true, "credits_charged": 5 }
]
}
Whole batch fails (429) up-front if it would exceed your monthly limit.
GET /health
No auth. Liveness probe.
{ "ok": true }
Errors
| HTTP | When |
|---|---|
| 400 | Missing url, malformed JSON, batch >50 URLs |
| 401 | Missing or invalid X-API-Key |
| 429 | Monthly credit limit reached |
| 502 | Scrape failed (timeout, browser crash). Body has error field. |
Retry guidance: retry transient 502s once with backoff. Don't retry 401 (key issue) or 429 (need limit increase).
TypeScript client
const SCRAPER_URL = 'https://scraperbox.aicontent.engineering'
const SCRAPER_KEY = process.env.SCRAPER_API_KEY! // sc_... (or legacy ro_...)
export async function scrapePage(url: string, waitMs?: number) {
const res = await fetch(`${SCRAPER_URL}/scrape`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': SCRAPER_KEY },
body: JSON.stringify({ url, ...(waitMs ? { wait_ms: waitMs } : {}) }),
signal: AbortSignal.timeout(90_000),
})
if (res.status === 401) throw new Error('scraper: invalid key')
if (res.status === 429) throw new Error('scraper: quota exceeded')
const data = await res.json()
if (data.error) throw new Error(`scraper: ${data.error}`)
return data as {
html: string
text: string
status: number
title: string
render_ms: number
credits_charged: number
}
}
Python client
import os, requests
SCRAPER_URL = 'https://scraperbox.aicontent.engineering'
KEY = os.environ['SCRAPER_API_KEY']
def scrape_page(url: str, wait_ms: int | None = None) -> dict:
body = {'url': url}
if wait_ms:
body['wait_ms'] = wait_ms
r = requests.post(
f'{SCRAPER_URL}/scrape',
json=body,
headers={'X-API-Key': KEY},
timeout=90,
)
r.raise_for_status()
data = r.json()
if data.get('error'):
raise RuntimeError(f"scraper: {data['error']}")
return data
Admin SQL
Until the /admin/scraper UI ships, manage clients with psql against the aicontent
Supabase pooler.
Internal table names use scraper_* (renamed from the original render_* on 2026-05-28).
-- Issue a new client. Generate the raw key first:
-- KEY="sc_$(openssl rand -base64 24 | tr '+/=' '_-')"
-- echo $KEY # hand to the client
-- PREFIX=$(echo $KEY | cut -c1-8)
-- HASH=$(printf '%s' "$KEY" | shasum -a 256 | awk '{print $1}')
INSERT INTO scraper_api_clients (name, contact_email, key_prefix, key_hash, monthly_credit_limit)
VALUES ('ClientName', 'contact@client.com', '<PREFIX>', '<HASH>', 100000);
-- Wire metered billing (after creating Stripe customer + subscription):
UPDATE scraper_api_clients
SET stripe_customer_id = 'cus_...',
stripe_subscription_id = 'sub_...',
stripe_subscription_item_id = 'si_...',
stripe_billing_enabled = TRUE
WHERE name = 'ClientName';
-- Bump or remove a credit limit
UPDATE scraper_api_clients SET monthly_credit_limit = 500000 WHERE name = 'ClientName';
UPDATE scraper_api_clients SET monthly_credit_limit = 0 WHERE name = 'ClientName'; -- 0 = unlimited
-- Disable a key (auth fails immediately)
UPDATE scraper_api_clients SET status = 'disabled' WHERE name = 'ClientName';
-- Current month usage per client
SELECT c.name,
scraper_client_credits_used_this_month(c.id) AS used,
c.monthly_credit_limit AS limit,
c.stripe_billing_enabled
FROM scraper_api_clients c
WHERE c.status = 'active';
-- Which scrapes were NOT reported to Stripe? (success+billing_enabled+not reported)
SELECT u.occurred_at, u.url, u.credits_charged
FROM scraper_usage u
JOIN scraper_api_clients c ON c.id = u.client_id
WHERE c.stripe_billing_enabled = TRUE
AND u.success = TRUE
AND u.stripe_event_reported = FALSE
ORDER BY u.occurred_at DESC
LIMIT 50;
Stripe references (test mode)
| Object | ID |
|---|---|
| Product | prod_UbL9M1ahNphKFu (AiContent Scraper) |
| Meter | mtr_test_61UlWQqaFHOMIlMTd41PFJueV2quz1ua (event_name render_credit, display "AiContent Scraper Credits") |
| Price | price_1TcAfBPFJueV2quzmlJ6pr1I (metered, monthly, $0.025/credit) |
Preview a customer's upcoming invoice:
curl -u "$STRIPE_SECRET_KEY:" -X POST \
https://api.stripe.com/v1/invoices/create_preview \
-d subscription=sub_...