proxlane

API reference

Every endpoint, parameter and header the gateway implements.

Everything here is implemented. Parameters the gateway does not read are not listed, and pnpm docs:check fails the build if this page and the code disagree.

There is also a machine-readable description at /openapi.json. It is generated from the same outcome taxonomy the router uses, so its status codes and enums are the real ones. Point a client generator at it, or open it in any OpenAPI viewer.

Endpoints

Method Path Purpose
GET /v1 Scrape a URL
POST /v1 Scrape a URL and forward a request body
GET /health Liveness and the running version. Needs no key
GET /health/providers Per-provider health. Needs PROXLANE_HEALTH=on to be meaningful
GET /health/cooldowns What is cooling now, and what expired recently

PUT and DELETE on /v1 return 404. They are not treated as GET.

Parameters

Parameter Required Values Meaning
url yes absolute URL The page to fetch
api_key yes string The gateway's key. Authorization: Bearer is preferred
render no true, 1 Run JavaScript on the target
premium no none, residential, stealth Proxy tier. Defaults to none
country_code no ISO 3166-1 alpha-2 Where the request should appear to come from
provider no adapter id Force one provider and disable failover
timeout no milliseconds Deadline for the whole request. Capped at the server's own
binary no true, 1 Return the body byte for byte. Routes only to providers that can
wait_for no CSS selector, ≤256 chars Wait for this element before snapshotting. Implies render=true. Routes only to providers that can express it

url

Rejected at the edge if it resolves to a private range, a denylisted host, or a cloud metadata address. That returns TARGET_FORBIDDEN.

render

Only true and 1 enable rendering. Every other value, including leaving the parameter out, means false.

This is deliberate. Presence alone never counts as true. Otherwise render=false would render the page and cost about five times as much.

provider

A benchmarking escape hatch. It pins one provider, so there is no failover. If that provider cannot serve the request, you get NO_PROVIDER_AVAILABLE rather than a silent substitution.

timeout

The deadline for the whole request, in milliseconds, including every failover hop. Default is the server's PROXLANE_DEADLINE_MS.

You can ask for less time than the server budgeted. You cannot ask for more: the ceiling is what bounds how long one request holds a slot, and the gateway's memory sizing depends on it.

The floor is 8000. Below that a single attempt cannot finish, so the request would time out without having tried anything — that is a 400, not a 504.

When the deadline runs out the outcome is BUDGET_EXCEEDED.

POST requests

The body is forwarded as text, byte for byte. Proxlane does not parse it. Guessing between JSON and form encoding would corrupt one of them.

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  --data '{"q":"example"}' \
  "https://your-gateway/v1?url=https://example.com/search"
res = requests.post(
    "https://your-gateway/v1",
    params={"url": "https://example.com/search"},
    headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
    data='{"q":"example"}',
)
const res = await fetch(
  `https://your-gateway/v1?url=${encodeURIComponent("https://example.com/search")}`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
    body: JSON.stringify({ q: "example" }),
  },
);

Request bodies use the same size cap as responses. Over it, you get RESPONSE_TOO_LARGE.

Response headers

Header Sent Meaning
X-Outcome every scrape What happened. See outcomes
X-Outcome-Class always The coarse class. Branch on this one
X-Attempts always How many providers were tried
X-Chain when a provider was tried Every attempt as provider:outcome, in order
X-Cost-Estimate always Cost across all attempts, in the unit below. mixed if the chain spent in two units
X-Cost-Unit when one unit provider-credits or usd-cents
X-Cost-Source when anything was charged reported if the providers gave us the figure, estimated if we worked it out, mixed
Server-Timing always gw is Proxlane, up is the providers, total is both
X-Request-Id always Quote this in a support thread
X-Provider-Used when one served Omitted, never empty, if nothing served
X-Detect-Rule when a rule fired Which block-page rule matched. On a SOFT_BLOCK it is what produced the outcome; on a HARD_BLOCK the provider already said blocked and this names the vendor that did it
X-Provider-Health when health is on, or when a floor fired demoted-forced: every provider was demoted and the least bad was used. cooling-forced: every provider was cooling and one was tried anyway, rather than take the domain off the air
X-Ignored-Params when you sent one we don't read The query parameters we threw away, sorted and comma-separated
Retry-After when known Seconds, rounded up

