Testing Webhooks Globally with Rotating Proxies in Python
September 7, 2026
Webhooks are a cornerstone of modern integrations, letting services push data to your endpoints in real time. When your application serves users worldwide, you need to be confident that webhooks arrive correctly regardless of where they originate. Testing this locally with a single IP address can miss region‑specific issues such as firewall rules, latency‑induced timeouts, or geo‑based rate limits.
Using rotating proxies lets you simulate webhook calls from many different geographic points without provisioning servers in each location. This guide shows you how to build a lightweight, production‑ready tester in Python that:
- Picks a proxy at random from a pool
- Sends a webhook payload through that proxy
- Verifies the source IP seen by your endpoint matches the proxy’s location
- Implements simple retry and back‑off logic
- Logs results for further analysis
Why Geo‑Distributed Webhook Testing Matters
When a third‑party service (e.g., a payment gateway, CI system, or marketing platform) sends a webhook, it often does so from IP ranges tied to its data centers. If your endpoint blocks certain regions, enforces strict rate limits per IP, or relies on IP‑based whitelisting, a webhook may be silently dropped. By testing from multiple locations you can:
- Confirm that your firewall or cloud security groups allow traffic from the provider’s actual egress points
- Validate that any rate‑limiting or abuse‑prevention logic works correctly under distributed load
- Detect DNS or SSL certificate issues that only appear for certain geographic routes
- Ensure that your logging and monitoring capture the correct client IP for audit trails
Setting Up a Rotating Proxy Pool in Python
First, obtain a list of proxy endpoints. For demonstration we’ll assume you have a text file proxies.txt where each line is in the format host:port:username:password (or host:port for unauthenticated proxies). If you use a proxy provider like RoProxy, you can fetch the list via their API and store it locally.
import random
def load_proxies(path: str = "proxies.txt") -> list[dict]:
"""Load proxy credentials and return a list of dicts ready for requests."""
proxies = []
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(":")
if len(parts) == 2: # host:port
host, port = parts
proxies.append({
"http": f"http://{host}:{port}",
"https": f"http://{host}:{port}"
})
elif len(parts) == 4: # host:port:user:pass
host, port, user, pwd = parts
auth = f"{user}:{pwd}"
proxies.append({
"http": f"http://{auth}@{host}:{port}",
"https": f"http://{auth}@{host}:{port}"
})
else:
raise ValueError(f"Invalid proxy line: {line}")
return proxies
PROXIES = load_proxies()
The PROXIES variable now holds a list of dictionaries that can be passed directly to the proxies argument of requests.
Implementing a Webhook Sender with Proxy Rotation
Next, we write a function that attempts to POST a webhook payload using a randomly selected proxy. On failure (connection error, timeout, or non‑2xx response), it picks another proxy and retries up to a configurable limit.
import time
import requests
from requests.exceptions import RequestException
def send_webhook(
url: str,
payload: dict,
proxies_list: list[dict],
max_attempts: int = 5,
backoff_factor: float = 1.0,
timeout: int = 10
) -> tuple[bool, int, str]:
"""
Send a JSON webhook via a rotating proxy.
Returns (success, status_code, final_error_message).
"""
attempt = 0
while attempt < max_attempts:
attempt += 1
proxy = random.choice(proxies_list)
try:
resp = requests.post(
url,
json=payload,
proxies=proxy,
timeout=timeout,
headers={"Content-Type": "application/json"}
)
# Consider 2xx as success
if 200 <= resp.status_code < 300:
return True, resp.status_code, ""
else:
# Non‑2xx but we got a response; treat as failure and retry
raise RequestException(f"HTTP {resp.status_code}")
except RequestException as exc:
if attempt == max_attempts:
return False, 0, str(exc)
# Exponential backoff
sleep_time = backoff_factor * (2 ** (attempt - 1))
time.sleep(sleep_time)
# Should never reach here
return False, 0, "Max attempts exceeded"
Verifying Geo‑Location of Outbound IP
To be certain that the request truly exited through the chosen proxy, we can call an external IP‑echo service (e.g., https://ipinfo.io/json) through the same proxy and compare the returned ip field with the proxy’s host. This step is optional but adds confidence, especially when using authenticated proxies where DNS leaks could otherwise expose your real IP.
def get_outbound_ip(proxy: dict) -> str:
"""Return the public IP seen by ipinfo.io when using the given proxy."""
try:
r = requests.get("https://ipinfo.io/json", proxies=proxy, timeout=8)
r.raise_for_status()
data = r.json()
return data.get("ip", "")
except Exception:
return ""
You can integrate this check inside send_webhook or run it as a separate validation step before sending the actual webhook.
Handling Failures and Circuit Breaker‑Like Behavior
If a proxy consistently fails (e.g., authentication errors or timeouts), it’s wise to temporarily remove it from the pool to avoid wasting attempts. A simple approach is to track failure counts per proxy and skip those that exceed a threshold.
from collections import defaultdict
FAILURE_COUNT = defaultdict(int)
MAX_FAILURES = 3
def send_webhook_resilient(url: str, payload: dict) -> tuple[bool, int, str]:
attempt = 0
while attempt < len(PROXIES):
# Filter out proxies that have failed too many times
healthy = [p for p in PROXIES if FAILURE_COUNT[tuple(p.items())] < MAX_FAILURES]
if not healthy:
# All proxies are unhealthy; reset counts and try again
FAILURE_COUNT.clear()
healthy = PROXIES[:]
proxy = random.choice(healthy)
try:
resp = requests.post(
url,
json=payload,
proxies=proxy,
timeout=10,
headers={"Content-Type": "application/json"}
)
if 200 <= resp.status_code < 300:
return True, resp.status_code, ""
# Treat non‑2xx as failure for this proxy
raise RequestException(f"HTTP {resp.status_code}")
except RequestException as exc:
FAILURE_COUNT[tuple(proxy.items())] += 1
attempt += 1
time.sleep(0.5) # small pause before next try
return False, 0, "All proxies exhausted"
This pattern gives you a lightweight circuit breaker without adding external dependencies.
Full Example Script
Below is a self‑contained script that reads a list of proxies, loads a sample webhook payload (you can replace it with your own), sends the webhook, validates the outbound IP, and prints a concise report.
#!/usr/bin/env python3
import json
import random
import time
import requests
from requests.exceptions import RequestException
from collections import defaultdict
# ---------- Configuration ----------
PROXIES_FILE = "proxies.txt"
WEBHOOK_URL = "https://yourdomain.com/receive-webhook" # replace with your endpoint
PAYLOAD = {
"event": "test.webhook",
"timestamp": time.time(),
"data": {
"user_id": 12345,
"action": "ping"
}
}
MAX_ATTEMPTS = 5
BACKOFF = 1.0
TIMEOUT = 10
MAX_FAILURES = 3
# ----------------------------------
def load_proxies(path: str) -> list[dict]:
proxies = []
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(":")
if len(parts) == 2:
host, port = parts
proxies.append({
"http": f"http://{host}:{port}",
"https": f"http://{host}:{port}"
})
elif len(parts) == 4:
host, port, user, pwd = parts
auth = f"{user}:{pwd}"
proxies.append({
"http": f"http://{auth}@{host}:{port}",
"https": f"http://{auth}@{host}:{port}"
})
else:
raise ValueError(f"Bad proxy line: {line}")
return proxies
PROXIES = load_proxies(PROXIES_FILE)
FAILURE_COUNT = defaultdict(int)
def get_outbound_ip(proxy: dict) -> str:
try:
r = requests.get("https://ipinfo.io/json", proxies=proxy, timeout=8)
r.raise_for_status()
return r.json().get("ip", "")
except Exception:
return ""
def send_webhook(url: str, payload: dict) -> tuple[bool, int, str]:
attempt = 0
while attempt < len(PROXIES):
healthy = [p for p in PROXIES if FAILURE_COUNT[tuple(p.items())] < MAX_FAILURES]
if not healthy:
FAILURE_COUNT.clear()
healthy = PROXIES[:]
proxy = random.choice(healthy)
# Optional IP verification
outbound_ip = get_outbound_ip(proxy)
if outbound_ip:
print(f"[INFO] Using proxy {proxy['http']} -> outbound IP {outbound_ip}")
else:
print(f"[WARN] Could not verify outbound IP for proxy {proxy['http']}")
try:
resp = requests.post(
url,
json=payload,
proxies=proxy,
timeout=TIMEOUT,
headers={"Content-Type": "application/json"}
)
if 200 <= resp.status_code < 300:
return True, resp.status_code, ""
raise RequestException(f"HTTP {resp.status_code}")
except RequestException as exc:
FAILURE_COUNT[tuple(proxy.items())] += 1
attempt += 1
if attempt < len(PROXIES):
sleep = BACKOFF * (2 ** (attempt - 1))
print(f"[RETRY] {exc}. Waiting {sleep:.1f}s...")
time.sleep(sleep)
return False, 0, "All proxies exhausted"
if __name__ == "__main__":
success, code, error = send_webhook(WEBHOOK_URL, PAYLOAD)
if success:
print(f"[SUCCESS] Webhook delivered, status {code}")
else:
print(f"[FAILURE] Could not deliver webhook: {error}")
Make the script executable (chmod +x test_webhook.py) and run it. Adjust PROXIES_FILE, WEBHOOK_URL, and PAYLOAD to match your environment.
Best Practices and Tips
- Keep your proxy list fresh. Proxy providers often rotate IPs automatically; refresh your
proxies.txtevery few hours or use the provider’s API to pull a live list. - Respect rate limits. Even with rotating IPs, the target webhook endpoint may enforce limits per account or per signature. Space out your test calls (e.g., one request every few seconds) to avoid unintentionally blocking your own test traffic.
- Log the proxy used. For debugging, store which proxy (or at least its geographic region) succeeded for each webhook. This helps correlate failures with specific locations.
- Secure credentials. If your proxies require authentication, avoid committing usernames/passwords to version control. Use environment variables or a secrets manager.
- Validate TLS certificates. When testing HTTPS webhooks, ensure your proxy does not perform SSL interception that could break certificate validation. If you need to bypass validation for testing only, set
verify=Falseinrequests(but never do this in production). - Consider IPv6 proxies. Some services only expose IPv6 addresses in certain regions. If your endpoint must support IPv6, include IPv6‑capable proxies in your pool.
- Automate in CI/CD. Integrate this script into your pipeline to run on every deploy, ensuring that webhook listeners remain reachable from the geographic regions your providers actually use.
Conclusion
Testing webhooks from a single location leaves blind spots that can lead to missed deliveries, silent failures, or unexpected latency issues in production. By leveraging a rotating proxy pool, you can emulate the true geographic diversity of incoming webhook traffic, verify that your infrastructure accepts connections from those locations, and build resilience into your integration layer.
The Python example above provides a solid foundation: proxy loading, intelligent rotation, optional IP verification, retry with back‑off, and a simple failure‑tracking mechanism that mimics a circuit breaker. Adapt the script to your specific needs—add logging to a monitoring system, plug in secret management for proxy credentials, or extend the payload validation to check response bodies.
With this approach, you gain confidence that your webhook endpoints will work reliably for users and partners no matter where they are on the globe. Happy testing!