Salesforce Marketing Cloud Engagement Login IP Allowlist
Two ways to use this guide: Setting it up yourself? Start with the Guided setup (Quickstart). Working with an AI coding agent? Share the page and use the in-depth programmatic reference to configure the integration, verify the connection and troubleshoot.
To let your agent set up and test the connection, provide the required proxy connection details through its secure credential mechanism rather than sharing your QuotaGuard account password. Review any changes it proposes.
Salesforce Marketing Cloud Engagement can restrict access by IP address. That’s useful for security, but it can be frustrating when your application runs somewhere that doesn’t have a predictable outbound IP.
QuotaGuard gives your application two static outbound IP addresses you can add to your Salesforce allowlist. This guide walks you through setting it up, testing the connection, and avoiding disruptions for existing users and integrations.
Scope: This page covers the MCE login IP allowlist for UI login and API access. The Salesforce AppExchange integration guide covers the separate External Client App Refresh Token IP Allowlist.
Start Here: Blocking Mode or Monitoring Mode
The login IP allowlist is a standing security control in Marketing Cloud Engagement (MCE), and Salesforce requires every account to use it in blocking mode. The steps on this page are the same whatever mode your account is in today. What changes is the order in which you make each change safe.
- Your account is already in blocking mode (Log Violations and Deny Access). Add both QuotaGuard IPs with the API login source before you switch an API client to the proxy. In blocking mode a request from an address that is not on the applicable list is denied, so moving a client first and allowlisting second causes an outage. After the switch, check the Access Log for denied attempts from that client.
- Your account is in monitoring mode (Log Allowlist Violations). Add both QuotaGuard IPs, switch the client, and use the Access Log to confirm that its requests no longer appear as violations. Salesforce recommends at least two weeks of monitoring before you enable blocking.
- The allowlist is disabled, or you are not sure. Open Security Settings and read Restrict Logins by IP Address (IP Allowlisting) first. Then follow the monitoring-mode path.
Each business unit can use its own list, so check the mode and the list source for every business unit your integration uses (Step 1).
How the MCE Login IP Allowlist Works
This summary reflects Salesforce’s documentation for the Login IP Allowlist.
- It covers UI and API access. When the allowlist is on, it applies to logins to the MCE web interface and through the API.
- Each range has a login source. A range can allow API access, User Interface access, or Any (both). An API client’s address needs the API or Any source. An administrator’s browser network needs User Interface or Any.
- Lists can be per business unit or inherited. In Enterprise 2.0 accounts, ranges can be defined at the business unit level or inherited from the Enterprise level.
- Entries are ranges. Each entry has a beginning and an ending IP address. A single address uses the same value in both fields.
- Existing sessions persist. When an administrator switches to blocking mode, users who are already logged in stay logged in. Enforcement applies at their next login.
- The Access Log records out-of-list attempts in both monitoring and blocking mode.
The Restrict Logins by IP Address (IP Allowlisting) setting has three values:
| Setting | Also called | Logins from addresses not on the list | Access Log |
|---|---|---|---|
| IP Allowlisting Disabled | Allowed | Not tracked | |
| Log Allowlist Violations | Monitoring mode | Allowed | Recorded |
| Log Violations and Deny Access | Blocking mode | Denied | Recorded |
See Enable and Configure the Login IP Allowlist.
Blocking Mode Is Required for Every Account
Salesforce requires all MCE accounts to use the login IP allowlist in blocking mode, and its notice states that customers cannot opt out. Salesforce describes blocking mode as applying to both UI login and API access. Its guidance is to review the allowlist and its Access Log regularly, and add denied addresses that are legitimate. See Salesforce’s MCE enforcement notice.
Enforcement background
The dated facts below come from Salesforce’s enforcement notice. They explain how Salesforce introduced the requirement and the timeline it announced. They are not a deadline for your account: Salesforce sets each affected account’s enforcement date individually and notifies the customer by email.
- June 2026: Salesforce published the enforcement notice (Knowledge Article 005387198). The notice describes two stages.
- Stage 1, recommended IP ranges: Salesforce began offering recommended IP ranges for administrators to review and accept. The recommendations come from successful authentications over the past six months, excluding known malicious addresses. They are offered only when the total is under 100 unique IP ranges, and they don’t override ranges already on the allowlist.
- Stage 2, phased blocking-mode enforcement: Salesforce’s notice describes phased blocking-mode enforcement by January 2027 for accounts where the login allowlist is disabled or in monitoring mode, including enterprise and business-unit configurations. Affected customers receive their specific enforcement date by email. There is no universal date that applies to every account. Accounts already using blocking mode keep their settings.
Salesforce’s notice describes what enforcement does to each configuration:
| Level | Configuration before enforcement | What Salesforce does |
|---|---|---|
| Enterprise (EID) | Blocking mode | No change |
| Enterprise (EID) | Disabled, or no IP list | Enables blocking mode and applies Salesforce-recommended baseline ranges |
| Enterprise (EID) | Monitoring mode | Enables blocking mode, keeps your list, and appends Salesforce-recommended ranges |
| Business unit (MID) with its own configuration | Blocking mode | No change |
| Business unit (MID) with its own configuration | Monitoring mode | Enables blocking mode and appends Salesforce-recommended ranges to your list |
| Business unit (MID) with its own configuration | Disabled | Enables blocking mode with the Enterprise-level list source |
All other business units inherit the Enterprise-level configuration. Recommendations are provided at the Enterprise level, not for individual business units.
If enforcement has already moved your account to blocking mode, review the ranges Salesforce appended. Recommendations are built from past successful logins, so they can include addresses a cloud workload used before and miss addresses it will use next. Replace broad or transient cloud ranges with the stable egress addresses your clients actually use.
Why MCE API Clients Need a Stable Egress IP
Blocking mode only works if your API traffic arrives from addresses you can list. Salesforce’s guidance for broad or dynamic IP ranges is to establish a stable egress path before you turn on blocking mode. Common MCE API clients whose default outbound address changes include:
- PaaS apps on Heroku, Render, Railway, Fly.io, or similar platforms, where the outbound address can change when the app is deployed, restarted, or scaled.
- Serverless functions on AWS Lambda, Vercel, Netlify Functions, Google Cloud Run, or Azure Functions without a NAT gateway.
- Containers and Kubernetes pods whose node pool egresses through ephemeral public IPs.
- CI/CD jobs and scheduled automations, such as GitHub Actions hosted runners or n8n workflows.
- Middleware and integration services that sync subscribers, data extensions, or journey events between MCE and a CRM, data warehouse, or e-commerce platform.
QuotaGuard and the other stable-egress options
Salesforce lists several ways to give MCE traffic a stable source address. You can keep a suitable path you already run, use QuotaGuard instead of it, or use QuotaGuard alongside it for some clients. QuotaGuard fits Salesforce’s egress proxy option. It covers runtimes that have no practical native static egress, such as many PaaS and serverless platforms. Teams that do have native options also choose it for portability, because the same proxy URL and IP pair go with a client when it moves between hosts or clouds; for managed infrastructure, because you don’t build or operate NAT gateways, Elastic IPs, or proxy servers; and for redundancy, because the pair runs across two Availability Zones with automatic failover.
| Approach (from Salesforce’s list) | Good fit when | What to account for |
|---|---|---|
| Static cloud egress, such as an Elastic IP | The client runs on one instance or network you control | Usually not available on PaaS or serverless runtimes |
| Centralized cloud NAT or egress gateway | Workloads already sit in a VPC you manage | You build and pay for the NAT path in each cloud and region |
| Corporate VPN | Administrators and on-premises jobs | Cloud workloads rarely egress through it |
| Enterprise gateway | API traffic already passes through a gateway or service bus | Configure the gateway’s outbound addresses |
| Egress proxy, such as QuotaGuard | PaaS, serverless, multi-cloud, or hosted clients that support an HTTP proxy, and teams that want one managed egress identity across hosts | Each client must be configured to use the proxy |
What QuotaGuard provides for an MCE API client:
- Two static IPs per subscription. Every subscription is assigned a load-balanced pair, and you allowlist both. An account can hold several subscriptions, and each has its own pair.
- Failover without a new address. The two proxy nodes run in separate AWS Availability Zones. If one fails, new connections continue through the other in about 90 seconds and egress from your other allowlisted IP. You never see a third IP.
- Addresses that stay put. The pair is reserved for the life of the subscription. Ordinary plan upgrades and downgrades don’t change it. QuotaGuard’s plan-change documentation lists two downgrade exceptions; contact support before either one. A subscription on a dedicated proxy that downgrades below Enterprise moves to a shared proxy and receives new IPs. On Heroku, AWS, and Azure marketplace subscriptions, downgrading below the Micro plan affects inbound proxies, because inbound is not included below that tier. Moving to dedicated IPs after an Enterprise upgrade is optional, and it also assigns a new pair. Switching between Static and Shield or changing region changes the pair too (see Troubleshooting).
- One configuration across hosts. The same proxy URL works from any platform that lets the HTTP client set a proxy.
- 12 AWS regions. Choose the region closest to your client.
Shared and dedicated IPs: On Starter, Production, and Business plans, the static IP pair is shared with other QuotaGuard customers. Every session is authenticated by your subscription’s own proxy credentials and traffic is never mixed, but an allowlist entry for a shared address is not a single-tenant control. MCE’s own authentication still governs access. If your security policy requires source addresses used only by your organization, Enterprise plans include dedicated IPs.
Guided setup (Quickstart)
This first setup connects a server-to-server Installed Package integration through QuotaGuard and checks that MCE accepts it. You need a Marketing Cloud Administrator to edit the allowlist, your package’s API credentials and Authorization Base URI, and access to the runtime where the application runs. For the command-line checks, have curl and jq available there.
1. Get your connection details
Open your QuotaGuard subscription’s dashboard, or create an account. Copy its two static IPv4 addresses and exact proxy connection URL. Choose Static or Shield before adding addresses, since they use different IP pairs. For Static, store the dashboard URL securely as QUOTAGUARDSTATIC_URL in the application’s runtime. Don’t put credentials in source control or share your account password. See connection details for region selection and the URL format.
2. Check the list before changing traffic
In MCE Security Settings > Edit, inspect Restrict Logins by IP Address (IP Allowlisting) and IP Allowlisting List Source for each business unit the integration uses. Keep the administrator’s approved browser range. If blocking is already enabled, add the new entries before switching the client. If monitoring is enabled, leave it in monitoring while you verify. The blocking-versus-monitoring guidance explains the distinction.
3. Add both dashboard IPs
In MCE Setup, search for Login IP Allowlist, click Create, and enter the first IP as both the beginning and ending address. Choose API as the Login Source and save. Repeat for the second IP on every applicable business-unit list, or on the Enterprise list when inherited. Don’t use the address obtained by resolving the proxy hostname, and don’t replace existing approved ranges. See the full allowlist instructions.
4. Confirm the proxy route
From the application’s runtime, run:
curl -x "$QUOTAGUARDSTATIC_URL" https://ip.quotaguard.com
The JSON ip must match one of the two dashboard addresses. Repeated calls need not show both. This checks routing, not MCE access.
5. Verify authentication and a read-only API call
Securely supply MCE_AUTH_BASE_URI, MCE_CLIENT_ID and MCE_CLIENT_SECRET from your Installed Package to that runtime. Run the curl example, which requests a token through the proxy, then calls GET /platform/v1/tokenContext through the same proxy using the returned rest_instance_url. Confirm the token response includes an access_token and rest_instance_url, and the REST request succeeds with HTTP 200 and JSON token-context data.
To inspect HTTP status with curl, add --write-out '\nHTTP %{http_code}\n' to the final REST command, not to the token request whose output is parsed as JSON. Keep tokens out of shared logs. Follow Exercise both IP addresses to repeat the token and API checks through each node; restore the normal proxy hostname afterward to retain failover.
6. Connect the application and check real work
Apply the matching language example to both its authentication and API clients, then run its actual workflow in each intended business unit. Open Login IP Allowlist > View Access Log and investigate new violations before adding any address.
In monitoring mode, follow the monitoring review, including Salesforce’s recommended two weeks, before enabling blocking. If already blocking, keep it enabled and validate each client after switching. Verify a fresh administrator login too; an existing session doesn’t test the new rules. If a check fails, use Troubleshooting rather than disabling the control.
These checks must be run against your own tenant. The documented and untested behavior section explains what has and has not been validated against live MCE.
Programmatic reference
The detailed steps below cover access-path inventory, business-unit list selection, tenant endpoints, token and API proxy configuration in multiple languages, and safe allowlist changes. Continue through Testing Your Integration and Troubleshooting to verify the actual workflow and diagnose failures. For SOAP clients, SDKs, and authorization-code integrations, use the additional integration guidance. The earlier allowlist behavior, blocking-versus-monitoring guidance, and stable-egress comparison apply to both reader paths.
Step 1: Identify Both Access Paths and the Applicable List
| Access path | Source addresses to identify |
|---|---|
| Application, middleware or scheduled job to MCE APIs | The API client’s actual public egress. For requests routed through QuotaGuard, use both addresses shown for that subscription in the QuotaGuard dashboard. |
| Administrator’s browser to MCE | The browser network’s public egress, such as an approved office or corporate VPN range. Configuring an application proxy does not configure the browser’s network path. |
Inventory each API client and scheduled workload. Keep a suitable existing stable egress path; use a proxy only where the client supports and is configured for it. Salesforce lists static cloud egress, an egress proxy, corporate VPN and cloud NAT among the available approaches in its enforcement guidance.
Include every way your organization reaches MCE APIs:
- Installed Package API integrations. Server-to-server integrations use the client credentials grant. Web app and public app integrations use the authorization code grant.
- SOAP API users that authenticate with a username and password, where your tenant still has them.
- Third-party tools that call MCE on your behalf, such as ETL connectors, reverse-ETL tools, and agency platforms. Their requests come from the vendor’s network, not yours, so ask the vendor which outbound addresses to allowlist. A QuotaGuard subscription can only cover a client that you configure to use it.
For Enterprise 2.0 accounts, open Security Settings > Edit and inspect IP Allowlisting List Source. Defined at Business Unit level uses separate business-unit ranges; Defined at Enterprise level inherits the parent account’s ranges. Confirm which list applies to each business unit your integration uses. See Define IP Allowlist Source.
Step 2: Confirm the Application’s Egress and Tenant Endpoints
Use the exact proxy connection details for your QuotaGuard subscription and configure the HTTP client making the requests. Setting an environment variable alone does not configure every client.
Get your QuotaGuard connection details
After creating a QuotaGuard account, you are redirected to your dashboard, where you can find your proxy credentials and two static IP addresses.
Choose the right proxy region: Select the QuotaGuard region closest to where your MCE API client runs to minimize latency. The region is set at sign-up. Changes after sign-up require contacting support.
Choose Static or Shield before you allowlist: QuotaGuard Static and QuotaGuard Shield use separate IP pools. If you switch products later, you get new IPs and must update the MCE allowlist. See QuotaGuard Static vs QuotaGuard Shield.
Store the connection URL as a secret in your runtime:
QUOTAGUARDSTATIC_URL="http://username:password@us-east-static-01.quotaguard.com:9293"
Use the exact QUOTAGUARDSTATIC_URL value from your dashboard. The proxy host is specific to your region.
Your QuotaGuard static IPs are individual IPv4 addresses. Allowlist each one as a single address, not as a CIDR block, and use the two addresses shown in the dashboard. Don’t look up the IP behind the proxy hostname: it is not one of your allowlist addresses.
Verify the egress address
For an application already using QUOTAGUARDSTATIC_URL, run this diagnostic from the same runtime:
curl -x "$QUOTAGUARDSTATIC_URL" https://ip.quotaguard.com
The JSON response contains an ip value that should match one of the two dashboard addresses:
{"ip":"<one of your two QuotaGuard static IPs>"}
Repeated requests can keep showing the same address. They do not prove that both addresses were exercised. Register both dashboard addresses, and validate the MCE authentication and API workflows separately.
QuotaGuard picks one of the two IPs for each new connection, not for each request. A client that reuses a connection, or one machine making several connections within a minute, will usually show one address. To check each address on purpose, see Exercise both IP addresses.
Use your tenant-specific endpoints
For an MCE server-to-server integration in Installed Packages, use its actual Authorization Base URI, appending v2/token as Salesforce documents. Use the returned rest_instance_url or soap_instance_url for resource requests. Configure the clients handling authentication and resource requests for the intended egress path. See MCE access-token instructions.
The Authorization Base URI has the form https://<your-subdomain>.auth.marketingcloudapis.com/, and it is shown on the API Integration component of your package in Installed Packages. Salesforce requires tenant-specific marketingcloudapis.com endpoints. You can’t use legacy endpoints or the exacttargetapis.com and exacttarget.com domains. See Your Subdomain and Your Tenant’s Endpoints.
Salesforce documents that the allowlist applies to API access. It does not say whether the address check runs on the v2/token request, on each REST or SOAP call, or on both. Route both through the same egress path so either check sees an allowlisted address.
Access tokens are valid for 20 minutes, and expires_in reports 1,080 seconds so you refresh two minutes early. Reuse a token for many calls instead of requesting one per call. To target a specific business unit, add account_id with that business unit’s MID to the token request.
Route token and API requests through QuotaGuard
Each example requests a server-to-server access token through the proxy, then makes a read-only REST call, GET /platform/v1/tokenContext, through the same proxy. That route needs no extra permission scope. The examples expect these environment variables:
QUOTAGUARDSTATIC_URL: your QuotaGuard connection URL.MCE_AUTH_BASE_URI: your package’s Authorization Base URI.MCE_CLIENT_IDandMCE_CLIENT_SECRET: the API integration’s credentials.
Attach the proxy to each client explicitly. Don’t rely on a global proxy setting unless your runtime guarantees it.
curl
TOKEN_JSON=$(curl -sS -x "$QUOTAGUARDSTATIC_URL" \
-H "Content-Type: application/json" \
--data @- "${MCE_AUTH_BASE_URI%/}/v2/token" <<EOF
{"grant_type":"client_credentials","client_id":"$MCE_CLIENT_ID","client_secret":"$MCE_CLIENT_SECRET"}
EOF
)
ACCESS_TOKEN=$(echo "$TOKEN_JSON" | jq -r .access_token)
REST_BASE=$(echo "$TOKEN_JSON" | jq -r .rest_instance_url)
curl -sS -x "$QUOTAGUARDSTATIC_URL" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
"${REST_BASE%/}/platform/v1/tokenContext"
The request body is read from standard input so the client secret does not appear in the process list.
Python (requests)
import os
import requests
proxy = os.environ["QUOTAGUARDSTATIC_URL"]
proxies = {"http": proxy, "https": proxy}
auth_base = os.environ["MCE_AUTH_BASE_URI"].rstrip("/")
token_resp = requests.post(
f"{auth_base}/v2/token",
json={
"grant_type": "client_credentials",
"client_id": os.environ["MCE_CLIENT_ID"],
"client_secret": os.environ["MCE_CLIENT_SECRET"],
# "account_id": 12345, # MID of the target business unit, if needed
},
proxies=proxies,
timeout=30,
)
token_resp.raise_for_status()
token = token_resp.json()
rest_base = token["rest_instance_url"].rstrip("/")
context = requests.get(
f"{rest_base}/platform/v1/tokenContext",
headers={"Authorization": f"Bearer {token['access_token']}"},
proxies=proxies,
timeout=30,
)
context.raise_for_status()
print(context.json())
Python (httpx)
import os
import httpx
auth_base = os.environ["MCE_AUTH_BASE_URI"].rstrip("/")
with httpx.Client(proxy=os.environ["QUOTAGUARDSTATIC_URL"], timeout=30) as client:
token = client.post(
f"{auth_base}/v2/token",
json={
"grant_type": "client_credentials",
"client_id": os.environ["MCE_CLIENT_ID"],
"client_secret": os.environ["MCE_CLIENT_SECRET"],
},
).raise_for_status().json()
rest_base = token["rest_instance_url"].rstrip("/")
context = client.get(
f"{rest_base}/platform/v1/tokenContext",
headers={"Authorization": f"Bearer {token['access_token']}"},
).raise_for_status()
print(context.json())
The proxy= argument needs httpx 0.26 or later. Older releases use proxies=.
Node.js (fetch with undici)
import { ProxyAgent, fetch } from 'undici';
const dispatcher = new ProxyAgent(process.env.QUOTAGUARDSTATIC_URL);
const authBase = process.env.MCE_AUTH_BASE_URI.replace(/\/$/, '');
const tokenRes = await fetch(`${authBase}/v2/token`, {
method: 'POST',
dispatcher,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.MCE_CLIENT_ID,
client_secret: process.env.MCE_CLIENT_SECRET,
}),
});
if (!tokenRes.ok) throw new Error(`MCE token request failed: ${tokenRes.status}`);
const token = await tokenRes.json();
const restBase = token.rest_instance_url.replace(/\/$/, '');
const contextRes = await fetch(`${restBase}/platform/v1/tokenContext`, {
dispatcher,
headers: { Authorization: `Bearer ${token.access_token}` },
});
console.log(await contextRes.json());
Pass the dispatcher on every call. Node’s built-in fetch and undici’s fetch ignore the HTTPS_PROXY environment variable unless Node 24 runs with NODE_USE_ENV_PROXY=1, so an environment-variable-only setup can succeed from your platform’s own address with no error.
Node.js (axios with https-proxy-agent)
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
const mce = axios.create({
httpsAgent: new HttpsProxyAgent(process.env.QUOTAGUARDSTATIC_URL),
proxy: false, // use the agent above, not axios's own env-var proxy handling
timeout: 30000,
});
const authBase = process.env.MCE_AUTH_BASE_URI.replace(/\/$/, '');
const { data: token } = await mce.post(`${authBase}/v2/token`, {
grant_type: 'client_credentials',
client_id: process.env.MCE_CLIENT_ID,
client_secret: process.env.MCE_CLIENT_SECRET,
});
const restBase = token.rest_instance_url.replace(/\/$/, '');
const { data: context } = await mce.get(`${restBase}/platform/v1/tokenContext`, {
headers: { Authorization: `Bearer ${token.access_token}` },
});
console.log(context);
Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
func main() {
proxyURL, err := url.Parse(os.Getenv("QUOTAGUARDSTATIC_URL"))
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 30 * time.Second,
}
body, _ := json.Marshal(map[string]string{
"grant_type": "client_credentials",
"client_id": os.Getenv("MCE_CLIENT_ID"),
"client_secret": os.Getenv("MCE_CLIENT_SECRET"),
})
authBase := strings.TrimSuffix(os.Getenv("MCE_AUTH_BASE_URI"), "/")
resp, err := client.Post(authBase+"/v2/token", "application/json", bytes.NewReader(body))
if err != nil {
panic(err)
}
defer resp.Body.Close()
var token struct {
AccessToken string `json:"access_token"`
RestInstanceURL string `json:"rest_instance_url"`
}
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
panic(err)
}
req, _ := http.NewRequest("GET", strings.TrimSuffix(token.RestInstanceURL, "/")+"/platform/v1/tokenContext", nil)
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
ctxResp, err := client.Do(req)
if err != nil {
panic(err)
}
defer ctxResp.Body.Close()
out, _ := io.ReadAll(ctxResp.Body)
fmt.Println(string(out))
}
Ruby (Net::HTTP)
require "json"
require "net/http"
require "uri"
proxy = URI(ENV.fetch("QUOTAGUARDSTATIC_URL"))
auth_base = ENV.fetch("MCE_AUTH_BASE_URI").chomp("/")
def proxied_request(uri, proxy, request)
Net::HTTP.start(uri.host, uri.port, proxy.host, proxy.port,
proxy.user, proxy.password,
use_ssl: true, open_timeout: 30, read_timeout: 30) do |http|
http.request(request)
end
end
token_uri = URI("#{auth_base}/v2/token")
token_req = Net::HTTP::Post.new(token_uri, "Content-Type" => "application/json")
token_req.body = {
grant_type: "client_credentials",
client_id: ENV.fetch("MCE_CLIENT_ID"),
client_secret: ENV.fetch("MCE_CLIENT_SECRET")
}.to_json
token = JSON.parse(proxied_request(token_uri, proxy, token_req).body)
context_uri = URI("#{token.fetch('rest_instance_url').chomp('/')}/platform/v1/tokenContext")
context_req = Net::HTTP::Get.new(context_uri, "Authorization" => "Bearer #{token.fetch('access_token')}")
puts proxied_request(context_uri, proxy, context_req).body
Java (HttpClient)
// Java disables Basic auth on HTTPS CONNECT tunnels by default. Run the JVM with:
// -Djdk.http.auth.tunneling.disabledSchemes=""
// Uses Jackson (com.fasterxml.jackson.core:jackson-databind) for JSON.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
public class MceToken {
public static void main(String[] args) throws Exception {
URI proxyUri = URI.create(System.getenv("QUOTAGUARDSTATIC_URL"));
// Sent on the CONNECT request to QuotaGuard, not to MCE.
String proxyAuth = "Basic " + Base64.getEncoder()
.encodeToString(proxyUri.getUserInfo().getBytes(StandardCharsets.UTF_8));
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress(proxyUri.getHost(), proxyUri.getPort())))
.build();
ObjectMapper json = new ObjectMapper();
String authBase = System.getenv("MCE_AUTH_BASE_URI").replaceAll("/$", "");
String body = json.writeValueAsString(Map.of(
"grant_type", "client_credentials",
"client_id", System.getenv("MCE_CLIENT_ID"),
"client_secret", System.getenv("MCE_CLIENT_SECRET")));
HttpRequest tokenRequest = HttpRequest.newBuilder(URI.create(authBase + "/v2/token"))
.header("Proxy-Authorization", proxyAuth)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> tokenResponse = client.send(tokenRequest, HttpResponse.BodyHandlers.ofString());
if (tokenResponse.statusCode() != 200) {
throw new IllegalStateException("MCE token request failed: " + tokenResponse.statusCode());
}
JsonNode token = json.readTree(tokenResponse.body());
String restBase = token.get("rest_instance_url").asText().replaceAll("/$", "");
HttpRequest contextRequest = HttpRequest.newBuilder(URI.create(restBase + "/platform/v1/tokenContext"))
.header("Proxy-Authorization", proxyAuth)
.header("Authorization", "Bearer " + token.get("access_token").asText())
.GET()
.build();
HttpResponse<String> contextResponse = client.send(contextRequest, HttpResponse.BodyHandlers.ofString());
System.out.println(contextResponse.body());
}
}
The JVM flag is required because Java disables proxy Basic authentication over HTTPS tunnels by default. The example sends Proxy-Authorization itself instead of registering an Authenticator on the client. In our tests on JDK 21, a client with an Authenticator dropped the request’s own Authorization header, so the Bearer token never reached the REST endpoint. Apache HttpClient 5 is an alternative that handles proxy auth without the flag.
C# (.NET HttpClient)
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
var proxyUri = new Uri(Environment.GetEnvironmentVariable("QUOTAGUARDSTATIC_URL")!);
var creds = proxyUri.UserInfo.Split(':', 2).Select(Uri.UnescapeDataString).ToArray();
var handler = new HttpClientHandler
{
Proxy = new WebProxy($"{proxyUri.Scheme}://{proxyUri.Host}:{proxyUri.Port}")
{
Credentials = new NetworkCredential(creds[0], creds[1])
},
UseProxy = true
};
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
var authBase = Environment.GetEnvironmentVariable("MCE_AUTH_BASE_URI")!.TrimEnd('/');
var tokenBody = JsonSerializer.Serialize(new
{
grant_type = "client_credentials",
client_id = Environment.GetEnvironmentVariable("MCE_CLIENT_ID"),
client_secret = Environment.GetEnvironmentVariable("MCE_CLIENT_SECRET")
});
var tokenResponse = await client.PostAsync($"{authBase}/v2/token",
new StringContent(tokenBody, Encoding.UTF8, "application/json"));
tokenResponse.EnsureSuccessStatusCode();
var token = await tokenResponse.Content.ReadFromJsonAsync<JsonElement>();
var restBase = token.GetProperty("rest_instance_url").GetString()!.TrimEnd('/');
using var request = new HttpRequestMessage(HttpMethod.Get, $"{restBase}/platform/v1/tokenContext");
request.Headers.Authorization = new("Bearer", token.GetProperty("access_token").GetString());
var context = await client.SendAsync(request);
Console.WriteLine(await context.Content.ReadAsStringAsync());
PHP (cURL)
<?php
$proxy = getenv('QUOTAGUARDSTATIC_URL');
$authBase = rtrim(getenv('MCE_AUTH_BASE_URI'), '/');
function mce_request(string $url, string $proxy, array $headers, ?string $body = null): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_PROXY => $proxy,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}
$response = curl_exec($ch);
if ($response === false) {
throw new RuntimeException(curl_error($ch));
}
return json_decode($response, true);
}
$token = mce_request("$authBase/v2/token", $proxy, ['Content-Type: application/json'], json_encode([
'grant_type' => 'client_credentials',
'client_id' => getenv('MCE_CLIENT_ID'),
'client_secret' => getenv('MCE_CLIENT_SECRET'),
]));
$restBase = rtrim($token['rest_instance_url'], '/');
print_r(mce_request("$restBase/platform/v1/tokenContext", $proxy, [
'Authorization: Bearer ' . $token['access_token'],
]));
SOAP clients, SDKs, and web app integrations
- SOAP API calls go to the
soap_instance_urlreturned with the token. Configure your SOAP library’s HTTP transport to use the same proxy as the token request. - SDKs and integration tools often ship their own HTTP client. Confirm that the SDK or tool exposes a proxy setting and that the setting covers both authentication and API calls. If it does not, the client keeps egressing from your platform’s own address.
- Web app and public app integrations use the authorization code grant. The end user authorizes your app by logging in to MCE in their own browser, and your server then exchanges the authorization code at
v2/token. Route your server’s token and API requests through QuotaGuard. Salesforce does not document which login source each step of this flow is evaluated against, and we have not tested it. Allowlist the users’ browser networks for User Interface access and your server’s QuotaGuard IPs for API access, then confirm in monitoring mode before you rely on blocking mode.
Step 3: Add Addresses for the Intended Login Source
A Marketing Cloud Administrator can add entries in MCE Setup:
- Search for allowlist and select Login IP Allowlist.
- Click Create.
- Enter the beginning and ending addresses. For one address, enter the same value in both fields.
- Choose Login Source: API for API access, User Interface for browser access, or Any for both.
- Save. Repeat for the second QuotaGuard address and any other approved ranges required for the relevant access paths.
Preserve the administrator’s approved browser range. See Add IP Addresses to the Login IP Allowlist.
For QuotaGuard, create two single-address entries, one per IP in your dashboard, each with the API login source. Use Any only if browsers on your network also egress through the same addresses, which an application proxy does not do. If business units define their own lists, add both entries to the list of every business unit the client accesses, or to the Enterprise list when business units inherit it. To change an existing entry, use Edit an IP Range.
Review Salesforce’s recommended ranges with the network owner. Accept recommendations from the top-level Enterprise business unit; child business units can view them but cannot accept them. See Add Recommended IP Ranges.
Accepted recommendations move to the Login IP Allowlist table with the description “Recommendation accepted based on tenant traffic patterns”, so you can tell them apart from the entries you created.
Step 4: Review Monitoring Results Before Enabling Blocking
Salesforce recommends at least two weeks of monitoring before switching to blocking. In Security Settings > Edit > Restrict Logins by IP Address (IP Allowlisting):
- Log Allowlist Violations permits logins and records attempts from outside the allowlist.
- Log Violations and Deny Access restricts logins to listed addresses and records out-of-list attempts.
See Configure the Login IP Allowlist and the preparation recommendation.
Open Login IP Allowlist > View Access Log to investigate violations. A monitoring-mode violation is a logged out-of-list attempt, not evidence that monitoring denied access. Confirm an address belongs to an authorized client or user before adding it. See View the Access Log.
Exercise scheduled workloads during preparation. Before enabling blocking, ensure the administrator’s current address is included. After enabling it, validate a fresh administrator login and each intended API workflow in its business-unit context. Do not use an existing browser session or an IP-check result as the sole evidence of successful MCE access.
If browser access succeeds while an API workflow fails, compare the API client’s egress, the entry’s Login Source and the applicable business-unit list. Browser success does not validate a separate API path.
If your account is already in blocking mode, the same checks apply after each change. Add the QuotaGuard entries first, then move one client at a time to the proxy and run its workflow. A denied attempt in the Access Log that shows your platform’s own address means that client is not using the proxy yet. One that shows a QuotaGuard address means the entry or its login source is missing from the list that business unit uses.
Testing Your Integration
Test in this order. Each check proves something the previous one does not.
- The client uses the proxy. From the same runtime as your client, request
https://ip.quotaguard.comthrough the proxy and confirm theipvalue is one of your two dashboard addresses. Then request it without the proxy and confirm the result changes. This proves routing only. It does not prove MCE accepts the address. - MCE accepts each QuotaGuard address. Request an access token and make a read-only API call, such as
GET /platform/v1/tokenContext, through each IP address (see below). - The real workflow succeeds. Run each scheduled job, sync, or journey integration in the business-unit context it uses, and check the Access Log for new violations from the client.
The QuotaGuard dashboard’s Log Viewer shows each proxied connection. A call that is missing from the log did not pass through the proxy.
Exercise both IP addresses
Register both IPs, and test both. Normal traffic reaches either address, so a missing entry causes intermittent failures that look random. To test one address on purpose, add -a or -b to your proxy hostname. For example, us-east-static-01.quotaguard.com becomes us-east-static-01-a.quotaguard.com. Each node hostname always egresses from the same one of your two static IPs.
PROXY_A=$(echo "$QUOTAGUARDSTATIC_URL" | sed -E 's/(@[^.:]+)/\1-a/')
PROXY_B=$(echo "$QUOTAGUARDSTATIC_URL" | sed -E 's/(@[^.:]+)/\1-b/')
curl -x "$PROXY_A" https://ip.quotaguard.com
curl -x "$PROXY_B" https://ip.quotaguard.com
Run your token request through each URL in turn. Use the node hostnames for diagnostics only. They bypass the load balancer, so production traffic sent to one node loses failover. See What if I only want one QG Static IP Address?.
Troubleshooting
API requests fail right after you enable blocking mode
The address MCE sees is not on the applicable list with an API or Any login source. Open View Access Log and find the denied attempt. If it shows your platform’s own address, the client is not using the proxy. If it shows a QuotaGuard address, add that address to the list for the business unit the client uses.
Intermittent, random-looking failures
Only one of the two QuotaGuard IPs is on the list, or it is on one business unit’s list and not another’s. Add both IPs everywhere the client authenticates, then test each node as shown in Exercise both IP addresses.
Browser login works but the API fails
Browser success does not validate the API path. Compare the API client’s egress address, the entry’s login source (User Interface does not cover API access), and the list source for that business unit.
The API works but administrators are locked out
The administrator’s browser network is not on the list with a User Interface or Any login source. Salesforce warns that if you log out before your own address is on the list, you can’t log back in. Ask another administrator whose network is allowlisted to add the range. Don’t give administrators an application’s proxy credentials to work around this.
The Access Log shows a cloud provider address instead of a QuotaGuard IP
The request bypassed the proxy. Common causes are an HTTP client that ignores proxy environment variables (for example Node’s fetch), an SDK with its own HTTP client, or a code path that only proxies the token request. Attach the proxy to every client that calls the auth, REST, or SOAP endpoints.
Requests were allowed in monitoring mode but denied after the switch
That is expected for any address recorded as a violation. Monitoring mode logs out-of-list attempts without denying them. Review the Access Log entries from the monitoring period, add the addresses that belong to authorized clients, and remove broad ranges you no longer need.
Access stopped after a QuotaGuard product or region change
Switching between Static and Shield assigns new IPs. Downgrading a subscription on a dedicated proxy below Enterprise moves it to a shared proxy with new IPs. A region move also changes the IP pair, and it moves all egress through that subscription to the new IPs. Add the new pair to the MCE allowlist before you cut over. During a QuotaGuard proxy change, old credentials stay active for a configurable overlap window, by default 7 days, so you can register and test the new IPs first.
407 Proxy Authentication Required
This is the QuotaGuard proxy, not MCE. Your QUOTAGUARDSTATIC_URL credentials are wrong or incomplete. Confirm them against your dashboard. If the password contains special characters, make sure your client decodes the URL-encoded credentials.
Token requests fail on an exacttarget.com or legacy endpoint
Salesforce requires tenant-specific marketingcloudapis.com endpoints. Use the Authorization Base URI from Installed Packages and the rest_instance_url or soap_instance_url returned with the token.
IPv6 entries
QuotaGuard egress is IPv4 only. The address MCE sees from a QuotaGuard-routed request is always one of your two IPv4 static IPs, so add those IPv4 addresses.
Keep Address Classification, Authentication and Allowlisting Separate
Salesforce has reviewed and whitelisted QuotaGuard’s current static IP addresses, resolving the anonymizing-proxy classification issue documented for AppExchange integrations. See Salesforce Now Recognizes QuotaGuard Static IPs. That resolution concerns address classification; you still configure the MCE account’s login allowlist.
The External Client App Refresh Token IP Allowlist uses another UI and protects different OAuth flows. MCE server-to-server Installed Packages use the client credentials grant and do not issue refresh tokens. Their API access still falls within MCE’s login-allowlist scope. See MCE server-to-server authentication.
MCE web app and public app integrations do receive refresh tokens. MCE’s own authorization server issues them through Installed Packages, not through an External Client App, so the External Client App allowlist is not where you register addresses for them.
For outbound HTTPS, both QuotaGuard Static and Shield tunnel the destination TLS session without decrypting its payload. Shield additionally encrypts the application-to-proxy hop. Choose the connection model according to your application’s security requirements, separately from configuring permitted source addresses. See How QuotaGuard Proxies HTTPS.
What Is Documented and What We Have Not Tested
Documented by Salesforce (linked throughout this page): the allowlist covers UI and API access; ranges carry an API, User Interface, or Any login source; lists can be defined per business unit or inherited from the Enterprise level; monitoring mode logs violations without denying access; blocking mode denies out-of-list logins and applies at each user’s next login; recommended ranges come from six months of successful authentications; the enforcement behavior described in the enforcement table above.
QuotaGuard product behavior: two static IPv4 addresses per subscription, the per-connection choice between them, failover behavior, the -a and -b node hostnames, and separate Static and Shield IP pools.
Not tested by QuotaGuard against a live MCE tenant:
- Whether MCE evaluates the address on the
v2/tokenrequest, on each REST or SOAP call, or on both. - The exact error response MCE returns to an API client from a non-allowlisted address.
- Which login source applies to each step of the web app authorization code flow.
- How quickly a newly added or edited range takes effect.
The code examples on this page follow Salesforce’s documented token request and QuotaGuard’s documented proxy configuration. They have not been run against a live MCE account. Validate each workflow in monitoring mode, or with the steps in Testing Your Integration, before you depend on blocking mode.
QuotaGuard Static vs QuotaGuard Shield
| Feature | QuotaGuard Static | QuotaGuard Shield |
|---|---|---|
| Protocol | HTTP / HTTPS / SOCKS5 | HTTPS / SOCKS5 over TLS |
| Customer-to-proxy hop | Plaintext (proxy credential sent in the clear) | TLS-encrypted |
| HTTPS payload | Tunneled end-to-end, never decrypted at the proxy | Tunneled end-to-end, never decrypted at the proxy |
| Best for | Most apps | Regulated data or environments that require TLS on every hop |
| Starting price | $19/month | $29/month |
Static is right for most MCE API clients, because the allowlist is a network-layer requirement. Choose Shield if the workload handles regulated data, such as healthcare records or payment information in subscriber data, or if your environment requires TLS between the app and the proxy itself. Prices are for direct plans on quotaguard.com. Decide before you register IPs with MCE, because the two products use different IP pairs.
Ready to Get Started?
Get in touch or create a free trial account.
Read: QuotaGuard and Salesforce Integration Guide