---
title: "Environment & proxy · Scrape.ly Docs"
description: "Run environment, Scrape.ly browser, platform proxy, and HTTP serving."
url: https://scrape.ly/docs/sdk-python-environment
markdown: https://scrape.ly/docs/sdk-python-environment.md
---

# Environment & proxy

> Run environment, Scrape.ly browser, platform proxy, and HTTP serving.

The platform describes each run to the container through environment variables, and the SDK parses them into a configuration object. This page lists what `Actor.get_env()` returns and which variables the platform sets. It also covers the remote browser, the Scrape.ly proxy and serving HTTP from a Python Actor.

## Run environment

### `Actor.get_env()`

`Actor.get_env()` is synchronous and works before `init()`. It returns an `ApifyEnv` object. You can read fields as attributes (`env.actor_run_id`) or by key (`env["actor_run_id"]`), and `env.keys()` lists the fields that are set. Fields without a value are `None`, except the storage IDs, which default to `"default"`, and `input_key`, which defaults to `"INPUT"`.

| Field | Source |
| --- | --- |
| `actor_id` | `ACTOR_ID` |
| `actor_run_id` | `ACTOR_RUN_ID` |
| `actor_task_id` | `ACTOR_TASK_ID` |
| `actor_full_name` | `ACTOR_FULL_NAME` |
| `actor_build_id`, `actor_build_number`, `actor_build_tags` | `ACTOR_BUILD_ID`, `ACTOR_BUILD_NUMBER`, `ACTOR_BUILD_TAGS` |
| `default_dataset_id`, `default_key_value_store_id`, `default_request_queue_id` | `ACTOR_DEFAULT_*_ID` |
| `actor_storages_json` | `ACTOR_STORAGES_JSON` |
| `input_key` | `ACTOR_INPUT_KEY` |
| `token` | `SCRAPELY_TOKEN`, or `APIFY_TOKEN` |
| `api_base_url` | `SCRAPELY_API_URL`, or `APIFY_API_BASE_URL` |
| `api_public_base_url` | `APIFY_API_PUBLIC_BASE_URL` |
| `is_at_home` | `"1"` when `SCRAPELY_IS_AT_HOME` or `APIFY_IS_AT_HOME` is truthy |
| `actor_events_ws_url` | `ACTOR_EVENTS_WEBSOCKET_URL` |
| `container_port`, `container_url` | `ACTOR_WEB_SERVER_PORT`, `ACTOR_WEB_SERVER_URL` |
| `memory_mbytes` | `ACTOR_MEMORY_MBYTES`, as `int` |
| `started_at`, `timeout_at` | `ACTOR_STARTED_AT`, `ACTOR_TIMEOUT_AT`, as `datetime` |
| `max_total_charge_usd` | `ACTOR_MAX_TOTAL_CHARGE_USD`, as `float` |
| `actor_max_paid_dataset_items` | `ACTOR_MAX_PAID_DATASET_ITEMS`, as `int` |
| `actor_pricing_info`, `charged_event_counts` | `APIFY_ACTOR_PRICING_INFO`, `APIFY_CHARGED_ACTOR_EVENT_COUNTS` (JSON strings) |
| `restart_on_error` | `ACTOR_RESTART_ON_ERROR` |
| `meta_origin` | `ACTOR_META_ORIGIN`: how the run started (`API`, `WEB`, `SCHEDULER`, `ACTOR`, `STANDBY`, ...) |
| `standby_port`, `standby_url` | `ACTOR_STANDBY_PORT` (`4321`), `ACTOR_STANDBY_URL` |
| `user_id`, `user_is_paying` | `APIFY_USER_ID`, `APIFY_USER_IS_PAYING` |
| `headless` | `"1"` when `APIFY_HEADLESS` is truthy |
| `proxy_hostname`, `proxy_port`, `proxy_password`, `proxy_status_url` | `APIFY_PROXY_*` |
| `persist_state_interval_millis` | `APIFY_PERSIST_STATE_INTERVAL_MILLIS` |

`ApifyEnv` also has compatibility fields, such as `log_level`, `log_format`, `chrome_executable_path`, `dedicated_cpus` and `xvfb`. Scrape.ly doesn't set them.

> [!NOTE]
> Two fields are always `None` on Scrape.ly. `user_plan` is `None` because the SDK doesn't map `SCRAPELY_USER_PLAN`; read `os.environ.get("SCRAPELY_USER_PLAN")` instead. `actor_permission_level` is `None` because the platform's values, `LIMITED_PERMISSIONS` and `FULL_PERMISSIONS`, aren't members of `ActorPermissionLevel`; read `os.environ.get("ACTOR_PERMISSION_LEVEL")` instead.

