Proxies
Send a session's or a request's traffic through a residential, datacenter or your own proxy, from the country, US state or city you choose, with a different proxy per site if you need one.
By default, a session reaches the web from Boxline’s own cloud addresses. Some sites show different content to cloud addresses, or show content that depends on where the visitor is. A proxy changes where your traffic comes from.
You can set a proxy when you create a session or start an agent run, change it while the session runs, and use one on single requests to the fetch, screenshot, PDF, extract and crawl APIs. One proxy can serve every site, or you can pick a different proxy for each site.
Residential, datacenter or your own
| Type | What it is | Use it when |
|---|---|---|
residential | IP addresses of home internet connections, through our proxy provider. Choose a country, a US state or a city. | A site treats cloud traffic differently, or you need to see a page the way a local visitor sees it. The most expensive type. |
datacenter | IP addresses in data centers in many countries, through the same provider. Choose a country. | You need content for a country and the site accepts data center traffic. Cheaper and usually faster than residential. |
custom | Your own proxy server (http:// or https://), with an optional username and password. | You already pay for a proxy, or a partner allows only a fixed IP you control. Boxline does not charge for its data. |
Start without a proxy. Add one when a site blocks you or shows the wrong content, and try datacenter before residential.
Add a proxy to a session
Pass proxy when you create the session. The proxy is in place before the browser opens anything, so no request leaves from the platform’s own address first.
import { Boxline } from "@boxline/sdk";
import { chromium } from "playwright";
const client = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URL
const session = await client.sessions.create({
browser: true,
proxy: { type: "residential", country: "US", state: "us_california" },
});
try {
const browser = await chromium.connectOverCDP(session.connectUrl!);
const page = browser.contexts()[0].pages()[0];
await page.goto("https://example.com");
console.log(session.data.proxy); // the settings, never a password
} finally {
await session.release();
}from boxline import Boxline
bx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URL
with bx.sessions.create(
browser=True,
proxy={"type": "residential", "country": "US", "state": "us_california"},
) as session:
session.goto("https://example.com")
print(session.content()["title"])
print(session.proxy) # the settings, never a password
# leaving the block releases the sessioncurl -X POST https://api.staging.boxline.dev/v1/sessions \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{
"browser": true,
"proxy": {"type": "residential", "country": "US", "state": "us_california"}
}'The session object shows the proxy with its defaults filled in. Without a proxy, proxy is null.
"proxy": {
"type": "residential",
"country": "US",
"state": "us_california",
"ip": "sticky",
"scope": "browser"
}The short form: proxy: true
proxy: true is short for { "type": "residential", "country": "US" }: a residential IP in the US, with the default ip and scope. It works everywhere proxy does.
const session = await client.sessions.create({ browser: true, proxy: true });session = bx.sessions.create(proxy=True)curl -X POST https://api.staging.boxline.dev/v1/sessions \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{"browser": true, "proxy": true}'The session shows the full settings: { "type": "residential", "country": "US", "ip": "sticky", "scope": "browser" }. To change any of them, give the object instead.
Proxy options
| Field | Types | Description |
|---|---|---|
type | all | "residential", "datacenter" or "custom" (your own proxy). In a list of rules, also "none" (straight out). Required. |
country | residential, datacenter | Two-letter country code, e.g. "US", "DE", "GB". |
state | residential | A US state, e.g. "us_california". Implies country "US". |
city | residential | A city, e.g. "los_angeles". Needs country. |
ip | all | "sticky" (one IP until you ask for a new one) or "rotating" (a new IP per connection). Default: sticky for sessions and crawls, rotating for fetch, screenshot, pdf and extract. |
scope | all | "browser" (default) or "all": the shell uses the proxy too. Sessions only. |
server | custom | Your proxy, "http://host:port" or "https://host:port". The port is required. Required for custom. |
username | custom | Your proxy's username, if it needs one. |
password | custom | Your proxy's password. Stored encrypted, never returned. |
domainPattern | all | Which sites the rule is for: a regular expression tested against the site's host name. See Different proxies for different sites. |
Your own proxy
Give the server as http://host:port or https://host:port. Keep the login in environment variables, not in your code:
const session = await client.sessions.create({
browser: true,
proxy: {
type: "custom",
server: "http://proxy.example.net:8080",
username: process.env.MY_PROXY_USER,
password: process.env.MY_PROXY_PASSWORD,
},
});import os
session = bx.sessions.create(
proxy={
"type": "custom",
"server": "http://proxy.example.net:8080",
"username": os.environ["MY_PROXY_USER"],
"password": os.environ["MY_PROXY_PASSWORD"],
},
)curl -X POST https://api.staging.boxline.dev/v1/sessions \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d @- <<EOF
{
"browser": true,
"proxy": {
"type": "custom",
"server": "http://proxy.example.net:8080",
"username": "$MY_PROXY_USER",
"password": "$MY_PROXY_PASSWORD"
}
}
EOFYour proxy must be on a public internet address. Location fields (country, state, city) are not accepted: the location is wherever your proxy is.
Traffic to your own proxy still goes through Boxline’s proxy service, which checks the address, blocks mail ports and counts the data. It is not a way around that service: when proxies are unavailable on the platform (503 proxy_unavailable), your own proxy is too.
Choose a location
With the platform’s proxies you choose where your traffic appears to come from. Leave the location out to get an IP from anywhere in the provider’s pool.
| Field | Format | Examples |
|---|---|---|
country | Two-letter country code (ISO 3166-1). Residential and datacenter. | "US", "DE", "JP" |
state | us_ and the state’s name, in lower case with underscores. Residential, US only. | "us_california", "us_new_york" |
city | The city’s name in lower case with underscores. Residential only, and it needs country. | "los_angeles", "london" |
- A
statesets the country toUSfor you. A state with another country is refused. - Datacenter proxies take a
countryonly. - The API turns
"Los Angeles"into"los_angeles". It checks the format of a name, not whether the provider has IPs there.
The smaller the place, the fewer IPs there are to choose from. Ask for a city only when the site really changes by city.
Sticky or rotating IPs
ip | What happens | Good for |
|---|---|---|
"sticky" | One IP for the whole session (or crawl) until you ask for a new one. The default for sessions and crawls. | Logins, carts, forms: anything that spans several pages. |
"rotating" | A new IP for every new connection. The default for fetch, screenshot, PDF and extract. | Many independent page loads. |
With rotating IPs, the connections of one page can come from different IPs. If a site misbehaves, try sticky.
Residential IPs belong to real home connections. The provider keeps a sticky IP for as long as it can, but one can occasionally change.
Get a new IP
POST/v1/sessions/:id/proxy/rotate
Gives the session’s proxy a new IP with the same settings, and returns the session. With a list of rules, every proxy in the list gets a new IP. New connections use the new IP at once. Connections that are already open are not cut, so a site you already have open may keep seeing the old IP until the browser opens a new connection to it.
await session.rotateProxy();session.rotate_proxy()curl -X POST https://api.staging.boxline.dev/v1/sessions/$SESSION_ID/proxy/rotate \
-H "x-api-key: $BOXLINE_API_KEY"A session without a proxy gets 400 invalid_request. With your own proxy, which IP is used is up to your proxy.
Change or remove the proxy
PATCH/v1/sessions/:id with proxy sets, changes or (with null) removes the proxy of a running or paused session. Like a new IP, the change applies to new connections at once; open connections finish as they are. A PATCH without proxy leaves it unchanged.
// Another place, or another type
await session.setProxy({ type: "residential", country: "GB", city: "london" });
// No proxy: straight out from the platform's own addresses
await session.setProxy(null);# Another place, or another type
session.set_proxy({"type": "residential", "country": "GB", "city": "london"})
# No proxy: straight out from the platform's own addresses
session.set_proxy(None)curl -X PATCH https://api.staging.boxline.dev/v1/sessions/$SESSION_ID \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{"proxy": {"type": "residential", "country": "GB", "city": "london"}}'
curl -X PATCH https://api.staging.boxline.dev/v1/sessions/$SESSION_ID \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{"proxy": null}'Cookies and logins stay as they are. A site that ties a login to an IP may ask you to sign in again.
In realistic browser mode, the clock and language move with the proxy: open tabs follow within a second or two.
The shell too: scope "all"
By default (scope: "browser") only Chrome uses the proxy, and commands in the shell connect directly. With scope: "all", the shell gets HTTP_PROXY, HTTPS_PROXY and ALL_PROXY, so curl, wget, pip, npm, git and Python’s requests go out through the same proxy and IP as the browser.
const session = await client.sessions.create({
browser: true,
shell: true,
proxy: { type: "datacenter", country: "DE", scope: "all" },
});
const run = await session.exec("curl -sI https://example.com | head -n 1");
console.log(run.stdout);with bx.sessions.create(
shell=True,
proxy={"type": "datacenter", "country": "DE", "scope": "all"},
) as session:
print(session.exec("curl -sI https://example.com | head -n 1")["stdout"])curl -X POST https://api.staging.boxline.dev/v1/sessions \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{
"browser": true,
"shell": true,
"proxy": {"type": "datacenter", "country": "DE", "scope": "all"}
}'- Programs that ignore these variables connect directly.
localhostand127.0.0.1are never sent through the proxy. - The shell’s traffic counts toward your proxy data, like the browser’s.
- Shell commands you run after a change use the new setting, also when you switch from
"all"back to"browser". An interactive terminal that is already open keeps its environment until you reopen it.
Different proxies for different sites
Give proxy a list of rules instead of one proxy. Each rule is a proxy, or { "type": "none" } to go straight out, plus an optional domainPattern: the sites the rule is for. For every connection, the first rule that matches the site decides where it goes.
This session sends your company’s intranet through your company’s proxy, Wikipedia straight out, and every other site through a residential IP in the US:
const session = await client.sessions.create({
browser: true,
proxy: [
// The intranet: through your company's proxy
{
type: "custom",
server: "http://proxy.example.com:3128",
domainPattern: "(^|\\.)intranet\\.example\\.com$",
},
// Wikipedia: straight out, no proxy
{ type: "none", domainPattern: "(^|\\.)wikipedia\\.org$" },
// Every other site: a residential IP in the US
{ type: "residential", country: "US" },
],
});session = bx.sessions.create(
proxy=[
# The intranet: through your company's proxy
{
"type": "custom",
"server": "http://proxy.example.com:3128",
"domainPattern": r"(^|\.)intranet\.example\.com$",
},
# Wikipedia: straight out, no proxy
{"type": "none", "domainPattern": r"(^|\.)wikipedia\.org$"},
# Every other site: a residential IP in the US
{"type": "residential", "country": "US"},
],
)curl -X POST https://api.staging.boxline.dev/v1/sessions \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{
"browser": true,
"proxy": [
{
"type": "custom",
"server": "http://proxy.example.com:3128",
"domainPattern": "(^|\\.)intranet\\.example\\.com$"
},
{"type": "none", "domainPattern": "(^|\\.)wikipedia\\.org$"},
{"type": "residential", "country": "US"}
]
}'Your company’s proxy gets the site’s name and looks it up itself, so names that only exist inside your network work. The proxy must still be on a public address (see Your own proxy). If it needs a login, add username and password to its rule.
How matching works
- The rules are tried in order for every connection. The first match wins; later rules are not checked. Put specific rules first.
- A rule without
domainPatternmatches every site. Put it last, as the catch-all. - No match means straight out, from the platform’s own addresses. A list without a catch-all only proxies the sites you name.
- The pattern is a regular expression tested against the host name only (
en.wikipedia.org), not the scheme, port or path. Upper and lower case are the same. - The pattern is not anchored: it can match anywhere in the name. Use
^(start) and$(end) for an exact name. - Escape dots. A plain
.matches any character, so write\.. In JSON and JavaScript strings the backslash is written twice ("\\."); in Python, use a raw string (r"\.").
| Pattern | Matches | Does not match |
|---|---|---|
^example\.com$ | example.com | www.example.com |
(^|\.)example\.com$ | example.com, www.example.com | notexample.com, example.com.au |
\.de$ | shop.example.de | example.com |
example\.com (no anchors) | example.com, but also notexample.com and example.com.other.net | — |
Good to know
- At most 10 rules. A pattern has at most 256 characters and must be a valid regular expression; otherwise you get
400 invalid_request. - Each rule takes the same options as a single proxy (
country,ipand so on). Each proxy in the list keeps its own sticky IP, and getting a new IP gives all of them new ones. - Rules that set
scopemust agree. Withscope: "all", the shell follows the same rules as the browser. - The session shows the list as you gave it, with
ipfilled in, and never a password. - A single proxy with a
domainPatternis a list of one rule: matching sites use the proxy, every other site goes straight out. - Lists work wherever
proxydoes: new sessions, PATCH (setProxy,set_proxy), agent runs and the quick APIs. - Straight-out connections are not proxy data. Each proxied connection counts toward the type of proxy it used. Your plan’s proxy allowance is checked only when the list has a residential or datacenter rule.
Fetch, screenshot, PDF, extract and crawl
These APIs take the same proxy option on each request. The proxy is used for that request only.
| API | Default ip |
|---|---|
POST /v1/fetch, /v1/screenshot, /v1/pdf, /v1/extract | "rotating". With "sticky", everything one request loads uses one IP; the next request gets another. |
POST /v1/crawl | "sticky": one IP for the whole crawl. |
const page = await client.fetch("https://example.com", {
format: "markdown",
proxy: { type: "residential", country: "FR" },
});
const png = await client.screenshot("https://example.com", {
proxy: { type: "datacenter", country: "JP" },
});
// A crawl keeps one IP from start to finish
const job = await client.crawl.start({
url: "https://example.com",
maxPages: 50,
proxy: { type: "residential", country: "US" },
});page = bx.fetch("https://example.com", proxy={"type": "residential", "country": "FR"})
png = bx.screenshot("https://example.com", proxy={"type": "datacenter", "country": "JP"})
# A crawl keeps one IP from start to finish
job = bx.crawl.start("https://example.com", max_pages=50,
proxy={"type": "residential", "country": "US"})curl -X POST https://api.staging.boxline.dev/v1/fetch \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://example.com",
"format": "markdown",
"proxy": {"type": "residential", "country": "FR"}
}'scope has no effect here, since there is no shell. proxy: true and lists of rules work here too; in a list, each rule gets the default ip above.
All of them also take "browser": { "mode": "realistic" }, so pages are rendered with the clock and language of the proxy’s country (a crawl applies it to every page). See Browser mode.
Agent runs
POST/v1/agent/runs takes the same proxy option, for the session the run creates. With sessionId, the run uses that session’s proxy and ignores proxy. See the Agent API.
const run = await client.agent.run({
task: "Find the price of a large pizza on example.com",
proxy: { type: "residential", country: "US", state: "us_new_york" },
});run = bx.agent.run(
"Find the price of a large pizza on example.com",
proxy={"type": "residential", "country": "US", "state": "us_new_york"},
)curl -X POST https://api.staging.boxline.dev/v1/agent/runs \
-H "x-api-key: $BOXLINE_API_KEY" \
-H "content-type: application/json" \
-d '{
"task": "Find the price of a large pizza on example.com",
"proxy": {"type": "residential", "country": "US", "state": "us_new_york"}
}'Pause, resume and move
The proxy belongs to the session, not to the machine. When a session is resumed, moved, or brought back after its machine stopped, the new machine gets the same proxy before it reopens any page. A sticky session keeps its IP as long as the provider still has it; after a long pause it may continue on a new IP from the same location. Resuming checks your plan’s monthly proxy data first, like creating a session does. See Move, pause & resume.
Billing and plan allowances
Data through the platform’s proxies is billed per GB, by type. Every byte counts, in both directions: pages, images, video, and the shell’s traffic with scope: "all". A GB is 1,000,000,000 bytes.
| Type | Price per GB | Counts toward your plan’s allowance |
|---|---|---|
| Residential | To be announced | Yes |
| Datacenter | To be announced | Yes |
| Your own proxy | No data charge | No |
Current prices are on the pricing page and in GET /v1/pricing (proxies).
Your plan sets how much data you can use through the platform’s proxies each calendar month (UTC), residential and datacenter together. It is a ceiling, not free data. Find yours in GET /v1/auth/me as project.limits.proxyGbPerMonth (null means no cap). It is checked when you create a session with a proxy, set one with PATCH, resume a paused session that has one, or send a quick-API request with one.
GET /v1/usage reports proxy data separately, and its costUsd includes it:
"proxy": {
"residentialGb": 1.284,
"datacenterGb": 0.2,
"customGb": 3.5,
"costUsd": …
}Use less data
Images and video are most of a page’s weight. When you only need the text, skip them in Playwright:
await page.route("**/*", (route) =>
["image", "media", "font"].includes(route.request().resourceType())
? route.abort()
: route.continue(),
);Errors
When you set a proxy
| Status and code | What it means |
|---|---|
400 invalid_request | A proxy option is not valid (for example a city without a country, a state outside the US, a state or city on a datacenter proxy, a location on your own proxy, a server without a port, more than 10 rules, a pattern that is not a valid regular expression, or rules with different scopes), or you asked for a new IP on a session without a proxy. The message names the field. |
402 plan_limit | Your plan does not include the platform’s proxies. Your own proxy is not affected. |
402 spend_limit | Your project used this month’s proxy data, so a session with one of the platform’s proxies cannot be created, switched to it or resumed. It resets on the 1st (UTC). Your own proxy does not count toward it. |
409 session_not_running | The session has ended. |
503 proxy_unavailable | The platform’s proxy service is not available right now, so no proxy works, your own included. Retry later. |
When a page does not load
When the proxy refuses a connection, the browser shows a network error: the page does not load, and the failed request appears in the session’s network events. The reason is in the X-Boxline-Proxy-Error header of the proxy’s reply. The browser does not show it, but a request from the shell does (with scope: "all"):
curl -sv -o /dev/null https://example.com 2>&1 | grep -i x-boxline-proxy-error| Reason | What it means | What to do |
|---|---|---|
target_restricted | The proxy provider does not serve this site. The platform's proxies do not open some kinds of sites, such as banking, streaming, government, ticketing and webmail sites. | Load the site without a proxy, or through your own proxy. |
port_blocked | The page or command tried to reach an outgoing mail port (25, 465, 587 or 2525). These are blocked through every proxy. | Send mail through your mail provider's HTTPS API instead. |
proxy_not_public | Your own proxy's address is not a public internet address (for example a private network or localhost). | Use an address of your proxy that is reachable from the internet. |
proxy_auth_failed | The proxy refused the username or password. | With your own proxy, check the login and set it again with PATCH. With the platform's proxies the problem is on our side: retry later, and contact us if it continues. |
proxy_type_unavailable | Residential or datacenter proxies are not set up on the platform right now. | Try the other type, or retry later. |
upstream_timeout | The proxy did not answer in time. | Retry. With your own proxy, check that it is running and not overloaded. |
upstream_unreachable | The platform could not connect to the proxy. | Retry. With your own proxy, check the host and port, and that it accepts connections from the internet. |
upstream_error | The proxy answered with an error. | Retry. If it keeps happening, get a new IP or choose a broader location. |
too_many_connections | The session has more than 512 connections open through the proxy at once. | Close pages and connections you no longer need, or spread the work over more sessions. |
bad_target | The destination of the connection was not a valid host and port. | Check the address the page or command connects to. |
bad_route | A rule sent the connection to a proxy the session does not have. | Set the proxy again with PATCH. If it keeps happening, contact us. |
token_invalid | The session's pass for the proxy is no longer valid: the session has ended, or its proxy was changed. | If the session is still running, retry: new connections use its current proxy. |
Security
- Your proxy’s password is stored encrypted. Only Boxline’s API and its proxy gateway can read it, never the session machine, so nothing that runs in the session (an agent included) can see it.
- It is never returned by the API. The session object, the SDKs, the event log and crawl jobs show your proxy without it.
- Use a login made for Boxline. Create a separate user on your proxy with only the access it needs, so you can change or revoke it without touching anything else. To switch a running session to a new login, set the proxy again with PATCH.
- Session URLs give access to the proxy. Anyone with a session’s
connectUrlcan browse through its proxy and use your data. Treat the URLs like passwords, and rotate them if one leaks (POST /v1/sessions/:id/rotate-urls). - With scope "all", everything in the shell uses the proxy, including commands an agent runs. Use the default scope unless the shell needs it.
- Private addresses and mail ports are refused. Your own proxy must be on a public address, and outgoing mail ports are blocked through every proxy.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.