Skip to content

feat: support recipient BSUIDs on WhatsApp status webhooks (6.4.0) - #284

Open
ccaseypusher wants to merge 3 commits into
masterfrom
feat/whatsapp-recipient-parent-user-id
Open

feat: support recipient BSUIDs on WhatsApp status webhooks (6.4.0)#284
ccaseypusher wants to merge 3 commits into
masterfrom
feat/whatsapp-recipient-parent-user-id

Conversation

@ccaseypusher

@ccaseypusher ccaseypusher commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

Meta added parent business-scoped user IDs to the WhatsApp Cloud API webhook payloads as part of the BSUID rollout. Our API now forwards them; this models them in the Java SDK.

Meta field Location Our API SDK
from_parent_user_id messages[] metadata.sender.parentUserId ConversationSenderMetadata.getParentUserId()
recipient_user_id statuses[] status.metadata.recipient.userId ConversationRecipientMetadata.getUserId()
recipient_parent_user_id statuses[] status.metadata.recipient.parentUserId ConversationRecipientMetadata.getParentUserId()

Why recipient.userId is here too

Status webhooks did not expose the recipient's identity at all — messageMetadata.to is only an echo of the address the customer sent to. Forwarding the parent alone would hand customers a parent identifier with no base BSUID beside it, so both ship together. It also closes a real gap: a customer who addressed a phone number had no way to learn that contact's BSUID, which matters ahead of the June 2026 change where phone numbers may stop being present.

Shape

{
  "type": "statusDelivered",
  "status": {
    "metadata": {
      "pricing": { "...": "unchanged" },
      "conversation": { "...": "unchanged" },
      "recipient": {
        "userId": "US.13491208655302741918",
        "parentUserId": "US.ENT.11815799212886844830"
      }
    }
  }
}

Nested rather than flat, mirroring message.metadata.sender.{userId,parentUserId} on the inbound side.

Commits

1. feat: model recipient BSUIDs on WhatsApp status webhooks

  • ConversationStatusMetadata is new and models the enclosing status.metadata block, which the SDK had no type for. pricing and conversation are exposed as raw Map<String, Object> — they are near-verbatim passthroughs of Meta's own objects and keep their snake_case keys (pricing_model, country_code), so modelling them would couple us to Meta's schema. recipient is ours and follows our camelCase convention. That makes the object mixed-case; nesting under one recipient key contains the inconsistency to a single place.
  • Unmodelled keys are kept, not dropped — anything else on the block (e.g. biz_opaque_callback_data) lands in getAdditionalProperties() via @JsonAnySetter.

2. fix: tolerate unknown fields on webhook payload POJOs

The payload types added in 6.3.0 are documented for consumers to deserialize 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 added a field to a payload they already parse.

@JsonIgnoreProperties(ignoreUnknown = true) is added to the four types on the status payload path: ConversationStatusMessageMetadata, ConversationMessageMetadata, ConversationSenderMetadata and ConversationContent. The 6.3.0 test no longer disables the feature by hand, and a new test parses a payload carrying unknown keys at every nesting level under a plain mapper.

Not breaking: ignoreUnknown only loosens deserialization, so nothing that parsed before stops parsing, and serialization is untouched — the existing send-request tests cover that. Scope is the status payload path; deeper content sub-objects (hsm, email, media) are not on it and are unchanged.

3. chore(release): bump version to 6.4.0

Backward compatibility

recipient is omitted entirely from payloads for accounts that never receive BSUIDs, and deserialises to null there — covered by a dedicated test. All additions are new getters/setters on existing POJOs plus two new classes; nothing existing changes shape.

Deliberately out of scope

  • contacts[].parent_user_id — the sender's parent BSUID is read from the messages object, mirroring how from_user_id is read, so a contacts field would be an unread member.
  • Group statuses (recipient_participant_user_id, recipient_participant_parent_user_id) — this SDK has no WhatsApp group support.
  • The status webhook envelope itself ({type, status}) is still unmodelled, consistent with ConversationStatusMessageMetadata added in 6.3.0: these are standalone POJOs for consumers parsing sub-objects of their own webhook bodies.

Verification

Check Result
mvn -Ptest test 134 tests, 0 failures (4 new)
mvn -Ptest package BUILD SUCCESS, incl. javadoc jar

Note this repo has no GitHub Actions — only a legacy .travis.yml — so no checks run on this PR. The above was run locally.

Version bumped to 6.4.0 in api/pom.xml and examples/pom.xml (additive, no breaking changes).

🤖 Generated with Claude Code

ccaseypusher and others added 3 commits July 31, 2026 12:08
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ccaseypusher
ccaseypusher force-pushed the feat/whatsapp-recipient-parent-user-id branch from 9456fc8 to 4f1152d Compare July 31, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant