{@code
+ * int count = Contentstack.refreshRegions();
+ * // now getContentstackEndpoint() resolves from the freshly downloaded registry
+ * }
+ *
+ * @return the number of regions loaded from the live registry
+ * @throws RuntimeException if the download fails
+ */
+ public static int refreshRegions() {
+ return Endpoint.refresh();
+ }
+
+ /**
+ * Resolves a Contentstack service endpoint URL for the given region.
+ *
+ * {@code
+ * // Full URL
+ * String url = Contentstack.getContentstackEndpoint("eu", "contentManagement");
+ * // → "https://eu-api.contentstack.com"
+ *
+ * // Host only (for Builder.setHost)
+ * String host = Contentstack.getContentstackEndpoint("eu", "contentManagement", true);
+ * // → "eu-api.contentstack.com"
+ * }
+ *
+ * @param region region ID or alias (e.g. {@code na}, {@code eu}, {@code azure-na})
+ * @param service service key (e.g. {@code contentManagement}, {@code contentDelivery})
+ * @param omitHttps when {@code true}, strips {@code https://} from the returned URL
+ * @return the resolved URL string
+ * @throws IllegalArgumentException if region or service is unknown
+ */
+ public static String getContentstackEndpoint(String region, String service, boolean omitHttps) {
+ return Endpoint.getContentstackEndpoint(region, service, omitHttps);
+ }
+
+ /**
+ * Resolves a Contentstack service endpoint URL for the given region (with scheme).
+ *
+ * @param region region ID or alias
+ * @param service service key
+ * @return the resolved URL including {@code https://}
+ * @throws IllegalArgumentException if region or service is unknown
+ */
+ public static String getContentstackEndpoint(String region, String service) {
+ return Endpoint.getContentstackEndpoint(region, service);
+ }
+
+ /**
+ * Returns all endpoint URLs for the given region as an ordered map.
+ *
+ * @param region region ID or alias
+ * @return map of service name → URL (includes {@code https://})
+ * @throws IllegalArgumentException if region is unknown or empty
+ */
+ public static java.util.MapThis is a convenience alternative to calling {@link #setHost(String)} with + * a manually constructed hostname. + * + *
{@code
+ * Contentstack client = new Contentstack.Builder()
+ * .setAuthtoken("authtoken")
+ * .setRegion("eu")
+ * .build();
+ * }
+ *
+ * @param region region ID or alias (e.g. {@code "na"}, {@code "eu"}, {@code "azure-na"},
+ * {@code "azure-eu"}, {@code "gcp-na"}, {@code "gcp-eu"}, {@code "au"}).
+ * @return this Builder
+ * @throws IllegalArgumentException if the region is unknown or empty
+ */
+ public Builder setRegion(@NotNull String region) {
+ this.hostname = Endpoint.getContentstackEndpoint(region, "contentManagement", true);
+ return this;
+ }
+
/**
* Set port for client instance
*
diff --git a/src/main/java/com/contentstack/cms/core/Endpoint.java b/src/main/java/com/contentstack/cms/core/Endpoint.java
new file mode 100644
index 00000000..1d5afabb
--- /dev/null
+++ b/src/main/java/com/contentstack/cms/core/Endpoint.java
@@ -0,0 +1,238 @@
+package com.contentstack.cms.core;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Resolves Contentstack API endpoints for any region and service.
+ *
+ * Endpoint data is loaded from the bundled {@code regions.json} classpath resource. + * The parsed result is cached for the lifetime of the JVM process. + * If the bundled file is absent, a live download from + * {@code https://artifacts.contentstack.com/regions.json} is attempted as a fallback. + * + *
{@code
+ * // Get a specific service URL
+ * String cdnUrl = Endpoint.getContentstackEndpoint("eu", "contentDelivery");
+ * // → "https://eu-cdn.contentstack.com"
+ *
+ * // Get the host without the https:// scheme (for use with Builder.setHost)
+ * String host = Endpoint.getContentstackEndpoint("eu", "contentDelivery", true);
+ * // → "eu-cdn.contentstack.com"
+ *
+ * // Get all endpoints for a region
+ * Map all = Endpoint.getContentstackEndpoints("eu");
+ * }
+ */
+public class Endpoint {
+
+ private static final String REGIONS_URL = "https://artifacts.contentstack.com/regions.json";
+ private static final String REGIONS_RESOURCE = "regions.json";
+
+ private static volatile JsonArray regionsData = null;
+
+ private Endpoint() {}
+
+ /**
+ * Returns the URL for a specific service in the given region.
+ *
+ * @param region canonical region ID ({@code na}, {@code eu}, {@code au}, {@code azure-na},
+ * {@code azure-eu}, {@code gcp-na}, {@code gcp-eu}) or any accepted alias.
+ * Case-insensitive; {@code -} and {@code _} separators are equivalent.
+ * @param service service name (e.g. {@code contentManagement}, {@code contentDelivery})
+ * @return full URL including {@code https://}
+ * @throws IllegalArgumentException if region or service is unknown, or region is empty
+ * @throws RuntimeException if {@code regions.json} cannot be loaded
+ */
+ public static String getContentstackEndpoint(String region, String service) {
+ return getContentstackEndpoint(region, service, false);
+ }
+
+ /**
+ * Returns the URL for a specific service in the given region.
+ *
+ * @param region canonical region ID or alias
+ * @param service service name
+ * @param omitHttps when {@code true}, strips {@code https://} from the result
+ * @return URL string, with or without scheme depending on {@code omitHttps}
+ * @throws IllegalArgumentException if region or service is unknown, or region is empty
+ * @throws RuntimeException if {@code regions.json} cannot be loaded
+ */
+ public static String getContentstackEndpoint(String region, String service, boolean omitHttps) {
+ if (service == null || service.trim().isEmpty()) {
+ throw new IllegalArgumentException(
+ "Service must not be empty. Use getContentstackEndpoints(region) to retrieve all endpoints.");
+ }
+ JsonObject regionRow = resolveRegion(region);
+ JsonObject endpoints = regionRow.getAsJsonObject("endpoints");
+ if (endpoints == null || !endpoints.has(service)) {
+ throw new IllegalArgumentException(
+ "Service \"" + service + "\" not found for region \"" + regionRow.get("id").getAsString() + "\"");
+ }
+ String url = endpoints.get(service).getAsString();
+ return omitHttps ? stripHttps(url) : url;
+ }
+
+ /**
+ * Returns all endpoint URLs for the given region as an ordered map.
+ *
+ * @param region canonical region ID or alias
+ * @return map of service name → URL (includes {@code https://})
+ * @throws IllegalArgumentException if region is unknown or empty
+ * @throws RuntimeException if {@code regions.json} cannot be loaded
+ */
+ public static MapCall this if you suspect new Contentstack regions or service URLs have + * been published since the SDK JAR was built, without needing to upgrade + * the SDK version. + * + *
{@code
+ * int count = Endpoint.refresh();
+ * System.out.println("Loaded " + count + " regions from live registry.");
+ * }
+ *
+ * @return the number of regions loaded from the live registry
+ * @throws RuntimeException if the download fails or the response is not valid JSON
+ */
+ public static synchronized int refresh() {
+ try {
+ String json = downloadRegions();
+ JsonArray fresh = JsonParser.parseString(json).getAsJsonObject().getAsJsonArray("regions");
+ regionsData = fresh;
+ return fresh.size();
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "contentstack/cms: Failed to refresh regions.json from " + REGIONS_URL + ". " +
+ "Check network connectivity and try again.", e);
+ }
+ }
+
+ private static String stripHttps(String url) {
+ return url.replaceAll("^https?://", "");
+ }
+
+ /** Clears the in-memory cache. For use in tests only. */
+ static void resetCache() {
+ regionsData = null;
+ }
+}
diff --git a/src/test/java/com/contentstack/cms/TestClient.java b/src/test/java/com/contentstack/cms/TestClient.java
index 2503e39b..e246501b 100644
--- a/src/test/java/com/contentstack/cms/TestClient.java
+++ b/src/test/java/com/contentstack/cms/TestClient.java
@@ -23,6 +23,7 @@ public class TestClient {
: "managementToken99999999";
public final static String DEV_HOST = (env.get("dev_host") != null) ? env.get("dev_host").trim() : "api.contentstack.io";
+ public final static String REGION = (env.get("region") != null) ? env.get("region").trim() : "na";
public final static String VARIANT_GROUP_UID = (env.get("variantGroupUid") != null) ? env.get("variantGroupUid")
: "variantGroupUid99999999";
private static Contentstack instance;
diff --git a/src/test/java/com/contentstack/cms/UnitTestSuite.java b/src/test/java/com/contentstack/cms/UnitTestSuite.java
index 97df7a51..6c514051 100644
--- a/src/test/java/com/contentstack/cms/UnitTestSuite.java
+++ b/src/test/java/com/contentstack/cms/UnitTestSuite.java
@@ -1,6 +1,7 @@
package com.contentstack.cms;
import com.contentstack.cms.core.AuthInterceptorTest;
+import com.contentstack.cms.core.EndpointTest;
import com.contentstack.cms.stack.EnvironmentUnitTest;
import com.contentstack.cms.stack.GlobalFieldUnitTests;
import com.contentstack.cms.stack.LocaleUnitTest;
@@ -23,6 +24,7 @@
// Core tests
AuthInterceptorTest.class,
ContentstackUnitTest.class,
+ EndpointTest.class,
// Stack module tests (only public classes)
EnvironmentUnitTest.class,
diff --git a/src/test/java/com/contentstack/cms/core/EndpointTest.java b/src/test/java/com/contentstack/cms/core/EndpointTest.java
new file mode 100644
index 00000000..8584ef1c
--- /dev/null
+++ b/src/test/java/com/contentstack/cms/core/EndpointTest.java
@@ -0,0 +1,411 @@
+package com.contentstack.cms.core;
+
+import com.contentstack.cms.Contentstack;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class EndpointTest {
+
+ @BeforeEach
+ @AfterEach
+ void resetCache() {
+ Endpoint.resetCache();
+ }
+
+ // ── canonical IDs — contentDelivery ───────────────────────────────────────
+
+ @Test
+ void testNaContentDelivery() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("na", "contentDelivery"));
+ }
+
+ @Test
+ void testNaContentManagement() {
+ assertEquals("https://api.contentstack.io",
+ Endpoint.getContentstackEndpoint("na", "contentManagement"));
+ }
+
+ @Test
+ void testEuContentDelivery() {
+ assertEquals("https://eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentDelivery"));
+ }
+
+ @Test
+ void testEuContentManagement() {
+ assertEquals("https://eu-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentManagement"));
+ }
+
+ @Test
+ void testAuContentDelivery() {
+ assertEquals("https://au-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("au", "contentDelivery"));
+ }
+
+ @Test
+ void testAzureNaContentDelivery() {
+ assertEquals("https://azure-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-na", "contentDelivery"));
+ }
+
+ @Test
+ void testAzureEuContentDelivery() {
+ assertEquals("https://azure-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-eu", "contentDelivery"));
+ }
+
+ @Test
+ void testGcpNaContentDelivery() {
+ assertEquals("https://gcp-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-na", "contentDelivery"));
+ }
+
+ @Test
+ void testGcpEuContentDelivery() {
+ assertEquals("https://gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-eu", "contentDelivery"));
+ }
+
+ // ── region aliases resolve to the same endpoint ───────────────────────────
+
+ @ParameterizedTest
+ @ValueSource(strings = {"na", "us", "aws-na", "aws_na", "NA", "US", "AWS-NA", "AWS_NA"})
+ void testNaAliasesAllResolveToNaCdn(String alias) {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint(alias, "contentDelivery"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"eu", "aws-eu", "aws_eu", "EU", "AWS-EU", "AWS_EU"})
+ void testEuAliasesAllResolveToEuCdn(String alias) {
+ assertEquals("https://eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint(alias, "contentDelivery"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"au", "aws-au", "aws_au", "AU", "AWS-AU", "AWS_AU"})
+ void testAuAliasesAllResolveToAuCdn(String alias) {
+ assertEquals("https://au-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint(alias, "contentDelivery"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"azure-na", "azure_na", "AZURE-NA", "AZURE_NA"})
+ void testAzureNaAliasesResolve(String alias) {
+ assertEquals("https://azure-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint(alias, "contentDelivery"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"azure-eu", "azure_eu", "AZURE-EU", "AZURE_EU"})
+ void testAzureEuAliasesResolve(String alias) {
+ assertEquals("https://azure-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint(alias, "contentDelivery"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"gcp-na", "gcp_na", "GCP-NA", "GCP_NA"})
+ void testGcpNaAliasesResolve(String alias) {
+ assertEquals("https://gcp-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint(alias, "contentDelivery"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"gcp-eu", "gcp_eu", "GCP-EU", "GCP_EU"})
+ void testGcpEuAliasesResolve(String alias) {
+ assertEquals("https://gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint(alias, "contentDelivery"));
+ }
+
+ // ── omitHttps ─────────────────────────────────────────────────────────────
+
+ @Test
+ void testOmitHttpsFalseKeepsScheme() {
+ String url = Endpoint.getContentstackEndpoint("na", "contentDelivery", false);
+ assertTrue(url.startsWith("https://"));
+ }
+
+ @Test
+ void testOmitHttpsTrueStripsScheme() {
+ String host = Endpoint.getContentstackEndpoint("na", "contentDelivery", true);
+ assertFalse(host.startsWith("https://"));
+ assertEquals("cdn.contentstack.io", host);
+ }
+
+ @Test
+ void testOmitHttpsEuCdn() {
+ assertEquals("eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentDelivery", true));
+ }
+
+ @Test
+ void testOmitHttpsAzureNaApi() {
+ assertEquals("azure-na-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-na", "contentManagement", true));
+ }
+
+ @Test
+ void testOmitHttpsGcpEuCdn() {
+ assertEquals("gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-eu", "contentDelivery", true));
+ }
+
+ // ── various service keys ───────────────────────────────────────────────────
+
+ @Test
+ void testNaAuth() {
+ assertEquals("https://auth-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "auth"));
+ }
+
+ @Test
+ void testNaGraphqlDelivery() {
+ assertEquals("https://graphql.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "graphqlDelivery"));
+ }
+
+ @Test
+ void testNaPreview() {
+ assertEquals("https://rest-preview.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "preview"));
+ }
+
+ @Test
+ void testNaApplication() {
+ assertEquals("https://app.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "application"));
+ }
+
+ @Test
+ void testNaAssetManagement() {
+ assertEquals("https://am-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "assetManagement"));
+ }
+
+ @Test
+ void testNaDeveloperHub() {
+ assertEquals("https://developerhub-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "developerHub"));
+ }
+
+ @Test
+ void testEuAutomate() {
+ assertEquals("https://eu-prod-automations-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "automate"));
+ }
+
+ @Test
+ void testNaLaunch() {
+ assertEquals("https://launch-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "launch"));
+ }
+
+ // ── all-endpoints map ──────────────────────────────────────────────────────
+
+ @Test
+ void testGetAllEndpointsForNaNotEmpty() {
+ MapURL-structure tests verify the resolved host is wired into every outgoing
+ * request. Live-call tests make real network requests to the NA management API
+ * using credentials from {@code .env}.
+ */
+@Tag("api")
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class EndpointAPITest {
+
+ private static final String API_KEY = TestClient.API_KEY;
+ private static final String MANAGEMENT_TOKEN = TestClient.MANAGEMENT_TOKEN;
+ private static final String REGION = TestClient.REGION;
+
+ // ── URL structure — all 7 regions ────────────────────────────────────────
+
+ @Order(1)
+ @Test
+ void testNaRegionRequestUrlIsCorrect() {
+ Stack stack = clientForRegion("na").stack(API_KEY, MANAGEMENT_TOKEN);
+ Request req = stack.contentType("test").find().request();
+ assertTrue(req.url().isHttps());
+ assertEquals("api.contentstack.io", req.url().host());
+ assertEquals("v3", req.url().pathSegments().get(0));
+ }
+
+ @Order(2)
+ @Test
+ void testEuRegionRequestUrlIsCorrect() {
+ Stack stack = clientForRegion("eu").stack(API_KEY, MANAGEMENT_TOKEN);
+ Request req = stack.contentType("test").find().request();
+ assertTrue(req.url().isHttps());
+ assertEquals("eu-api.contentstack.com", req.url().host());
+ }
+
+ @Order(3)
+ @Test
+ void testAuRegionRequestUrlIsCorrect() {
+ Stack stack = clientForRegion("au").stack(API_KEY, MANAGEMENT_TOKEN);
+ Request req = stack.contentType("test").find().request();
+ assertTrue(req.url().isHttps());
+ assertEquals("au-api.contentstack.com", req.url().host());
+ }
+
+ @Order(4)
+ @Test
+ void testAzureNaRegionRequestUrlIsCorrect() {
+ Stack stack = clientForRegion("azure-na").stack(API_KEY, MANAGEMENT_TOKEN);
+ Request req = stack.contentType("test").find().request();
+ assertTrue(req.url().isHttps());
+ assertEquals("azure-na-api.contentstack.com", req.url().host());
+ }
+
+ @Order(5)
+ @Test
+ void testAzureEuRegionRequestUrlIsCorrect() {
+ Stack stack = clientForRegion("azure-eu").stack(API_KEY, MANAGEMENT_TOKEN);
+ Request req = stack.contentType("test").find().request();
+ assertTrue(req.url().isHttps());
+ assertEquals("azure-eu-api.contentstack.com", req.url().host());
+ }
+
+ @Order(6)
+ @Test
+ void testGcpNaRegionRequestUrlIsCorrect() {
+ Stack stack = clientForRegion("gcp-na").stack(API_KEY, MANAGEMENT_TOKEN);
+ Request req = stack.contentType("test").find().request();
+ assertTrue(req.url().isHttps());
+ assertEquals("gcp-na-api.contentstack.com", req.url().host());
+ }
+
+ @Order(7)
+ @Test
+ void testGcpEuRegionRequestUrlIsCorrect() {
+ Stack stack = clientForRegion("gcp-eu").stack(API_KEY, MANAGEMENT_TOKEN);
+ Request req = stack.contentType("test").find().request();
+ assertTrue(req.url().isHttps());
+ assertEquals("gcp-eu-api.contentstack.com", req.url().host());
+ }
+
+ // ── region aliases are wired correctly ────────────────────────────────────
+
+ @Order(8)
+ @Test
+ void testUsAliasResolvesToNaHost() {
+ Stack stack = clientForRegion("us").stack(API_KEY, MANAGEMENT_TOKEN);
+ assertEquals("api.contentstack.io", stack.contentType("t").find().request().url().host());
+ }
+
+ @Order(9)
+ @Test
+ void testAwsEuAliasResolvesToEuHost() {
+ Stack stack = clientForRegion("aws-eu").stack(API_KEY, MANAGEMENT_TOKEN);
+ assertEquals("eu-api.contentstack.com", stack.contentType("t").find().request().url().host());
+ }
+
+ @Order(10)
+ @Test
+ void testAzureNaUnderscoreAliasResolvesCorrectly() {
+ Stack stack = clientForRegion("azure_na").stack(API_KEY, MANAGEMENT_TOKEN);
+ assertEquals("azure-na-api.contentstack.com", stack.contentType("t").find().request().url().host());
+ }
+
+ @Order(11)
+ @Test
+ void testUppercaseRegionAliasResolvesCorrectly() {
+ Stack stack = clientForRegion("GCP-EU").stack(API_KEY, MANAGEMENT_TOKEN);
+ assertEquals("gcp-eu-api.contentstack.com", stack.contentType("t").find().request().url().host());
+ }
+
+ // ── setRegion wires host into Retrofit base URL ───────────────────────────
+
+ @Order(12)
+ @Test
+ void testSetRegionWiresCorrectBaseUrl() {
+ Contentstack client = new Contentstack.Builder()
+ .setRegion("eu")
+ .build();
+ assertTrue(client.getBaseUrl().contains("eu-api.contentstack.com"),
+ "Base URL should contain EU CMA host");
+ }
+
+ @Order(13)
+ @Test
+ void testSetRegionNaWiresDefaultHost() {
+ Contentstack client = new Contentstack.Builder()
+ .setRegion("na")
+ .build();
+ assertEquals("api.contentstack.io", client.getHost());
+ assertTrue(client.getBaseUrl().contains("api.contentstack.io"));
+ }
+
+ // ── getContentstackEndpoints() returns full map ───────────────────────────
+
+ @Order(14)
+ @Test
+ void testGetAllEndpointsForRegionNotEmpty() {
+ Map