diff --git a/pom.xml b/pom.xml
index 9db3d20ba46e2cf98903080bce48c4ef43aec257..98925c046dffd638fe1d099687cfff7a74652177 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
   <artifactId>git-eca</artifactId>
   <version>1.1.0</version>
   <properties>
-    <eclipse-api-version>0.7.1</eclipse-api-version>
+    <eclipse-api-version>0.7.3</eclipse-api-version>
     <compiler-plugin.version>3.8.1</compiler-plugin.version>
     <maven.compiler.parameters>true</maven.compiler.parameters>
     <maven.compiler.source>11</maven.compiler.source>
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java
index 4f4032b46fc5e62238d1e63829ca3855df1df378..292d6f3c88c0bc7ef7a1852d12e8c02705bc4ffb 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/GithubAPI.java
@@ -11,6 +11,7 @@
  */
 package org.eclipsefoundation.git.eca.api;
 
+import javax.ws.rs.BeanParam;
 import javax.ws.rs.GET;
 import javax.ws.rs.HeaderParam;
 import javax.ws.rs.POST;
@@ -20,8 +21,10 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.Response;
 
 import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters;
 import org.eclipsefoundation.git.eca.api.models.GithubAccessToken;
 import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest;
+import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest;
 import org.jboss.resteasy.util.HttpHeaderNames;
 
 /**
@@ -34,19 +37,88 @@ import org.jboss.resteasy.util.HttpHeaderNames;
 @Produces("application/json")
 public interface GithubAPI {
 
+    /**
+     * Retrieves information about a certain pull request in a repo if it exists
+     * 
+     * @param bearer authorization header value, access token for application with access to repo
+     * @param apiVersion the version of the GH API to target when making the request
+     * @param repoFull the full repo name that is being targeted
+     * @param pullNumber the pull request number
+     * @return information about the given pull request if it exists.
+     */
+    @GET
+    @Path("repos/{repoFull}/pulls/{pullNumber}")
+    public PullRequest getPullRequest(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer,
+            @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("repoFull") String repoFull,
+            @PathParam("pullNumber") int pullNumber);
+
+    /**
+     * Retrieves a list of commits related to the given pull request.
+     * 
+     * @param bearer authorization header value, access token for application with access to repo
+     * @param apiVersion the version of the GH API to target when making the request
+     * @param repoFull the full repo name that is being targeted
+     * @param pullNumber the pull request number
+     * @return list of commits associated with the pull request, wrapped in a jax-rs response
+     */
     @GET
     @Path("repos/{repoFull}/pulls/{pullNumber}/commits")
     public Response getCommits(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer,
             @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("repoFull") String repoFull,
             @PathParam("pullNumber") int pullNumber);
 
+    /**
+     * Posts an update to the Github API, using an access token to update a given pull requests commit status, targeted
+     * using the head sha.
+     * 
+     * @param bearer authorization header value, access token for application with access to repo
+     * @param apiVersion the version of the GH API to target when making the request
+     * @param repoFull the full repo name that is being targeted
+     * @param prHeadSha the head SHA for the request
+     * @param commitStatusUpdate the status body sent with the request
+     * @return JAX-RS response to check for success or failure based on status code.
+     */
     @POST
     @Path("repos/{repoFull}/statuses/{prHeadSha}")
-    public Response updateStatus(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer, @HeaderParam("X-GitHub-Api-Version") String apiVersion,
-            @PathParam("repoFull") String repoFull, @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate);
+    public Response updateStatus(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer,
+            @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("repoFull") String repoFull,
+            @PathParam("prHeadSha") String prHeadSha, GithubCommitStatusRequest commitStatusUpdate);
 
+    /**
+     * Requires a JWT bearer token for the application to retrieve installations for. Returns a list of installations for
+     * the given application.
+     * 
+     * @param params the general params for requests, including pagination
+     * @param bearer JWT bearer token for the target application
+     * @return list of installations for the application
+     */
+    @GET
+    @Path("app/installations")
+    public Response getInstallations(@BeanParam BaseAPIParameters params, @HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer);
+
+    /**
+     * Retrieves an access token for a specific installation, given the applications JWT bearer and the api version.
+     * 
+     * @param bearer the authorization header value, should contain the apps signed JWT token
+     * @param apiVersion the API version to target with the request
+     * @param installationId the installation to generate an access token for
+     * @return the Github access token for the GH app installation
+     */
     @POST
     @Path("app/installations/{installationId}/access_tokens")
     public GithubAccessToken getNewAccessToken(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer,
             @HeaderParam("X-GitHub-Api-Version") String apiVersion, @PathParam("installationId") String installationId);
+
+    /**
+     * Returns a list of repositories for the given installation.
+     * 
+     * @param params the general params for requests, including pagination
+     * @param bearer JWT bearer token for the target installation
+     * @return list of repositories for the installation as a response for pagination
+     */
+    @GET
+    @Path("installation/repositories")
+    public Response getInstallationRepositories(@BeanParam BaseAPIParameters params,
+            @HeaderParam(HttpHeaderNames.AUTHORIZATION) String bearer);
+
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java
index 437e4a6c19466e7864c22203a7a6db409c4602e1..887e1cb7dae71064e0bdd46e2e5f89539ccbe2f7 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubAccessToken.java
@@ -18,7 +18,9 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
 import com.google.auto.value.AutoValue;
 
 /**
- * @author martin
+ * Contains the information about the Github application access tokens
+ * 
+ * @author Martin Lowe
  *
  */
 @AutoValue
