diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0829f09..8ce104d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,6 +10,13 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ # Opt in to Node 24 ahead of GitHub's 2026-06-02 forced upgrade. Both
+ # actions/checkout@v4 and actions/setup-java@v4 are already the latest
+ # major versions and ship Node 20 binaries; this env var lets the runner
+ # execute them under Node 24 today.
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
+
jobs:
test:
runs-on: ubuntu-latest
@@ -25,3 +32,18 @@ jobs:
- name: Run tests
run: ./mvnw -B test
+
+ javadoc:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 11
+ cache: maven
+
+ - name: Verify Javadoc
+ run: ./mvnw -B javadoc:jar
diff --git a/pom.xml b/pom.xml
index ad404de..b647b23 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.accessgrid
access-grid-sdk
- 1.4.0
+ 1.4.1
Access Grid SDK
Java SDK for Access Grid API
@@ -174,6 +174,9 @@
org.apache.maven.plugins
maven-javadoc-plugin
3.4.1
+
+ true
+
attach-javadocs
diff --git a/src/main/java/com/organization/accessgrid/AccessGridClient.java b/src/main/java/com/organization/accessgrid/AccessGridClient.java
index a735082..803787d 100644
--- a/src/main/java/com/organization/accessgrid/AccessGridClient.java
+++ b/src/main/java/com/organization/accessgrid/AccessGridClient.java
@@ -22,7 +22,7 @@
*/
public class AccessGridClient {
private static final String DEFAULT_BASE_URL = "https://api.accessgrid.com/v1";
- private static final String VERSION = "1.3.0";
+ private static final String VERSION = "1.4.1";
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
private final String accountId;
@@ -77,6 +77,8 @@ public AccessGridClient(String accountId, String apiSecret, HttpSender httpSende
/**
* Get the account ID.
+ *
+ * @return the AccessGrid account identifier this client was constructed with
*/
public String getAccountId() {
return this.accountId;
@@ -84,6 +86,8 @@ public String getAccountId() {
/**
* Access Cards API operations.
+ *
+ * @return an {@link AccessCardsApi} for issuing, listing, and managing access cards
*/
public AccessCardsApi accessCards() {
return new AccessCardsApi(this);
@@ -91,6 +95,8 @@ public AccessCardsApi accessCards() {
/**
* Console Management API operations.
+ *
+ * @return a {@link ConsoleApi} for card templates, landing pages, webhooks, HID, and billing
*/
public ConsoleApi console() {
return new ConsoleApi(this);
@@ -108,6 +114,9 @@ public static class AccessCardsApi {
/**
* Provision a new access card.
+ *
+ * @param request issuance parameters (template id, employee fields, expiration, etc.)
+ * @return the newly issued Card with server-assigned id, state, and install_url
*/
public Models.Card provision(Models.ProvisionCardRequest request) {
String payload = client.serialize(request);
@@ -116,6 +125,9 @@ public Models.Card provision(Models.ProvisionCardRequest request) {
/**
* Get details about a specific access card.
+ *
+ * @param cardId the access pass ex_id
+ * @return the Card record, including current state and metadata
*/
public Models.Card get(String cardId) {
return client.get("/key-cards/" + cardId, cardId, Models.Card.class);
@@ -123,6 +135,9 @@ public Models.Card get(String cardId) {
/**
* Update an existing access card.
+ *
+ * @param request update parameters; the card id is read from {@code request.getCardId()}
+ * @return the updated Card
*/
public Models.Card update(Models.UpdateCardRequest request) {
String payload = client.serialize(request);
@@ -131,6 +146,9 @@ public Models.Card update(Models.UpdateCardRequest request) {
/**
* List access cards with optional filters.
+ *
+ * @param params optional filters (template_id, state); may be null for no filters
+ * @return matching cards; empty list if none match
*/
public java.util.List list(Models.ListKeysParams params) {
StringBuilder query = new StringBuilder();
@@ -150,6 +168,8 @@ public java.util.List list(Models.ListKeysParams params) {
/**
* List access cards without filters.
+ *
+ * @return all cards on the account
*/
public java.util.List list() {
return list(null);
@@ -157,6 +177,8 @@ public java.util.List list() {
/**
* Suspend an access card.
+ *
+ * @param cardId the access pass ex_id to suspend
*/
public void suspend(String cardId) {
client.postEmpty("/key-cards/" + cardId + "/suspend", cardId);
@@ -164,6 +186,8 @@ public void suspend(String cardId) {
/**
* Resume a suspended access card.
+ *
+ * @param cardId the access pass ex_id to resume
*/
public void resume(String cardId) {
client.postEmpty("/key-cards/" + cardId + "/resume", cardId);
@@ -171,6 +195,8 @@ public void resume(String cardId) {
/**
* Unlink an access card from its device.
+ *
+ * @param cardId the access pass ex_id to unlink
*/
public void unlink(String cardId) {
client.postEmpty("/key-cards/" + cardId + "/unlink", cardId);
@@ -178,6 +204,8 @@ public void unlink(String cardId) {
/**
* Delete an access card.
+ *
+ * @param cardId the access pass ex_id to delete
*/
public void delete(String cardId) {
client.postEmpty("/key-cards/" + cardId + "/delete", cardId);
@@ -203,6 +231,9 @@ public static class ConsoleApi {
/**
* Create a new card template.
+ *
+ * @param request template configuration (name, platform, use_case, protocol, styling, etc.)
+ * @return the created Template with server-assigned id
*/
public Models.Template createTemplate(Models.CreateTemplateRequest request) {
String payload = client.serialize(request);
@@ -211,6 +242,9 @@ public Models.Template createTemplate(Models.CreateTemplateRequest request) {
/**
* Update an existing card template.
+ *
+ * @param request fields to update; the template id is read from {@code request.getCardTemplateId()}
+ * @return the updated Template
*/
public Models.Template updateTemplate(Models.UpdateTemplateRequest request) {
String payload = client.serialize(request);
@@ -219,6 +253,9 @@ public Models.Template updateTemplate(Models.UpdateTemplateRequest request) {
/**
* Read a card template by ID.
+ *
+ * @param templateId the card template ex_id
+ * @return the full Template, including styling and association data
*/
public Models.Template readTemplate(String templateId) {
return client.get("/console/card-templates/" + templateId, templateId, Models.Template.class);
@@ -228,6 +265,9 @@ public Models.Template readTemplate(String templateId) {
* Publish a card template. For Apple templates this transitions the
* template to "in-review"; for Android (Google) templates it becomes
* "ready" immediately.
+ *
+ * @param templateId the card template ex_id to publish
+ * @return the template id and resulting status ("publishing", "in-review", or "ready")
*/
public Models.PublishTemplateResponse publishTemplate(String templateId) {
return client.post(
@@ -245,6 +285,9 @@ public Models.PublishTemplateResponse publishTemplate(String templateId) {
* + AES-256-GCM) so the private key never leaves this host in
* plaintext. Each call must use a fresh public key — the server rejects
* reuse.
+ *
+ * @param templateId the card template ex_id (must be a published Google SmartTap template)
+ * @return the decrypted SmartTap private key PEM plus key version, collector id, and pubkey fingerprint
*/
public Models.RevealTemplatePrivateKeyResponse revealTemplatePrivateKey(String templateId) {
if (templateId == null || templateId.isEmpty())
@@ -280,6 +323,10 @@ public Models.RevealTemplatePrivateKeyResponse revealTemplatePrivateKey(String t
/**
* Get event logs for a card template.
+ *
+ * @param templateId the card template ex_id
+ * @param filters optional filters (device, start_date, end_date, event_type); may be null
+ * @return matching events; empty list if none
*/
public java.util.List eventLog(String templateId, Models.EventLogFilters filters) {
StringBuilder query = new StringBuilder();
@@ -305,6 +352,9 @@ public java.util.List eventLog(String templateId, Models.EventLogF
/**
* Get event logs for a card template without filters.
+ *
+ * @param templateId the card template ex_id
+ * @return all events for the template
*/
public java.util.List eventLog(String templateId) {
return eventLog(templateId, null);
@@ -312,6 +362,9 @@ public java.util.List eventLog(String templateId) {
/**
* Get ledger/billing items.
+ *
+ * @param params optional pagination + date filters (page, per_page, start_date, end_date); may be null
+ * @return the matching ledger items with pagination metadata
*/
public Models.LedgerItemsResult ledgerItems(Models.LedgerItemsParams params) {
StringBuilder query = new StringBuilder();
@@ -335,6 +388,8 @@ public Models.LedgerItemsResult ledgerItems(Models.LedgerItemsParams params) {
/**
* Get ledger/billing items without filters.
+ *
+ * @return the first page of ledger items at the server's default page size
*/
public Models.LedgerItemsResult ledgerItems() {
return ledgerItems(null);
@@ -342,6 +397,10 @@ public Models.LedgerItemsResult ledgerItems() {
/**
* iOS In-App Provisioning preflight.
+ *
+ * @param cardTemplateId the card template ex_id
+ * @param accessPassExId the access pass ex_id being provisioned
+ * @return the identifiers required to drive the Apple Wallet In-App Provisioning flow
*/
public Models.IosPreflightResponse iosPreflight(String cardTemplateId, String accessPassExId) {
String payload = client.serialize(java.util.Map.of("access_pass_ex_id", accessPassExId));
@@ -350,6 +409,8 @@ public Models.IosPreflightResponse iosPreflight(String cardTemplateId, String ac
/**
* List all landing pages.
+ *
+ * @return every landing page on the account
*/
public java.util.List listLandingPages() {
return java.util.Arrays.asList(
@@ -359,6 +420,9 @@ public java.util.List listLandingPages() {
/**
* Create a new landing page.
+ *
+ * @param request landing-page configuration (name, kind, password-protection, styling, etc.)
+ * @return the created LandingPage with server-assigned ex_id
*/
public Models.LandingPage createLandingPage(Models.CreateLandingPageRequest request) {
String payload = client.serialize(request);
@@ -367,6 +431,9 @@ public Models.LandingPage createLandingPage(Models.CreateLandingPageRequest requ
/**
* Update an existing landing page.
+ *
+ * @param request fields to update; the landing page id is read from {@code request.getLandingPageId()}
+ * @return the updated LandingPage
*/
public Models.LandingPage updateLandingPage(Models.UpdateLandingPageRequest request) {
String payload = client.serialize(request);
@@ -375,6 +442,8 @@ public Models.LandingPage updateLandingPage(Models.UpdateLandingPageRequest requ
/**
* List pass template pairs.
+ *
+ * @return every pass template pair on the account
*/
public java.util.List listPassTemplatePairs() {
Models.PassTemplatePairsResponse response = client.getWithParams(
@@ -387,6 +456,9 @@ public java.util.List listPassTemplatePairs() {
/**
* Create a pass template pair.
+ *
+ * @param request the iOS + Android template ids to pair (both must be published and use the same protocol)
+ * @return the created PassTemplatePair
*/
public Models.PassTemplatePair createPassTemplatePair(Models.CreatePassTemplatePairRequest request) {
String payload = client.serialize(request);
@@ -395,6 +467,8 @@ public Models.PassTemplatePair createPassTemplatePair(Models.CreatePassTemplateP
/**
* Credential profile operations.
+ *
+ * @return a {@link CredentialProfilesApi} for listing and creating credential profiles
*/
public CredentialProfilesApi credentialProfiles() {
return new CredentialProfilesApi(client);
@@ -402,6 +476,8 @@ public CredentialProfilesApi credentialProfiles() {
/**
* Webhook operations.
+ *
+ * @return a {@link WebhooksApi} for listing, creating, and deleting webhook subscriptions
*/
public WebhooksApi webhooks() {
return new WebhooksApi(client);
@@ -409,6 +485,8 @@ public WebhooksApi webhooks() {
/**
* HID-related services.
+ *
+ * @return an {@link HIDApi} entry point for HID Origo organization operations
*/
public HIDApi hid() {
return new HIDApi(client);
@@ -434,6 +512,8 @@ public static class HIDApi {
/**
* HID Organizations API.
+ *
+ * @return a {@link HIDOrgsApi} for creating, listing, and activating HID organizations
*/
public HIDOrgsApi orgs() {
return new HIDOrgsApi(client);
@@ -452,6 +532,9 @@ public static class HIDOrgsApi {
/**
* Create a new HID organization.
+ *
+ * @param params organization creation parameters (name, contact, address)
+ * @return the created HIDOrg; check {@code status} for current state
*/
public Models.HIDOrg create(Models.CreateHIDOrgParams params) {
String payload = client.serialize(params);
@@ -460,6 +543,8 @@ public Models.HIDOrg create(Models.CreateHIDOrgParams params) {
/**
* List all HID organizations.
+ *
+ * @return every HID organization on the account
*/
public java.util.List list() {
return java.util.Arrays.asList(
@@ -469,6 +554,9 @@ public java.util.List list() {
/**
* Complete HID org registration with credentials.
+ *
+ * @param params activation parameters (org slug + the credentials returned by HID)
+ * @return the activated HIDOrg
*/
public Models.HIDOrg activate(Models.CompleteHIDOrgParams params) {
String payload = client.serialize(params);
@@ -488,6 +576,8 @@ public static class CredentialProfilesApi {
/**
* List all credential profiles.
+ *
+ * @return every credential profile on the account
*/
public java.util.List list() {
return java.util.Arrays.asList(
@@ -497,6 +587,9 @@ public java.util.List list() {
/**
* Create a new credential profile.
+ *
+ * @param request credential profile configuration (name, kind, bit format, key diversification, etc.)
+ * @return the created CredentialProfile with server-assigned ex_id
*/
public Models.CredentialProfile create(Models.CreateCredentialProfileRequest request) {
String payload = client.serialize(request);
@@ -516,6 +609,8 @@ public static class WebhooksApi {
/**
* List all webhooks.
+ *
+ * @return every webhook subscription on the account
*/
public java.util.List list() {
Models.WebhooksResponse response = client.getWithParams(
@@ -528,6 +623,9 @@ public java.util.List list() {
/**
* Create a new webhook.
+ *
+ * @param request webhook configuration (URL, auth_method, subscribed_events)
+ * @return the created Webhook; on bearer_token auth the {@code privateKey} is only present here
*/
public Models.Webhook create(Models.CreateWebhookRequest request) {
String payload = client.serialize(request);
@@ -536,6 +634,8 @@ public Models.Webhook create(Models.CreateWebhookRequest request) {
/**
* Delete a webhook.
+ *
+ * @param webhookId the webhook id to delete
*/
public void delete(String webhookId) {
client.delete("/console/webhooks/" + webhookId);
diff --git a/src/test/java/com/organization/accessgrid/VersionConsistencyTest.java b/src/test/java/com/organization/accessgrid/VersionConsistencyTest.java
new file mode 100644
index 0000000..5101be4
--- /dev/null
+++ b/src/test/java/com/organization/accessgrid/VersionConsistencyTest.java
@@ -0,0 +1,41 @@
+package com.organization.accessgrid;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Locks the AccessGridClient.VERSION constant to the pom.xml project version.
+ * Drift between the two has shipped before (1.3.0 constant under a 1.4.0 pom),
+ * so this test fails the build if they disagree.
+ */
+public class VersionConsistencyTest {
+
+ @Test
+ public void versionConstantMatchesPomXml() throws IOException, NoSuchFieldException, IllegalAccessException {
+ String pom = Files.readString(Paths.get("pom.xml"));
+
+ Matcher m = Pattern.compile(
+ "access-grid-sdk\\s*([^<]+)"
+ ).matcher(pom);
+
+ assertTrue(m.find(), "Could not find for access-grid-sdk in pom.xml");
+ String pomVersion = m.group(1);
+
+ Field versionField = AccessGridClient.class.getDeclaredField("VERSION");
+ versionField.setAccessible(true);
+ String constantVersion = (String) versionField.get(null);
+
+ assertEquals(
+ pomVersion,
+ constantVersion,
+ "AccessGridClient.VERSION must match pom.xml . Update both when bumping."
+ );
+ }
+}