From b0aed4597772d25dc18876d61f8ed65df5316527 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Wed, 9 Sep 2026 16:40:41 -0300 Subject: [PATCH 1/3] feat(webhooks): add signing secret rotation endpoint Co-Authored-By: Claude Fable 5.1 --- .../resend/services/webhooks/Webhooks.java | 18 +++++++ ...teWebhookSigningSecretResponseSuccess.java | 50 +++++++++++++++++++ .../services/webhooks/WebhooksTest.java | 17 +++++++ 3 files changed, 85 insertions(+) create mode 100644 src/main/java/com/resend/services/webhooks/model/RotateWebhookSigningSecretResponseSuccess.java diff --git a/src/main/java/com/resend/services/webhooks/Webhooks.java b/src/main/java/com/resend/services/webhooks/Webhooks.java index ae37e3a..b4a06d4 100644 --- a/src/main/java/com/resend/services/webhooks/Webhooks.java +++ b/src/main/java/com/resend/services/webhooks/Webhooks.java @@ -20,6 +20,7 @@ import com.resend.services.webhooks.model.ListWebhookEventsParams; import com.resend.services.webhooks.model.ListWebhookEventsResponseSuccess; import com.resend.services.webhooks.model.ReplayWebhookEventResponseSuccess; +import com.resend.services.webhooks.model.RotateWebhookSigningSecretResponseSuccess; import com.resend.services.webhooks.model.VerifyWebhookOptions; import okhttp3.MediaType; import javax.crypto.Mac; @@ -204,6 +205,23 @@ public ReplayWebhookEventResponseSuccess replayEvent(String webhookId, String ev return resendMapper.readValue(response.getBody(), ReplayWebhookEventResponseSuccess.class); } + /** + * Rotates the signing secret of a webhook. The previous secret stops verifying immediately. + * + * @param webhookId The unique identifier of the webhook. + * @return A RotateWebhookSigningSecretResponseSuccess containing the webhook ID and the new signing secret. + * @throws ResendException If an error occurs while rotating the signing secret. + */ + public RotateWebhookSigningSecretResponseSuccess rotateSigningSecret(String webhookId) throws ResendException { + AbstractHttpResponse response = httpClient.perform("/webhooks/" + webhookId + "/signing-secret/rotate", super.apiKey, HttpMethod.POST, "", MediaType.get("application/json")); + + if (!response.isSuccessful()) { + throw new ResendException(response.getCode(), response.getBody()); + } + + return resendMapper.readValue(response.getBody(), RotateWebhookSigningSecretResponseSuccess.class); + } + /** * Retrieves the delivery attempts for a webhook event. * diff --git a/src/main/java/com/resend/services/webhooks/model/RotateWebhookSigningSecretResponseSuccess.java b/src/main/java/com/resend/services/webhooks/model/RotateWebhookSigningSecretResponseSuccess.java new file mode 100644 index 0000000..b29aa24 --- /dev/null +++ b/src/main/java/com/resend/services/webhooks/model/RotateWebhookSigningSecretResponseSuccess.java @@ -0,0 +1,50 @@ +package com.resend.services.webhooks.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a successful response from rotating a webhook signing secret. + */ +public class RotateWebhookSigningSecretResponseSuccess { + @JsonProperty("object") + private String object; + + @JsonProperty("id") + private String id; + + @JsonProperty("signing_secret") + private String signingSecret; + + /** + * Constructs an empty webhook signing secret rotation response. + */ + public RotateWebhookSigningSecretResponseSuccess() { + } + + /** + * Gets the object type. + * + * @return The object type. + */ + public String getObject() { + return object; + } + + /** + * Gets the webhook ID. + * + * @return The webhook ID. + */ + public String getId() { + return id; + } + + /** + * Gets the new signing secret for webhook verification. + * + * @return The signing secret. + */ + public String getSigningSecret() { + return signingSecret; + } +} diff --git a/src/test/java/com/resend/services/webhooks/WebhooksTest.java b/src/test/java/com/resend/services/webhooks/WebhooksTest.java index 2fb5e71..9ac754f 100644 --- a/src/test/java/com/resend/services/webhooks/WebhooksTest.java +++ b/src/test/java/com/resend/services/webhooks/WebhooksTest.java @@ -60,6 +60,9 @@ public class WebhooksTest { private static final String REPLAY_WEBHOOK_EVENT_JSON = "{\"object\":\"webhook_event\",\"id\":\"" + EVENT_ID + "\"}"; + private static final String ROTATE_WEBHOOK_SIGNING_SECRET_JSON = + "{\"object\":\"webhook\",\"id\":\"" + WEBHOOK_ID + "\",\"signing_secret\":\"whsec_rotated_secret\"}"; + private static final String LIST_WEBHOOK_EVENT_ATTEMPTS_JSON = "{\"object\":\"list\",\"has_more\":false,\"data\":[{\"id\":\"atmpt_1srOrx2ZWZBpBUvZwXKQmoEYga2\",\"http_status_code\":200,\"response\":\"{\\\"ok\\\":true}\",\"sent_at\":\"2026-08-22T15:33:12.000Z\"}]}"; @@ -218,6 +221,20 @@ public void testReplayWebhookEvent_Success() throws ResendException { assertEquals(EVENT_ID, response.getId()); } + @Test + public void testRotateWebhookSigningSecret_Success() throws ResendException { + AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, ROTATE_WEBHOOK_SIGNING_SECRET_JSON, true); + + when(httpClient.perform(eq("/webhooks/" + WEBHOOK_ID + "/signing-secret/rotate"), anyString(), eq(HttpMethod.POST), eq(""), any(MediaType.class))) + .thenReturn(httpResponse); + + RotateWebhookSigningSecretResponseSuccess response = webhooks.rotateSigningSecret(WEBHOOK_ID); + + assertEquals("webhook", response.getObject()); + assertEquals(WEBHOOK_ID, response.getId()); + assertEquals("whsec_rotated_secret", response.getSigningSecret()); + } + @Test public void testListWebhookEventAttemptsWithPagination_Success() throws ResendException { AbstractHttpResponse httpResponse = new AbstractHttpResponse<>(200, LIST_WEBHOOK_EVENT_ATTEMPTS_JSON, true); From d36cb068eb0a5f073889d580bb02b22507ec41b1 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 10 Sep 2026 09:42:23 -0300 Subject: [PATCH 2/3] chore: bump version to 4.24.0 Co-Authored-By: Claude Fable 5.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8ed9dd9..7a57b5c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.resend -VERSION_NAME=4.23.0 +VERSION_NAME=4.24.0 POM_URL=https://github.com/resendlabs/resend-java POM_SCM_URL=https://github.com/resendlabs/resend-java.git From c4faa312ad1bd378eb4864723310216711142f90 Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Thu, 10 Sep 2026 10:52:17 -0300 Subject: [PATCH 3/3] docs(webhooks): note the 24 hour grace period after rotating the signing secret Co-Authored-By: Claude Fable 5.1 --- src/main/java/com/resend/services/webhooks/Webhooks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/resend/services/webhooks/Webhooks.java b/src/main/java/com/resend/services/webhooks/Webhooks.java index b4a06d4..9bab52f 100644 --- a/src/main/java/com/resend/services/webhooks/Webhooks.java +++ b/src/main/java/com/resend/services/webhooks/Webhooks.java @@ -206,7 +206,7 @@ public ReplayWebhookEventResponseSuccess replayEvent(String webhookId, String ev } /** - * Rotates the signing secret of a webhook. The previous secret stops verifying immediately. + * Rotates the signing secret of a webhook. The previous secret keeps working for 24 hours. * * @param webhookId The unique identifier of the webhook. * @return A RotateWebhookSigningSecretResponseSuccess containing the webhook ID and the new signing secret.