Proxy Authentication for AI Agents: API Keys, Rotating Credentials, and Session Tokens

Most agents fail on proxy auth not because the method is wrong, but because the credential is in the wrong place, in the wrong format, or gets rotated at the wrong time.

Security lock on a server representing proxy authentication for AI agents in 2026
TL;DR

Proxy authentication for AI agents has three layers most guides ignore: the format of the credential, where it's stored, and when rotation happens relative to active sessions. Getting any one wrong silently breaks your agent.

  • Two proxy auth methods that matter: username/password embedded in the proxy URL, or IP whitelisting. Username/password works from any server. IP whitelisting is cleaner for fixed-IP production infrastructure.
  • Special characters in passwords must be URL-encoded. A password containing @, :, /, or # will break the proxy URL silently. The agent makes requests. Nothing gets proxied. No obvious error.
  • Session tokens control sticky behavior. Embed a session ID in your credentials to hold the same IP across requests. Critical for multi-step agent workflows where IP changes break task state.
  • Rotate credentials at task boundaries, not between requests. In-flight rotation breaks session continuity. Assign one credential per task and hold it for the task's duration.
  • Don't store proxy credentials in env vars for production. LangSmith traces can capture env context. Use a secrets manager and inject credentials at runtime.

Proxy authentication for AI agents sounds simple until you hit the first silent failure. The agent runs. Requests go out. Nothing gets blocked outright. But your IP is still your server's IP, your session breaks every ten minutes, or half your agent crew is sharing one credential and fighting over a session ID. The thing is, most proxy auth problems are not about which method you chose. They're about the details of implementation that the docs don't cover.

This guide covers all three layers: the two authentication methods and when to use each, how session tokens work with sticky sessions in agent pipelines, and how to handle rotating credentials without breaking active task state. There's working code throughout.


The Two Proxy Authentication Methods That Actually Matter

Proxy providers offer two ways to verify that a request is authorized to use the proxy. Both work. The choice depends on your infrastructure.

Method How It Works Best For Watch Out For
Username/Password Credentials embedded in proxy URL or sent via Proxy-Authorization header Dynamic deployments, cloud functions, local development, any setup where your IP changes Special characters in passwords that need URL-encoding; credentials appearing in logs
IP Whitelisting Your server's IP is registered as authorized in the proxy dashboard Fixed-IP production servers, dedicated VMs, corporate infrastructure Breaks when your server IP changes; one whitelist per account, so shared pools get complicated

For most AI agent deployments, username/password is the right starting point. Cloud-hosted agents, containerized pipelines, and local development all involve IP addresses that change. IP whitelisting on a dynamic IP means your agent loses proxy access every time a container restarts on a new host. That said, if you're running agents on a fixed dedicated server where the IP genuinely doesn't change, whitelisting is cleaner. No credentials in the URL, no credential rotation to manage.

The two methods can also be used together. Whitelist your primary production server, use username/password for everything else. Honestly, this is the configuration I'd recommend for any serious deployment.


Username and Password Authentication: The Working Implementation

The Basic URL Format

The standard way to authenticate a proxy via username/password is embedding the credentials directly in the proxy URL. The format is:

Format Proxy URL with credentials
# HTTP/HTTPS targets — global residential pool, port 6011
http://username:[email protected]:6011

# SOCKS5 — separate host, port 6014
socks5://username:[email protected]:6014

# With targeting params appended to password (colon-separated)
http://username:password:[email protected]:6011
http://username:password:country-us:[email protected]:6011

In Python's requests library, pass this as the proxies dict value:

Python Basic proxy auth with requests
import requests

proxies = {
    "http":  "http://your_username:your_password:[email protected]:6011",
    "https": "http://your_username:your_password:[email protected]:6011",
}

response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print(response.json())  # Should show the proxy IP, not your server IP

The Special Character Problem

