diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java
index d1d29d67b1b2002b7f4ecdba4f45f5ddbfe985b6..b308f719370ced23737609fef91f8b5900ca2733 100644
--- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java
@@ -221,13 +221,11 @@ public class NetcdfServiceHazardCurves extends
   static class HazardSourceModel extends SourceModel {
     public final List<Imt> imts;
     public final JsonElement map;
-    public final JsonElement sites;
 
     HazardSourceModel(NetcdfHazardCurves netcdf) {
       super(netcdf);
       imts = netcdf.netcdfData().imts().stream().sorted().collect(Collectors.toList());
       map = netcdf.netcdfData().map().toJsonTree();
-      sites = netcdf.netcdfData().sites().toJsonTree();
     }
   }
 }
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/TestSitesController.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/TestSitesController.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a1b436e5f6f33e564ff57ff32d23b7c43a545ba
--- /dev/null
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/TestSitesController.java
@@ -0,0 +1,91 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import java.nio.file.Path;
+
+import gov.usgs.earthquake.nshmp.geo.json.FeatureCollection;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesHazardCurves;
+import gov.usgs.earthquake.nshmp.netcdf.www.TestSitesService.RequestData;
+import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet;
+import gov.usgs.earthquake.nshmp.www.ResponseBody;
+
+import io.micronaut.context.annotation.Value;
+import io.micronaut.context.event.StartupEvent;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.MediaType;
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.PathVariable;
+import io.micronaut.http.annotation.QueryValue;
+import io.micronaut.runtime.event.annotation.EventListener;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.inject.Inject;
+
+@Tag(name = TestSitesService.NAME)
+@Controller("/sites")
+public class TestSitesController {
+
+  @Inject
+  private NshmpMicronautServlet servlet;
+
+  /**
+   * The path to a hazard NetCDF file
+   */
+  @Value("${nshmp-ws-static.netcdf-path}")
+  Path netcdfPath;
+
+  NetcdfServiceHazardCurves service;
+
+  /**
+   * Read in data type and return the appropriate service to use.
+   */
+  @EventListener
+  void startup(StartupEvent event) {
+    var netcdfDataFiles = new NetcdfDataFilesHazardCurves(netcdfPath);
+    service = new NetcdfServiceHazardCurves(netcdfDataFiles);
+  }
+
+  @Operation(
+      summary = "Get the GeoJSON Feature Collection of test sites",
+      description = "Returns the feature collection of test sites",
+      operationId = "test-sites")
+  @ApiResponse(
+      description = "NSHM test sites",
+      responseCode = "200",
+      content = @Content(
+          schema = @Schema(implementation = Response.class)))
+  @Get(uri = "{?raw}", produces = MediaType.APPLICATION_JSON)
+  public HttpResponse<String> doGet(
+      HttpRequest<?> http,
+      @QueryValue(defaultValue = "false") @Nullable Boolean raw) {
+    try {
+      return HttpResponse.ok(TestSitesService.handleSites(http, service.netcdf(), raw));
+    } catch (Exception e) {
+      return NetcdfWsUtils.handleError(e, TestSitesService.NAME, http.getUri().toString());
+    }
+  }
+
+  @Operation(
+      summary = "Get the GeoJSON Feature Collection of test sites",
+      description = "Returns the feature collection of test sites",
+      operationId = "test-sites-slash")
+  @ApiResponse(
+      description = "NSHM test sites",
+      responseCode = "200",
+      content = @Content(
+          schema = @Schema(implementation = Response.class)))
+  @Get(uri = "/{raw}", produces = MediaType.APPLICATION_JSON)
+  public HttpResponse<String> doGetSlash(
+      HttpRequest<?> http,
+      @PathVariable(defaultValue = "false") @Nullable Boolean raw) {
+    return doGet(http, raw);
+  }
+
+  // Swagger schema
+  private static class Response extends ResponseBody<RequestData, FeatureCollection> {};
+}
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/TestSitesService.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/TestSitesService.java
new file mode 100644
index 0000000000000000000000000000000000000000..771ef45aa50137c8343bd1d72250827eba955df7
--- /dev/null
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/TestSitesService.java
@@ -0,0 +1,49 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import com.google.gson.JsonElement;
+
+import gov.usgs.earthquake.nshmp.geo.json.FeatureCollection;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfHazardCurves;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfVersion;
+import gov.usgs.earthquake.nshmp.www.ResponseBody;
+import gov.usgs.earthquake.nshmp.www.ResponseMetadata;
+
+import io.micronaut.http.HttpRequest;
+import jakarta.inject.Singleton;
+
+/**
+ * Test sites handler for {@link TestSitesController}.
+ *
+ * @author U.S. Geological Survey
+ */
+@Singleton
+public class TestSitesService {
+  static final String NAME = "Test Sites";
+
+  static String handleSites(HttpRequest<?> http, NetcdfHazardCurves netcdf, Boolean raw) {
+    RequestData requestData = new RequestData(raw);
+    FeatureCollection sites = netcdf.netcdfData().sites();
+
+    if (requestData.raw) {
+      return sites.toJson();
+    } else {
+      var response = ResponseBody.<RequestData, JsonElement> success()
+          .name(NAME)
+          .url(http.getUri().toString())
+          .metadata(new ResponseMetadata(NetcdfVersion.appVersions()))
+          .request(requestData)
+          .response(sites.toJsonTree())
+          .build();
+
+      return NetcdfWsUtils.GSON.toJson(response);
+    }
+  }
+
+  static class RequestData {
+    public boolean raw;
+
+    RequestData(Boolean raw) {
+      this.raw = raw == null ? false : raw;
+    }
+  }
+}