@@ -26,8 +28,9 @@ import com.google.auto.value.AutoValue;
 public abstract class GithubAccessToken {
 
     public abstract String getToken();
+
     public abstract LocalDateTime getExpiresAt();
-    
+
     public static Builder builder() {
         return new AutoValue_GithubAccessToken.Builder();
     }
@@ -36,6 +39,7 @@ public abstract class GithubAccessToken {
     @JsonPOJOBuilder(withPrefix = "set")
     public abstract static class Builder {
         public abstract Builder setToken(String token);
+
         public abstract Builder setExpiresAt(LocalDateTime expiresAt);
 
         public abstract GithubAccessToken build();
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java
new file mode 100644
index 0000000000000000000000000000000000000000..3783a36b912a0e5e3bc15e9197447ad3562b3826
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubApplicationInstallation.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * 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@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.api.models;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
+/**
+ * Information about the given Github ECA application installation
+ * 
+ * @author Martin Lowe
+ *
+ */
+@AutoValue
+@JsonDeserialize(builder = AutoValue_GithubApplicationInstallation.Builder.class)
+public abstract class GithubApplicationInstallation {
+
+    public abstract int getId();
+
+    public abstract String getTargetType();
+
+    public abstract String getTargetId();
+
+    public static Builder builder() {
+        return new AutoValue_GithubApplicationInstallation.Builder();
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setId(int id);
+
+        public abstract Builder setTargetType(String targetType);
+
+        public abstract Builder setTargetId(String targetId);
+
+        public abstract GithubApplicationInstallation build();
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubInstallationRepositoriesResponse.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubInstallationRepositoriesResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c81446e41432330070e8fd51053d3ab9f025849
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubInstallationRepositoriesResponse.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * 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@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.api.models;
+
+import java.util.List;
+
+import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.Repository;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
+/**
+ * Model response for /installations/repositories
+ * 
+ * @author Martin Lowe
+ */
+@AutoValue
+@JsonDeserialize(builder = AutoValue_GithubInstallationRepositoriesResponse.Builder.class)
+public abstract class GithubInstallationRepositoriesResponse {
+
+    public abstract List<Repository> getRepositories();
+
+    public static Builder builder() {
+        return new AutoValue_GithubInstallationRepositoriesResponse.Builder();
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+
+        public abstract Builder setRepositories(List<Repository> repository);
+
+        public abstract GithubInstallationRepositoriesResponse build();
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java
index 90d8b50ba8237e94fb027933e3b49951bf0b8f1e..5bf0d7dbc72c80ed7eda7d74b7918c7227831f0b 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/models/GithubWebhookRequest.java
@@ -104,6 +104,8 @@ public abstract class GithubWebhookRequest {
     @JsonDeserialize(builder = AutoValue_GithubWebhookRequest_PullRequest.Builder.class)
     public abstract static class PullRequest {
 
+        public abstract String getState();
+
         public abstract Integer getNumber();
 
         public abstract PullRequestHead getHead();
@@ -115,6 +117,8 @@ public abstract class GithubWebhookRequest {
         @AutoValue.Builder
         @JsonPOJOBuilder(withPrefix = "set")
         public abstract static class Builder {
+            public abstract Builder setState(String state);
+
             public abstract Builder setNumber(Integer number);
 
             public abstract Builder setHead(PullRequestHead head);
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
index 14a21fc8b77bb7d885abe862d66529405b3f5ab5..7c82d88fff239c59c4854064017d138545769912 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationStatus.java
@@ -46,6 +46,7 @@ public class CommitValidationStatus extends BareNode {
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
     private String commitHash;
+    private String userMail;
     private String project;
     private String repoUrl;
     @Enumerated(EnumType.STRING)
@@ -79,6 +80,20 @@ public class CommitValidationStatus extends BareNode {
         this.commitHash = commitHash;
     }
 
+    /**
+     * @return the userMail
+     */
+    public String getUserMail() {
+        return userMail;
+    }
+
+    /**
+     * @param userMail the userMail to set
+     */
+    public void setUserMail(String userMail) {
+        this.userMail = userMail;
+    }
+
     /**
      * @return the project
      */
@@ -184,6 +199,8 @@ public class CommitValidationStatus extends BareNode {
         builder.append(id);
         builder.append(", sha=");
         builder.append(commitHash);
+        builder.append(", userMail=");
+        builder.append(userMail);
         builder.append(", project=");
         builder.append(project);
         builder.append(", repoUrl=");
@@ -213,26 +230,25 @@ public class CommitValidationStatus extends BareNode {
             // sha check
             String commitHash = params.getFirst(GitEcaParameterNames.SHA.getName());
             if (StringUtils.isNumeric(commitHash)) {
-                stmt.addClause(
-                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash = ?",
-                                new Object[] { commitHash }));
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash = ?", new Object[] { commitHash }));
             }
             String projectId = params.getFirst(GitEcaParameterNames.PROJECT_ID.getName());
             if (StringUtils.isNumeric(projectId)) {
-                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".projectId = ?",
-                        new Object[] { projectId }));
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".projectId = ?", new Object[] { projectId }));
             }
             List<String> commitHashes = params.get(GitEcaParameterNames.SHAS.getName());
             if (commitHashes != null && !commitHashes.isEmpty()) {
-                stmt.addClause(
-                        new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash IN ?",
-                                new Object[] { commitHashes }));
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitHash IN ?", new Object[] { commitHashes }));
             }
             String repoUrl = params.getFirst(GitEcaParameterNames.REPO_URL.getName());
             if (StringUtils.isNotBlank(repoUrl)) {
-                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".repoUrl = ?",
-                        new Object[] { repoUrl }));
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".repoUrl = ?", new Object[] { repoUrl }));
             }
+            String userMail = params.getFirst(GitEcaParameterNames.USER_MAIL.getName());
+            if (StringUtils.isNotBlank(userMail)) {
+                stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".userMail = ?", new Object[] { userMail }));
+            }
+
             return stmt;
         }
 
diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java b/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java
index 4135793778db40a2705c4c07d4b133492cb6232e..e1c5ebe1280cd2039e473407e4d44db3a778dfa8 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/dto/GithubWebhookTracking.java
@@ -209,6 +209,24 @@ public class GithubWebhookTracking extends BareNode {
                         .addClause(
                                 new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".fingerprint = ?", new Object[] { fingerprint }));
             }
+            String installationId = params.getFirst(GitEcaParameterNames.INSTALLATION_ID_RAW);
+            if (StringUtils.isNotBlank(installationId)) {
+                statement
+                        .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".installationId = ?",
+                                new Object[] { installationId }));
+            }
+            String pullRequestNumber = params.getFirst(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW);
+            if (StringUtils.isNumeric(pullRequestNumber)) {
+                statement
+                        .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".pullRequestNumber = ?",
+                                new Object[] { Integer.parseInt(pullRequestNumber) }));
+            }
+            String repositoryFullName = params.getFirst(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW);
+            if (StringUtils.isNotBlank(repositoryFullName)) {
+                statement
+                        .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".repositoryFullName = ?",
+                                new Object[] { repositoryFullName }));
+            }
 
             return statement;
         }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java