X-Ignored-Params is worth wiring into your logs. We don't reject a parameter we don't recognise — ScraperAPI accepts a dozen we don't implement, and rejecting them would break the hostname change this whole thing is built on. So the request runs, and the header names what went nowhere.

The one that catches people is render. js_render=true is ScrapingBee's spelling and js=true is Scrapfly's; ours is render=true. Send the wrong one and you get HTTP 200, a real page, and no JavaScript — at about a fifth of the cost, which is the tell:

code
X-Ignored-Params: js_render
X-Cost-Estimate: 1.000000     # rendered, this would be ~5

Two of those are easy to misread.

A request rejected before a provider was chosen — no url, a bad premium, a wrong key — carries X-Outcome-Class, X-Attempts: 0 and X-Cost-Estimate: 0.000000. It has no X-Outcome, because the taxonomy describes what happened to a scrape and that request never became one. This is the reason to branch on the class.

X-Cost-Source tells you whose number it is. Three of the four providers report what they charged on the response itself, and the adapter passes that straight through. estimated means they said nothing and we applied our own table for that provider — a figure worth treating with more suspicion than a reported one, because it is our model of their pricing rather than their answer. mixed means a chain used both, which a failover across providers routinely does.

X-Cost-Estimate covers every attempt, not just the one that worked. A failover that burned two charged hops reports both.

X-Chain is the one that says who failed. X-Provider-Used names the winner, so a request that failed over and then succeeded returns a clean 200 that hides the provider which cost you 22 seconds:

code
X-Chain: scraperapi:PROVIDER_TIMEOUT>scrapfly:OK
X-Provider-Used: scrapfly
X-Attempts: 2

A single-attempt request has a one-element chain, which matters: an absent header could not otherwise say "nothing failed". It is omitted, never empty, when no provider was tried at all — a bad URL, a refused target, every provider cooling. X-Attempts: 0 says that already.

Retry-After is never guessed. If it is absent, Proxlane does not know when to retry. A number you can trust is worth more than a number that is always present.

Branching on results

Branch on X-Outcome-Class, not X-Outcome.

X-Outcome is open and gains members as adapters land. X-Outcome-Class has six values and does not grow. Code written against the class keeps working when the vocabulary expands.

switch (res.headers.get("x-outcome-class")) {
  case "ok":       return res;           // you have the page
  case "blocked":  return retryLater();  // every provider was blocked
  case "target":   return giveUp();      // the site itself said no
  case "client":   throw new Error("fix the request");
  case "provider": // fallthrough: transient, safe to retry
  case "gateway":  return retryLater();
}
outcome_class = res.headers["X-Outcome-Class"]

if outcome_class == "ok":
    return res.text                      # you have the page
if outcome_class == "target":
    return None                          # the site itself said no
if outcome_class == "client":
    raise ValueError("fix the request")
return retry_later()                     # blocked, provider or gateway

Errors

When there is no page to return, the body is JSON instead of an empty success.

json
{
  "requestId": "01JD8F2K9WQ3",
  "error": {
    "code": "NO_PROVIDER_AVAILABLE",
    "class": "gateway",
    "message": "No adapter matches the capability, or the chain is exhausted",
    "docs": "https://github.com/proxlane/proxlane#outcomes"
  },
  "attempts": [
    { "provider": "scraperapi", "outcome": "SOFT_BLOCK", "detectRuleId": "cf-challenge" }
  ]
}

error.code is the outcome. One vocabulary covers failures at a provider and failures before one was reached.

UNAUTHORIZED is the exception. It is not an outcome, because outcomes describe what happened to a scrape, and a request rejected at the door never became one.

attempts lists what was tried and what each provider said. That is the grain you need when debugging a failover.

Backpressure

Past its concurrency ceiling the gateway returns 429 GATEWAY_BUSY with Retry-After.

It sheds rather than queues. A queued scrape holds your socket open while its own deadline runs down, so you get a timeout with no explanation instead of a 429 you can act on.

The class is gateway, not provider. No provider throttled you. /health is never shed, so a busy gateway does not fail its own health check.