This is the silent failure that trips people up most often. If your password contains any of these characters: @, :, /, ?, #, %, or +, the URL parser splits on them incorrectly and the credential format breaks. The proxy rejects authentication. No meaningful error is thrown. Your requests go through without proxying.

URL-encode the password before embedding it. Python's urllib.parse.quote handles this correctly:

Python URL-encode passwords with special characters
import requests
from urllib.parse import quote

username = "your_username"
password = "p@ss:word/with#special&chars"  # This would break a plain URL

# URL-encode ONLY the password, not the full URL
encoded_password = quote(password, safe="")

proxy_url = f"http://{username}:{encoded_password}:[email protected]:6011"
proxies = {"http": proxy_url, "https": proxy_url}

response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print(response.json())
Safe Characters in Quote()
Pass safe="" to quote(), not the default. The default safe parameter preserves forward slashes, which will break your proxy URL if your password contains them. With safe="", every special character gets encoded.

Using requests.auth.HTTPProxyAuth

There's an alternative to embedding credentials in the URL. requests.auth.HTTPProxyAuth handles the Proxy-Authorization header directly, which avoids the URL-encoding problem entirely:

Python HTTPProxyAuth avoids URL encoding issues
import requests
from requests.auth import HTTPProxyAuth

proxies = {
    "http":  "http://geox.torchproxies.com:6011",
    "https": "http://geox.torchproxies.com:6011",
}

auth = HTTPProxyAuth("your_username", "p@ss:word/with#special")

response = requests.get(
    "https://httpbin.org/ip",
    proxies=proxies,
    auth=auth,
    timeout=15
)
print(response.json())

The tradeoff: HTTPProxyAuth sends the credential as a Proxy-Authorization header. For HTTPS targets, some proxy implementations don't forward this header correctly through the CONNECT tunnel. If you're seeing 407 errors on HTTPS targets but not HTTP, the embedded URL approach is more reliable.


IP Whitelisting: Setup and When It Breaks

IP whitelisting adds your server's IP address to an approved list in your proxy provider dashboard. Any request arriving from that IP is automatically authorized, no credentials needed in the URL.

The setup from the TorchProxies dashboard: navigate to your proxy settings, find the IP Whitelisting section, add your server's public IP. Requests from that IP use the proxy without credentials. All other IPs get rejected.

Python No credentials needed with IP whitelisting
import requests

# With IP whitelisting, no username:password in the URL
proxies = {
    "http":  "http://geox.torchproxies.com:6011",
    "https": "http://geox.torchproxies.com:6011",
}

response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
print(response.json())

The three ways IP whitelisting breaks for AI agents:

First, containerized agents. Docker containers and Kubernetes pods frequently get new host IPs on restart. Your whitelisted IP is the old container's IP. The new container gets a 407. This is the most common failure mode in cloud deployments.

Second, scaling out. If your agent runs on multiple instances, each instance needs its IP whitelisted separately. That's manageable at 2-3 instances. At 50, it becomes a maintenance problem.

Third, cloud function cold starts. AWS Lambda, Google Cloud Functions, and Azure Functions assign IPs from large, rotating pools. Whitelisting is essentially impossible for function-based agent deployments.

For fixed dedicated servers running long-lived agent processes, IP whitelisting is the cleaner option. For anything cloud-native and dynamically scaled, use username/password authentication.


Session Tokens and Sticky Sessions: How They Work in Agent Pipelines

This is the part most proxy auth guides miss completely.

A sticky session tells the proxy server to return the same IP address for multiple requests within a time window. For AI agents doing multi-step tasks (a browser agent navigating checkout, an account manager maintaining a LinkedIn session, a scraper collecting paginated results), this is not optional. IP rotation between steps breaks session state on the target platform and forces re-authentication.

Sticky sessions are enabled by embedding a session identifier in your proxy credentials. The format varies by provider, but the concept is universal: the session ID tells the proxy which pool of sticky sessions to draw from and which specific session to maintain.

TorchProxies Sticky Session Format

On TorchProxies Plan X, sticky sessions require no dashboard setup. The session ID is appended directly to the password, colon-separated, alongside any other targeting parameters. The session ID must be exactly 8 lowercase alphanumeric characters and persists for up to 168 hours:

Python Sticky session with session ID in credentials
import requests
import uuid

# Generate a unique session ID for this task
session_id = str(uuid.uuid4()).replace("-", "")[:8]  # exactly 8 chars required

# Session ID appended to password, colon-separated
# Format: username:password:country-XX:session-XXXXXXXX@host:port
proxy_username = "your_username"
proxy_password = f"your_password:country-us:session-{session_id}"
proxy_host     = "geox.torchproxies.com"
proxy_port     = 6011

proxy_url = f"http://{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}"
proxies   = {"http": proxy_url, "https": proxy_url}

# Both requests will use the same IP because they share a session ID
r1 = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
r2 = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)

print("First request IP:",  r1.json()["origin"])
print("Second request IP:", r2.json()["origin"])
# Both should show the same IP
Session Duration Sizing
Set your sticky session duration to at least 15-20 minutes longer than your expected task duration. Sessions that expire mid-task fall back to the standard rotating pool, which assigns a new IP. The task then gets blocked or re-authenticated on the target. Plan X supports a maximum sticky session duration of 168 hours.

One Session ID Per Agent Task

The right pattern for multi-agent systems is one unique session ID per task, assigned at task creation and held for the task's duration. Don't share session IDs across agents running different tasks concurrently. Two agents sharing a session ID share an IP, which creates two problems: it links their identity on the target platform, and it means each new request from either agent renews the session timer from the other's perspective, creating unpredictable session expiry behavior.

Python Multi-agent session ID assignment
import requests
import uuid
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class AgentProxyConfig:
    username: str
    password: str
    host:     str = "geox.torchproxies.com"
    port:     int = 6011
    session_id: str = field(default_factory=lambda: str(uuid.uuid4()).replace("-", "")[:8])  # 8 chars required

    def as_proxies(self) -> dict:
        # Session appended to password, colon-separated
        proxy_pass = f"{self.password}:country-us:session-{self.session_id}"
        url = f"http://{self.username}:{proxy_pass}@{self.host}:{self.port}"
        return {"http": url, "https": url}


# Each agent task gets its own session ID automatically
researcher_proxy = AgentProxyConfig(username="your_username", password="your_password")
analyst_proxy    = AgentProxyConfig(username="your_username", password="your_password")

# researcher_proxy.session_id != analyst_proxy.session_id
# They will use different IPs from the proxy pool

print("Researcher session:", researcher_proxy.session_id)
print("Analyst session:",    analyst_proxy.session_id)

Rotating Credentials Without Breaking Active Sessions

Credential rotation is a security best practice. Rotating proxy credentials periodically limits the damage from a leaked credential. But rotation done wrong breaks agent sessions mid-task, causes cascading re-authentication failures, and wastes LLM API tokens on recovery.

The rule is simple. Rotate at task boundaries, not between requests.

What this looks like in practice: maintain a credential pool. Each task draws one credential from the pool at task start. That credential is held for the entire task duration. When the task completes, the credential is returned to the pool. New tasks draw from the pool again. Rotation happens at the pool level, not during active sessions.

Python Credential pool with task-boundary rotation
import requests
import threading
import random
from contextlib import contextmanager

class ProxyCredentialPool:
    """Thread-safe credential pool for multi-agent setups."""

    def __init__(self, credentials: list[dict]):
        # Each credential: {"username": "...", "password": "..."}
        self._pool    = credentials.copy()
        self._in_use  = []
        self._lock    = threading.Lock()

    @contextmanager
    def acquire(self):
        """Acquire a credential for the duration of a task."""
        cred = None
        with self._lock:
            available = [c for c in self._pool if c not in self._in_use]
            if not available:
                raise RuntimeError("No proxy credentials available")
            cred = random.choice(available)
            self._in_use.append(cred)
        try:
            yield cred
        finally:
            with self._lock:
                if cred in self._in_use:
                    self._in_use.remove(cred)

    def as_proxies(self, cred: dict) -> dict:
        user = cred["username"]
        pw   = cred["password"]
        url  = f"http://{user}:{pw}:[email protected]:6011"
        return {"http": url, "https": url}