index 33d22c81224149bf2f036c092969b73d7798b2c1..3fd168db02f562e8972c389950d032ce419ea720 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/helper/JwtHelper.java
@@ -80,7 +80,7 @@ public class JwtHelper {
      * 
      * @return signed JWT using the issuer and secret defined in the secret properties.
      */
-    private String generateJwt() {
+    public String generateJwt() {
         return Jwt.subject("EclipseWebmaster").sign(JwtHelper.getExternalPrivateKey(location));
     }
 
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java b/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java
index e13ed00850fad948eef180475c56120acc388d40..a140d7d06cf478fa012f225fff2be2504c36356c 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java
@@ -11,6 +11,7 @@
 **********************************************************************/
 package org.eclipsefoundation.git.eca.model;
 
+import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -29,48 +30,50 @@ import com.google.auto.value.AutoValue;
 @AutoValue
 @JsonDeserialize(builder = AutoValue_Commit.Builder.class)
 public abstract class Commit {
-	@Nullable
-	public abstract String getHash();
+    public abstract String getHash();
 
-	@Nullable
-	public abstract String getSubject();
+    @Nullable
+    public abstract String getSubject();
 
-	@Nullable
-	public abstract String getBody();
+    @Nullable
+    public abstract String getBody();
 
-	@Nullable
-	public abstract List<String> getParents();
+    @Nullable
+    public abstract List<String> getParents();
 
-	@Nullable
-	public abstract GitUser getAuthor();
+    public abstract GitUser getAuthor();
 
-	@Nullable
-	public abstract GitUser getCommitter();
+    public abstract GitUser getCommitter();
 
-	@Nullable
-	public abstract Boolean getHead();
+    @Nullable
+    public abstract Boolean getHead();
 
-	public static Builder builder() {
-		return new AutoValue_Commit.Builder().setParents(new ArrayList<>());
-	}
+    @Nullable
+    public abstract ZonedDateTime getLastModificationDate();
 
-	@AutoValue.Builder
-	@JsonPOJOBuilder(withPrefix = "set")
-	public abstract static class Builder {
-		public abstract Builder setHash(@Nullable String hash);
+    public static Builder builder() {
+        return new AutoValue_Commit.Builder().setParents(new ArrayList<>());
+    }
 
-		public abstract Builder setSubject(@Nullable String subject);
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setHash(String hash);
 
-		public abstract Builder setBody(@Nullable String body);
+        public abstract Builder setSubject(@Nullable String subject);
 
-		public abstract Builder setParents(@Nullable List<String> parents);
+        public abstract Builder setBody(@Nullable String body);
 
-		public abstract Builder setAuthor(@Nullable GitUser author);
+        public abstract Builder setParents(@Nullable List<String> parents);
 
-		public abstract Builder setCommitter(@Nullable GitUser committer);
+        public abstract Builder setAuthor(GitUser author);
 
-		public abstract Builder setHead(@Nullable Boolean head);
+        public abstract Builder setCommitter(GitUser committer);
 
-		public abstract Commit build();
-	}
+        public abstract Builder setHead(@Nullable Boolean head);
+
+        public abstract Builder setLastModificationDate(@Nullable ZonedDateTime lastModificationDate);
+
+        public abstract Commit build();
+    }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
index e75ca1706576d3a16a2a60ce6fa950c393d65b8d..a16499237e51d4313a1e7505d0e73f187f702327 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/namespace/GitEcaParameterNames.java
@@ -35,6 +35,10 @@ public final class GitEcaParameterNames implements UrlParameterNamespace {
     public static final String STATUS_DELETED_RAW = "deleted";
     public static final String SINCE_RAW = "since";
     public static final String UNTIL_RAW = "until";
+    public static final String REPOSITORY_FULL_NAME_RAW = "repository_full_name";
+    public static final String INSTALLATION_ID_RAW = "installation_id";
+    public static final String PULL_REQUEST_NUMBER_RAW = "pull_request_number";
+    public static final String USER_MAIL_RAW = "user_mail";
     public static final UrlParameter COMMIT_ID = new UrlParameter(COMMIT_ID_RAW);
     public static final UrlParameter SHA = new UrlParameter(SHA_RAW);
     public static final UrlParameter SHAS = new UrlParameter(SHAS_RAW);
@@ -50,11 +54,17 @@ public final class GitEcaParameterNames implements UrlParameterNamespace {
     public static final UrlParameter STATUS_DELETED = new UrlParameter(STATUS_DELETED_RAW);
     public static final UrlParameter SINCE = new UrlParameter(SINCE_RAW);
     public static final UrlParameter UNTIL = new UrlParameter(UNTIL_RAW);
+    public static final UrlParameter REPOSITORY_FULL_NAME = new UrlParameter(REPOSITORY_FULL_NAME_RAW);
+    public static final UrlParameter INSTALLATION_ID = new UrlParameter(INSTALLATION_ID_RAW);
+    public static final UrlParameter PULL_REQUEST_NUMBER = new UrlParameter(PULL_REQUEST_NUMBER_RAW);
+    public static final UrlParameter USER_MAIL = new UrlParameter(USER_MAIL_RAW);
 
     @Override
     public List<UrlParameter> getParameters() {
-        return Arrays.asList(COMMIT_ID, SHA, SHAS, PROJECT_ID, PROJECT_IDS, NOT_IN_PROJECT_IDS, REPO_URL,
-                FINGERPRINT, USER_ID, PROJECT_PATH, PARENT_PROJECT, STATUS_ACTIVE, STATUS_DELETED, SINCE, UNTIL);
+        return Arrays
+                .asList(COMMIT_ID, SHA, SHAS, PROJECT_ID, PROJECT_IDS, NOT_IN_PROJECT_IDS, REPO_URL, FINGERPRINT, USER_ID, PROJECT_PATH,
+                        PARENT_PROJECT, STATUS_ACTIVE, STATUS_DELETED, SINCE, UNTIL, REPOSITORY_FULL_NAME, INSTALLATION_ID,
+                        PULL_REQUEST_NUMBER, USER_MAIL);
     }
 
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubAdjacentResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubAdjacentResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..8393c154aaf1154fb0e2b868bc17bf701ad916b0
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubAdjacentResource.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * 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@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.resource;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.model.RequestWrapper;
+import org.eclipsefoundation.core.service.APIMiddleware;
+import org.eclipsefoundation.git.eca.api.GithubAPI;
+import org.eclipsefoundation.git.eca.api.models.GithubCommit;
+import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest;
+import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest;
+import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking;
+import org.eclipsefoundation.git.eca.helper.JwtHelper;
+import org.eclipsefoundation.git.eca.model.Commit;
+import org.eclipsefoundation.git.eca.model.GitUser;
+import org.eclipsefoundation.git.eca.model.ValidationRequest;
+import org.eclipsefoundation.git.eca.model.ValidationResponse;
+import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
+import org.eclipsefoundation.git.eca.namespace.GithubCommitStatuses;
+import org.eclipsefoundation.git.eca.namespace.ProviderType;
+import org.eclipsefoundation.git.eca.service.ValidationService;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.eclipsefoundation.persistence.service.FilterService;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Contains operations and properties that are common to resources that interact with Github validation.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public abstract class GithubAdjacentResource {
+    private static final Logger LOGGER = LoggerFactory.getLogger(GithubAdjacentResource.class);
+
+    private static final String VALIDATION_LOGGING_MESSAGE = "Setting validation state for {}/#{} to {}";
+
+    @ConfigProperty(name = "eclipse.webhooks.github.context")
+    String context;
+    @ConfigProperty(name = "eclipse.webhooks.github.server-target")
+    String serverTarget;
+    @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28")
+    String apiVersion;
+
+    @Inject
+    JwtHelper jwtHelper;
+    @Inject
+    APIMiddleware middleware;
+    @Inject
+    PersistenceDao dao;
+    @Inject
+    FilterService filters;
+
+    @Inject
+    ValidationService validationService;
+
+    @Inject
+    RequestWrapper wrapper;
+
+    @RestClient
+    GithubAPI ghApi;
+
+    /**
+     * Generate a ValidationRequest object based on data pulled from Github, grabbing commits from the noted pull request
+     * using the installation ID for access/authorization.
+     * 
+     * @param installationId the ECA app installation ID for the organization
+     * @param repositoryFullName the full name of the repository where the PR resides
+     * @param pullRequestNumber the pull request number that is being validated
+     * @param repositoryUrl the URL of the repository that contains the commits to validate
+     * @return the populated validation request for the Github request information
+     */
+    ValidationRequest generateRequest(String installationId, String repositoryFullName, int pullRequestNumber, String repositoryUrl) {
+        checkRequestParameters(installationId, repositoryFullName, pullRequestNumber);
+        // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful
+        List<GithubCommit> commits = middleware
+                .getAll(i -> ghApi
+                        .getCommits(jwtHelper.getGhBearerString(installationId), apiVersion, repositoryFullName, pullRequestNumber),
+                        GithubCommit.class);
+        LOGGER.trace("Retrieved {} commits for PR {} in repo {}", commits.size(), pullRequestNumber, repositoryUrl);
+        // set up the validation request from current data
+        return ValidationRequest
+                .builder()
+                .setProvider(ProviderType.GITHUB)
+                .setRepoUrl(URI.create(repositoryUrl))
+                .setStrictMode(true)
+                .setCommits(commits
+                        .stream()
+                        .map(c -> Commit
+                                .builder()
+                                .setHash(c.getSha())
+                                .setAuthor(GitUser
+                                        .builder()
+                                        .setMail(c.getCommit().getAuthor().getEmail())
+                                        .setName(c.getCommit().getAuthor().getName())
+                                        .build())
+                                .setCommitter(GitUser
+                                        .builder()
+                                        .setMail(c.getCommit().getCommitter().getEmail())
+                                        .setName(c.getCommit().getCommitter().getName())
+                                        .build())
+                                .build())
+                        .collect(Collectors.toList()))
+                .build();
+    }
+
+    /**
+     * Attempts to retrieve a webhook tracking record given the installation, repository, and pull request number.
+     * 
+     * @param installationId the installation ID for the ECA app in the given repository
+     * @param repositoryFullName the full repository name for the target repo, e.g. eclipse/jetty
+     * @param pullRequestNumber the pull request number that is being processed currently
+     * @return the webhook tracking record if it can be found, or an empty optional.
+     */
+    Optional<GithubWebhookTracking> getExistingRequestInformation(String installationId, String repositoryFullName, int pullRequestNumber) {
+        checkRequestParameters(installationId, repositoryFullName, pullRequestNumber);
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.add(GitEcaParameterNames.INSTALLATION_ID_RAW, installationId);
+        params.add(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW, repositoryFullName);
+        params.add(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW, Integer.toString(pullRequestNumber));
+        return dao.get(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class), params)).stream().findFirst();
+    }
+
+    /**
+     * Process the current request and update the checks state to pending then success or failure. Contains verbose TRACE
+     * logging for more info on the states of the validation for more information
+     * 
+     * @param request information about the request from the GH webhook on what resource requested revalidation. Used to
+     * target the commit status of the resources
+     * @param vr the pseudo request generated from the contextual webhook data. Used to make use of existing validation
+     * logic.
+     * @return true if the validation passed, false otherwise.
+     */
+    boolean handleGithubWebhookValidation(GithubWebhookRequest request, ValidationRequest vr) {
+        // update the status before processing
+        LOGGER
+                .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
+                        GithubCommitStatuses.PENDING);
+        updateCommitStatus(request, GithubCommitStatuses.PENDING);
+
+        // validate the response
+        LOGGER
+                .trace("Begining validation of request for {}/#{}", request.getRepository().getFullName(),
+                        request.getPullRequest().getNumber());
+        ValidationResponse r = validationService.validateIncomingRequest(vr, wrapper);
+        if (r.getPassed()) {
+            LOGGER
+                    .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
+                            GithubCommitStatuses.SUCCESS);
+            updateCommitStatus(request, GithubCommitStatuses.SUCCESS);
+            return true;
+        }
+        LOGGER
+                .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
+                        GithubCommitStatuses.FAILURE);
+        updateCommitStatus(request, GithubCommitStatuses.FAILURE);
+        return false;
+    }
+
+    /**
+     * Sends off a POST to update the commit status given a context for the current PR.
+     * 
+     * @param request the current webhook update request
+     * @param state the state to set the status to
+     * @param fingerprint the internal unique string for the set of commits being processed
+     */
+    private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state) {
+        LOGGER
+                .trace("Generated access token for installation {}: {}", request.getInstallation().getId(),
+                        jwtHelper.getGithubAccessToken(request.getInstallation().getId()).getToken());
+        ghApi
+                .updateStatus(jwtHelper.getGhBearerString(request.getInstallation().getId()), apiVersion,
+                        request.getRepository().getFullName(), request.getPullRequest().getHead().getSha(),
+                        GithubCommitStatusRequest
+                                .builder()
+                                .setDescription(state.getMessage())
+                                .setState(state.toString())
+                                .setTargetUrl(serverTarget + "/git/eca/status/gh" + request.getRepository().getFullName() + '/'
+                                        + request.getPullRequest().getNumber())
+                                .setContext(context)
+                                .build());
+    }
+
+    /**
+     * Validates required fields for processing requests.
+     * 
+     * @param installationId the installation ID for the ECA app in the given repository
+     * @param repositoryFullName the full repository name for the target repo, e.g. eclipse/jetty
+     * @param pullRequestNumber the pull request number that is being processed currently
+     * @throws BadRequestException if at least one of the parameters is in an invalid state.
+     */
+    private void checkRequestParameters(String installationId, String repositoryFullName, int pullRequestNumber) {
+        List<String> missingFields = new ArrayList<>();
+        if (StringUtils.isBlank(installationId)) {
+            missingFields.add(GitEcaParameterNames.INSTALLATION_ID_RAW);
+        }
+        if (StringUtils.isBlank(repositoryFullName)) {
+            missingFields.add(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW);
+        }
+        if (pullRequestNumber < 1) {
+            missingFields.add(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW);
+        }
+
+        // throw exception if some fields are missing as we can't continue to process the request
+        if (!missingFields.isEmpty()) {
+            throw new BadRequestException("Missing fields in order to prepare request: " + StringUtils.join(missingFields, ' '));
+        }
+    }
+
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java
index 088f365913b4be3a25a42d613f39cf1766b46e82..feb89850b30231d105dccb111d598ead6be19f81 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/GithubWebhooksResource.java
@@ -15,7 +15,6 @@ import java.net.URI;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
-import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.ws.rs.BadRequestException;
@@ -23,38 +22,20 @@ import javax.ws.rs.FormParam;
 import javax.ws.rs.NotFoundException;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
 
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipse.microprofile.rest.client.inject.RestClient;
 import org.eclipsefoundation.core.helper.DateTimeHelper;
