Skip to content

Add BSUID support - rename UserNumber to UserId - #407

Merged
kzu merged 1 commit into
mainfrom
dev/userid
May 1, 2026
Merged

Add BSUID support - rename UserNumber to UserId#407
kzu merged 1 commit into
mainfrom
dev/userid

Conversation

@kzu

@kzu kzu commented May 1, 2026

Copy link
Copy Markdown
Member

Summary

WhatsApp for Business is rolling out Business-Scoped User IDs (BSUIDs) for privacy. Users who opt for username-based messaging will no longer expose their phone numbers to businesses. This PR adds full SDK support for the new identifier type.

Key Changes

User record

  • User(Name, Id, Number?)Id is always set (phone or BSUID), Number is nullable
  • IsBSUID computed property: true when Id matches the BSUID pattern
  • Normalization (strip leading +) only applied to Number in the constructor

IMessage / public API

  • Breaking rename: UserNumber to UserId throughout the public API
  • UserId now returns User.Id (BSUID or phone), not User.Number

Webhook deserialization (Message.jq)

  • Reads contacts[].user_id (BSUID) when present; falls back to messages[].from
  • Number is set only when a distinct phone number is available

Send path (WhatsAppClientExtensions)

  • recipient_type detected dynamically: "individual" for phone numbers, "business_scoped_user_id" for BSUIDs
  • Detection uses the canonical BSUID format: {ISO 3166 alpha-2}.{1-128 alphanumeric} (e.g. US.13491208655302741918)
  • Prefix must be exactly 2 ASCII letters — cleanly distinguishes BSUIDs from phone numbers (all-digit)
  • NormalizeNumber() removed from send methods; only used in User constructor

Storage / Idempotency

  • ConversationStorage, Idempotency: partition keys switch from User.Number to User.Id
  • Safe migration: conversations are short-lived; idempotency is best-effort

Flows

  • FlowDataRequest, FlowDataResponse, FlowToken updated to use UserId

Tests

  • BsuidTests.cs added with coverage for:
    • BSUID format detection (valid alpha-2 prefix + alphanumeric suffix)
    • Rejection of numeric-prefixed strings (phone numbers) and malformed ids
    • IsBSUID property on User
    • recipient_type selection based on id shape
  • Existing tests updated throughout

BSUID Detection Heuristic

Format: {ISO 3166 alpha-2 country code}.{up to 128 alphanumeric characters} (reference)

Input Result Reason
US.13491208655302741918 BSUID 2-letter prefix, alphanumeric suffix
AR.aBc123XyZ BSUID Valid alpha-2 + alphanumeric
5491122334455 Phone All-digit, no dot
54.aBc123XyZ Phone Prefix is digits, not alpha-2
USA.abc123 Invalid 3-letter prefix

Webhook Scenarios

Scenario contacts[].user_id messages[].from User.Id User.Number
Pre-migration absent phone phone phone
Post-migration, non-privacy BSUID phone BSUID phone
Post-migration, privacy user BSUID BSUID BSUID null

Breaking Changes

  • IMessage.UserNumber renamed to IMessage.UserId
  • All response constructors: userNumber parameter renamed to userId
  • IConversationStorage: number parameters renamed to userId
  • Conversation record: Number property renamed to UserId

@kzu
kzu enabled auto-merge (rebase) May 1, 2026 21:33
WhatsApp for Business is rolling out Business-Scoped User IDs (BSUIDs)
to support privacy-focused username-based messaging. Users who opt for
usernames will no longer expose their phone numbers to businesses.

Changes:
- User record gains Id (always set, phone or BSUID) and nullable Number
  (set only when a phone number is available and distinct from the Id)
- IMessage.UserNumber renamed to UserId throughout the public API
- Response and all subtypes (Text, Template, Reaction, Typing, CTA, Flow,
  Anonymous) updated accordingly
- Message.jq updated to extract contacts[].user_id (BSUID) when present,
  falling back to messages[].from; Number only set when distinguishable
- WhatsAppClientExtensions: all send methods now use UserId as the 'to'
  field without normalization; recipient_type is set dynamically:
  'individual' for phone numbers, 'business_scoped_user_id' for BSUIDs
- NormalizeNumber() removed from all send code; only applied in User
  constructor when setting the optional Number property
- FlowDataRequest, FlowDataResponse, FlowToken updated to use UserId
- Conversation, IConversationStorage, ConversationStorage, and
  ConversationHandler updated to use UserId as the partition key
- Idempotency switches from User.Number to User.Id for partition keys
- OpenTelemetryHandler telemetry tag updated to UserId
- BsuidTests added to cover BSUID-specific webhook deserialization,
  recipient_type detection, and send-path behavior
- AGENTS.md updated
@kzu

kzu commented May 1, 2026

Copy link
Copy Markdown
Member Author

219 passed 219 passed 22 skipped

🧪 Details on Ubuntu 24.04.4 LTS

from retest v1.1.0 on .NET 10.0.7 with 💜 by @devlooped

@kzu
kzu merged commit e786f62 into main May 1, 2026
4 checks passed
@kzu
kzu deleted the dev/userid branch May 1, 2026 21:38
@devlooped devlooped locked and limited conversation to collaborators Jun 2, 2026
@kzu kzu added the enhancement New feature or request label Jul 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant