diff --git a/.gitignore b/.gitignore
index 2daa72a3c2353e6102756aa7f9831b2169a34e4f..b23d367bcc7b5148f9f907c8857702dd81b96746 100644
--- a/.gitignore
+++ b/.gitignore
@@ -81,3 +81,4 @@ docker.secret.properties
 #Generated resources
 /.apt_generated/
 /.apt_generated_tests/
+src/test/resources/schemas
diff --git a/package.json b/package.json
index a8168eb75345dda8847d69535312dd32471ed96f..2d4fa8fc55674487174a7516749760a461ea3dfd 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
   },
   "private": true,
   "scripts": {
-    "start": "npm run generate-json-schema && npx @redocly/openapi-cli preview-docs spec/openapi.yaml -p 8093",
+    "start": "npm run generate-json-schema && npx @redocly/openapi-cli preview-docs spec/openapi.yaml -p 8097",
     "test": "npm run generate-json-schema && npx @redocly/openapi-cli lint spec/openapi.yaml",
     "generate-json-schema": "npm run clean && node src/main/js/openapi2schema.js -s spec/openapi.yaml -t src/test/resources",
     "clean": "rm -rf src/test/resources/schemas/"
diff --git a/pom.xml b/pom.xml
index 0eea4bbc85896af26a9ac3cd0bdeaa6dcba88e06..19697432a0f3195f4d63b2a8affea42889e151a5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,14 +69,6 @@
 			<groupId>io.quarkus</groupId>
 			<artifactId>quarkus-resteasy-jackson</artifactId>
 		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-oidc</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-oidc-client-filter</artifactId>
-		</dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
 			<artifactId>quarkus-rest-client</artifactId>
diff --git a/spec/openapi.yaml b/spec/openapi.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d2908ececb0f4de4fe0918a0fa1200918e3035ae
--- /dev/null
+++ b/spec/openapi.yaml
@@ -0,0 +1,282 @@
+openapi: '3.1.0'
+info:
+   version: 1.0.0
+   title: Eclipse Foundation Downloads API
+   license:
+      name: Eclipse Public License - 2.0
+      url: https://www.eclipse.org/legal/epl-2.0/
+servers:
+-  url: https://api.eclipse.org/download
+   description: Production endpoint for the download information
+tags:
+-  name: Files
+   description: Definitions in relation to retrieval of mailing lists
+-  name: Releases
+   description: Definitions in relation to retrieval of mailing lists
+paths:
+   /file/{file_id}:
+      get:
+         tags:
+         - Files
+         summary: File by ID
+         description: Returns a file indexes metadata by its file ID
+         responses:
+            200:
+               description: Success
+               content:
+                  application/json:
+                     schema:
+                        $ref: '#/components/schemas/File'
+            500:
+               description: Error while retrieving data
+   /release/{releaseType}:
+      parameters:
+      -  name: releaseType
+         in: path
+         description: The type of release to retrieve
+         required: true
+         schema:
+            type: string
+            enum:
+              - epp
+              - eclipse_packages
+      -  name: release_name
+         in: query
+         description: The name of the release to retrieve
+         required: true
+         schema:
+            type: string
+      -  name: release_version
+         in: query
+         description: The version of the release to retrieve
+         schema:
+            type: string
+      get:
+         tags:
+         - Releases
+         summary: Releases by release type
+         description: Returns a list of releases, or a single release, applicable to the query parameters
+         responses:
+            200:
+               description: Success
+               content:
+                  application/json:
+                     schema:
+                        $ref: '#/components/schemas/Release'
+            500:
+               description: Error while retrieving data
+   /release/{releaseName}:
+      parameters:
+      -  name: releaseName
+         in: path
+         description: The name of the release to retrieve
+         required: true
+         schema:
+            type: string
+      get:
+         tags:
+         - Releases
+         summary: Release Versions
+         description: Returns a list of versions available for the given release
+         responses:
+            200:
+               description: Success
+               content:
+                  application/json:
+                     schema:
+                        $ref: '#/components/schemas/Releases'
+            500:
+               description: Error while retrieving data
+   /release/{releaseName}/{releaseVersion}:
+      parameters:
+      -  name: releaseName
+         in: path
+         description: The name of the release to retrieve
+         required: true
+         schema:
+            type: string
+      -  name: releaseVersion
+         in: path
+         description: The version of the release to retrieve
+         required: true
+         schema:
+            type: string
+      get:
+         tags:
+         - Releases
+         summary: Release Version
+         description: Returns a given version for the named release
+         responses:
+            200:
+               description: Success
+               content:
+                  application/json:
+                     schema:
+                        $ref: '#/components/schemas/Release'
+            500:
+               description: Error while retrieving data
+
+components:
+   schemas:
+      NullableString:
+         description: A nullable String type value
+         oneOf:
+          - type: 'null'
+          - type: string
+      DateTime:
+         type: string
+         format: datetime
+         description: |
+            Date string in the RFC 3339 format. Example, `1990-12-31T15:59:60-08:00`.
+
+            More on this standard can be read at https://tools.ietf.org/html/rfc3339.
+      Files:
+         type: array
+         items:
+            $ref: '#/components/schemas/File'
+
+      File:
+         type: object
+         properties:
+            file_id:
+               type: integer
+               description: placeholder
+            file_name:
+               type: string
+               description: placeholder
+            download_count:
+               type: integer
+               description: placeholder
+            size_disk_bytes:
+               type: integer
+               description: placeholder
+            timestamp_disk:
+               type: integer
+               description: placeholder
+            md5sum:
+               $ref: '#/components/schemas/NullableString'
+               description: placeholder
+            sha1sum:
+               $ref: '#/components/schemas/NullableString'
+               description: placeholder
+
+      Releases:
+        type: array
+        items:
+          $ref: '#/components/schemas/Release'
+
+      Release:
+        type: object
+        properties:
+          release_name:
+            type: string
+            description: The name of the release for the packages
+          release_version:
+            type: string
+            description: The version of the release for the packages
+          packages:
+            type: object
+            properties:
+              java-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              jee-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              cpp-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              committers-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              php-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              dsl-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              embedcpp-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              modeling-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              rcp-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              parallel-package:
+                $ref: '#/components/schemas/ReleasePackage'
+              scout-package:
+                $ref: '#/components/schemas/ReleasePackage'
+
+      ReleasePackage:
+        type: object
+        properties:
+          name:
+            type: string
+            description: The name of the release package
+          package_bugzilla_id:
+            type: string
+            description: Placeholder
+          download_count:
+            type: string
+            description: Number of times this package has been downloaded
+          website_url:
+            type: string
+            description: The public URL for the package that includes more information about the release.
+          incubating:
+            type: boolean
+            description: placeholder
+          class:
+            type: string
+            description: placeholder
+          body:
+            type: string
+            description: Description of the release package
+          features:
+            type: array
+            items:
+              type: string
+          files:
+            type: object
+            properties:
+              mac:
+                $ref: '#/components/schemas/ReleaseFiles'
+              windows:
+                $ref: '#/components/schemas/ReleaseFiles'
+              linux:
+                $ref: '#/components/schemas/ReleaseFiles'
+
+      ReleaseFiles:
+        type: object
+        properties:
+          32:
+            oneOf:
+              - $ref: '#/components/schemas/ReleaseFile'
+              - type: 'null'
+          64:
+            oneOf:
+              - $ref: '#/components/schemas/ReleaseFile'
+              - type: 'null'
+      
+      ReleaseFile:
+        type: object
+        properties:
+          url:
+            type: string
+            description: The publicly available URL for the package
+          size:
+            type: string
+            description: the size of the file in bytes
+          file_id:
+            $ref: '#/components/schemas/NullableString'
+            description: The internal ID of the file
+          file_url:
+            type: string
+            description: the public facing URL for the file (no mirror)
+          download_count:
+            type: string
+            description: The number of times this file has been downloaded 
+          checksum:
+            type: object
+            properties:
+              sha1:
+                $ref: '#/components/schemas/NullableString'
+                description: the sha1 checksum for the release file
+              md5:
+                $ref: '#/components/schemas/NullableString'
+                description: the md5 checksum for the release file
+              sha512:
+                $ref: '#/components/schemas/NullableString'
+                description: the sha512 checksum for the release file
diff --git a/src/main/java/org/eclipsefoundation/downloads/api/DrupalAPI.java b/src/main/java/org/eclipsefoundation/downloads/api/DrupalAPI.java
index ef7eaec7dd736719879c758ad82626f679da8fdb..dcf98c828ab1462ca2a750752938357209f7e658 100644
--- a/src/main/java/org/eclipsefoundation/downloads/api/DrupalAPI.java
+++ b/src/main/java/org/eclipsefoundation/downloads/api/DrupalAPI.java
@@ -6,6 +6,7 @@ import javax.ws.rs.PathParam;
 
 import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
 import org.eclipsefoundation.downloads.models.ReleaseTrackerPackages;
+import org.eclipsefoundation.downloads.models.TrackedReleases;
 import org.jboss.resteasy.annotations.GZIP;
 
 @GZIP
@@ -13,6 +14,10 @@ import org.jboss.resteasy.annotations.GZIP;
 public interface DrupalAPI {
 
     @GET
-    @Path("downloads/packages/admin/release_tracker/json/{releasePackageName}/all")
-    ReleaseTrackerPackages get(@PathParam("releasePackageName") String releasePackageName);
+    @Path("downloads/packages/admin/release_tracker/json/")
+    TrackedReleases getTrackedReleases();
+
+    @GET
+    @Path("downloads/packages/admin/release_tracker/json/{releaseName}%20{version}/all")
+    ReleaseTrackerPackages get(@PathParam("releaseName") String releaseName, @PathParam("version") String version);
 }
diff --git a/src/main/java/org/eclipsefoundation/downloads/dto/DownloadFileIndex.java b/src/main/java/org/eclipsefoundation/downloads/dto/DownloadFileIndex.java
index 42e451aebc6d0c32038a12d07bc606bd847c5f2e..5b32d810ef3a5f4bee00b72d2c6d57c0cf1db4b1 100644
--- a/src/main/java/org/eclipsefoundation/downloads/dto/DownloadFileIndex.java
+++ b/src/main/java/org/eclipsefoundation/downloads/dto/DownloadFileIndex.java
@@ -8,6 +8,7 @@ import javax.persistence.Table;
 import javax.validation.constraints.NotNull;
 import javax.ws.rs.core.MultivaluedMap;
 
+import org.apache.commons.lang3.StringUtils;
 import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
 import org.eclipsefoundation.downloads.namespaces.DownloadsUrlParameterNames;
 import org.eclipsefoundation.persistence.dto.BareNode;
@@ -148,9 +149,9 @@ public class DownloadFileIndex extends BareNode {
             if (isRoot) {
                 // ID check
                 String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
-                if (id != null) {
+                if (StringUtils.isNumeric(id)) {
                     stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".fileId = ?",
-                            new Object[] { id }));
+                            new Object[] { Integer.valueOf(id) }));
                 }
                 // file name check
                 String fileName = params.getFirst(DownloadsUrlParameterNames.FILE_NAME.getName());
diff --git a/src/main/java/org/eclipsefoundation/downloads/models/ReleaseTrackerPackages.java b/src/main/java/org/eclipsefoundation/downloads/models/ReleaseTrackerPackages.java
index 7503dcd7bb3dce4f3eadc36f4d3302bece732f19..8627bbf70a2ba9f805be34d6d6616820f8f0ccf7 100644
--- a/src/main/java/org/eclipsefoundation/downloads/models/ReleaseTrackerPackages.java
+++ b/src/main/java/org/eclipsefoundation/downloads/models/ReleaseTrackerPackages.java
@@ -114,7 +114,7 @@ public abstract class ReleaseTrackerPackages {
 
         public abstract List<String> getFeatures();
 
-        public abstract Object getFiles();
+        public abstract OSReleases getFiles();
 
         public static Builder builder() {
             return new AutoValue_ReleaseTrackerPackages_ReleaseTrackerPackage.Builder();
@@ -140,7 +140,7 @@ public abstract class ReleaseTrackerPackages {
 
             public abstract Builder setFeatures(List<String> features);
 
-            public abstract Builder setFiles(Object files);
+            public abstract Builder setFiles(OSReleases files);
 
             public abstract ReleaseTrackerPackage build();
         }
@@ -206,6 +206,7 @@ public abstract class ReleaseTrackerPackages {
 
         public abstract String getSize();
 
+        @Nullable
         public abstract String getFileId();
 
         public abstract String getFileUrl();
@@ -225,7 +226,7 @@ public abstract class ReleaseTrackerPackages {
 
             public abstract Builder setSize(String size);
 
-            public abstract Builder setFileId(String fileId);
+            public abstract Builder setFileId(@Nullable String fileId);
 
             public abstract Builder setFileUrl(String fileUrl);
 
@@ -240,10 +241,13 @@ public abstract class ReleaseTrackerPackages {
     @AutoValue
     @JsonDeserialize(builder = AutoValue_ReleaseTrackerPackages_Checksums.Builder.class)
     public abstract static class Checksums {
+        @Nullable
         public abstract String getMd5();
 
+        @Nullable
         public abstract String getSha1();
 
+        @Nullable
         public abstract String getSha512();
 
         public static Builder builder() {
@@ -253,11 +257,11 @@ public abstract class ReleaseTrackerPackages {
         @AutoValue.Builder
         @JsonPOJOBuilder(withPrefix = "set")
         public abstract static class Builder {
-            public abstract Builder setMd5(String md5);
+            public abstract Builder setMd5(@Nullable String md5);
 
-            public abstract Builder setSha1(String sha1);
+            public abstract Builder setSha1(@Nullable String sha1);
 
-            public abstract Builder setSha512(String sha512);
+            public abstract Builder setSha512(@Nullable String sha512);
 
             public abstract Checksums build();
         }
diff --git a/src/main/java/org/eclipsefoundation/downloads/models/ReleaseVersionPackages.java b/src/main/java/org/eclipsefoundation/downloads/models/ReleaseVersionPackages.java
new file mode 100644
index 0000000000000000000000000000000000000000..bbcc147f8422d3094147abd87b85136c66b7f4b1
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/downloads/models/ReleaseVersionPackages.java
@@ -0,0 +1,31 @@
+package org.eclipsefoundation.downloads.models;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+@JsonDeserialize(builder = AutoValue_ReleaseVersionPackages.Builder.class)
+public abstract class ReleaseVersionPackages {
+    public abstract String getReleaseName();
+
+    public abstract String getReleaseVersion();
+
+    public abstract ReleaseTrackerPackages getPackages();
+
+    public static Builder builder() {
+        return new AutoValue_ReleaseVersionPackages.Builder();
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setReleaseName(String releaseName);
+
+        public abstract Builder setReleaseVersion(String releaseVersion);
+
+        public abstract Builder setPackages(ReleaseTrackerPackages packages);
+
+        public abstract ReleaseVersionPackages build();
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/downloads/models/TrackedReleases.java b/src/main/java/org/eclipsefoundation/downloads/models/TrackedReleases.java
index 12d763a5c3ce1bac138c48428c305035d4654479..b84fc339a1643b1f1624ad8440a2c75f0c7837d4 100644
--- a/src/main/java/org/eclipsefoundation/downloads/models/TrackedReleases.java
+++ b/src/main/java/org/eclipsefoundation/downloads/models/TrackedReleases.java
@@ -31,7 +31,7 @@ public abstract class TrackedReleases {
     public abstract static class Release {
         public abstract String getName();
 
-        public abstract List<ReleasePackage> getReleasePackages();
+        public abstract List<ReleaseVersion> getVersions();
 
         public static Builder builder() {
             return new AutoValue_TrackedReleases_Release.Builder();
@@ -42,15 +42,15 @@ public abstract class TrackedReleases {
         public abstract static class Builder {
             public abstract Builder setName(String name);
 
-            public abstract Builder setReleasePackages(List<ReleasePackage> releases);
+            public abstract Builder setVersions(List<ReleaseVersion> version);
 
             public abstract Release build();
         }
     }
 
     @AutoValue
-    @JsonDeserialize(builder = AutoValue_TrackedReleases_ReleasePackage.Builder.class)
-    public abstract static class ReleasePackage {
+    @JsonDeserialize(builder = AutoValue_TrackedReleases_ReleaseVersion.Builder.class)
+    public abstract static class ReleaseVersion {
         public abstract String getName();
 
         public abstract String getType();
@@ -61,7 +61,7 @@ public abstract class TrackedReleases {
         public abstract Boolean getIsCurrent();
 
         public static Builder builder() {
-            return new AutoValue_TrackedReleases_ReleasePackage.Builder();
+            return new AutoValue_TrackedReleases_ReleaseVersion.Builder();
         }
 
         @AutoValue.Builder
@@ -75,7 +75,7 @@ public abstract class TrackedReleases {
 
             public abstract Builder setIsCurrent(@Nullable Boolean isCurrent);
 
-            public abstract ReleasePackage build();
+            public abstract ReleaseVersion build();
         }
     }
 }
diff --git a/src/main/java/org/eclipsefoundation/downloads/namespaces/DownloadsUrlParameterNames.java b/src/main/java/org/eclipsefoundation/downloads/namespaces/DownloadsUrlParameterNames.java
index a3dee86d8c6a6f788f4512226e081ff238d1d634..781c72e29d14dd079851a16da1f85f31ea5eb7bc 100644
--- a/src/main/java/org/eclipsefoundation/downloads/namespaces/DownloadsUrlParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/downloads/namespaces/DownloadsUrlParameterNames.java
@@ -11,14 +11,19 @@ import org.eclipsefoundation.core.namespace.UrlParameterNamespace;
 
 @Singleton
 public final class DownloadsUrlParameterNames implements UrlParameterNamespace {
+    public static final String RELEASE_NAME_VALUE = "release_name";
+    public static final String RELEASE_VERSION_VALUE = "release_version";
+
     public static final UrlParameter MIRROR_ID = new UrlParameter("mirror_id");
     public static final UrlParameter CCODE = new UrlParameter("ccode");
     public static final UrlParameter REMOTE_ADDR = new UrlParameter("remote_addr");
     public static final UrlParameter REMOTE_HOST = new UrlParameter("remote_host");
     public static final UrlParameter FILE_NAME = new UrlParameter("file_name");
+    public static final UrlParameter RELEASE_NAME = new UrlParameter(RELEASE_NAME_VALUE);
+    public static final UrlParameter RELEASE_VERSION = new UrlParameter(RELEASE_VERSION_VALUE);
 
-    private static final List<UrlParameter> params = Collections
-            .unmodifiableList(Arrays.asList(MIRROR_ID, CCODE, REMOTE_ADDR, REMOTE_HOST, FILE_NAME));
+    private static final List<UrlParameter> params = Collections.unmodifiableList(
+            Arrays.asList(MIRROR_ID, CCODE, REMOTE_ADDR, REMOTE_HOST, FILE_NAME, RELEASE_NAME, RELEASE_VERSION));
 
     @Override
     public List<UrlParameter> getParameters() {
diff --git a/src/main/java/org/eclipsefoundation/downloads/resources/DownloadsResource.java b/src/main/java/org/eclipsefoundation/downloads/resources/DownloadsResource.java
index 1df0915ddd7fb543071a1fe93be6170e2860a84c..a342b5ccbb2762a321ca58ce7824edbca41df992 100644
--- a/src/main/java/org/eclipsefoundation/downloads/resources/DownloadsResource.java
+++ b/src/main/java/org/eclipsefoundation/downloads/resources/DownloadsResource.java
@@ -1,26 +1,32 @@
 package org.eclipsefoundation.downloads.resources;
 
+import java.util.List;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 
+import org.apache.commons.lang3.StringUtils;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.model.Error;
 import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.core.namespace.DefaultUrlParameterNames;
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.downloads.api.DrupalAPI;
 import org.eclipsefoundation.downloads.dto.DownloadFileIndex;
 import org.eclipsefoundation.downloads.models.ReleaseTrackerPackages;
+import org.eclipsefoundation.downloads.models.ReleaseVersionPackages;
 import org.eclipsefoundation.downloads.models.TrackedReleases.Release;
-import org.eclipsefoundation.downloads.models.TrackedReleases.ReleasePackage;
-import org.eclipsefoundation.downloads.services.RawHttpClientService;
+import org.eclipsefoundation.downloads.namespaces.DownloadsUrlParameterNames;
 import org.eclipsefoundation.downloads.services.ReleaseTrackerService;
 import org.eclipsefoundation.persistence.dao.impl.DefaultHibernateDao;
 import org.eclipsefoundation.persistence.model.RDBMSQuery;
@@ -46,7 +52,7 @@ public class DownloadsResource {
     @Inject
     @RestClient
     DrupalAPI api;
-    
+
     @Inject
     ReleaseTrackerService releaseTrackerService;
 
@@ -59,31 +65,80 @@ public class DownloadsResource {
     @GET
     @Path("file/{id}")
     public Response byID(@PathParam("id") String id) {
-
+        // filter by ID for the file index
         MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
         params.add(DefaultUrlParameterNames.ID.getName(), id);
-        return Response.ok(dao.get(new RDBMSQuery<>(wrap, filters.get(DownloadFileIndex.class), params))).build();
+        // get the index, and make sure we have a result
+        List<DownloadFileIndex> dfis = dao.get(new RDBMSQuery<>(wrap, filters.get(DownloadFileIndex.class), params));
+        if (dfis.isEmpty()) {
+            String message = String.format("No DownloadFileIndex found with id '%s'", id);
+            LOGGER.debug(message);
+            return new Error(404, message).asResponse();
+        }
+        return Response.ok(dfis.get(0)).build();
+    }
+
+    @GET
+    @Path("release/{type:(epp|eclipse_packages)}")
+    public Response packageRelease(@QueryParam(DownloadsUrlParameterNames.RELEASE_NAME_VALUE) String releaseName,
+            @QueryParam(DownloadsUrlParameterNames.RELEASE_VERSION_VALUE) String releaseVersion) {
+        if (StringUtils.isNotBlank(releaseName) && StringUtils.isNotBlank(releaseVersion)) {
+            return releaseVersion(releaseName, releaseVersion);
+        } else if (StringUtils.isNotBlank(releaseName)) {
+            return release(releaseName);
+        }
+        return new Error(400, "At least the release_name query parameter is required to use this endpoint")
+                .asResponse();
+    }
+
+    @GET
+    @Path("release/{releaseName}")
+    public Response release(@PathParam("releaseName") String releaseName) {
+        Optional<Release> r = releaseTrackerService.getReleaseByName(releaseName);
+        if (r.isEmpty()) {
+            String message = String.format("No release named '%s' found in store, cannot continue", releaseName);
+            LOGGER.debug(message);
+            return new Error(404, message).asResponse();
+        }
+        // get all of the release version packages to return
+        try {
+            return Response.ok(r.get().getVersions().stream()
+                    .map(v -> ReleaseVersionPackages.builder().setPackages(api.get(releaseName, v.getName()))
+                            .setReleaseName(releaseName).setReleaseVersion(v.getName()).build())
+                    .collect(Collectors.toList())).build();
+        } catch (WebApplicationException e) {
+            String message = String.format("Error while retrieving versioned packages for release '%s': %s",
+                    releaseName, e.getLocalizedMessage());
+            LOGGER.error(message);
+            return new Error(503, message).asResponse();
+        }
     }
 
     @GET
-    @Path("release/{releaseName:[^%]+}%20{packageName}")
-    public Response release(@PathParam("releaseName") String releaseName,
-            @PathParam("packageName") String packageName) {
+    @Path("release/{releaseName}/{version}")
+    public Response releaseVersion(@PathParam("releaseName") String releaseName, @PathParam("version") String version) {
         Optional<Release> r = releaseTrackerService.getReleaseByName(releaseName);
         if (r.isEmpty()) {
-            return Response.status(404).build();
+            String message = String.format("No release named '%s' found in store, cannot continue", releaseName);
+            LOGGER.debug(message);
+            return new Error(404, message).asResponse();
         }
-        Optional<ReleasePackage> releasePackage = r.get().getReleasePackages().stream()
-                .filter(rp -> rp.getName().equals(packageName)).findFirst();
-        if (releasePackage.isEmpty()) {
-            return Response.status(404).build();
+        // check if there is a release version that matches the given version
+        if (r.get().getVersions().stream().noneMatch(rp -> rp.getName().equals(version))) {
+            String message = String.format("No version named '%s' found for release named '%s'", version, releaseName);
+            LOGGER.debug(message);
+            return new Error(404, message).asResponse();
 
         }
         // get the release package from the Drupal API
-        ReleaseTrackerPackages rtp = api.get(releaseName + " " + packageName);
+        ReleaseTrackerPackages rtp = api.get(releaseName, version);
         if (rtp == null) {
-            return Response.status(503).build();
+            String message = String.format("Could not find package definitions for release '%s', version '%s'",
+                    releaseName, version);
+            LOGGER.error(message);
+            return new Error(503, message).asResponse();
         }
-        return Response.ok(rtp).build();
+        return Response.ok(ReleaseVersionPackages.builder().setPackages(rtp).setReleaseName(releaseName)
+                .setReleaseVersion(version).build()).build();
     }
 }
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/downloads/services/RawHttpClientService.java b/src/main/java/org/eclipsefoundation/downloads/services/RawHttpClientService.java
deleted file mode 100644
index 7cc20c271a3b97acd199a7e4cd6a48b81d75c12d..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/downloads/services/RawHttpClientService.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.eclipsefoundation.downloads.services;
-
-import java.util.List;
-
-import javax.ws.rs.core.Response;
-
-public interface RawHttpClientService {
-
-    Response getResponse(String url);
-
-    <T> T getTypedResponse(String url, Class<T> type);
-
-    <T> List<T> getTypedResponseList(String url, Class<T> type);
-}
diff --git a/src/main/java/org/eclipsefoundation/downloads/services/impl/DefaultHttpClientService.java b/src/main/java/org/eclipsefoundation/downloads/services/impl/DefaultHttpClientService.java
deleted file mode 100644
index 22c05dacf2a1b78e20acefadb57676cf6cb8c3b5..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/downloads/services/impl/DefaultHttpClientService.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package org.eclipsefoundation.downloads.services.impl;
-
-import java.io.InputStream;
-import java.util.Arrays;
-import java.util.List;
-
-import javax.enterprise.context.ApplicationScoped;
-import javax.inject.Inject;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.core.Response;
-
-import org.eclipsefoundation.downloads.services.RawHttpClientService;
-import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
-import org.jboss.resteasy.plugins.interceptors.AcceptEncodingGZIPFilter;
-import org.jboss.resteasy.plugins.interceptors.GZIPDecodingInterceptor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-/**
- * Default implementation for getting HTTP requests by dynamic URL.
- * 
- * @author Martin Lowe
- *
- */
-@ApplicationScoped
-public class DefaultHttpClientService implements RawHttpClientService {
-    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpClientService.class);
-
-    @Inject
-    ObjectMapper objectMapper;
-
-    private Client client;
-
-    @Override
-    public Response getResponse(String url) {
-        LOGGER.error("Getting raw data for: {}", url);
-        return getClient().target(url).request().accept("application/json").get();
-    }
-
-    @Override
-    public <T> T getTypedResponse(String url, Class<T> type) {
-        List<T> out = readInValue(getResponse(url), type);
-        return out == null || out.isEmpty() ? null : out.get(0);
-    }
-
-    @Override
-    public <T> List<T> getTypedResponseList(String url, Class<T> type) {
-        return readInValue(getResponse(url), type);
-    }
-
-    /**
-     * Retrieve an HTTP client with optional SSL support enabled.
-     * 
-     * TODO: This should use an executor for threaded access, as otherwise this may have issues with threaded access.
-     * 
-     * @return an HTTP client for retrieving raw responses.
-     */
-    @SuppressWarnings("java:S3252")
-    private Client getClient() {
-        if (client == null) {
-            client = ResteasyClientBuilder.newBuilder().register(AcceptEncodingGZIPFilter.class)
-                    .register(GZIPDecodingInterceptor.class).build();
-        }
-        return client;
-    }
-
-    @SuppressWarnings({ "unchecked" })
-    private <T> List<T> readInValue(Response r, Class<T> type) {
-        Object o = r.getEntity();
-        try {
-            if (o instanceof InputStream) {
-                return objectMapper.readerForListOf(String.class).readValue((InputStream) o);
-            } else if (type.isAssignableFrom(o.getClass())) {
-                return Arrays.asList((T) o);
-            } else if (o instanceof List) {
-                return (List<T>) o;
-            }
-            throw new RuntimeException("Unexpected response body type, could not convert body to list");
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-}
diff --git a/src/main/java/org/eclipsefoundation/downloads/services/impl/DefaultReleaseTrackerService.java b/src/main/java/org/eclipsefoundation/downloads/services/impl/DefaultReleaseTrackerService.java
index 69d490bd6d36d5a3cd29764ac3946ef320d06d4f..987c306ae942022e325d91990358b989ef7d693e 100644
--- a/src/main/java/org/eclipsefoundation/downloads/services/impl/DefaultReleaseTrackerService.java
+++ b/src/main/java/org/eclipsefoundation/downloads/services/impl/DefaultReleaseTrackerService.java
@@ -10,7 +10,7 @@ import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipsefoundation.downloads.models.TrackedReleases;
 import org.eclipsefoundation.downloads.models.TrackedReleases.Release;
-import org.eclipsefoundation.downloads.models.TrackedReleases.ReleasePackage;
+import org.eclipsefoundation.downloads.models.TrackedReleases.ReleaseVersion;
 import org.eclipsefoundation.downloads.services.ReleaseTrackerService;
 
 @ApplicationScoped
@@ -21,7 +21,7 @@ public class DefaultReleaseTrackerService implements ReleaseTrackerService {
     @PostConstruct
     void init() {
         List<Release> tmp = new ArrayList<>();
-        tmp.add(Release.builder().setName("2021-12").setReleasePackages(generateFakePackages()).build());
+        tmp.add(Release.builder().setName("2021-12").setVersions(generateFakePackages()).build());
         this.releases = TrackedReleases.builder().setReleases(tmp).build();
     }
 
@@ -33,7 +33,7 @@ public class DefaultReleaseTrackerService implements ReleaseTrackerService {
     @Override
     public List<Release> getActiveReleases() {
         return releases().getReleases().stream()
-                .filter(r -> r.getReleasePackages().stream().anyMatch(rp -> rp.getIsCurrent()))
+                .filter(r -> r.getVersions().stream().anyMatch(ReleaseVersion::getIsCurrent))
                 .collect(Collectors.toList());
     }
 
@@ -42,12 +42,12 @@ public class DefaultReleaseTrackerService implements ReleaseTrackerService {
         return releases;
     }
 
-    private List<ReleasePackage> generateFakePackages() {
-        List<ReleasePackage> out = new ArrayList<>();
-        out.add(ReleasePackage.builder().setName("r").setType("release")
+    private List<ReleaseVersion> generateFakePackages() {
+        List<ReleaseVersion> out = new ArrayList<>();
+        out.add(ReleaseVersion.builder().setName("r").setType("release")
                 .setUrl("https://www.eclipse.org/downloads/packages/admin/release_tracker/json/2021-12%20r/all")
                 .setIsCurrent(true).build());
-        out.add(ReleasePackage.builder().setName("m1").setType("milestone")
+        out.add(ReleaseVersion.builder().setName("m1").setType("milestone")
                 .setUrl("https://www.eclipse.org/downloads/packages/admin/release_tracker/json/2021-12%20m1/all")
                 .build());
         return out;
diff --git a/src/main/js/openapi2schema.js b/src/main/js/openapi2schema.js
new file mode 100644
index 0000000000000000000000000000000000000000..2680ae038916273e5f7f6ab942ac47bff04fc3e5
--- /dev/null
+++ b/src/main/js/openapi2schema.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2021 Eclipse
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * Author: Martin Lowe <martin.lowe@eclipsefoundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema');
+const Resolver = require('@stoplight/json-ref-resolver');
+const yaml = require('js-yaml');
+const fs = require('fs');
+const decamelize = require('decamelize');
+const args = require('yargs')
+  .option('s', {
+    alias: 'src',
+    desc: 'The fully qualified path to the YAML spec.',
+  })
+  .option('t', {
+    alias: 'target',
+    desc: 'The fully qualified path to write the JSON schema to',
+  }).argv;
+if (!args.s || !args.t) {
+  process.exit(1);
+}
+
+run();
+
+/**
+ * Generates JSON schema files for consumption of the Java tests.
+ */
+async function run() {
+  try {
+    // load in the openapi yaml spec as an object
+    const doc = yaml.load(fs.readFileSync(args.s, 'utf8'));
+    // resolve $refs in openapi spec
+    let resolvedInp = await new Resolver.Resolver().resolve(doc);
+    const out = toJsonSchema(resolvedInp.result);
+    // if folder doesn't exist, create it
+    if (!fs.existsSync(`${args.t}/schemas`)) {
+      fs.mkdirSync(`${args.t}/schemas`);
+    }
+    // for each of the schemas, generate a JSON schema file
+    for (let schemaName in out.components.schemas) {
+      fs.writeFileSync(`${args.t}/schemas/${decamelize(schemaName, { separator: '-' })}-schema.json`, JSON.stringify(out.components.schemas[schemaName]));
+    }
+  } catch (e) {
+    console.log(e);
+  }
+}
diff --git a/src/test/java/org/eclipsefoundation/downloads/resources/DownloadsResourceTest.java b/src/test/java/org/eclipsefoundation/downloads/resources/DownloadsResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..616af102da8ea6c675c25e029def9cd043973d27
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/downloads/resources/DownloadsResourceTest.java
@@ -0,0 +1,122 @@
+package org.eclipsefoundation.downloads.resources;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.downloads.namespaces.DownloadsUrlParameterNames;
+import org.eclipsefoundation.downloads.test.helper.SchemaNamespaceHelper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@TestInstance(Lifecycle.PER_CLASS)
+@QuarkusTest
+public class DownloadsResourceTest {
+    public static final String DOWNLOADS_BASE_URL = "/downloads";
+    public static final String FILE_BY_ID_URL = DOWNLOADS_BASE_URL + "/file/{id}";
+
+    public static final String RELEASE_BASE_URL = DOWNLOADS_BASE_URL + "/release";
+    public static final String LEGACY_RELEASE_URL = RELEASE_BASE_URL + "/{releaseType}";
+    public static final String RELEASES_URL = RELEASE_BASE_URL + "/{releaseName}";
+    public static final String RELEASE_VERSION_URL = RELEASES_URL + "/{releaseVersion}";
+
+    @Inject
+    ObjectMapper json;
+
+    @Test
+    void getFileByID_missingFile() {
+        given().when().get(FILE_BY_ID_URL, "99999").then().statusCode(404);
+    }
+
+    @Test
+    void getFileByID_success() {
+        given().when().get(FILE_BY_ID_URL, "1").then().statusCode(200);
+    }
+
+    @Test
+    void getFileByID_success_format() {
+        given().when().get(FILE_BY_ID_URL, "1").then().assertThat()
+                .body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.FILE_SCHEMA_PATH));
+    }
+
+    @Test
+    void getReleaseLegacy_missingReleaseName() {
+        given().when().get(LEGACY_RELEASE_URL, "epp").then().statusCode(400);
+        given().when().get(LEGACY_RELEASE_URL, "eclipse_packages").then().statusCode(400);
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_VERSION_VALUE, "r").get(LEGACY_RELEASE_URL, "epp")
+                .then().statusCode(400);
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_VERSION_VALUE, "r")
+                .get(LEGACY_RELEASE_URL, "eclipse_packages").then().statusCode(400);
+    }
+
+    @Test
+    void getReleaseLegacy_success() {
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_NAME_VALUE, "2021-12")
+                .get(LEGACY_RELEASE_URL, "epp").then().statusCode(200);
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_NAME_VALUE, "2021-12")
+                .get(LEGACY_RELEASE_URL, "eclipse_packages").then().statusCode(200);
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_NAME_VALUE, "2021-12")
+                .queryParam(DownloadsUrlParameterNames.RELEASE_VERSION_VALUE, "r").get(LEGACY_RELEASE_URL, "epp").then()
+                .statusCode(200);
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_NAME_VALUE, "2021-12")
+                .queryParam(DownloadsUrlParameterNames.RELEASE_VERSION_VALUE, "r")
+                .get(LEGACY_RELEASE_URL, "eclipse_packages").then().statusCode(200);
+    }
+
+    @Test
+    void getReleaseLegacy_success_format() {
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_NAME_VALUE, "2021-12")
+                .get(LEGACY_RELEASE_URL, "epp").then().assertThat()
+                .body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.RELEASES_SCHEMA_PATH));
+    }
+
+    @Test
+    void getReleaseLegacyWithVersion_success_format() {
+        given().when().queryParam(DownloadsUrlParameterNames.RELEASE_NAME_VALUE, "2021-12")
+                .queryParam(DownloadsUrlParameterNames.RELEASE_VERSION_VALUE, "r").get(LEGACY_RELEASE_URL, "epp").then()
+                .assertThat().body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.RELEASE_SCHEMA_PATH));
+    }
+
+    @Test
+    void getRelease_missingRelease() {
+        given().when().get(RELEASES_URL, "missing-version").then().statusCode(404);
+    }
+
+    @Test
+    void getRelease_success() {
+        given().when().get(RELEASES_URL, "2021-12").then().statusCode(200);
+    }
+
+    @Test
+    void getRelease_success_format() {
+        given().when().get(RELEASES_URL, "2021-12").then().assertThat()
+                .body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.RELEASES_SCHEMA_PATH));
+    }
+
+    @Test
+    void getReleaseVersion_missingRelease() {
+        given().when().get(RELEASE_VERSION_URL, "missing-version", "r").then().statusCode(404);
+    }
+
+    @Test
+    void getReleaseVersion_missingVersion() {
+        given().when().get(RELEASE_VERSION_URL, "2021-12", "some-release").then().statusCode(404);
+    }
+
+    @Test
+    void getReleaseVersion_success() {
+        given().when().get(RELEASE_VERSION_URL, "2021-12", "r").then().statusCode(200);
+    }
+
+    @Test
+    void getReleaseVersion_success_format() {
+        given().when().get(RELEASE_VERSION_URL, "2021-12", "r").then().assertThat()
+                .body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.RELEASE_SCHEMA_PATH));
+    }
+}
diff --git a/src/test/java/org/eclipsefoundation/downloads/test/helper/SchemaNamespaceHelper.java b/src/test/java/org/eclipsefoundation/downloads/test/helper/SchemaNamespaceHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c60845b6f642f6d5cc7c2f78e3c156f847034ec
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/downloads/test/helper/SchemaNamespaceHelper.java
@@ -0,0 +1,10 @@
+package org.eclipsefoundation.downloads.test.helper;
+
+public final class SchemaNamespaceHelper {
+    public static final String BASE_SCHEMAS_PATH = "schemas/";
+    public static final String BASE_SCHEMAS_PATH_SUFFIX = "-schema.json";
+    public static final String FILES_SCHEMA_PATH = BASE_SCHEMAS_PATH + "files" + BASE_SCHEMAS_PATH_SUFFIX;
+    public static final String FILE_SCHEMA_PATH = BASE_SCHEMAS_PATH + "file" + BASE_SCHEMAS_PATH_SUFFIX;
+    public static final String RELEASES_SCHEMA_PATH = BASE_SCHEMAS_PATH + "releases" + BASE_SCHEMAS_PATH_SUFFIX;
+    public static final String RELEASE_SCHEMA_PATH = BASE_SCHEMAS_PATH + "release" + BASE_SCHEMAS_PATH_SUFFIX;
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
new file mode 100644
index 0000000000000000000000000000000000000000..4e10abb9f979e15bab9892f4818dbb7fb561cdab
--- /dev/null
+++ b/src/test/resources/application.properties
@@ -0,0 +1,9 @@
+## DATASOURCE CONFIG
+quarkus.datasource.db-kind=h2
+eclipse.db.default.limit=25
+eclipse.db.default.limit.max=100
+quarkus.hibernate-orm.database.generation=none
+
+# Flyway configuration for the default datasource
+quarkus.flyway.locations=classpath:database/default
+quarkus.flyway.migrate-at-start=true
diff --git a/src/test/resources/database/default/V1.0.0__default.sql b/src/test/resources/database/default/V1.0.0__default.sql
new file mode 100644
index 0000000000000000000000000000000000000000..a1266ecc842464ac44881e5a9d1a2a9809d66f70
--- /dev/null
+++ b/src/test/resources/database/default/V1.0.0__default.sql
@@ -0,0 +1,22 @@
+CREATE TABLE downloads (
+  file_id int(10) unsigned NOT NULL DEFAULT 0,
+  download_date datetime DEFAULT NULL,
+  remote_host varchar(100) DEFAULT NULL,
+  remote_addr varchar(15) DEFAULT NULL,
+  mirror_id int(10) unsigned DEFAULT NULL,
+  ccode char(2) DEFAULT NULL
+);
+INSERT INTO downloads(file_id, download_date, remote_host, remote_addr,mirror_id, ccode)
+  VALUES (1, NOW(),'http://somehost.co/file/location', '127.0.0.1', 101, 'ca');
+
+CREATE TABLE download_file_index (
+  file_id int(10) unsigned NOT NULL AUTO_INCREMENT,
+  file_name varchar(255) NOT NULL DEFAULT '',
+  download_count int(10) unsigned NOT NULL DEFAULT 0,
+  size_disk_bytes bigint(20) NOT NULL DEFAULT 0,
+  timestamp_disk bigint(20) NOT NULL DEFAULT 0,
+  md5sum char(32) DEFAULT NULL,
+  sha1sum char(40) DEFAULT NULL
+);
+INSERT INTO download_file_index(file_id, file_name, download_count, size_disk_bytes, timestamp_disk, md5sum, sha1sum)
+  VALUES (1, 'sample_file.zip',123456, 987654, 1642175700000, 'md5sum_sample', 'sha1sum_sample');
\ No newline at end of file