-import org.eclipsefoundation.core.model.RequestWrapper;
-import org.eclipsefoundation.core.service.APIMiddleware;
 import org.eclipsefoundation.core.service.CachingService;
-import org.eclipsefoundation.git.eca.api.GithubAPI;
-import org.eclipsefoundation.git.eca.api.models.GithubCommit;
-import org.eclipsefoundation.git.eca.api.models.GithubCommitStatusRequest;
 import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest;
 import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking;
 import org.eclipsefoundation.git.eca.helper.CaptchaHelper;
-import org.eclipsefoundation.git.eca.helper.JwtHelper;
-import org.eclipsefoundation.git.eca.model.Commit;
-import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.RevalidationResponse;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
-import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
-import org.eclipsefoundation.git.eca.namespace.GithubCommitStatuses;
 import org.eclipsefoundation.git.eca.namespace.HCaptchaErrorCodes;
-import org.eclipsefoundation.git.eca.namespace.ProviderType;
-import org.eclipsefoundation.git.eca.service.ValidationService;
-import org.eclipsefoundation.persistence.dao.PersistenceDao;
 import org.eclipsefoundation.persistence.model.RDBMSQuery;
-import org.eclipsefoundation.persistence.service.FilterService;
 import org.jboss.resteasy.annotations.jaxrs.HeaderParam;
-import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,38 +46,14 @@ import org.slf4j.LoggerFactory;
  *
  */
 @Path("webhooks/github")