# Usage: each agent task acquires and releases one credential
pool = ProxyCredentialPool([
    {"username": "user_a", "password": "pass_a"},
    {"username": "user_b", "password": "pass_b"},
    {"username": "user_c", "password": "pass_c"},
])

with pool.acquire() as cred:
    proxies = pool.as_proxies(cred)
    r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)
    print("Task completed with IP:", r.json()["origin"])
# Credential automatically released when the with block exits
S
Sachin Supunthaka — Senior Software Engineer
What this actually means in practice is that "credential rotation" in the security documentation sense and "proxy rotation" in the scraping sense are two different things, and confusing them causes real problems. Security rotation is about periodically replacing the credential string itself to limit exposure windows. Proxy rotation is about using different IPs across requests to avoid rate limiting. Both need to happen, but they operate at completely different levels. Rotating the proxy credential mid-task to get a new IP is almost always wrong. Handle IP rotation through the proxy provider's session config, not through credential rotation during active sessions.

Handling 407 Proxy Authentication Required in Agent Pipelines

HTTP 407 is what the proxy server sends when it rejects your authentication. It's different from 403 (which comes from the target site) and 429 (rate limiting). A 407 means the proxy itself turned away your request before it reached anyone.

The four most common causes for AI agents:

Expired credentials. Proxy provider credentials have TTLs. If your agent has been running for days with a credential that expired overnight, every request after expiry returns 407.

IP not whitelisted. If you use IP whitelisting and your server was re-provisioned, the new IP isn't in the whitelist yet. Switch to username/password auth or add the new IP.

Unencoded special characters. The URL parser splits on @, :, or / in the password. The credential looks malformed to the proxy server.

HTTPS header propagation. Some HTTP libraries don't forward the Proxy-Authorization header through HTTPS CONNECT tunnels correctly. Use the credential-in-URL format rather than HTTPProxyAuth for HTTPS targets.

Python 407 detection and handling in agent tool calls
import requests
import time
from requests.exceptions import ProxyError

def fetch_with_auth_retry(url: str, proxies: dict, max_retries: int = 2) -> str:
    for attempt in range(max_retries):
        try:
            r = requests.get(url, proxies=proxies, timeout=30)

            if r.status_code == 407:
                # 407 = proxy rejected auth. Not a target-site block.
                # Check: credentials expired? IP not whitelisted?
                raise ValueError(
                    f"Proxy authentication failed (407). Check credentials. URL: {url}"
                )

            if r.status_code == 403:
                # 403 = target site blocked the request (bot detection)
                # Different problem: IP quality or behavioral detection
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue
                raise ValueError(f"Target site blocked request (403). URL: {url}")

            r.raise_for_status()
            return r.text

        except ProxyError as e:
            # ProxyError wraps connection failures to the proxy itself
            raise ConnectionError(f"Cannot reach proxy server: {e}") from e

    raise RuntimeError(f"All {max_retries} attempts failed for {url}")
407 vs 403: Know the Difference
407 is a proxy error. Your proxy rejected the request. Fix your credentials or IP whitelist. Switching to a different proxy IP won't help. 403 is a target site error. The site blocked the request. Switching to a cleaner proxy IP may help. Your credential format is not the issue. Misdiagnosis here wastes time debugging the wrong thing.

Credential Security: What Actually Goes Wrong in Production

Proxy credentials are less sensitive than your LLM API key, but they still control access to your proxy plan's bandwidth and account. Leaked proxy credentials result in someone else consuming your bandwidth, getting your IPs flagged for abuse, and leaving your agent without proxy access at the worst moment.

