From be42898afb15ff8e6e29a46b7f15125c328e716b Mon Sep 17 00:00:00 2001 From: Venkateswaran S Date: Wed, 9 Apr 2025 10:31:09 +0200 Subject: [PATCH 01/17] support for copy code --- .../conversations/MessageComponentType.java | 3 +- .../objects/conversations/MessageParam.java | 9 ++ .../conversations/TemplateMediaType.java | 3 +- ...leConversationSendHSMCopyCodeTemplate.java | 92 +++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java b/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java index cdb695cd..9f9290cf 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java +++ b/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java @@ -13,7 +13,8 @@ public enum MessageComponentType { BUTTON("button"), CARD("card"), CAROUSEL("carousel"), - LIMITED_TIME_OFFER("limited_time_offer"); + LIMITED_TIME_OFFER("limited_time_offer"), + COPY_CODE("copy_code"); private static final Map TYPE_MAP; diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java index 38ae18f6..7772d2cd 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java +++ b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java @@ -15,6 +15,8 @@ public class MessageParam { private Media video; @JsonProperty("expiration_time") private String expirationTime; + @JsonProperty("coupon_code") + private String couponCode; public TemplateMediaType getType() { return type; @@ -93,6 +95,13 @@ public void setExpirationTime(String expirationTime) { this.expirationTime = expirationTime; } + public void setCouponCode(String couponCode) { + if (StringUtils.isBlank(couponCode)) { + throw new IllegalArgumentException("couponCode cannot be null or empty"); + } + this.couponCode = couponCode; + } + @Override public String toString() { StringBuilder sb = new StringBuilder("MessageParam{"); diff --git a/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java b/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java index fd600ab2..580dcc97 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java +++ b/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java @@ -15,7 +15,8 @@ public enum TemplateMediaType { CURRENCY("currency"), DATETIME("date_time"), PAYLOAD("payload"), - EXPIRATION_TIME("expiration_time"); + EXPIRATION_TIME("expiration_time"), + COUPON_CODE("coupon_code"); private static final Map TYPE_MAP; diff --git a/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java new file mode 100644 index 00000000..44f482bb --- /dev/null +++ b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java @@ -0,0 +1,92 @@ +import com.messagebird.MessageBirdClient; +import com.messagebird.MessageBirdService; +import com.messagebird.MessageBirdServiceImpl; +import com.messagebird.exceptions.GeneralException; +import com.messagebird.exceptions.UnauthorizedException; +import com.messagebird.objects.conversations.*; + +import java.util.ArrayList; +import java.util.List; + +public class ExampleConversationSendHSMCopyCodeTemplate { + + public static void main(String[] args) { + if (args.length < 6) { + System.out.println("Please at least specify your access key, the channel id and destination address.\n" + + "Usage : java -jar test_accesskey(Required) channel_id(Required) from(Required) destination(Required) templateName(Required) namespace(Required) expirationTimeInput(Required)"); + return; + } + + final String accessKey = args[0]; + final String from = args[1]; + final String destination = args[2]; + final String templateName = args[3]; + final String namespace = args[4]; + final String couponCodeInput = args[5]; + + //First create your service object + final MessageBirdService wsr = new MessageBirdServiceImpl(accessKey); + //Add the service to the client + final MessageBirdClient messageBirdClient = new MessageBirdClient(wsr); + + ConversationContent conversationContent = new ConversationContent(); + ConversationContentHsm conversationContentHsm = new ConversationContentHsm(); + conversationContentHsm.setNamespace(namespace); + conversationContentHsm.setTemplateName(templateName); + ConversationHsmLanguage language = new ConversationHsmLanguage(); + language.setCode("en"); + conversationContentHsm.setLanguage(language); + List messageComponents = new ArrayList<>(); + + // Add LTO component + MessageComponent messageCopyCodeComponent = new MessageComponent(); + messageCopyCodeComponent.setType(MessageComponentType.BUTTON); + messageCopyCodeComponent.setSub_type(MessageComponentType.COPY_CODE.toString()); + List messageCCParams = new ArrayList<>(); + + MessageParam couponCodeParam = new MessageParam(); + couponCodeParam.setType(TemplateMediaType.COUPON_CODE); + couponCodeParam.setCouponCode(couponCodeInput); + messageCCParams.add(couponCodeParam); + + messageCopyCodeComponent.setParameters(messageCCParams); + + // Add body component + MessageComponent messageBodyComponent = new MessageComponent(); + messageBodyComponent.setType(MessageComponentType.BODY); + List messageBodyParams = new ArrayList<>(); + + MessageParam text = new MessageParam(); + text.setType(TemplateMediaType.TEXT); + text.setText("Bob"); + messageBodyParams.add(text); + + messageBodyComponent.setParameters(messageBodyParams); + + messageComponents.add(messageCopyCodeComponent); + messageComponents.add(messageBodyComponent); + conversationContentHsm.setComponents(messageComponents); + conversationContent.setHsm(conversationContentHsm); + ConversationSendRequest request = new ConversationSendRequest( + destination, + ConversationContentType.HSM, + conversationContent, + from, + "", + null, + null, + null); + + try { + System.out.println(request.toString()); + ConversationSendResponse sendResponse = messageBirdClient.sendMessage(request); + System.out.println(sendResponse.toString()); + + } catch (UnauthorizedException e) { + System.err.println("Authorization failed. Please check your access key: " + e.getMessage()); + } catch (GeneralException e) { + System.err.println("An error occurred while sending the message: " + e.getMessage()); + } + } + +} From 41052be1a88176e6ea0ce1bb2453baaf7ad5c390 Mon Sep 17 00:00:00 2001 From: Venkat Sankaran Date: Wed, 9 Apr 2025 13:23:02 +0200 Subject: [PATCH 02/17] Update to version 6.2.3 --- api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pom.xml b/api/pom.xml index 2d99afc2..fd0dbced 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.messagebird messagebird-api - 6.2.2 + 6.2.3 jar ${project.groupId}:${project.artifactId} From edf16851ea6d5c8f6f45fc09dda3cce8778380f1 Mon Sep 17 00:00:00 2001 From: Venkat Sankaran Date: Wed, 9 Apr 2025 13:24:40 +0200 Subject: [PATCH 03/17] Update examples --- examples/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index 4a98866a..ba4ab890 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ com.messagebird examples - 6.2.2 + 6.2.3 @@ -20,7 +20,7 @@ com.messagebird messagebird-api - 6.2.2 + 6.2.3 compile From 94e0aee94e3e63d9e247db43d791a75cf53a49fd Mon Sep 17 00:00:00 2001 From: Venkat Sankaran Date: Wed, 9 Apr 2025 13:25:15 +0200 Subject: [PATCH 04/17] Update client version string --- api/src/main/java/com/messagebird/MessageBirdServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java index a5e897dd..33f6f8e6 100644 --- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java +++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java @@ -72,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService { private final String accessKey; private final String serviceUrl; - private final String clientVersion = "6.2.2"; + private final String clientVersion = "6.2.3"; private final String userAgentString; private Proxy proxy = null; From 2abd1cdad7b32cf5d10f6cef8cdeaa04320c6822 Mon Sep 17 00:00:00 2001 From: Venkateswaran S Date: Wed, 9 Apr 2025 16:38:43 +0200 Subject: [PATCH 05/17] update to version v6.2.4 --- api/pom.xml | 2 +- api/src/main/java/com/messagebird/MessageBirdServiceImpl.java | 2 +- examples/pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index fd0dbced..cdfd9969 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.messagebird messagebird-api - 6.2.3 + 6.2.4 jar ${project.groupId}:${project.artifactId} diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java index 33f6f8e6..b36427fc 100644 --- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java +++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java @@ -72,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService { private final String accessKey; private final String serviceUrl; - private final String clientVersion = "6.2.3"; + private final String clientVersion = "6.2.4"; private final String userAgentString; private Proxy proxy = null; diff --git a/examples/pom.xml b/examples/pom.xml index ba4ab890..4b56224c 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ com.messagebird examples - 6.2.3 + 6.2.4 @@ -20,7 +20,7 @@ com.messagebird messagebird-api - 6.2.3 + 6.2.4 compile From 51dcd3911fd0855a2f5f8ecc038a442e8fefc0d6 Mon Sep 17 00:00:00 2001 From: Venkateswaran S Date: Thu, 10 Apr 2025 10:53:28 +0200 Subject: [PATCH 06/17] adding venkat to devs --- api/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/pom.xml b/api/pom.xml index cdfd9969..d060594d 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -27,6 +27,12 @@ MessageBird https://www.messagebird.com + + Venkateswaran S + venkat.sankaran@bird.com + MessageBird + https://www.messagebird.com + Sam Wierema sam@messagebird.com From 24b8bdb05553977c5437b903f3ab8b02a2fb4a04 Mon Sep 17 00:00:00 2001 From: Venkateswaran S Date: Fri, 11 Apr 2025 12:17:36 +0200 Subject: [PATCH 07/17] adding helper method to get copyCode --- .../com/messagebird/objects/conversations/MessageParam.java | 5 +++++ .../messagebird/objects/conversations/MessageParamTest.java | 2 +- .../java/ExampleConversationSendHSMCopyCodeTemplate.java | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java index 7772d2cd..6432369f 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java +++ b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java @@ -95,6 +95,10 @@ public void setExpirationTime(String expirationTime) { this.expirationTime = expirationTime; } + public String getCouponCode() { + return couponCode; + } + public void setCouponCode(String couponCode) { if (StringUtils.isBlank(couponCode)) { throw new IllegalArgumentException("couponCode cannot be null or empty"); @@ -114,6 +118,7 @@ public String toString() { .append(", image=").append(image) .append(", video=").append(video) .append(", expirationTime='").append(expirationTime).append('\'') + .append(", couponCode='").append(couponCode).append('\'') .append('}'); return sb.toString(); } diff --git a/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java b/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java index 6ddabad7..f1147d2f 100644 --- a/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java +++ b/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java @@ -11,7 +11,7 @@ public void testMessageParamToString() { param.setText("Sample text"); param.setPayload("Sample payload"); - String expected = "MessageParam{type=image, text='Sample text', payload='Sample payload', currency=null, dateTime='null', document=null, image=null, video=null, expirationTime='null'}"; + String expected = "MessageParam{type=image, text='Sample text', payload='Sample payload', currency=null, dateTime='null', document=null, image=null, video=null, expirationTime='null', couponCode='null'}"; assertEquals(expected, param.toString()); } diff --git a/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java index 44f482bb..4d5dcd3f 100644 --- a/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java +++ b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java @@ -13,7 +13,7 @@ public class ExampleConversationSendHSMCopyCodeTemplate { public static void main(String[] args) { if (args.length < 6) { System.out.println("Please at least specify your access key, the channel id and destination address.\n" + - "Usage : java -jar test_accesskey(Required) channel_id(Required) from(Required) destination(Required) templateName(Required) namespace(Required) expirationTimeInput(Required)"); + "Usage : java -jar test_accesskey(Required) channel_id(Required) from(Required) destination(Required) templateName(Required) namespace(Required) couponCodeInput(Required)"); return; } From 5746f75677b0e0fd81cf966428bb5b28ad451db4 Mon Sep 17 00:00:00 2001 From: Venkateswaran S Date: Fri, 11 Apr 2025 12:28:38 +0200 Subject: [PATCH 08/17] update version 6.2.5 --- api/pom.xml | 2 +- api/src/main/java/com/messagebird/MessageBirdServiceImpl.java | 2 +- examples/pom.xml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index d060594d..e17d2358 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.messagebird messagebird-api - 6.2.4 + 6.2.5 jar ${project.groupId}:${project.artifactId} diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java index b36427fc..3cc1f25e 100644 --- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java +++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java @@ -72,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService { private final String accessKey; private final String serviceUrl; - private final String clientVersion = "6.2.4"; + private final String clientVersion = "6.2.5"; private final String userAgentString; private Proxy proxy = null; diff --git a/examples/pom.xml b/examples/pom.xml index 4b56224c..a0089732 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ com.messagebird examples - 6.2.4 + 6.2.5 @@ -20,7 +20,7 @@ com.messagebird messagebird-api - 6.2.4 + 6.2.5 compile From 8ec864ea95ae45c6b00150d46c442136fa4832fb Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Thu, 21 May 2026 10:44:53 +0100 Subject: [PATCH 09/17] feat: add WhatsApp BSUID support and split test/integration profiles - Add ConversationMessageMetadata, ConversationSenderMetadata, and ConversationStatusMessageMetadata to model BSUID webhook payloads - Add metadata field to ConversationMessage - Split Maven test/integration profiles so unit tests run without credentials - Fix ContactTest and MessageBirdClientTest to skip gracefully via assumeNotNull - Bump version to 6.3.0 Co-Authored-By: Claude Sonnet 4.6 --- api/pom.xml | 38 ++++++++- .../messagebird/MessageBirdServiceImpl.java | 2 +- .../conversations/ConversationMessage.java | 10 +++ .../ConversationMessageMetadata.java | 39 +++++++++ .../ConversationSenderMetadata.java | 47 +++++++++++ .../ConversationStatusMessageMetadata.java | 79 +++++++++++++++++++ .../java/com/messagebird/ContactTest.java | 2 + .../com/messagebird/ConversationsTest.java | 1 + .../messagebird/MessageBirdClientTest.java | 6 +- 9 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java diff --git a/api/pom.xml b/api/pom.xml index e17d2358..fb65b2b0 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.messagebird messagebird-api - 6.2.5 + 6.3.0 jar ${project.groupId}:${project.artifactId} @@ -62,19 +62,51 @@ + test false + + + UTF-8 + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + **/ContactTest.java + **/MessageBirdClientTest.java + + + + + + + + + integration + + false + UTF-8 + disable-doclint - [8,11,) + [1.8,11) none - true UTF-8 diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java index 3cc1f25e..ce3c10e7 100644 --- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java +++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java @@ -72,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService { private final String accessKey; private final String serviceUrl; - private final String clientVersion = "6.2.5"; + private final String clientVersion = "6.3.0"; private final String userAgentString; private Proxy proxy = null; diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java index 2be027be..1dea8280 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessage.java @@ -22,6 +22,7 @@ public class ConversationMessage { private Date updatedDatetime; private Map source; private ConversationMessageTag tag; + private ConversationMessageMetadata metadata; /** * See: {@link ConversationPlatformConstants} */ @@ -115,6 +116,14 @@ public void setTag(ConversationMessageTag tag) { this.tag = tag; } + public ConversationMessageMetadata getMetadata() { + return metadata; + } + + public void setMetadata(ConversationMessageMetadata metadata) { + this.metadata = metadata; + } + public String getPlatform() { return platform; } @@ -146,6 +155,7 @@ public String toString() { ", updatedDatetime=" + updatedDatetime + ", source=" + source + ", tag=" + tag + + ", metadata=" + metadata + ", platform='" + platform + '\'' + '}'; } diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java new file mode 100644 index 00000000..7d908e6a --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java @@ -0,0 +1,39 @@ +package com.messagebird.objects.conversations; + +import java.util.Date; + +/** + * Inner metadata attached to a conversation message. Present on both incoming + * messages and status webhook payloads. {@code sender.userId} always contains + * the BSUID when Meta provides one. When both identifiers exist, the phone + * number appears in the parent {@code from} field, not in this object. + */ +public class ConversationMessageMetadata { + + private ConversationSenderMetadata sender; + private Date receivedAt; + + public ConversationSenderMetadata getSender() { + return sender; + } + + public void setSender(ConversationSenderMetadata sender) { + this.sender = sender; + } + + public Date getReceivedAt() { + return receivedAt; + } + + public void setReceivedAt(Date receivedAt) { + this.receivedAt = receivedAt; + } + + @Override + public String toString() { + return "ConversationMessageMetadata{" + + "sender=" + sender + + ", receivedAt=" + receivedAt + + '}'; + } +} diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java new file mode 100644 index 00000000..33d7e225 --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java @@ -0,0 +1,47 @@ +package com.messagebird.objects.conversations; + +/** + * Metadata about the sender of a WhatsApp message. {@code userId} always + * contains the BSUID (e.g. "US.13491208655302741918") when Meta supplies one. + * When both a phone number and a BSUID are available, the phone number appears + * in the parent message's {@code from} field — not here. + */ +public class ConversationSenderMetadata { + + private String userId; + private String username; + private String displayName; + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public String toString() { + return "ConversationSenderMetadata{" + + "userId='" + userId + '\'' + + ", username='" + username + '\'' + + ", displayName='" + displayName + '\'' + + '}'; + } +} diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java new file mode 100644 index 00000000..ce5635ae --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java @@ -0,0 +1,79 @@ +package com.messagebird.objects.conversations; + +/** + * The {@code messageMetadata} block delivered in status webhook events (e.g. + * {@code statusSent}). Reflects the original message that triggered the status. + * + *

Both {@code from} and {@code to} accept either a phone number or a + * WhatsApp Business-Scoped User ID (BSUID, e.g. "US.13491208655302741918"). + * The BSUID is also available via {@code metadata.sender.userId}. + */ +public class ConversationStatusMessageMetadata { + + private String id; + private String from; + private String to; + private String type; + private ConversationContent content; + private ConversationMessageMetadata metadata; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public ConversationContent getContent() { + return content; + } + + public void setContent(ConversationContent content) { + this.content = content; + } + + public ConversationMessageMetadata getMetadata() { + return metadata; + } + + public void setMetadata(ConversationMessageMetadata metadata) { + this.metadata = metadata; + } + + @Override + public String toString() { + return "ConversationStatusMessageMetadata{" + + "id='" + id + '\'' + + ", from='" + from + '\'' + + ", to='" + to + '\'' + + ", type='" + type + '\'' + + ", content=" + content + + ", metadata=" + metadata + + '}'; + } +} diff --git a/api/src/test/java/com/messagebird/ContactTest.java b/api/src/test/java/com/messagebird/ContactTest.java index 2377733d..5dfdea38 100644 --- a/api/src/test/java/com/messagebird/ContactTest.java +++ b/api/src/test/java/com/messagebird/ContactTest.java @@ -8,6 +8,7 @@ import org.mockito.Mockito; import static org.junit.Assert.*; import static org.mockito.Mockito.*; +import static org.junit.Assume.assumeNotNull; import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; /** @@ -30,6 +31,7 @@ public class ContactTest { @BeforeClass public static void setUpClass() throws UnauthorizedException, GeneralException { String accessKey = System.getProperty("messageBirdAccessKey"); + assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey to run", accessKey); msisdn = generateMsisdn(); diff --git a/api/src/test/java/com/messagebird/ConversationsTest.java b/api/src/test/java/com/messagebird/ConversationsTest.java index 4e7624e8..e08f0175 100644 --- a/api/src/test/java/com/messagebird/ConversationsTest.java +++ b/api/src/test/java/com/messagebird/ConversationsTest.java @@ -17,6 +17,7 @@ public class ConversationsTest { private static final String JSON_CONVERSATION = "{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://example.com/assets/image.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678 },\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"chname\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-22T16:05:15Z\",\"lastReceivedDatetime\": \"2018-08-22T15:47:34Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 1,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}"; private static final String JSON_CONVERSATION_LIST = "{\"offset\": 20,\"limit\": 10,\"count\": 1,\"totalCount\": 1,\"items\": [{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://s3-eu-west-1.amazonaws.com/messagebird-chat/telegram/0d1dae7t5b7d7eb4531c14n04328336/6de655at5b7d859309f821n60607065/d0b705dt5b7d859309f987n05372263.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678},\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"TestChannel\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-24T09:49:01Z\",\"lastReceivedDatetime\": \"2018-08-24T09:49:01Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 3,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}]}"; private static final String JSON_UNAUTHORIZED_ERROR = "{\"errors\": [{\"code\": 2,\"description\": \"Request was not authenticated\"}]}"; + // Status-update payload where Meta supplies both a phone number and a BSUID on the contact. @Test(expected = UnauthorizedException.class) public void testItThrowsErrors() throws GeneralException, UnauthorizedException { diff --git a/api/src/test/java/com/messagebird/MessageBirdClientTest.java b/api/src/test/java/com/messagebird/MessageBirdClientTest.java index ee2dc890..b11c2f54 100644 --- a/api/src/test/java/com/messagebird/MessageBirdClientTest.java +++ b/api/src/test/java/com/messagebird/MessageBirdClientTest.java @@ -16,6 +16,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; +import static org.junit.Assume.assumeNotNull; import java.math.BigInteger; import java.util.Collections; @@ -44,7 +45,10 @@ public class MessageBirdClientTest { @BeforeClass public static void setUpClass() { messageBirdAccessKey = System.getProperty("messageBirdAccessKey"); - messageBirdMSISDN = new BigInteger(System.getProperty("messageBirdMSISDN")); + String msisdn = System.getProperty("messageBirdMSISDN"); + assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey and -DmessageBirdMSISDN to run", + messageBirdAccessKey, msisdn); + messageBirdMSISDN = new BigInteger(msisdn); } @Before From fa2a89860e0647accd1c4653a429ab99915dfa3b Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Thu, 21 May 2026 12:18:24 +0100 Subject: [PATCH 10/17] test: add deserialization tests for BSUID metadata; refine status metadata javadoc - Add JSON round-trip tests covering ConversationMessage.metadata and ConversationStatusMessageMetadata so the new types are exercised. - Clarify in javadoc that ConversationStatusMessageMetadata is a standalone POJO for consumers parsing incoming webhook payloads (not produced by any SDK request). - Remove dangling code comment in ConversationsTest. Co-Authored-By: Claude Sonnet 4.6 --- .../ConversationStatusMessageMetadata.java | 9 ++++- .../messagebird/ConversationMessagesTest.java | 39 +++++++++++++++++++ .../com/messagebird/ConversationsTest.java | 1 - 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java index ce5635ae..df7feebf 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java @@ -1,8 +1,13 @@ package com.messagebird.objects.conversations; /** - * The {@code messageMetadata} block delivered in status webhook events (e.g. - * {@code statusSent}). Reflects the original message that triggered the status. + * The {@code messageMetadata} block delivered inside status webhook payloads + * (e.g. {@code statusSent}, {@code statusDelivered}). Reflects the original + * message that triggered the status update. + * + *

This class is not produced by any SDK request — it is a standalone POJO + * intended for consumers who deserialize incoming webhook payloads in their + * own HTTP handlers. Use it via {@code ObjectMapper.readValue(body, ...)}. * *

Both {@code from} and {@code to} accept either a phone number or a * WhatsApp Business-Scoped User ID (BSUID, e.g. "US.13491208655302741918"). diff --git a/api/src/test/java/com/messagebird/ConversationMessagesTest.java b/api/src/test/java/com/messagebird/ConversationMessagesTest.java index 81357f88..5bd0a5c5 100644 --- a/api/src/test/java/com/messagebird/ConversationMessagesTest.java +++ b/api/src/test/java/com/messagebird/ConversationMessagesTest.java @@ -1,5 +1,7 @@ package com.messagebird; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.messagebird.exceptions.GeneralException; import com.messagebird.exceptions.NotFoundException; import com.messagebird.exceptions.UnauthorizedException; @@ -23,6 +25,8 @@ public class ConversationMessagesTest { private static final String JSON_CONVERSATION_MESSAGE_TEXT = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}"; private static final String JSON_CONVERSATION_MESSAGE_VIDEO = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"video\",\"direction\": \"received\",\"content\": {\"video\": { \"url\": \"https://example.com/video.mp4\" } },\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}"; private static final String JSON_CONVERSATION_SEND_MESSAGE_RESPONSE = "{\"id\":\"mesid\",\"status\":\"accepted\",\"fallback\":{\"id\":\"mesid\"}}"; + private static final String JSON_CONVERSATION_MESSAGE_BSUID = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"metadata\": {\"sender\": {\"displayName\": \"Alice\",\"username\": \"alice_shop\",\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"2025-04-15T16:00:00Z\"},\"createdDatetime\": \"2025-04-15T16:00:00Z\",\"updatedDatetime\": \"2025-04-15T16:00:00Z\"}"; + private static final String JSON_STATUS_MESSAGE_METADATA = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"content\": {\"text\": \"Hello! Your order has been shipped.\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"0001-01-01T00:00:00Z\"}}"; /** * Epsilon to use when checking two latitudes or longitudes for equality. @@ -183,6 +187,41 @@ public void testViewConversationMessageLocation() throws GeneralException, NotFo assertEquals(4.911627, location.getLongitude(), EPSILON_LOCATION_EQUALITY); } + @Test + public void testViewConversationMessageWithBsuidMetadata() throws GeneralException, NotFoundException, UnauthorizedException { + MessageBirdService messageBirdService = SpyService + .expects("GET", "messages/mesid") + .withConversationsAPIBaseURL() + .andReturns(new APIResponse(JSON_CONVERSATION_MESSAGE_BSUID)); + MessageBirdClient messageBirdClient = new MessageBirdClient(messageBirdService); + + ConversationMessage message = messageBirdClient.viewConversationMessage("mesid"); + + ConversationMessageMetadata metadata = message.getMetadata(); + assertNotNull(metadata); + assertNotNull(metadata.getReceivedAt()); + ConversationSenderMetadata sender = metadata.getSender(); + assertEquals("Alice", sender.getDisplayName()); + assertEquals("alice_shop", sender.getUsername()); + assertEquals("US.13491208655302741918", sender.getUserId()); + } + + @Test + public void testStatusMessageMetadataDeserializes() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + + ConversationStatusMessageMetadata md = mapper.readValue( + JSON_STATUS_MESSAGE_METADATA, ConversationStatusMessageMetadata.class); + + assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId()); + assertEquals("15551234567", md.getFrom()); + assertEquals("US.13491208655302741918", md.getTo()); + assertEquals("text", md.getType()); + assertEquals("Hello! Your order has been shipped.", md.getContent().getText()); + assertEquals("US.13491208655302741918", md.getMetadata().getSender().getUserId()); + } + @Test public void testViewConversationMessageText() throws GeneralException, NotFoundException, UnauthorizedException { MessageBirdService messageBirdService = SpyService diff --git a/api/src/test/java/com/messagebird/ConversationsTest.java b/api/src/test/java/com/messagebird/ConversationsTest.java index e08f0175..4e7624e8 100644 --- a/api/src/test/java/com/messagebird/ConversationsTest.java +++ b/api/src/test/java/com/messagebird/ConversationsTest.java @@ -17,7 +17,6 @@ public class ConversationsTest { private static final String JSON_CONVERSATION = "{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://example.com/assets/image.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678 },\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"chname\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-22T16:05:15Z\",\"lastReceivedDatetime\": \"2018-08-22T15:47:34Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 1,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}"; private static final String JSON_CONVERSATION_LIST = "{\"offset\": 20,\"limit\": 10,\"count\": 1,\"totalCount\": 1,\"items\": [{\"id\": \"convid\",\"contactId\": \"contid\",\"contact\": {\"id\": \"contid\",\"href\": \"https://chat.messagebird.com/1/contacts/contid\",\"msisdn\": 31612345678,\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"customDetails\": {\"avatar\": \"https://s3-eu-west-1.amazonaws.com/messagebird-chat/telegram/0d1dae7t5b7d7eb4531c14n04328336/6de655at5b7d859309f821n60607065/d0b705dt5b7d859309f987n05372263.jpg\",\"firstName\": \"Foo\",\"lastName\": \"Bar\",\"userId\": 12345678},\"createdDatetime\": \"2018-08-22T15:47:32Z\",\"updatedDatetime\": null},\"channels\": [{\"id\": \"chid\",\"name\": \"TestChannel\",\"platformId\": \"telegram\",\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:18:11Z\",\"updatedDatetime\": \"2018-08-22T15:18:13Z\"}],\"status\": \"active\",\"createdDatetime\": \"2018-08-22T15:47:34Z\",\"updatedDatetime\": \"2018-08-24T09:49:01Z\",\"lastReceivedDatetime\": \"2018-08-24T09:49:01Z\",\"lastUsedChannelId\": \"chid\",\"messages\": {\"totalCount\": 3,\"href\": \"https://conversations.messagebird.com/v1/conversations/convid/messages\"}}]}"; private static final String JSON_UNAUTHORIZED_ERROR = "{\"errors\": [{\"code\": 2,\"description\": \"Request was not authenticated\"}]}"; - // Status-update payload where Meta supplies both a phone number and a BSUID on the contact. @Test(expected = UnauthorizedException.class) public void testItThrowsErrors() throws GeneralException, UnauthorizedException { From 805aa5af470ab14924e389648c6383e4d449bb94 Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Thu, 28 May 2026 16:37:05 +0100 Subject: [PATCH 11/17] chore: migrate deploy plugin from OSSRH to Sonatype Central Portal OSSRH (oss.sonatype.org) was sunset in March 2025. Replace nexus-staging-maven-plugin with central-publishing-maven-plugin and drop the now-unused snapshotRepository distributionManagement block. autoPublish=false keeps the manual review/release behaviour the old config had via autoReleaseAfterClose=false. Co-Authored-By: Claude Sonnet 4.6 --- api/pom.xml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index fb65b2b0..37e4f157 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -161,13 +161,6 @@ - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - @@ -262,14 +255,22 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://oss.sonatype.org/ - false + central + false + validated From 365e12ffcc27b1ae7aef325506f0d5a54241b1f3 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 20 Jul 2026 11:24:04 +0200 Subject: [PATCH 12/17] fix(deps): pin plexus-utils to 3.6.1 to resolve CVE-2025-67030 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit maven-artifact:3.9.6 (used only for ComparableVersion) transitively pulls plexus-utils:3.5.1, which is affected by CVE-2025-67030 — a directory traversal / Zip Slip in org.codehaus.plexus.util.Expand.extractFile (CVSS 8.8). The vulnerable code is not reachable from this client, but the jar is flagged by SCA scanners. Add a dependencyManagement pin forcing the patched plexus-utils 3.6.1. Staying on the 3.x line avoids the plexus-xml package split introduced in 4.x. dependency:tree confirms plexus-utils now resolves to 3.6.1 and the build/tests are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- api/pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/pom.xml b/api/pom.xml index 37e4f157..bb736110 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -114,6 +114,17 @@ + + + + + org.codehaus.plexus + plexus-utils + 3.6.1 + + + + com.fasterxml.jackson.core From d330cbfc725ef50eb55a2e1b8af817ddeafb4c8f Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 20 Jul 2026 12:53:44 +0200 Subject: [PATCH 13/17] chore(release): bump version to 6.3.1 Release the CVE-2025-67030 plexus-utils fix as a patch. Bumps the Maven artifact version and the hardcoded clientVersion used in the User-Agent header to keep them in sync. Co-Authored-By: Claude Opus 4.8 (1M context) --- api/pom.xml | 2 +- api/src/main/java/com/messagebird/MessageBirdServiceImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index bb736110..d9c1f273 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.messagebird messagebird-api - 6.3.0 + 6.3.1 jar ${project.groupId}:${project.artifactId} diff --git a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java index ce3c10e7..ef179b45 100644 --- a/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java +++ b/api/src/main/java/com/messagebird/MessageBirdServiceImpl.java @@ -72,7 +72,7 @@ public class MessageBirdServiceImpl implements MessageBirdService { private final String accessKey; private final String serviceUrl; - private final String clientVersion = "6.3.0"; + private final String clientVersion = "6.3.1"; private final String userAgentString; private Proxy proxy = null; From 6b39ec409ce9c5d0e17d80b27a02d57ac54e6303 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 20 Jul 2026 13:02:52 +0200 Subject: [PATCH 14/17] chore(release): bump examples module to 6.3.1 The examples module was left at 6.2.5 (missed in the 6.3.0 release). Bring its own version and its messagebird-api dependency in line with the 6.3.1 release, matching the convention in #275. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index a0089732..9ffe9f18 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ com.messagebird examples - 6.2.5 + 6.3.1 @@ -20,7 +20,7 @@ com.messagebird messagebird-api - 6.2.5 + 6.3.1 compile From c53da1787dca061d66c9d3c4bd6d2022991b017d Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Fri, 31 Jul 2026 12:08:06 +0100 Subject: [PATCH 15/17] feat: model recipient BSUIDs on WhatsApp status webhooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Meta's parent business-scoped user ID rollout adds two identifiers our webhooks now forward, neither of which the SDK could express. - ConversationSenderMetadata gains parentUserId, the sender's parent BSUID (from Meta's messages[].from_parent_user_id). - ConversationRecipientMetadata is new, modelling status.metadata.recipient.{userId,parentUserId}. Status payloads did not carry the recipient's own identity before — messageMetadata.to is only an echo of the address the customer addressed — so this is the first way to learn a contact's BSUID from a status webhook. - ConversationStatusMetadata is new, modelling the enclosing status.metadata block. pricing and conversation stay raw maps: they are near-verbatim passthroughs of Meta's objects and keep their snake_case keys, while recipient is ours and is camelCase. Unmodelled keys (e.g. biz_opaque_callback_data) are collected rather than dropped. Both new types parse under a plain ObjectMapper, so consumers need not disable FAIL_ON_UNKNOWN_PROPERTIES. recipient is absent from payloads for accounts that receive no BSUIDs, and deserialises to null there. Co-Authored-By: Claude Opus 5 --- .../ConversationRecipientMetadata.java | 50 +++++++++++ .../ConversationSenderMetadata.java | 16 ++++ .../ConversationStatusMessageMetadata.java | 5 ++ .../ConversationStatusMetadata.java | 85 +++++++++++++++++++ .../messagebird/ConversationMessagesTest.java | 50 ++++++++++- 5 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java create mode 100644 api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java new file mode 100644 index 00000000..7537da85 --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java @@ -0,0 +1,50 @@ +package com.messagebird.objects.conversations; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Identifies the recipient of an outbound WhatsApp message, as reported back on + * status webhook payloads under {@code status.metadata.recipient}. Mirrors + * {@link ConversationSenderMetadata} on the inbound side. + * + *

{@code userId} is the recipient's BSUID (e.g. "US.13491208655302741918"); + * {@code parentUserId} is the parent business-scoped user ID of the enterprise + * that owns the business portfolio it was scoped against (e.g. + * "US.ENT.11815799212886844830"). + * + *

Either field may be {@code null}: Meta only supplies them for accounts + * enrolled in the BSUID rollout, and the enclosing {@code recipient} object is + * omitted entirely when neither is present. This is the only place a status + * payload carries the recipient's own identity — {@code messageMetadata.to} is + * an echo of the address the message was addressed to. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ConversationRecipientMetadata { + + private String userId; + private String parentUserId; + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getParentUserId() { + return parentUserId; + } + + public void setParentUserId(String parentUserId) { + this.parentUserId = parentUserId; + } + + @Override + public String toString() { + return "ConversationRecipientMetadata{" + + "userId='" + userId + '\'' + + ", parentUserId='" + parentUserId + '\'' + + '}'; + } +} diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java index 33d7e225..d7f60b9f 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java @@ -5,10 +5,17 @@ * contains the BSUID (e.g. "US.13491208655302741918") when Meta supplies one. * When both a phone number and a BSUID are available, the phone number appears * in the parent message's {@code from} field — not here. + * + *

{@code parentUserId} carries the sender's parent business-scoped user ID + * (e.g. "US.ENT.11815799212886844830"), which identifies the enterprise that + * owns the business portfolio the {@code userId} was scoped against. It is only + * present for accounts enrolled in Meta's parent-BSUID rollout; for everyone + * else it stays {@code null}. */ public class ConversationSenderMetadata { private String userId; + private String parentUserId; private String username; private String displayName; @@ -20,6 +27,14 @@ public void setUserId(String userId) { this.userId = userId; } + public String getParentUserId() { + return parentUserId; + } + + public void setParentUserId(String parentUserId) { + this.parentUserId = parentUserId; + } + public String getUsername() { return username; } @@ -40,6 +55,7 @@ public void setDisplayName(String displayName) { public String toString() { return "ConversationSenderMetadata{" + "userId='" + userId + '\'' + + ", parentUserId='" + parentUserId + '\'' + ", username='" + username + '\'' + ", displayName='" + displayName + '\'' + '}'; diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java index df7feebf..537ad7bb 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java @@ -12,6 +12,11 @@ *

Both {@code from} and {@code to} accept either a phone number or a * WhatsApp Business-Scoped User ID (BSUID, e.g. "US.13491208655302741918"). * The BSUID is also available via {@code metadata.sender.userId}. + * + *

{@code to} echoes back the address the message was originally addressed + * to, so it is not a reliable source of the recipient's BSUID. That identity + * lives alongside this block, under {@code status.metadata.recipient} — see + * {@link ConversationStatusMetadata}. */ public class ConversationStatusMessageMetadata { diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java new file mode 100644 index 00000000..0b4d5f7f --- /dev/null +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java @@ -0,0 +1,85 @@ +package com.messagebird.objects.conversations; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The {@code status.metadata} block delivered inside status webhook payloads + * (e.g. {@code statusSent}, {@code statusDelivered}). + * + *

This class is not produced by any SDK request — it is a standalone POJO + * intended for consumers who deserialize incoming webhook payloads in their own + * HTTP handlers. Use it via {@code ObjectMapper.readValue(body, ...)}. + * + *

Note the mixed casing of this object. {@code pricing} and + * {@code conversation} are near-verbatim passthroughs of Meta's own objects and + * so keep their snake_case keys ({@code pricing_model}, {@code category}, …); + * they are exposed here as raw maps rather than modelled types, because their + * contents track Meta's schema rather than ours. {@code recipient} is ours and + * follows the camelCase convention used everywhere else in the API. Any other + * key present on the payload — for example {@code biz_opaque_callback_data} — + * is collected into {@link #getAdditionalProperties()} rather than dropped. + * + *

{@code recipient} is absent from payloads for accounts that never receive + * BSUIDs, in which case {@link #getRecipient()} returns {@code null}. + */ +public class ConversationStatusMetadata { + + private Map pricing; + private Map conversation; + private ConversationRecipientMetadata recipient; + private final Map additionalProperties = new LinkedHashMap<>(); + + public Map getPricing() { + return pricing; + } + + public void setPricing(Map pricing) { + this.pricing = pricing; + } + + public Map getConversation() { + return conversation; + } + + public void setConversation(Map conversation) { + this.conversation = conversation; + } + + public ConversationRecipientMetadata getRecipient() { + return recipient; + } + + public void setRecipient(ConversationRecipientMetadata recipient) { + this.recipient = recipient; + } + + /** + * Every key on the payload that has no dedicated accessor above, in the + * order it was encountered. Empty when the payload holds nothing else. + * + * @return the unmodelled remainder of the metadata object + */ + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + additionalProperties.put(name, value); + } + + @Override + public String toString() { + return "ConversationStatusMetadata{" + + "pricing=" + pricing + + ", conversation=" + conversation + + ", recipient=" + recipient + + ", additionalProperties=" + additionalProperties + + '}'; + } +} diff --git a/api/src/test/java/com/messagebird/ConversationMessagesTest.java b/api/src/test/java/com/messagebird/ConversationMessagesTest.java index 5bd0a5c5..da670fca 100644 --- a/api/src/test/java/com/messagebird/ConversationMessagesTest.java +++ b/api/src/test/java/com/messagebird/ConversationMessagesTest.java @@ -25,9 +25,13 @@ public class ConversationMessagesTest { private static final String JSON_CONVERSATION_MESSAGE_TEXT = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}"; private static final String JSON_CONVERSATION_MESSAGE_VIDEO = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"video\",\"direction\": \"received\",\"content\": {\"video\": { \"url\": \"https://example.com/video.mp4\" } },\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}"; private static final String JSON_CONVERSATION_SEND_MESSAGE_RESPONSE = "{\"id\":\"mesid\",\"status\":\"accepted\",\"fallback\":{\"id\":\"mesid\"}}"; - private static final String JSON_CONVERSATION_MESSAGE_BSUID = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"metadata\": {\"sender\": {\"displayName\": \"Alice\",\"username\": \"alice_shop\",\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"2025-04-15T16:00:00Z\"},\"createdDatetime\": \"2025-04-15T16:00:00Z\",\"updatedDatetime\": \"2025-04-15T16:00:00Z\"}"; + private static final String JSON_CONVERSATION_MESSAGE_BSUID = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"metadata\": {\"sender\": {\"displayName\": \"Alice\",\"username\": \"alice_shop\",\"userId\": \"US.13491208655302741918\",\"parentUserId\": \"US.ENT.11815799212886844830\"},\"receivedAt\": \"2025-04-15T16:00:00Z\"},\"createdDatetime\": \"2025-04-15T16:00:00Z\",\"updatedDatetime\": \"2025-04-15T16:00:00Z\"}"; private static final String JSON_STATUS_MESSAGE_METADATA = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"content\": {\"text\": \"Hello! Your order has been shipped.\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"0001-01-01T00:00:00Z\"}}"; + private static final String JSON_STATUS_METADATA_WITH_RECIPIENT = "{\"pricing\": {\"billable\": true,\"pricing_model\": \"CBP\",\"category\": \"utility\"},\"conversation\": {\"id\": \"a1b2c3d4\",\"origin\": {\"type\": \"utility\"}},\"biz_opaque_callback_data\": \"order-1234\",\"recipient\": {\"userId\": \"US.13491208655302741918\",\"parentUserId\": \"US.ENT.11815799212886844830\"}}"; + private static final String JSON_STATUS_METADATA_WITHOUT_RECIPIENT = "{\"pricing\": {\"billable\": true,\"pricing_model\": \"CBP\",\"category\": \"utility\"},\"conversation\": {\"id\": \"a1b2c3d4\",\"origin\": {\"type\": \"utility\"}}}"; + private static final String JSON_STATUS_METADATA_PARENT_ONLY = "{\"recipient\": {\"parentUserId\": \"US.ENT.11815799212886844830\"}}"; + /** * Epsilon to use when checking two latitudes or longitudes for equality. */ @@ -204,6 +208,50 @@ public void testViewConversationMessageWithBsuidMetadata() throws GeneralExcepti assertEquals("Alice", sender.getDisplayName()); assertEquals("alice_shop", sender.getUsername()); assertEquals("US.13491208655302741918", sender.getUserId()); + assertEquals("US.ENT.11815799212886844830", sender.getParentUserId()); + } + + @Test + public void testStatusMetadataDeserializesRecipient() throws Exception { + // A plain mapper: these payload POJOs must not require the caller to + // disable FAIL_ON_UNKNOWN_PROPERTIES. + ConversationStatusMetadata metadata = new ObjectMapper().readValue( + JSON_STATUS_METADATA_WITH_RECIPIENT, ConversationStatusMetadata.class); + + ConversationRecipientMetadata recipient = metadata.getRecipient(); + assertNotNull(recipient); + assertEquals("US.13491208655302741918", recipient.getUserId()); + assertEquals("US.ENT.11815799212886844830", recipient.getParentUserId()); + + // Meta's own objects pass through untouched, snake_case keys and all. + assertEquals("CBP", metadata.getPricing().get("pricing_model")); + assertEquals("a1b2c3d4", metadata.getConversation().get("id")); + + // Anything else on the payload is kept rather than dropped. + assertEquals("order-1234", metadata.getAdditionalProperties().get("biz_opaque_callback_data")); + } + + @Test + public void testStatusMetadataWithoutRecipientIsNull() throws Exception { + ConversationStatusMetadata metadata = new ObjectMapper().readValue( + JSON_STATUS_METADATA_WITHOUT_RECIPIENT, ConversationStatusMetadata.class); + + // Accounts that never receive BSUIDs get payloads with no recipient key + // at all — the rest of the metadata must still parse. + assertNull(metadata.getRecipient()); + assertEquals("CBP", metadata.getPricing().get("pricing_model")); + assertTrue(metadata.getAdditionalProperties().isEmpty()); + } + + @Test + public void testStatusMetadataRecipientWithParentOnly() throws Exception { + ConversationStatusMetadata metadata = new ObjectMapper().readValue( + JSON_STATUS_METADATA_PARENT_ONLY, ConversationStatusMetadata.class); + + ConversationRecipientMetadata recipient = metadata.getRecipient(); + assertNotNull(recipient); + assertNull(recipient.getUserId()); + assertEquals("US.ENT.11815799212886844830", recipient.getParentUserId()); } @Test From 59d1fc22f6b35a82ccb914648af5f50b917b7572 Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Fri, 31 Jul 2026 12:24:32 +0100 Subject: [PATCH 16/17] fix: tolerate unknown fields on webhook payload POJOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The payload types added in 6.3.0 are meant to be deserialized by consumers in their own HTTP handlers, but only parsed under a mapper with FAIL_ON_UNKNOWN_PROPERTIES disabled — which the SDK's internal mapper does and a plain ObjectMapper does not. A consumer following the javadoc got an UnrecognizedPropertyException, and would get one again every time the platform adds a field to a payload they already parse. Adds @JsonIgnoreProperties(ignoreUnknown = true) to the four types on the status payload path: ConversationStatusMessageMetadata, ConversationMessageMetadata, ConversationSenderMetadata and ConversationContent. This matches the new types added alongside it and makes the webhook-parsing story uniform. Deserialization only loosens, so nothing that parsed before stops parsing; serialization is untouched, which the existing send-request tests cover. The 6.3.0 test no longer needs to disable the feature by hand, and a new test parses a payload carrying unknown keys at every nesting level under a plain mapper. Co-Authored-By: Claude Opus 5 --- .../conversations/ConversationContent.java | 7 +++++ .../ConversationMessageMetadata.java | 3 +++ .../ConversationSenderMetadata.java | 3 +++ .../ConversationStatusMessageMetadata.java | 3 +++ .../messagebird/ConversationMessagesTest.java | 27 +++++++++++++++---- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java index 9ba82bd8..8b4fe164 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationContent.java @@ -1,9 +1,16 @@ package com.messagebird.objects.conversations; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * ConversationContent wraps actual content. The field that should be set here * is indicated by ConversationContentType. + * + *

Unknown keys are ignored on deserialization so that consumers parsing + * webhook payloads in their own handlers are not broken by content fields added + * after their SDK version. Serialization is unaffected. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class ConversationContent { private ConversationContentMedia audio; diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java index 7d908e6a..c41e814b 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java @@ -1,5 +1,7 @@ package com.messagebird.objects.conversations; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import java.util.Date; /** @@ -8,6 +10,7 @@ * the BSUID when Meta provides one. When both identifiers exist, the phone * number appears in the parent {@code from} field, not in this object. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class ConversationMessageMetadata { private ConversationSenderMetadata sender; diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java index d7f60b9f..b7fdcbb0 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java @@ -1,5 +1,7 @@ package com.messagebird.objects.conversations; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Metadata about the sender of a WhatsApp message. {@code userId} always * contains the BSUID (e.g. "US.13491208655302741918") when Meta supplies one. @@ -12,6 +14,7 @@ * present for accounts enrolled in Meta's parent-BSUID rollout; for everyone * else it stays {@code null}. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class ConversationSenderMetadata { private String userId; diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java index 537ad7bb..47a4a8d1 100644 --- a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java +++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java @@ -1,5 +1,7 @@ package com.messagebird.objects.conversations; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * The {@code messageMetadata} block delivered inside status webhook payloads * (e.g. {@code statusSent}, {@code statusDelivered}). Reflects the original @@ -18,6 +20,7 @@ * lives alongside this block, under {@code status.metadata.recipient} — see * {@link ConversationStatusMetadata}. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class ConversationStatusMessageMetadata { private String id; diff --git a/api/src/test/java/com/messagebird/ConversationMessagesTest.java b/api/src/test/java/com/messagebird/ConversationMessagesTest.java index da670fca..beac543a 100644 --- a/api/src/test/java/com/messagebird/ConversationMessagesTest.java +++ b/api/src/test/java/com/messagebird/ConversationMessagesTest.java @@ -1,6 +1,5 @@ package com.messagebird; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.messagebird.exceptions.GeneralException; import com.messagebird.exceptions.NotFoundException; @@ -32,6 +31,13 @@ public class ConversationMessagesTest { private static final String JSON_STATUS_METADATA_WITHOUT_RECIPIENT = "{\"pricing\": {\"billable\": true,\"pricing_model\": \"CBP\",\"category\": \"utility\"},\"conversation\": {\"id\": \"a1b2c3d4\",\"origin\": {\"type\": \"utility\"}}}"; private static final String JSON_STATUS_METADATA_PARENT_ONLY = "{\"recipient\": {\"parentUserId\": \"US.ENT.11815799212886844830\"}}"; + /** + * The same payload as JSON_STATUS_MESSAGE_METADATA with an unrecognised key + * added at every nesting level, standing in for fields the platform adds + * after this SDK version ships. + */ + private static final String JSON_STATUS_MESSAGE_METADATA_UNKNOWN_FIELDS = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"futureTopLevelField\": \"ignored\",\"content\": {\"text\": \"Hello! Your order has been shipped.\",\"futureContentField\": \"ignored\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\",\"futureSenderField\": \"ignored\"},\"receivedAt\": \"0001-01-01T00:00:00Z\",\"futureMetadataField\": \"ignored\"}}"; + /** * Epsilon to use when checking two latitudes or longitudes for equality. */ @@ -211,6 +217,20 @@ public void testViewConversationMessageWithBsuidMetadata() throws GeneralExcepti assertEquals("US.ENT.11815799212886844830", sender.getParentUserId()); } + @Test + public void testStatusMessageMetadataToleratesUnknownFields() throws Exception { + // A plain mapper, and unknown keys at every level: webhook payload POJOs + // must not force consumers to disable FAIL_ON_UNKNOWN_PROPERTIES, and + // must survive fields the platform adds after this version ships. + ConversationStatusMessageMetadata md = new ObjectMapper().readValue( + JSON_STATUS_MESSAGE_METADATA_UNKNOWN_FIELDS, ConversationStatusMessageMetadata.class); + + assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId()); + assertEquals("Hello! Your order has been shipped.", md.getContent().getText()); + assertEquals("US.13491208655302741918", md.getMetadata().getSender().getUserId()); + assertNotNull(md.getMetadata().getReceivedAt()); + } + @Test public void testStatusMetadataDeserializesRecipient() throws Exception { // A plain mapper: these payload POJOs must not require the caller to @@ -256,10 +276,7 @@ public void testStatusMetadataRecipientWithParentOnly() throws Exception { @Test public void testStatusMessageMetadataDeserializes() throws Exception { - ObjectMapper mapper = new ObjectMapper(); - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - - ConversationStatusMessageMetadata md = mapper.readValue( + ConversationStatusMessageMetadata md = new ObjectMapper().readValue( JSON_STATUS_MESSAGE_METADATA, ConversationStatusMessageMetadata.class); assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId()); From 4f1152db6a4a2c267a8e06b4f5c2a78cfdf36b49 Mon Sep 17 00:00:00 2001 From: Chris Casey Date: Fri, 31 Jul 2026 12:08:06 +0100 Subject: [PATCH 17/17] chore(release): bump version to 6.4.0 Co-Authored-By: Claude Opus 5 --- api/pom.xml | 2 +- examples/pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index d9c1f273..b64ee8b0 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.messagebird messagebird-api - 6.3.1 + 6.4.0 jar ${project.groupId}:${project.artifactId} diff --git a/examples/pom.xml b/examples/pom.xml index 9ffe9f18..cb9b7489 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -6,7 +6,7 @@ com.messagebird examples - 6.3.1 + 6.4.0 @@ -20,7 +20,7 @@ com.messagebird messagebird-api - 6.3.1 + 6.4.0 compile