-public class GithubWebhooksResource {
+public class GithubWebhooksResource extends GithubAdjacentResource {
     private static final Logger LOGGER = LoggerFactory.getLogger(GithubWebhooksResource.class);
 
-    private static final String VALIDATION_LOGGING_MESSAGE = "Setting validation state for {}/#{} to {}";
-
-    @ConfigProperty(name = "eclipse.webhooks.github.context")
-    String context;
-    @ConfigProperty(name = "eclipse.webhooks.github.server-target")
-    String serverTarget;
-    @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28")
-    String apiVersion;
-
-    @Inject
-    RequestWrapper wrapper;
-    @Inject
-    APIMiddleware middleware;
-    @Inject
-    ValidationService validationService;
-    @Inject
-    JwtHelper jwtHelper;
     @Inject
     CaptchaHelper captchaHelper;
 
     @Inject
     CachingService cache;
-    @Inject
-    PersistenceDao dao;
-    @Inject
-    FilterService filters;
-
-    @RestClient
-    GithubAPI ghApi;
 
     /**
      * Entry point for processing Github webhook requests. Accepts standard fields as described in the <a href=
@@ -120,11 +77,10 @@ public class GithubWebhooksResource {
         LOGGER.trace("Processing PR event for install {} with delivery ID of {}", request.getInstallation().getId(), deliveryId);
         // prepare for validation process by pre-processing into standard format
         ValidationRequest vr = generateRequest(request);
-        String fingerprint = validationService.generateRequestHash(vr);
         // track the request before we start processing
-        trackWebhookRequest(fingerprint, deliveryId, request);
+        trackWebhookRequest(deliveryId, request);
         // process the request
-        handleGithubWebhookValidation(request, vr, fingerprint);
+        handleGithubWebhookValidation(request, vr);
 
         return Response.ok().build();
     }
@@ -138,32 +94,36 @@ public class GithubWebhooksResource {
      * @return redirect to the pull request once done processing
      */
     @POST
-    @Path("revalidate/{fingerprint}")
-    public Response revalidateWebhookRequest(@PathParam("fingerprint") String fingerprint,
+    @Path("revalidate")
+    public Response revalidateWebhookRequest(@QueryParam(GitEcaParameterNames.REPOSITORY_FULL_NAME_RAW) String fullRepoName,
+            @QueryParam(GitEcaParameterNames.INSTALLATION_ID_RAW) String installationId,
+            @QueryParam(GitEcaParameterNames.PULL_REQUEST_NUMBER_RAW) Integer prNo,
             @FormParam("h-captcha-response") String captchaResponse) {
+        // get the tracking if it exists
+        Optional<GithubWebhookTracking> optTracking = getExistingRequestInformation(installationId, fullRepoName, prNo);
+        if (optTracking.isEmpty()) {
+            throw new NotFoundException(
+                    String.format("Cannot find a tracked pull request with for repo '%s', pull request number '%d'", fullRepoName, prNo));
+        }
+
         // check the captcha challenge response
         List<HCaptchaErrorCodes> errors = captchaHelper.validateCaptchaResponse(captchaResponse);
         if (!errors.isEmpty()) {
             // use debug logging as this could be incredibly noisy
             LOGGER
-                    .debug("Captcha challenge failed with the following errors for request with fingerprint '{}': {}", fingerprint,
-                            errors.stream().map(HCaptchaErrorCodes::getMessage));
+                    .debug("Captcha challenge failed with the following errors for revalidation request for '{}#{}': {}", fullRepoName,
+                            prNo, errors.stream().map(HCaptchaErrorCodes::getMessage));
             throw new BadRequestException("hCaptcha challenge response failed for this request");
         }
 
-        Optional<GithubWebhookTracking> optTracking = findTrackedRequest(fingerprint);
-        if (optTracking.isEmpty()) {
-            throw new NotFoundException("Cannot find a tracked pull request with fingerprint " + fingerprint);
-        }
-
         // get the tracking class, convert back to a GH webhook request, and validate the request
         GithubWebhookTracking tracking = optTracking.get();
         GithubWebhookRequest request = GithubWebhookRequest.buildFromTracking(tracking);
-        boolean isSuccessful = handleGithubWebhookValidation(request, generateRequest(request), fingerprint);
-        LOGGER.debug("Revalidation for request with fingerprint '{}' was {}successful.", fingerprint, isSuccessful ? "" : " not");
+        boolean isSuccessful = handleGithubWebhookValidation(request, generateRequest(request));
+        LOGGER.debug("Revalidation for request for '{}#{}' was {}successful.", fullRepoName, prNo, isSuccessful ? "" : " not");
 
         // update the tracking for the update time
-        trackWebhookRequest(fingerprint, tracking.getDeliveryId(), request);
+        trackWebhookRequest(tracking.getDeliveryId(), request);
         // build the url for pull request page
         StringBuilder sb = new StringBuilder();
         sb.append("https://github.com/");
@@ -174,66 +134,6 @@ public class GithubWebhooksResource {
         return Response.ok(RevalidationResponse.builder().setLocation(URI.create(sb.toString())).build()).build();
     }
 
-    /**
-     * Process the current request and update the checks state to pending then success or failure. Contains verbose TRACE
-     * logging for more info on the states of the validation for more information
-     * 
-     * @param request information about the request from the GH webhook on what resource requested revalidation. Used to
-     * target the commit status of the resources
-     * @param vr the pseudo request generated from the contextual webhook data. Used to make use of existing validation
-     * logic.
-     * @param fingerprint the generated SHA hash for the request
-     * @return true if the validation passed, false otherwise.
-     */
-    private boolean handleGithubWebhookValidation(GithubWebhookRequest request, ValidationRequest vr, String fingerprint) {
-        // update the status before processing
-        LOGGER
-                .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
-                        GithubCommitStatuses.PENDING);
-        updateCommitStatus(request, GithubCommitStatuses.PENDING, fingerprint);
-
-        // validate the response
-        LOGGER
-                .trace("Begining validation of request for {}/#{}", request.getRepository().getFullName(),
-                        request.getPullRequest().getNumber());
-        ValidationResponse r = validationService.validateIncomingRequest(vr, wrapper);
-        if (r.getPassed()) {
-            LOGGER
-                    .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
-                            GithubCommitStatuses.SUCCESS);
-            updateCommitStatus(request, GithubCommitStatuses.SUCCESS, fingerprint);
-            return true;
-        }
-        LOGGER
-                .trace(VALIDATION_LOGGING_MESSAGE, request.getRepository().getFullName(), request.getPullRequest().getNumber(),
-                        GithubCommitStatuses.FAILURE);
-        updateCommitStatus(request, GithubCommitStatuses.FAILURE, fingerprint);
-        return false;
-    }
-
-    /**
-     * Sends off a POST to update the commit status given a context for the current PR.
-     * 
-     * @param request the current webhook update request
-     * @param state the state to set the status to
-     * @param fingerprint the internal unique string for the set of commits being processed
-     */
-    private void updateCommitStatus(GithubWebhookRequest request, GithubCommitStatuses state, String fingerprint) {
-        LOGGER
-                .trace("Generated access token for installation {}: {}", request.getInstallation().getId(),
-                        jwtHelper.getGithubAccessToken(request.getInstallation().getId()).getToken());
-        ghApi
-                .updateStatus(jwtHelper.getGhBearerString(request.getInstallation().getId()), apiVersion,
-                        request.getRepository().getFullName(), request.getPullRequest().getHead().getSha(),
-                        GithubCommitStatusRequest
-                                .builder()
-                                .setDescription(state.getMessage())
-                                .setState(state.toString())
-                                .setTargetUrl(serverTarget + "/git/eca/status/" + fingerprint + "/ui")
-                                .setContext(context)
-                                .build());
-    }
-
     /**
      * Create a new entry in the webhook request tracking table to facilitate revalidation requests from the ECA system
      * rather than just from Github.
@@ -243,10 +143,10 @@ public class GithubWebhooksResource {
      * @param request the webhook event payload from Github
      * @return the persisted webhook tracking data, or null if there was an error in creating the tracking
      */
-    private GithubWebhookTracking trackWebhookRequest(String fingerprint, String deliveryId, GithubWebhookRequest request) {
-        Optional<GithubWebhookTracking> existingTracker = findTrackedRequest(fingerprint);
+    private GithubWebhookTracking trackWebhookRequest(String deliveryId, GithubWebhookRequest request) {
+        Optional<GithubWebhookTracking> existingTracker = getExistingRequestInformation(request.getInstallation().getId(),
+                request.getRepository().getFullName(), request.getPullRequest().getNumber());
         GithubWebhookTracking calculatedTracker = new GithubWebhookTracking();
-        calculatedTracker.setFingerprint(fingerprint);
         calculatedTracker.setInstallationId(request.getInstallation().getId());
         calculatedTracker.setDeliveryId(deliveryId);
         calculatedTracker.setPullRequestNumber(request.getPullRequest().getNumber());
@@ -261,25 +161,13 @@ public class GithubWebhooksResource {
                 .add(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class)), Arrays.asList(calculatedTracker));
         if (results.isEmpty()) {
             LOGGER
-                    .error("Could not save the webhook metadata, functionality will be restricted for request with fingerprint {}",
-                            fingerprint);
+                    .error("Could not save the webhook metadata, functionality will be restricted for request in repo '{}', pull request #{}",
+                            calculatedTracker.getRepositoryFullName(), calculatedTracker.getPullRequestNumber());
             return null;
         }
         return results.get(0);
     }
 
-    /**
-     * Retrieves the tracked information for a previous webhook validation request if available.
-     * 
-     * @param fingerprint the unique hash for the request that was previously tracked
-     * @return an optional containing the tracked webhook request information, or an empty optional if it can't be found.
-     */
-    private Optional<GithubWebhookTracking> findTrackedRequest(String fingerprint) {
-        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
-        params.add(GitEcaParameterNames.FINGERPRINT_RAW, fingerprint);
-        return dao.get(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class), params)).stream().findFirst();
-    }
-
     /**
      * Generate the validation request for the current GH Webhook request.
      * 
@@ -291,38 +179,4 @@ public class GithubWebhooksResource {
                 request.getPullRequest().getNumber(), request.getRepository().getHtmlUrl());
     }
 
-    private ValidationRequest generateRequest(String installationId, String repositoryFullName, int pullRequestNumber,
-            String repositoryUrl) {
-        // get the commits that will be validated, don't cache as changes can come in too fast for it to be useful
-        List<GithubCommit> commits = middleware
-                .getAll(i -> ghApi
-                        .getCommits(jwtHelper.getGhBearerString(installationId), apiVersion, repositoryFullName, pullRequestNumber),
-                        GithubCommit.class);
-        LOGGER.trace("Retrieved {} commits for PR {} in repo {}", commits.size(), pullRequestNumber, repositoryUrl);
-        // set up the validation request from current data
-        return ValidationRequest
-                .builder()
-                .setProvider(ProviderType.GITHUB)
-                .setRepoUrl(URI.create(repositoryUrl))
-                .setStrictMode(true)
-                .setCommits(commits
-                        .stream()
-                        .map(c -> Commit
-                                .builder()
-                                .setHash(c.getSha())
-                                .setAuthor(GitUser
-                                        .builder()
-                                        .setMail(c.getCommit().getAuthor().getEmail())
-                                        .setName(c.getCommit().getAuthor().getName())
-                                        .build())
-                                .setCommitter(GitUser
-                                        .builder()
-                                        .setMail(c.getCommit().getCommitter().getEmail())
-                                        .setName(c.getCommit().getCommitter().getName())
-                                        .build())
-                                .build())
-                        .collect(Collectors.toList()))
-                .build();
-    }
-
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..f71fdb02867ac5d41d7b525602d300b8921a14b1
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
@@ -0,0 +1,196 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * 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@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.resource;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.ServerErrorException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.core.helper.DateTimeHelper;
+import org.eclipsefoundation.core.service.CachingService;
+import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest;
+import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest;
+import org.eclipsefoundation.git.eca.api.models.Project;
+import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
+import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking;
+import org.eclipsefoundation.git.eca.model.Commit;
+import org.eclipsefoundation.git.eca.model.ValidationRequest;
+import org.eclipsefoundation.git.eca.service.GithubApplicationService;
+import org.eclipsefoundation.git.eca.service.ProjectsService;
+import org.eclipsefoundation.git.eca.service.ValidationService;
+import org.eclipsefoundation.persistence.model.RDBMSQuery;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.quarkus.qute.Location;
+import io.quarkus.qute.Template;
+
+/**
+ * REST resource containing endpoints related to checking the status of validation requests.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Path("eca/status")
+public class StatusResource extends GithubAdjacentResource {
+    private static final Logger LOGGER = LoggerFactory.getLogger(StatusResource.class);
+
+    @Inject
+    CachingService cache;
+
+    @Inject
+    ProjectsService projects;
+    @Inject
+    ValidationService validation;
+    @Inject
+    GithubApplicationService ghAppService;
+
+    // Qute templates, generates UI status page
+    @Location("simple_fingerprint_ui")
+    Template statusUiTemplate;
+
+    @GET
+    @Path("{fingerprint}")
+    public Response getCommitValidation(@PathParam("fingerprint") String fingerprint) {
+        return Response.ok(validation.getHistoricValidationStatus(wrapper, fingerprint)).build();
+    }
+
+    @GET
+    @Produces(MediaType.TEXT_HTML)
+    @Path("{fingerprint}/ui")
+    public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) {
+        List<CommitValidationStatus> statuses = validation.getHistoricValidationStatus(wrapper, fingerprint);
+        if (statuses.isEmpty()) {
+            return Response.status(404).build();
+        }
+        List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider());
+        return Response
+                .ok()
+                .entity(statusUiTemplate
+                        .data("statuses", statuses)
+                        .data("pullRequestNumber", null)
+                        .data("fullRepoName", null)
+                        .data("project", ps.isEmpty() ? null : ps.get(0))
+                        .data("repoUrl", statuses.get(0).getRepoUrl())
+                        .data("installationId", null)
+                        .render())
+                .build();
+    }
+
+    @GET
+    @Produces(MediaType.TEXT_HTML)
+    @Path("gh/{org}/{repoName}/{prNo}")
+    public Response getCommitValidationForGithub(@PathParam("org") String org, @PathParam("repoName") String repoName,
+            @PathParam("prNo") Integer prNo) {
+        String fullRepoName = org + '/' + repoName;
+        // get the installation ID for the given repo if it exists, and if the PR noted exists
+        String installationId = ghAppService.getInstallationForRepo(fullRepoName);
+        Optional<PullRequest> prResponse = ghAppService.getPullRequest(installationId, fullRepoName, prNo);
+        if (StringUtils.isBlank(installationId)) {
+            throw new BadRequestException("Could not find an ECA app installation for repo name: " + fullRepoName);
+        } else if (prResponse.isEmpty() || !"open".equalsIgnoreCase(prResponse.get().getState())) {
+            throw new NotFoundException(String.format("Could not find open PR '%d' in repo name '%s'", prNo, fullRepoName));
+        }
+
+        // prepare the request for consumption
+        String repoUrl = "https://github.com/" + fullRepoName;
+        ValidationRequest vr = generateRequest(installationId, fullRepoName, prNo, repoUrl);
+        // build the commit sha list based on the prepared request
+        List<String> commitShas = vr.getCommits().stream().map(Commit::getHash).collect(Collectors.toList());
+        // there should always be commits for a PR, but in case, lets check
+        if (commitShas.isEmpty()) {
+            throw new BadRequestException(String.format("Could not find any commits for %s#%d", fullRepoName, prNo));
+        }
+        LOGGER.debug("Found {} commits for '{}#{}'", commitShas.size(), fullRepoName, prNo);
+
+        // get the commit status of commits to use
+        List<CommitValidationStatus> statuses = validation.getHistoricValidationStatusByShas(wrapper, commitShas);
+        // check if this request has been validated in the past, and if not, run validation
+        Optional<GithubWebhookTracking> tracking = getExistingRequestInformation(installationId, fullRepoName, prNo);
+        if (tracking.isEmpty() || commitShas.size() != statuses.size()) {
+            LOGGER.debug("Validation for {}#{} does not seem to be current, revalidating commits", fullRepoName, prNo);
+            // update the tracking before revalidating the request
+            GithubWebhookTracking updatedTracking = updateGithubTrackingIfMissing(tracking, prResponse.get(), installationId, fullRepoName);
+            if (updatedTracking == null) {
+                throw new ServerErrorException("Error while attempting to revalidate request, try again later.", 500);
+            }
+            // using the updated tracking, perform the validation
+            handleGithubWebhookValidation(GithubWebhookRequest.buildFromTracking(updatedTracking), vr);
+            // call to retrieve the statuses once again since they will have changed at this point
+            statuses = validation.getHistoricValidationStatusByShas(wrapper, commitShas);
+        }
+
+        // get projects for use in status UI
+        List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider());
+        // render and return the status UI
+        return Response
+                .ok()
+                .entity(statusUiTemplate
+                        .data("statuses", statuses)
+                        .data("pullRequestNumber", prNo)
+                        .data("fullRepoName", fullRepoName)
+                        .data("project", ps.isEmpty() ? null : ps.get(0))
+                        .data("repoUrl", repoUrl)
+                        .data("installationId", installationId)
+                        .render())
+                .build();
+    }
+
+    /**
+     * Checks if the Github tracking is present for the current request, and if missing will generate a new record and save
+     * it.
+     * 
+     * @param tracking the optional tracking entry for the current request
+     * @param installationId the ECA app installation ID for the current request
+     * @param request the pull request that is being validated
+     * @param fullRepoName the full repo name for the validation request
+     */
+    private GithubWebhookTracking updateGithubTrackingIfMissing(Optional<GithubWebhookTracking> tracking, PullRequest request,
+            String installationId, String fullRepoName) {
+        // if there is no tracking present, create the missing tracking and persist it
+        if (tracking.isEmpty()) {
+            GithubWebhookTracking newTracking = new GithubWebhookTracking();
+            newTracking.setDeliveryId("");
+            newTracking.setFingerprint("");
+            newTracking.setHeadSha(request.getHead().getSha());
+            newTracking.setInstallationId(installationId);
+            newTracking.setLastUpdated(DateTimeHelper.now());
+            newTracking.setPullRequestNumber(request.getNumber());
+            newTracking.setRepositoryFullName(fullRepoName);
+
+            // save the data, and log on its success or failure
+            List<GithubWebhookTracking> savedTracking = dao
+                    .add(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class)), Arrays.asList(newTracking));
+            if (savedTracking.isEmpty()) {
+                LOGGER.warn("Unable to create new GH tracking record for request to validate {}#{}", fullRepoName, request.getNumber());
+                return null;
+            }
+            // return the updated tracking when successful
+            LOGGER.debug("Created new GH tracking record for request to validate {}#{}", fullRepoName, request.getNumber());
+            return savedTracking.get(0);
+        }
+        return tracking.get();
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
index 98b58b95568007af7639c90fd8548821517e26ea..90df077eab27d987906b83d80308fa98d3aae66c 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -12,7 +12,6 @@
 **********************************************************************/
 package org.eclipsefoundation.git.eca.resource;
 