The LangSmith Trace Problem

This is one that catches teams off guard. If you're using LangSmith for agent observability and you set HTTP_PROXY or HTTPS_PROXY as environment variables, LangSmith may capture those values as part of the execution context trace, particularly if a tool call includes environment inspection or if an error traceback includes the proxy URL string.

The same applies to any LLM observability platform that captures full environment state: Langfuse, Helicone, and similar tools. A print(os.environ) call anywhere in your agent code will log your proxy credentials to whatever logging infrastructure you're using.

The fix is not to stop using observability. It's to handle proxy credentials the same way you handle API keys:

Python Safer credential injection via secrets manager
import os
import boto3  # AWS Secrets Manager
import json
from urllib.parse import quote

def get_proxy_credentials() -> dict:
    """Retrieve proxy credentials from AWS Secrets Manager at runtime."""
    client = boto3.client("secretsmanager", region_name="us-east-1")
    secret = client.get_secret_value(SecretId="torchproxies/credentials")
    return json.loads(secret["SecretString"])


def build_proxy_url(username: str, password: str) -> str:
    encoded_pw = quote(password, safe="")
    return f"http://{username}:{encoded_pw}@geox.torchproxies.com:6011"


# Retrieve at runtime, not at import time
creds     = get_proxy_credentials()
proxy_url = build_proxy_url(creds["username"], creds["password"])

# Only inject into the specific components that need it
# Don't set global env vars - they appear in every subprocess and trace
proxies = {"http": proxy_url, "https": proxy_url}

If you're not yet using a secrets manager, the minimum acceptable approach for development is a .env file with python-dotenv, making sure that file is in .gitignore and is never committed. That keeps credentials out of your source code, but they're still readable by any process with environment access. For anything running in production, AWS Secrets Manager, Google Cloud Secret Manager, or HashiCorp Vault are the right tools.

The August 2025 breach reported by Nango, where stolen OAuth tokens exposed 700+ customer environments, is a reminder that credential security in agent pipelines is a real attack surface, not a theoretical one. Machine identities now outnumber human identities 82 to 1 in enterprise environments. Every agent credential is a machine identity. Treat it accordingly.


Framework-Specific Notes: LangChain and CrewAI

LangChain: Auth via requests_kwargs

The cleanest way to pass proxy auth in LangChain is directly in the component's requests_kwargs rather than env vars. This scopes the credential to exactly the component that needs it and prevents it from appearing in the broader process environment:

Python Component-scoped proxy auth in LangChain
from langchain_community.document_loaders import WebBaseLoader
from urllib.parse import quote

username = "your_username"
password = quote("your_password", safe="")

loader = WebBaseLoader(
    web_paths=["https://example.com"],
    requests_kwargs={
        "proxies": {
            "http":  f"http://{username}:{password}@geox.torchproxies.com:6011",
            "https": f"http://{username}:{password}@geox.torchproxies.com:6011",
        },
        "timeout": 30,
    }
)
docs = loader.load()

CrewAI: Per-Agent Credential Assignment

For CrewAI agents that need separate IP identities, pass credentials per tool call rather than via global env vars. This prevents agents from sharing credentials and ensures each has an isolated proxy session:

Python Per-agent proxy credentials in CrewAI
from crewai_tools import BaseTool
from pydantic import Field
from urllib.parse import quote
import requests

class AuthenticatedWebTool(BaseTool):
    name:        str = "Authenticated Web Fetcher"
    description: str = "Fetches a URL through an authenticated proxy"
    username:    str = Field(description="Proxy username")
    password:    str = Field(description="Proxy password (raw, not URL-encoded)")

    def _run(self, url: str) -> str:
        encoded_pw = quote(self.password, safe="")
        proxy_url  = f"http://{self.username}:{encoded_pw}@geox.torchproxies.com:6011"
        proxies    = {"http": proxy_url, "https": proxy_url}

        r = requests.get(
            url, proxies=proxies, timeout=30,
            headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
        )
        r.raise_for_status()
        return r.text[:5000]


