fetchflat

Docs

Base URL https://fetchflat.dev. JSON in, JSON out. Every billed response carries X-Credits-Charged and X-Credits-Remaining.

FAQ · Pricing · Support

Auth

Create a key in the console. Send it as a bearer token. ff_live_ and ff_test_ keys share one ledger; the prefix is cosmetic.

Authorization: Bearer ff_live_…

Missing or invalid key → 401 unauthorized. Revoked → 401 revoked_key. Out of credits → 402 insufficient_credits.

POST /v1/fetch

$ curl -s https://fetchflat.dev/v1/fetch \
    -H "Authorization: Bearer ff_live_…" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://example.com","format":"markdown","render":false,"proxy":"auto"}'
fieldvalues
urlhttps only. No userinfo. Public hosts only.
formatmarkdown (default) · html
renderfalse (default) · true = headless Chromium, 8 credits
proxyauto (default) · datacenter · residential (25 credits) · unblocker (40 credits)

200

{
  "ok": true,
  "id": "uuid",
  "credits": 1,
  "data": {
    "url": "https://example.com",
    "status": 200,
    "title": "Example Domain",
    "markdown": "# Example Domain\n\n...",
    "html": null,
    "json": null
  },
  "meta": { "render": false, "proxy": "datacenter", "ms": 412, "truncated": false, "empty": false }
}

409 upgrade_required

{ "ok": false, "id": "uuid", "error": "upgrade_required", "hint": "set proxy=residential" }

Returned when proxy=auto hit a 401/403/429 or a challenge page. Charged 0. Retry with proxy=residential. If residential is also blocked you get another 409 with hint: set proxy=unblocker, again charged 0. We never chain pools inside one call, so you always opt in to the higher price. With an explicit proxy=datacenter the target's 403 is returned as data and charged.

proxy=unblocker

Unblocker is optional. Use it when residential still returns upgrade_required. USD before tax.

$ curl -s https://fetchflat.dev/v1/fetch \
    -H "Authorization: Bearer ff_live_…" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://example.com","format":"markdown","proxy":"unblocker"}'

Flat 40 credits (44 with extract). render is ignored: the vendor renders when it needs to, so we never start Chromium and never add the 8. The same URL rules apply, public hosts only. Available on /v1/extract and on each /v1/batch item.

POST /v1/extract

$ curl -s https://fetchflat.dev/v1/extract \
    -H "Authorization: Bearer ff_live_…" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://example.com",
      "schema": { "type": "object", "properties": { "title": { "type": "string" } }, "required": ["title"] },
      "prompt": "optional guidance",
      "render": false,
      "proxy": "auto"
    }'

Extraction reads the page and nothing else. If the page carries no text, we return 422 empty_page rather than asking the model to fill your schema from nothing, because an invented answer would satisfy the schema and be indistinguishable from real data. A single-page app fetched without render=true is the usual cause. meta.empty on a fetch tells you the same thing.

data.json is the extracted object. Markdown sent to the model is capped at about 8,000 tokens; meta.truncated=true when clipped. Output that fails your schema → 422 extract_invalid, still charged fetch + 4 because the fetch succeeded.

POST /v1/batch

$ curl -s https://fetchflat.dev/v1/batch \
    -H "Authorization: Bearer ff_live_…" \
    -H "Content-Type: application/json" \
    -d '{
      "webhook": "https://customer.example/hooks/fetchflat",
      "items": [
        { "id": "sku-1", "url": "https://example.com", "op": "fetch", "format": "markdown", "render": false, "proxy": "auto" },
        { "id": "sku-2", "url": "https://example.com", "op": "extract", "schema": { "type": "object" }, "render": false }
      ]
    }'

{ "ok": true, "job_id": "uuid", "total": 2 }

1–1000 items. Webhook must be https. Each item is charged as if sent alone. One POST per item, timeout 10s, 3 retries with backoff:

{
  "job_id": "uuid",
  "item_id": "sku-1",
  "ok": true,
  "credits": 1,
  "data": { "url": "…", "status": 200, "title": "…", "markdown": "…", "html": null, "json": null },
  "error": null
}

On failure ok=false, error holds the code below, hint may explain. GET /v1/jobs/{id} returns { "job_id", "status", "total", "done", "ok", "errors" }.

GET /v1/usage

{ "plan": "starter", "period_grant": 28000, "balance": 15000, "period_ends_at": "2026-10-01T00:00:00Z" }

Error codes

HTTPerrormeaningcharged
401unauthorized · revoked_keybad or revoked keyno
402insufficient_creditsgrant + balance below costno
402card_requiredtrial used 200+ credits; subscribe to continueno
400invalid_url · ssrf_rejected · blocked_hostbad URL, private target, or blocklisted hostno
409upgrade_requiredauto proxy was blockedno
413too_largebody over 2 MByes
429concurrencytoo many in-flight requests for your planno
429unprofitable_throttle24h render/residential budget for the plan spent; static still worksno
422extract_invalidmodel output did not match schemayes
422empty_pagethe page had no text to extract from; retry with render=true if it is script-renderedfetch only
502target_errortarget unreachable or too many redirectsyes
503extract_unconfigured · proxy_unconfigured · billing_misconfiguredfeature not configured on this deploymentno
504timeouttarget did not answer in timeyes
404job_not_foundno such job for this workspaceno
400batch_too_largemore than 1000 itemsno

Limits

FAQ

Short answers. The API docs above are the contract.

How do credits work?

1 for a static datacenter fetch, 8 with render=true, 25 with proxy=residential, 40 with proxy=unblocker, extract is fetch cost +4. Headers X-Credits-Charged and X-Credits-Remaining are on every billed response.

Do unused Starter or Growth credits roll over?

No. The period grant is replaced at renewal. Unused grant is discarded. Top-up balance (+15,000 pack) does not expire.

Do you charge when the target site returns 404 or 403?

Yes, if we reached the host. No, if we reject the URL (SSRF, blocklist), if our API returns 5xx, or if the proxy pool is not configured.

What does upgrade_required mean?

The current proxy tier was blocked (403/429/challenge page). We do not silently upgrade. Retry with "proxy":"residential", then "proxy":"unblocker" if residential still fails. That 409 response costs 0 credits.

Is Fetchflat a stealth or Cloudflare-bypass product?

No. Default traffic is datacenter. Residential and Unblocker are explicit request fields you opt into and pay more credits for. You must be allowed to fetch the URL.

What is the trial?

1,000 credits once, concurrency 2. After 200 trial credits consumed you need a card on file (Starter, Growth, or a top-up) to keep calling. Trial grant does not renew.

Who handles invoices and VAT?

Creem is the merchant of record. Open Billing → Manage billing in /app. We do not issue tax invoices ourselves.

Do you store the full URL I fetch?

Usage logs store host, operation, status, credits, duration. Not the full path or query string.

What are the limits?

Concurrency: trial 2, Starter 10, Growth 30. If estimated proxy/render cost over 24h exceeds half your plan price, render, residential, and unblocker return 429 unprofitable_throttle. Static datacenter fetch still works. Sync fetch max ~30s; extract ~45s; batch 1–1000 items.

How do I get support?

Email [email protected] or use /support. Typical reply is 1–2 business days. Paste a request id when you have one. Billing receipts stay in Creem.

Support

Something wrong? Send a message and paste the id from the API response you are asking about. Typical reply: within 1–2 business days. Billing receipts and VAT invoices live in Creem under Manage billing.