-import java.net.MalformedURLException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -22,7 +21,6 @@ import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -31,8 +29,6 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.api.models.EclipseUser;
-import org.eclipsefoundation.git.eca.api.models.Project;
-import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
@@ -44,9 +40,6 @@ import org.jboss.resteasy.annotations.jaxrs.QueryParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.quarkus.qute.Location;
-import io.quarkus.qute.Template;
-
 /**
  * ECA validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate
  * commits passed to this endpoint. Should be as system agnostic as possible to allow for any service to request
@@ -82,10 +75,6 @@ public class ValidationResource {
     @Inject
     InterestGroupService ig;
 
-    // Qute templates, generates email bodies
-    @Location("simple_fingerprint_ui")
-    Template membershipTemplateWeb;
-
     /**
      * Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository
      * provider. These commits will be validated to ensure that all users are covered either by an ECA, or are committers on
@@ -95,7 +84,6 @@ public class ValidationResource {
      * @param req the request containing basic data plus the commits to be validated
      * @return a web response indicating success or failure for each commit, along with standard messages that may be used
      * to give users context on failure.
-     * @throws MalformedURLException
      */
     @POST
     public Response validate(ValidationRequest req) {
@@ -112,30 +100,6 @@ public class ValidationResource {
         }
     }
 