# Each agent gets its own credentials -- different identities on the proxy
researcher_tool = AuthenticatedWebTool(username="user_a", password="pass_a")
analyst_tool    = AuthenticatedWebTool(username="user_b", password="pass_b")

TorchProxies Endpoint and Port Reference

Plan X proxies use pool-specific hosts and ports. Targeting parameters — country, city, state, and session ID — are appended to the password, colon-separated. There is no separate dashboard toggle for these; they go directly in the proxy string at connection time.

Plan X Proxy String Reference
HTTP/HTTPS — global residential pool:
http://username:password:[email protected]:6011 Sticky session (session ID appended to password):
http://username:password:country-us:[email protected]:6011 SOCKS5:
socks5://username:password:[email protected]:6014 Session IDs must be exactly 8 lowercase alphanumeric characters. Maximum session duration: 168 hours. Use either city or state targeting — not both together.

Authentication supports both username/password and IP whitelisting. IP whitelisting is configured from the proxy settings page in the dashboard. Credentials for username/password authentication are generated from the same page.


Start With a Free Trial

All proxy types include HTTP, HTTPS, and SOCKS5 authentication. No credit card, no monthly commitment.

Start Free Trial

Plan X from $5/GB  ·  ISP Static from $2.3/IP/month  ·  No Rate Limits  ·  Pay-As-You-Go

Quick Reference

Proxy Auth Decisions at a Glance
Dynamic cloud deployments (Lambda, containers, functions) Use username/password in proxy URL. URL-encode the password with quote(password, safe="").
Fixed dedicated production servers IP whitelisting is cleaner. No credentials in URLs, no rotation to manage. Add new server IPs to whitelist before deploying.
Multi-step agent workflows needing session consistency Use sticky sessions with a session ID embedded in credentials. One session ID per task, not shared across agents.
Multi-agent systems with identity isolation needed Per-agent credentials via custom tool wrappers in CrewAI. Per-component proxies via requests_kwargs in LangChain.
Getting a 407 error Check credentials first (not 403). Expired credential, un-whitelisted IP, or unencoded special character in password. All three produce 407.
Production credential storage Secrets manager (AWS, GCP, Vault). Never global env vars for production. LangSmith and observability tools can capture env context in traces.

Frequently Asked Questions

Two methods cover most setups. Username/password: embed credentials in the proxy URL as http://username:password@host:port, then set this as HTTP_PROXY and HTTPS_PROXY env vars before importing your agent framework, or pass as requests_kwargs in LangChain components. IP whitelisting: register your server's IP in the proxy dashboard. Requests from that IP are automatically authenticated. Username/password works for dynamic deployments. IP whitelisting is cleaner for fixed-IP production servers.
HTTP 407 is returned by the proxy server when your request arrives without valid authentication. It's distinct from 403 (target site block). Common causes: expired credentials, a server IP not in the whitelist, special characters in a password that aren't URL-encoded, or the Proxy-Authorization header not propagating correctly through HTTPS tunnels. Test with a simple requests.get call before running a full agent pipeline.
A sticky session tells the proxy to return the same IP for multiple requests within a configured time window. A session ID embedded in your credentials identifies the session. For AI agents, sticky sessions are critical for multi-step workflows where IP changes break task state. Set session duration to cover your expected task length, plus a buffer. On TorchProxies, session duration is configured from the dashboard before generating credentials.
Safer than hardcoded strings, but not safe enough for production. LangSmith and other observability tools can capture environment context in traces, including proxy URL strings that contain embedded credentials. Use a secrets manager for production deployments. Retrieve credentials at runtime and inject them only into the specific components that need them.
Rotate at task boundaries, not between requests. Maintain a credential pool. Each task acquires one credential at start and holds it for the task's duration. Rotation happens at the pool level when tasks complete. In-flight rotation during active sessions breaks IP consistency and triggers re-authentication on the target platform.