Use QuotaGuard Static with the SendGrid HTTPS API
Configure a dedicated SendGrid HTTP client to use QuotaGuard Static, verify its outbound address, and validate a Mail Send request before sending an approved live message. The result is an explicit application egress path that you can register with SendGrid IP Access Management.
This guide covers the connection from your server-side application to the SendGrid API. It includes a Python HTTPX implementation and a curl-based alternative. Keep using SendGrid for email delivery, sender authentication, and any dedicated sending IPs in your email program.
Requirements
You will need:
- A QuotaGuard Static account, its complete outbound HTTP proxy URL, and both assigned static IPs.
- A SendGrid account and an API key with the permissions required by your application. The examples need Mail Send permission.
- A sender identity configured in SendGrid and a recipient approved for any later live send.
- Access to the environment that actually sends email requests, including workers or scheduled jobs.
- Python 3.10 or later. The main example uses HTTPX 0.28.x. The alternative uses Python’s standard library and curl.
- Permission to change SendGrid access controls and an approved administrative access path.
For the normal global SendGrid endpoint, use https://api.sendgrid.com. An EU regional subuser uses https://api.eu.sendgrid.com. Use the endpoint appropriate to the SendGrid account configuration; a proxy region does not convert a global account into an EU regional subuser. Mail Send endpoint reference
Step 1: identify every client that accesses the SendGrid account
List every caller before enabling or tightening SendGrid IP Access Management:
- Web processes that call the API directly.
- Email queue workers and retry workers.
- Scheduled tasks and one-off administrative scripts.
- Tools that access templates, contacts, or reporting APIs.
- SMTP clients using the same account.
- Administrators who need to access the SendGrid UI.
Assign an owner and an approved network route to each client. Configure the process that opens the SendGrid connection, not only the process that enqueues a job.
IP Access Management applies to the UI, API, and SMTP relay, and becomes active when the first allowed address is added. Include your administrator’s current approved address and plan for a stable future administrator route before saving that change. The application proxy does not automatically route your browser. SendGrid IP Access Management
Step 2 Store the connection values
Set the following values through the runtime’s secrets or protected environment settings. This table describes the values; it is not a request to paste secrets into a support ticket.
| Variable | Value |
|---|---|
QUOTAGUARDSTATIC_URL |
Complete HTTP proxy URL copied from the QuotaGuard Static dashboard |
QG_EXPECTED_IPS |
Both assigned QuotaGuard IPs, separated by a comma |
SENDGRID_API_KEY |
SendGrid API key with the necessary permissions |
SENDGRID_FROM_EMAIL |
Sender address configured in SendGrid |
SENDGRID_TO_EMAIL |
Approved recipient for a controlled live check |
SENDGRID_REGION |
global, or eu for the appropriate regional subuser |
A proxy URL has the form http://USER:PASSWORD@HOST:PORT. Copy the full dashboard value rather than constructing it from an example. Preserve any URL encoding in credentials. The HTTP proxy URL and the HTTPS SendGrid destination have different roles; do not change the proxy scheme to https simply because SendGrid uses HTTPS.
Keep proxy credentials and the API key separate. The proxy authenticates the network connection. The SendGrid key authenticates the API request. Neither replaces the other.
Prefer a mail-scoped key to an account-wide key. Use separate secrets for environments where possible, rotate them through your normal process, and do not put credentials in a client-side application. SendGrid API key scopes
Step 3 Create the dedicated HTTP client
Install the dependency in the application’s Python environment:
python -m pip install 'httpx>=0.28,<0.29'
Save the following as sendgrid_httpx.py. It supports three explicit modes:
ipchecks the proxy’s egress address without a SendGrid key.sandboxvalidates a Mail Send request without delivery.livesends one message and requires a separate confirmation environment value.
The client does not follow redirects or retry failed requests. It uses an explicit proxy, keeps TLS verification enabled, and ignores ambient proxy settings. The live recipient is always supplied through the trusted runtime environment rather than request parameters from an unauthenticated caller.
#!/usr/bin/env python3
"""Check QuotaGuard egress, validate SendGrid mail, or explicitly send one message."""
import argparse
import ipaddress
import os
import sys
from urllib.parse import urlsplit
import httpx
API_ORIGINS = {
"global": "https://api.sendgrid.com",
"eu": "https://api.eu.sendgrid.com",
}
def required(name):
value = os.environ.get(name, "")
if not value or any(ord(c) < 32 or ord(c) == 127 for c in value):
raise ValueError(f"Set a valid {name} in the trusted runtime environment")
return value
def proxy_url():
value = required("QUOTAGUARDSTATIC_URL")
parsed = urlsplit(value)
if not (parsed.scheme == "http" and parsed.hostname and parsed.port
and parsed.username and parsed.password
and parsed.path in ("", "/") and not parsed.query and not parsed.fragment):
raise ValueError("Use the complete HTTP proxy URL from the QuotaGuard Static dashboard")
return value
def expected_ips():
values = [v.strip() for v in required("QG_EXPECTED_IPS").split(",")]
addresses = {str(ipaddress.ip_address(v)) for v in values}
if len(addresses) != 2:
raise ValueError("QG_EXPECTED_IPS must contain the two assigned addresses, comma-separated")
return addresses
def mail_payload(live=False):
return {
"personalizations": [{"to": [{"email": required("SENDGRID_TO_EMAIL")}]}],
"from": {"email": required("SENDGRID_FROM_EMAIL")},
"subject": "QuotaGuard SendGrid connection check",
"content": [{"type": "text/plain", "value": "This is an authorized connection check."}],
"mail_settings": {"sandbox_mode": {"enable": not live}},
}
def run(mode):
# This client is private to the SendGrid workflow. Do not reuse it for unrelated APIs.
proxy = proxy_url()
if mode == "ip":
allowed = expected_ips()
url = "https://ip.quotaguard.com"
payload = None
headers = {} # Never send the SendGrid key to the public IP checker.
else:
region = os.environ.get("SENDGRID_REGION", "global")
if region not in API_ORIGINS:
raise ValueError("SENDGRID_REGION must be global or eu")
if mode == "live" and os.environ.get("SENDGRID_CONFIRM_LIVE_SEND") != "YES":
raise ValueError("Live mode needs SENDGRID_CONFIRM_LIVE_SEND=YES after recipient approval")
url = API_ORIGINS[region] + "/v3/mail/send"
payload = mail_payload(live=mode == "live")
headers = {"Authorization": "Bearer " + required("SENDGRID_API_KEY")}
# trust_env=False also ignores ambient CA settings. Supply an approved SSLContext
# explicitly if your organization requires a custom CA bundle. Never disable verification.
with httpx.Client(proxy=proxy, trust_env=False, follow_redirects=False,
verify=True, timeout=httpx.Timeout(30.0, connect=10.0)) as client:
if mode == "ip":
response = client.get(url)
else:
response = client.post(url, headers=headers, json=payload)
if mode == "ip":
if response.status_code != 200:
print(f"IP check returned HTTP {response.status_code}; inspect routing before proceeding")
return 1
address = str(ipaddress.ip_address(response.json()["ip"]))
if address not in allowed:
print("IP check did not match either assigned address; stop before changing the allowlist")
return 1
print(f"Verified assigned QuotaGuard egress IP: {address}")
return 0
expected = 202 if mode == "live" else 200
if response.status_code != expected:
# Response bodies can contain message data. Keep them out of shared logs.
print(f"SendGrid returned HTTP {response.status_code}; see the troubleshooting guide")
return 1
print("SendGrid accepted the message; delivery is a separate outcome" if mode == "live"
else "SendGrid accepted the sandbox request; no email was delivered")
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("mode", choices=["ip", "sandbox", "live"])
args = parser.parse_args()
try:
return run(args.mode)
except (ValueError, KeyError, TypeError):
print("Configuration or response validation failed; check required values without logging secrets", file=sys.stderr)
return 2
except (httpx.RequestError, httpx.InvalidURL):
print("Network or proxy request failed; no automatic retry was made", file=sys.stderr)
return 3
if __name__ == "__main__":
sys.exit(main())
HTTPX uses the proxy argument for an explicit proxy connection. Setting trust_env=False also ignores ambient CA variables such as SSL_CERT_FILE. If your deployment needs an approved custom trust store, pass a suitable SSL context explicitly. Do not solve certificate errors by setting verify=False. HTTPX proxies and environment behavior
The configured timeouts govern connection and network operations, not a guaranteed total wall-clock execution limit. Align your job timeout with the application’s operational requirements. HTTPX timeouts
Step 4 Verify egress from the sending runtime
After configuring QUOTAGUARDSTATIC_URL and QG_EXPECTED_IPS, run:
python sendgrid_httpx.py ip
No SendGrid API key is required for this mode. It sends one request to the QuotaGuard IP checker and verifies that the returned address matches either of the two addresses configured in QG_EXPECTED_IPS.
For example, successful output has this form:
Verified assigned QuotaGuard egress IP: 192.0.2.10
The address above is a documentation example, not a QuotaGuard address to use in your account. Your output must match your own dashboard.
Run the check in each sending runtime, such as a web service and a separate worker, if both call SendGrid. A successful laptop check does not prove that a deployed worker has the correct secret or client configuration.
Record both dashboard IPs even if repeated checks show only one. Keep both addresses available in the destination allowlist. QuotaGuard egress verification
Step 5 Configure SendGrid IP Access Management
With the approved administrator route available:
- Open the SendGrid console and go to Settings, then IP Access Management.
- Review the existing list and the caller inventory from Step 1.
- Add both assigned QuotaGuard static IPs.
- Include the required administrator and other legitimate client addresses.
- Review the complete set before saving.
- Confirm that the administrator can still access the account through the approved route.
Do not use a broad cloud-provider range merely to avoid configuring the actual caller. Do not remove existing legitimate entries as part of this setup unless their owners have approved the migration and verified their new routes.
For a first-time restriction, coordinate the change so legitimate clients are covered when the list becomes active. For an existing restriction, add the new path before switching the application. Keep the old approved path until the migration check is complete.
SendGrid’s Recent Access Attempts view can help correlate an attempt with a source IP, time, and access method. Verify the origin of an unfamiliar address before allowing it. SendGrid IP Access Management
Step 6 Validate the API request without delivery
Supply the SendGrid environment values, then run:
python sendgrid_httpx.py sandbox
This posts a complete Mail Send payload with mail_settings.sandbox_mode.enable set to the JSON boolean true. A successful response is HTTP 200. The script prints a concise confirmation without printing the API key, payload, or recipient address.
Sandbox mode does not deliver email, consume email credits, or generate Email Activity or Event Webhook events. It checks the request’s structure and parameters. It does not validate the contents of a referenced template. SendGrid sandbox mode
If the request fails, keep the restriction in place while checking the layer that failed. A proxy authentication problem, a missing API permission, and an invalid Mail Send payload need different fixes. Use the troubleshooting guide rather than automatically broadening the allowlist.
Step 7 Perform one approved live check
Only after the recipient and message are approved, set SENDGRID_CONFIRM_LIVE_SEND to YES in the trusted runtime and run:
python sendgrid_httpx.py live
This mode disables sandbox validation and requests actual delivery. Remove the confirmation value after the check so a later accidental command does not reuse it.
A normal HTTP 202 response means SendGrid accepted the request. Inspect SendGrid’s email activity or your normal event-processing path to establish what happened afterward. Do not report 202 as proof that the recipient received the message. Mail Send API reference
Do not enable automatic retries for live checks. A timeout after submission can leave the acceptance outcome uncertain. Review that outcome before sending another copy.
Use curl for an independent client check
The companion sendgrid_curl.py file runs curl with the proxy configuration and authorization header supplied through standard input. It avoids putting the secret-bearing proxy URL or API key in curl’s argument list.
Use it when you want a curl-based check without changing the application’s HTTPX configuration:
python sendgrid_curl.py ip
python sendgrid_curl.py sandbox
The helper accepts no live mode. It uses the same environment variables, disables ambient proxy bypasses, does not follow redirects, and does not retry. It does not print raw curl errors or response bodies because those can contain sensitive details.
Keep the helper beside the application example or include it in your operational toolkit. The public IP check requires only Python and curl; it needs no SendGrid key. curl configuration reference
Save this helper as sendgrid_curl.py:
#!/usr/bin/env python3
"""Use curl for a QuotaGuard IP check or a non-delivering SendGrid sandbox request."""
import argparse
import ipaddress
import json
import os
import subprocess
import sys
from urllib.parse import urlsplit
def required(name):
value = os.environ.get(name, "")
if not value or any(ord(c) < 32 or ord(c) == 127 for c in value):
raise ValueError("Missing or invalid configuration")
return value
def quote(value):
if any(ord(c) < 32 or ord(c) == 127 for c in value):
raise ValueError("Control characters are not allowed in curl configuration")
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
def configuration(mode):
proxy = required("QUOTAGUARDSTATIC_URL")
parsed = urlsplit(proxy)
if not (parsed.scheme == "http" and parsed.hostname and parsed.port
and parsed.username and parsed.password and parsed.path in ("", "/")
and not parsed.query and not parsed.fragment):
raise ValueError("Use the dashboard HTTP proxy URL")
lines = ["silent", "show-error", "connect-timeout = 10", "max-time = 30",
'proto = "=https"', 'noproxy = ""', 'retry = 0',
"proxy = " + quote(proxy), 'write-out = "\\n%{http_code}"']
if mode == "ip":
lines.append('url = "https://ip.quotaguard.com"')
else:
origins = {"global": "https://api.sendgrid.com", "eu": "https://api.eu.sendgrid.com"}
region = os.environ.get("SENDGRID_REGION", "global")
if region not in origins:
raise ValueError("Invalid SendGrid region")
payload = {
"personalizations": [{"to": [{"email": required("SENDGRID_TO_EMAIL")}]}],
"from": {"email": required("SENDGRID_FROM_EMAIL")},
"subject": "QuotaGuard SendGrid sandbox check",
"content": [{"type": "text/plain", "value": "Sandbox validation only."}],
"mail_settings": {"sandbox_mode": {"enable": True}},
}
lines += ["url = " + quote(origins[region] + "/v3/mail/send"),
'request = "POST"', 'header = "Content-Type: application/json"',
"header = " + quote("Authorization: Bearer " + required("SENDGRID_API_KEY")),
"data-raw = " + quote(json.dumps(payload))]
return "\n".join(lines) + "\n"
def run(mode):
allowed = None
if mode == "ip":
allowed = {str(ipaddress.ip_address(v.strip()))
for v in required("QG_EXPECTED_IPS").split(",")}
if len(allowed) != 2:
raise ValueError("Supply both assigned addresses")
config = configuration(mode)
# Secrets enter curl over stdin, not argv. -q must be the first curl option.
# Remove ambient proxy settings; noproxy="" also prevents an ambient bypass.
env = {k: v for k, v in os.environ.items()
if k.lower() not in {"http_proxy", "https_proxy", "all_proxy", "no_proxy"}
and not k.startswith(("SENDGRID_", "QUOTAGUARD", "QG_"))}
result = subprocess.run(["curl", "-q", "--config", "-"], input=config,
capture_output=True, text=True, env=env, timeout=35,
check=False)
if result.returncode:
print(f"curl failed with exit code {result.returncode}; details withheld to protect secrets")
return 3
body, separator, status = result.stdout.rpartition("\n")
if not separator or status != "200":
print("Request did not return expected HTTP 200; inspect routing or SendGrid configuration")
return 1
if mode == "ip":
address = str(ipaddress.ip_address(json.loads(body)["ip"]))
if address not in allowed:
print("IP check did not match an assigned QuotaGuard address")
return 1
print(f"Verified assigned QuotaGuard egress IP: {address}")
else:
print("SendGrid accepted the sandbox request; no email was delivered")
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("mode", choices=["ip", "sandbox"])
args = parser.parse_args()
try:
return run(args.mode)
except (ValueError, KeyError, TypeError, OSError, subprocess.TimeoutExpired):
print("Configuration, response, or process check failed; no automatic retry was made", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())
Adapt the pattern to your application
Background workers
Initialize or reuse a dedicated SendGrid client within the worker process according to the worker framework’s lifecycle. Do not share a live connection pool across process forks without following the client’s documented lifecycle guidance. Set the proxy secret on all workers that may process the queue.
Use a minimal controlled job to verify the route after deployment. Check the worker that sends the message, not merely the web endpoint that returns “queued.”
Other SendGrid API operations
The same routing pattern can be applied to HTTPS operations your application is authorized to perform, such as reading templates. Use the correct endpoint and permission scope. Do not expand a mail-only API key to full account access for convenience.
Keep fixed, approved destination hosts in application configuration. Do not turn a SendGrid-specific helper into a general URL-fetching endpoint that can send credentials to arbitrary destinations.
SDKs
If you use an official SendGrid SDK, inspect its supported HTTP transport options for the exact version installed. Configure the component that opens the socket. Run an egress check and an actual SendGrid sandbox call through that same transport.
A successful curl check establishes the curl route. It does not prove an SDK uses the same route. A global proxy variable can also affect unrelated libraries, so prefer a scoped client when practical.
SMTP and incoming webhooks
This guide’s code calls the HTTPS API. It does not configure SMTP, and an HTTP proxy argument should not be copied into an SMTP client’s configuration without reviewing the required protocol support.
SendGrid Event Webhooks travel from SendGrid toward your application. They are a separate inbound path. Keep your existing webhook authentication and verification controls; changing the application’s outbound route does not authenticate incoming events. SendGrid Event Webhook security
Operate and roll back deliberately
Record the expected IP pair, account owner, runtime, and change date in your internal runbook. Keep secrets in the secret manager rather than the runbook.
Monitor status codes, latency, queue age, and retry volume. Track an internal job identifier without logging full message bodies or recipient lists. Do not silently switch to direct egress if the proxy fails.
If a deployment fails, pause affected send workers when needed to avoid duplicate attempts. Restore the last known working client configuration or approved network path. Check whether requests may already have been accepted before replaying jobs.
Do not disable SendGrid restrictions or remove the administrator’s allowed route as a routine rollback. If a security policy change is necessary, obtain approval from the account owner and document the temporary state.
Completion checklist
- Both assigned QuotaGuard IPs are recorded and allowed.
- The administrator’s approved browser path still works.
- Every API-sending runtime uses the configured client.
- A proxy IP check returns an assigned address.
- A complete sandbox request returns HTTP
200. - An approved live check returns HTTP
202and its downstream outcome is inspected. - No secret-bearing URL, authorization header, or full email payload appears in shared logs.
- Retry, timeout, and rollback behavior match the application’s email requirements.
See QuotaGuard plans for shared and dedicated IP options, and SendGrid’s access management documentation for the account-side controls.
Download the reference examples · Integration overview