-    @GET
-    @Path("status/{fingerprint}")
-    public Response getCommitValidation(@PathParam("fingerprint") String fingerprint) {
-        return Response.ok(validation.getHistoricValidationStatus(wrapper, fingerprint)).build();
-    }
-
-    @GET
-    @Produces(MediaType.TEXT_HTML)
-    @Path("status/{fingerprint}/ui")
-    public Response getCommitValidationUI(@PathParam("fingerprint") String fingerprint) {
-        List<CommitValidationStatus> statuses = validation.getHistoricValidationStatus(wrapper, fingerprint);
-        if (statuses.isEmpty()) {
-            return Response.status(404).build();
-        }
-        List<Project> ps = projects.retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), statuses.get(0).getProvider());
-        return Response
-                .ok()
-                .entity(membershipTemplateWeb
-                        .data("statuses", statuses, "repoUrl", statuses.get(0).getRepoUrl(), "project", ps.isEmpty() ? null : ps.get(0),
-                                "fingerprint", fingerprint)
-                        .render())
-                .build();
-    }
-
     @GET
     @Path("/lookup")
     public Response getUserStatus(@QueryParam("email") String email) {
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a36f46cafccdc234ec3197048ece07b7cf86ec9
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/GithubApplicationService.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * 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@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.service;
+
+import java.util.Optional;
+
+import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest;
+
+/**
+ * Service for interacting with the Github API with caching for performance.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public interface GithubApplicationService {
+
+    /**
+     * Retrieves the installation ID for the ECA app on the given repo if it exists.
+     * 
+     * @param repoFullName the full repo name to retrieve an installation ID for. E.g. eclipse/jetty
+     * @return the numeric installation ID if it exists, or null
+     */
+    String getInstallationForRepo(String repoFullName);
+
+    /**
+     * Retrieves a pull request given the repo, pull request, and associated installation to action the fetch.
+     * 
+     * @param installationId installation ID to use when creating access tokens to query GH API
+     * @param repoFullName the full repo name, where the org and repo name are joined by a slash, e.g. eclipse/jetty
+     * @param pullRequest the pull request numeric ID
+     * @return the pull request if it exists, otherwise empty
+     */
+    Optional<PullRequest> getPullRequest(String installationId, String repoFullName, Integer pullRequest);
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
index 0110228cebf0d2544c337cd6a2614f798fdf6836..1f441ca0b783c220d831a8661b0734c336dbf081 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
@@ -50,6 +50,15 @@ public interface ValidationService {
      */
     public List<CommitValidationStatus> getHistoricValidationStatus(RequestWrapper wrapper, String fingerprint);
 
+    /**
+     * Retrieves a set of validation status objects given the target shas.
+     * 
+     * @param wrapper current request wrapper object
+     * @param shas list of shas to use when fetching historic commit statuses
+     * @return the list of historic validation status objects, or an empty list.
+     */
+    public List<CommitValidationStatus> getHistoricValidationStatusByShas(RequestWrapper wrapper, List<String> shas);
+
     /**
      * Retrieves a set of commit validation status objects given a validation request and target project.
      * 
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..9027ca48dbe1a08ad8472ea7ee1ed563d41df7b0
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultGithubApplicationService.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2023 Eclipse Foundation
+ *
+ * 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@eclipse-foundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.git.eca.service.impl;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.context.ManagedExecutor;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.service.APIMiddleware;
+import org.eclipsefoundation.core.service.CachingService;
+import org.eclipsefoundation.git.eca.api.GithubAPI;
+import org.eclipsefoundation.git.eca.api.models.GithubApplicationInstallation;
+import org.eclipsefoundation.git.eca.api.models.GithubInstallationRepositoriesResponse;
+import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest;
+import org.eclipsefoundation.git.eca.helper.JwtHelper;
+import org.eclipsefoundation.git.eca.service.GithubApplicationService;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+/**
+ * Default caching implementation of the GH app service. This uses a loading cache to keep installation info highly
+ * available to reduce latency in calls.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@ApplicationScoped
+public class DefaultGithubApplicationService implements GithubApplicationService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultGithubApplicationService.class);
+
+    @ConfigProperty(name = "eclipse.github.default-api-version", defaultValue = "2022-11-28")
+    String apiVersion;
+
+    @RestClient
+    GithubAPI gh;
+
+    @Inject
+    JwtHelper jwt;
+    @Inject
+    CachingService cache;
+    @Inject
+    APIMiddleware middle;
+
+    @Inject
+    ManagedExecutor exec;
+
+    private AsyncLoadingCache<String, MultivaluedMap<String, String>> installationRepositoriesCache;
+
+    @PostConstruct
+    void init() {
+        this.installationRepositoriesCache = Caffeine
+                .newBuilder()
+                .executor(exec)
+                .maximumSize(10)
+                .refreshAfterWrite(Duration.ofMinutes(60))
+                .buildAsync(k -> loadInstallationRepositories());
+        // do initial map population
+        getAllInstallRepos();
+    }
+
+    @Override
+    public String getInstallationForRepo(String repoFullName) {
+        MultivaluedMap<String, String> map = getAllInstallRepos();
+        return map.keySet().stream().filter(k -> map.get(k).contains(repoFullName)).findFirst().orElse(null);
+    }
+
+    private MultivaluedMap<String, String> getAllInstallRepos() {
+        try {
+            return this.installationRepositoriesCache.get("all").get(10L, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            LOGGER.error("Thread interrupted while building repository cache, no entries will be available for current call");
+            Thread.currentThread().interrupt();
+        } catch (Exception e) {
+            // rewrap exception and throw
+            throw new RuntimeException(e);
+        }
+        return new MultivaluedMapImpl<>();
+    }
+
+    @Override
+    public Optional<PullRequest> getPullRequest(String installationId, String repoFullName, Integer pullRequest) {
+        return cache
+                .get(repoFullName, new MultivaluedMapImpl<>(), PullRequest.class,
+                        () -> gh.getPullRequest(jwt.getGhBearerString(installationId), apiVersion, repoFullName, pullRequest));
+    }
+
+    /**
+     * Retrieves a fresh copy of installation repositories, mapped by installation ID to associated full repo names.
+     * 
+     * @return a multivalued map relating installation IDs to associated full repo names.
+     */
+    private MultivaluedMap<String, String> loadInstallationRepositories() {
+        // create map early for potential empty returns
+        MultivaluedMapImpl<String, String> out = new MultivaluedMapImpl<>();
+        // get JWT bearer and then get all associated installations of current app
+        String auth = "Bearer " + jwt.generateJwt();
+        List<GithubApplicationInstallation> installations = middle
+                .getAll(i -> gh.getInstallations(i, auth), GithubApplicationInstallation.class);
+        // check that there are installations
+        if (installations.isEmpty()) {
+            LOGGER.warn("Did not find any installations for the currently configured Github application");
+            return out;
+        }
+        // trace log the installations for more context
+        LOGGER.trace("Found the following installations: {}", installations);
+
+        // from installations, get the assoc. repos and grab their full repo name and collect them
+        installations
+                .stream()
+                .forEach(installation -> middle
+                        .getAll(i -> gh.getInstallationRepositories(i, jwt.getGhBearerString(Integer.toString(installation.getId()))),
+                                GithubInstallationRepositoriesResponse.class)
+                        .stream()
+                        .forEach(installRepo -> installRepo
+                                .getRepositories()
+                                .stream()
+                                .forEach(r -> out.add(Integer.toString(installation.getId()), r.getFullName()))));
+        LOGGER.trace("Final results for generating installation mapping: {}", out);
+        return out;
+    }
+
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
index 76d319e7d6b86a95c2307fada3e8fdc6b67e7431..a9fa1872f942f0cce6eeeed421a92dd67729fc19 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
@@ -15,6 +15,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import javax.enterprise.context.ApplicationScoped;
@@ -112,7 +113,7 @@ public class DefaultValidationService implements ValidationService {
             // get the current status if present
             Optional<CommitValidationStatus> status = statuses.stream().filter(s -> s.getCommitHash().equals(c.getHash())).findFirst();
             // skip the commit validation if already passed
-            if (status.isPresent() && status.get().getErrors().isEmpty()) {
+            if (isValidationStatusCurrentAndValid(status, c)) {
                 r.addMessage(c.getHash(), "Commit was previously validated, skipping processing", APIStatusCode.SUCCESS_SKIPPED);
                 return;
             }
@@ -136,6 +137,19 @@ public class DefaultValidationService implements ValidationService {
         return dao.get(q).stream().map(statusGrouping -> statusGrouping.getCompositeId().getCommit()).collect(Collectors.toList());
     }
 
+    @Override
+    public List<CommitValidationStatus> getHistoricValidationStatusByShas(RequestWrapper wrapper, List<String> shas) {
+        if (shas == null || shas.isEmpty()) {
+            return Collections.emptyList();
+        }
+        MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
+        params.put(GitEcaParameterNames.SHAS_RAW, shas);
+        RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class), params);
+        // set use limit to false to collect all data in one request
+        q.setUseLimit(false);
+        return dao.get(q);
+    }
+
     @Override
     public List<CommitValidationStatus> getRequestCommitValidationStatus(RequestWrapper wrapper, ValidationRequest req, String projectId) {
         RDBMSQuery<CommitValidationStatus> q = new RDBMSQuery<>(wrapper, filters.get(CommitValidationStatus.class),
@@ -151,6 +165,15 @@ public class DefaultValidationService implements ValidationService {
         // iterate over commit responses, and update statuses in DB
         List<CommitValidationStatus> updatedStatuses = new ArrayList<>();
         r.getCommits().entrySet().stream().filter(e -> !ValidationResponse.NIL_HASH_PLACEHOLDER.equalsIgnoreCase(e.getKey())).forEach(e -> {
+            // get the commit for current status
+            Optional<Commit> commit = req.getCommits().stream().filter(c -> e.getKey().equals(c.getHash())).findFirst();
+            if (commit.isEmpty()) {
+                // this should always have a match (response commits are built from request commits)
+                LOGGER.error("Could not find request commit associated with commit messages for commit hash '{}'", e.getKey());
+                return;
+            }
+            Commit c = commit.get();
+
             // update the status if present, otherwise make new one.
             Optional<CommitValidationStatus> status = statuses.stream().filter(s -> e.getKey().equals(s.getCommitHash())).findFirst();
             CommitValidationStatus base;
@@ -160,6 +183,7 @@ public class DefaultValidationService implements ValidationService {
                 base = new CommitValidationStatus();
                 base.setProject(CommitHelper.getProjectId(p));
                 base.setCommitHash(e.getKey());
+                base.setUserMail(c.getAuthor().getMail());
                 base.setProvider(req.getProvider());
                 base.setRepoUrl(req.getRepoUrl().toString());
                 base.setCreationDate(DateTimeHelper.now());
@@ -167,13 +191,7 @@ public class DefaultValidationService implements ValidationService {
             }
             base.setLastModified(DateTimeHelper.now());
             updatedStatuses.add(base);
-            // get the commit for current status
-            Optional<Commit> commit = req.getCommits().stream().filter(c -> e.getKey().equals(c.getHash())).findFirst();
-            if (commit.isEmpty()) {
-                LOGGER.error("Could not find request commit associated with commit messages for commit hash '{}'", e.getKey());
-                return;
-            }
-            Commit c = commit.get();
+
             // if there are errors, update validation messages
             if (!e.getValue().getErrors().isEmpty() || (base.getErrors() != null && !base.getErrors().isEmpty())) {
                 // generate new errors, looking for errors not found in current list
@@ -234,7 +252,7 @@ public class DefaultValidationService implements ValidationService {
         response.addMessage(c.getHash(), String.format("Authored by: %1$s <%2$s>", author.getName(), author.getMail()));
 
         // skip processing if a merge commit
-        if (c.getParents().size() > 1) {
+        if (c.getParents() != null && c.getParents().size() > 1) {
             response
                     .addMessage(c.getHash(),
                             String.format("Commit '%1$s' has multiple parents, merge commit detected, passing", c.getHash()));
@@ -434,4 +452,27 @@ public class DefaultValidationService implements ValidationService {
         }
         return null;
     }
+
+    /**
+     * <p>
+     * Checks the following to determine whether a commit has already been validated:
+     * </p>
+     * <ul>
+     * <li>The validation status exists
+     * <li>Validation status has no present errors
+     * <li>Modification date is either unset or matches when set
+     * <li>User mail is set and matches (ignores case)
+     * </ul>
+     * 
+     * If any of these checks fail, then the commit should be revalidated.
+     * 
+     * @param status the current commits validation status if it exists
+     * @param c the commit that is being validated
+     * @return true if the commit does not need to be (re)validated, false otherwise.
+     */
+    private boolean isValidationStatusCurrentAndValid(Optional<CommitValidationStatus> status, Commit c) {
+        return status.isPresent() && status.get().getErrors().isEmpty() && c.getAuthor() != null
+                && status.get().getUserMail().equalsIgnoreCase(c.getAuthor().getMail())
+                && (c.getLastModificationDate() == null || status.get().getLastModified().equals(c.getLastModificationDate()));
+    }
 }
diff --git a/src/main/resources/templates/simple_fingerprint_ui.html b/src/main/resources/templates/simple_fingerprint_ui.html
index 69af38b850d58c315e8dc24f8ecabcb003e97bf3..f0eb2bea4a13405b6465d80c7d7994b245eaa32a 100644
--- a/src/main/resources/templates/simple_fingerprint_ui.html
+++ b/src/main/resources/templates/simple_fingerprint_ui.html
@@ -140,9 +140,9 @@
         </div>
       </div>
       {/if}
-      {#if statuses.0.provider == ProviderType:GITHUB}
+      {#if statuses.0.provider == ProviderType:GITHUB && pullRequestNumber != null && fullRepoName != null}
       <div>
-        <form id="git-eca-hook-revalidation" data-request-id="{fingerprint}">
+        <form id="git-eca-hook-revalidation" data-request-number="{pullRequestNumber}" data-request-repo="{fullRepoName}" data-request-installation="{installationId}">
           <div class="captcha">
             <div class="h-captcha" data-sitekey="{config:['eclipse.hcaptcha.sitekey']}"></div>
           </div>
@@ -266,9 +266,15 @@
         const $submitButton = $form.find('button');
         // disable the button so that requests won't be spammed
         $submitButton.attr("disabled", "disabled");
+        // set up params to set up for revalidation
+        let params = $.param({
+            repo_full_name: $form.data('request-repo'),
+            pull_request_number: $form.data('request-number'),
+            installation_id: $form.data('request-installation')
+        });
         // use ajax to revalidate the commit with GH
         $.ajax({
-          url: `/git/webhooks/github/revalidate/${$form.data('request-id')}`,
+          url: `/git/webhooks/github/revalidate?${params}`,
           data: $form.serialize(),
           type: 'POST',
           success: function (data) {
diff --git a/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java b/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java
index bb24b9eb6b69cfd6d37228c08a135d192e2148d6..982eb6ae3e9e2a1b0f6799bb4e3b5fa0d9e74dab 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java
@@ -58,13 +58,6 @@ class CommitHelperTest {
         Assertions.assertFalse(CommitHelper.validateCommit(null), "Expected null commit to fail validation");
     }
 
-    @Test
-    void validateCommitNoAuthor() {
-        baseCommit.setAuthor(null);
-        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
-                "Expected basic commit to fail validation w/ no author");
-    }
-
     @Test
     void validateCommitNoAuthorMail() {
         baseCommit.setAuthor(GitUser.builder().setName("Some Name").build());
@@ -72,13 +65,6 @@ class CommitHelperTest {
                 "Expected basic commit to fail validation w/ no author mail address");
     }
 
-    @Test
-    void validateCommitNoCommitter() {
-        baseCommit.setCommitter(null);
-        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
-                "Expected basic commit to fail validation w/ no committer");
-    }
-
     @Test
     void validateCommitNoCommitterMail() {
         baseCommit.setCommitter(GitUser.builder().setName("Some Name").build());
@@ -86,13 +72,6 @@ class CommitHelperTest {
                 "Expected basic commit to fail validation w/ no committer mail address");
     }
 
-    @Test
-    void validateCommitNoHash() {
-        baseCommit.setHash(null);
-        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
-                "Expected basic commit to fail validation w/ no commit hash");
-    }
-
     @Test
     void validateCommitNoBody() {
         baseCommit.setBody(null);
diff --git a/src/test/resources/database/default/V1.0.0__default.sql b/src/test/resources/database/default/V1.0.0__default.sql
index 1f941ac350a9b30358da165ec1d11e50b8bb22db..ff4c33f79679e8b5cf09f6bfaa19a91d37d6827a 100644
--- a/src/test/resources/database/default/V1.0.0__default.sql
+++ b/src/test/resources/database/default/V1.0.0__default.sql
@@ -7,6 +7,7 @@ CREATE TABLE CommitValidationStatus (
   provider varchar(100) NOT NULL,
   estimatedLoc int DEFAULT 0,
   repoUrl varchar(255) NOT NULL,
+  userMail varchar(255) DEFAULT NULL,
   PRIMARY KEY (id)
 );
 INSERT INTO CommitValidationStatus(commitHash,project,lastModified,creationDate,provider, repoUrl) VALUES('123456789', 'sample.proj', NOW(), NOW(),'GITHUB','http://www.github.com/eclipsefdn/sample');