When a value is set under several names, the SDK applies them in this order, and later names win: `APIFY_*`, then `SCRAPELY_*`, then `ACTOR_*`, then `CRAWLEE_STORAGE_DIR`. So `SCRAPELY_TOKEN` overrides `APIFY_TOKEN`, and `ACTOR_ID` overrides `APIFY_ACTOR_ID`.

### Variables the platform sets

You don't set these yourself. Most of them also have an `APIFY_*` copy.

| Variable | Purpose |
| --- | --- |
| `SCRAPELY_IS_AT_HOME` | `1` on the platform |
| `SCRAPELY_API_URL` | Internal API URL for this container |
| `SCRAPELY_TOKEN` | Run-scoped API token |
| `ACTOR_ID`, `ACTOR_RUN_ID`, `ACTOR_TASK_ID`, `ACTOR_FULL_NAME` | Run identity (also as `APIFY_ACTOR_ID`, `APIFY_ACTOR_RUN_ID` and the older `APIFY_ACT_ID`, `APIFY_ACT_RUN_ID`) |
| `ACTOR_BUILD_ID`, `ACTOR_BUILD_NUMBER`, `ACTOR_BUILD_TAGS` | Build being run (also as `APIFY_ACTOR_BUILD_ID`, `APIFY_ACTOR_BUILD_NUMBER`) |
| `ACTOR_META_ORIGIN`, `APIFY_META_ORIGIN` | How the run started: `API`, `WEB`, `SCHEDULER`, `ACTOR`, `STANDBY`, ... |
| `ACTOR_DEFAULT_DATASET_ID`, `ACTOR_DEFAULT_KEY_VALUE_STORE_ID`, `ACTOR_DEFAULT_REQUEST_QUEUE_ID` | Default storages |
| `ACTOR_STORAGES_JSON` | Storage aliases for this run |
| `ACTOR_INPUT`, `ACTOR_INPUT_KEY` | Base64 JSON input (secrets already decrypted) and its record key |
| `ACTOR_METAMORPH_COUNT` | Number of metamorphs so far |
| `ACTOR_EVENTS_WEBSOCKET_URL` | Platform events |
| `CDP_URL` | [Scrape.ly browser](https://scrape.ly/docs/browser.md) WebSocket. Set on every run; used for launches only when the Actor's browser setting is on |
| `ACTOR_WEB_SERVER_PORT`, `ACTOR_WEB_SERVER_URL` | HTTP port (`4321`) and the URL that proxies to it |
| `ACTOR_STANDBY_PORT` | Port a standby server listens on (`4321`, the same port) |
| `ACTOR_MEMORY_MBYTES` | Memory limit |
| `ACTOR_STARTED_AT`, `ACTOR_TIMEOUT_AT` | ISO timestamps |
| `ACTOR_PERMISSION_LEVEL` | `LIMITED_PERMISSIONS` (default) or `FULL_PERMISSIONS` |
| `ACTOR_RESTART_ON_ERROR` | `1` or `0` |
| `ACTOR_MAX_TOTAL_CHARGE_USD`, `ACTOR_MAX_PAID_DATASET_ITEMS` | Spending limits, when set |
| `APIFY_ACTOR_PRICING_INFO`, `APIFY_CHARGED_ACTOR_EVENT_COUNTS` | Pay-per-event pricing and counts so far |
| `APIFY_USER_ID`, `APIFY_USER_IS_PAYING`, `SCRAPELY_USER_PLAN` | Account details (`SCRAPELY_USER_PLAN` is `paid` or `free`) |
| `SCRAPELY_PROXY_HOSTNAME`, `SCRAPELY_PROXY_PORT`, `SCRAPELY_PROXY_USERNAME`, `SCRAPELY_PROXY_PASSWORD`, `SCRAPELY_PROXY_STATUS_URL` | Platform proxy credentials, when proxy access is enabled |
| `SCRAPELY_LLM_BASE_URL` | [LLM gateway](https://scrape.ly/docs/llm-gateway.md) base URL (OpenRouter-compatible, ends in `/v2/llm/api/v1`). Use `SCRAPELY_TOKEN` as the API key |
| `ACTOR_STANDBY_URL`, `ACTOR_STANDBY_IDLE_TIMEOUT_SECS` | Set for standby runs |

Environment variables defined on the Actor version are added too. They can't override the run identity variables above (ids, build ids, `ACTOR_META_ORIGIN`, `ACTOR_STANDBY_PORT`).

### Configuration

`Actor.configuration` (or `Actor.config`, as in Apify's SDK) returns the parsed `Configuration`. Use `get(key, default=None)` and `set(key, value)` with the snake_case keys from the table above, or read them as attributes, e.g. `Actor.config.standby_port`. Off the platform, set `SCRAPELY_API_URL` and `SCRAPELY_TOKEN` yourself if you use `force_cloud` or the API clients.

### Run helpers

| Member | Returns |
| --- | --- |
| `Actor.is_at_home()` | `True` on the platform |
| `Actor.is_standby()` | `True` when `ACTOR_META_ORIGIN` is `STANDBY` |
| `Actor.is_paying` | `True` when the account has a paid plan |
| `Actor.get_remaining_time()` | Milliseconds until `ACTOR_TIMEOUT_AT`, as `int`, or `None` without a timeout |
| `Actor.log` | The SDK logger (`logging.Logger`) |

## Remote browser

Where a browser runs depends on the Actor's **Use Scrapely browser** setting, which is **off by default** and applied per build. See [Scrape.ly Browser](https://scrape.ly/docs/browser.md).

| Situation | What happens |
| --- | --- |
| Setting off (`SCRAPELY_BROWSER_PATCH=0`) | Browsers launch in the container, as in plain Crawlee |
| Setting on, on the platform | Launches connect to the Scrape.ly browser at `CDP_URL` with `connect_over_cdp`. Firefox, WebKit, and Camoufox requests go to the same Chromium |
| Setting on, no `CDP_URL` | The launch raises `RuntimeError` |
| Local run, no `CDP_URL` | Browsers launch locally |

Importing `scrapely` patches Crawlee's `PlaywrightBrowserPlugin`, raw Playwright (`chromium.launch()`, `launch_persistent_context()`), pyppeteer, and Camoufox. The `scrapely/actor-python*` images also load the patch through `sitecustomize`, so scripts that never import the SDK are covered. Your crawler code doesn't change:

```python
from crawlee.crawlers import PlaywrightCrawler

crawler = PlaywrightCrawler(request_handler=handle_page)
await crawler.run(["https://example.com"])
```

`set_cdp_url(url)` overrides `CDP_URL`. The SDK reads the URL lazily at each browser launch.

### Browser location and fingerprint

These options apply when the browser setting is on. The Scrape.ly browser has its own proxy and ignores custom proxies: a BYOP `proxy_configuration` passed to a browser crawler is ignored, with a warning. To choose the exit location and fingerprint, pass a `BrowserConfiguration`:

```python
browser_configuration = await Actor.create_browser_configuration(country_code="US", state="California")

crawler = PlaywrightCrawler(
    browser_configuration=browser_configuration,
    request_handler=handle_page,
)
```

`Actor.create_browser_configuration(actor_browser_input=None, *, country_code=None, state=None, city=None, os=None, timezone=None, language=None, adblock=None, solve_cloudflare=None)` is async. It fills `country_code`, `state` and `city` from the input's `browserCountry`, `browserState` and `browserCity` fields when you don't pass them. You can also construct `BrowserConfiguration(...)` with the same keyword arguments.

| Option | Format |
| --- | --- |
| `country_code` | ISO 3166-1 alpha-2, such as `US` |
| `state` | Region name, such as `California`, not an ISO code |
| `city` | City name, such as `San Jose` |
| `os` | `windows`, `macos` or `android` |
| `timezone` | IANA timezone, such as `Asia/Tokyo`. Defaults to the exit country's main zone |
| `language` | Comma-separated tags without q-values, such as `de-CH,de` |
| `adblock` | Block ads and trackers. Off by default |
| `solve_cloudflare` | Solve Cloudflare Turnstile automatically. On by default |

Invalid values raise `ValueError`. The browser's location comes from the first source that sets one: `browser_configuration`, then the country and subdivision of a platform `ProxyConfiguration`, then the run's default. The first page pins a browser's location. If a later page on the same browser resolves to a different location, the SDK logs a warning. Off the platform, and in builds with the setting off, the option is accepted and ignored.

`PlaywrightCrawler(browser_configuration=...)` only works because the SDK patches it, and passing any other type raises `TypeError`. Raw `chromium.launch(browser_configuration=...)` works too.

## Proxy configuration

`Actor.create_proxy_configuration()` builds a `ProxyConfiguration` for HTTP crawlers. It supports three modes:

- **Platform proxy.** The Scrape.ly proxy gateway, available in platform runs when the platform sets `SCRAPELY_PROXY_*`.
- **Your own proxies.** `proxy_urls`, `new_url_function` or tiered lists.
- **Mixed.** Your own lists with an `"auto"` (or `"http://auto"`) entry that stands for the platform proxy.

For plans, groups and locations, see [Proxy](https://scrape.ly/docs/proxy.md).

```python
# Platform proxy, optionally with groups and location
proxy_configuration = await Actor.create_proxy_configuration(country_code="US")

# Your own proxies
proxy_configuration = await Actor.create_proxy_configuration(
    proxy_urls=["http://user:pass@proxy-a.example.com:8000", "http://proxy-b.example.com:8000"],
)

# From a proxy editor field in the input
actor_input = await Actor.get_input() or {}
proxy_configuration = await Actor.create_proxy_configuration(actor_input.get("proxyConfiguration"))

crawler = BeautifulSoupCrawler(proxy_configuration=proxy_configuration, request_handler=handler)
```

```python
await Actor.create_proxy_configuration(
    actor_proxy_input=None, password=None, groups=None, country_code=None, subdivision_code=None,
    proxy_urls=None, new_url_function=None, tiered_proxy_urls=None, tiered_proxy_config=None,
    check_access=True,
)
```

The method needs an initialized SDK and returns `None` in two cases:

- The input has `useScrapelyProxy: false` and doesn't list any custom proxies.
- Nothing is configured and there are no platform proxy credentials. This happens, for example, off the platform, and the SDK logs a warning.

`actor_proxy_input` accepts the keys `useScrapelyProxy`, `scrapelyProxyGroups`, `scrapelyProxyCountry`, `scrapelyProxySubdivision`, `proxyUrls`, `tieredProxyUrls` and `tieredProxyConfig`. It also accepts the Apify aliases `useApifyProxy`, `apifyProxyGroups`, `apifyProxyCountry` and `apifyProxySubdivision`. When both names are set, the Scrapely name wins. Explicit arguments win over the input.

`Actor.get_input()` returns proxy objects under both names, so `actor_input["proxyConfiguration"]["useScrapelyProxy"]` and `["useApifyProxy"]` always agree. See [Apify names](https://scrape.ly/docs/proxy.md#apify-names).

With `check_access=True`, the SDK makes a request through the gateway to the proxy status URL: 2 attempts, with a 4-second timeout each. When the proxy refuses the configuration, for example a group other than `RESIDENTIAL` or `DATACENTER`, it raises `ConnectionError` with the proxy's message, such as `Proxy group "GOOGLE_SERP" is not available on Scrapely. Available groups: RESIDENTIAL, DATACENTER.` See [Unsupported groups](https://scrape.ly/docs/proxy.md#unsupported-groups). When the check gets no answer or an HTTP error, it only logs a warning.

### Validation

These combinations raise `ValueError` when the configuration is created:

- A `country_code` that isn't two uppercase letters.
- A `subdivision_code` that isn't 1 to 3 uppercase letters or digits, or that has no `country_code`.
- Real custom URLs, or `new_url_function`, combined with `groups`, `country_code`, `subdivision_code` or `password`.
- A `tiered_proxy_config` tier that sets `new_url_function`.
- Needing the platform proxy (no custom proxies, or an `"auto"` entry) when there are no platform proxy credentials.

### Using a `ProxyConfiguration`

```python
url = await proxy_configuration.new_url(session_id="session_1")
info = await proxy_configuration.new_proxy_info(session_id="session_1")
```

`new_url(session_id=None, request=None, proxy_tier=None)` returns the next URL, rotating round-robin. A session ID, at most 50 characters from `[a-zA-Z0-9._~]`, sticks to one URL within each tier. Platform URLs have the form `http://<username>,groups-A+B,session-X,country-US_CA:<password>@<host>:<port>`. `new_proxy_info()` returns a `ProxyInfo` with `url`, `scheme`, `hostname`, `port`, `username`, `password`, `session_id`, `proxy_tier`, `groups`, `country_code` and `subdivision_code`.

## Serving HTTP

A Python Actor can run its own HTTP server on `ACTOR_WEB_SERVER_PORT` (`4321`). The platform exposes that port at `ACTOR_WEB_SERVER_URL` during a normal run, and it sends [standby](https://scrape.ly/docs/api-standby.md) traffic to it for standby runs. The Python SDK has no standby helper server, so you start the server yourself. `Actor.is_standby()` tells you which mode you're in.

```python
import asyncio
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from scrapely import Actor


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"ok")


async def main():
    async with Actor:
        port = Actor.get_env().container_port or 4321
        server = ThreadingHTTPServer(("0.0.0.0", port), Handler)
        Actor.log.info("Listening on %s (standby=%s)", port, Actor.is_standby())
        await asyncio.to_thread(server.serve_forever)


if __name__ == "__main__":
    asyncio.run(main())
```

Bind to `0.0.0.0`, not `127.0.0.1`. The JavaScript SDK ships a standby helper. See [Standby](https://scrape.ly/docs/sdk-node-standby.md).

---

- Previous: [Actors & webhooks](https://scrape.ly/docs/sdk-python-actors.md)
- Next: [Errors & deployment](https://scrape.ly/docs/sdk-python-deploy.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
