Webhook Data Reference¶
Data structures for everything WhatsApp sends you
You don't need to understand the raw format, but here it is. Wappa transforms WhatsApp's complex webhook JSON into clean, predictable Python objects with helpful methods.
The Three Webhook Types¶
WhatsApp sends your conversational app three types of webhooks. Wappa handles the complexity and gives you simple interfaces:
When: User sends you anything - text, images, voice notes, button clicks, location
What you get: Clean message data with helper methods
async def process_message(self, webhook):
# Everything you need is here
user_id = webhook.user.user_id # "1234567890123"
message_text = webhook.get_message_text() # "Hello!"
message_type = webhook.get_message_type_name() # "text"
timestamp = webhook.timestamp # datetime object
platform = webhook.platform # PlatformType.WHATSAPP
# Optional context information
was_forwarded = webhook.was_forwarded() # bool
has_business_context = webhook.has_business_context() # bool
raw_data = webhook.get_raw_webhook_data() # dict (for debugging)
When: Messages you sent change status - delivered, read, failed
What you get: Delivery tracking and billing context
async def process_status(self, webhook):
# Track your message delivery
message_id = webhook.message_id # "wamid.abc123..."
status = webhook.status.value # "delivered", "read", "failed"
recipient = webhook.recipient_id # "1234567890123"
timestamp = webhook.timestamp # datetime object
# Check delivery status
delivered = webhook.is_delivered_status() # bool
failed = webhook.is_failed_status() # bool
# Error handling for failed messages
if webhook.has_errors():
error = webhook.get_primary_error() # ErrorDetailBase
print(f"Failed: {error.error_title}")
When: WhatsApp encounters platform-level errors
What you get: Structured error information with context
async def process_error(self, webhook):
# Handle platform errors
error_count = webhook.get_error_count() # int
primary_error = webhook.get_primary_error() # ErrorDetailBase
error_codes = webhook.get_error_codes() # [131051, 131052]
# Error classification
is_critical = webhook.has_critical_errors() # bool (5xx codes)
is_retryable = webhook.has_retryable_errors() # bool
# Detailed error information
error_title = primary_error.error_title # "Unsupported message type"
error_code = primary_error.error_code # 131051
IncomingMessageWebhook Reference¶
Core Properties¶
Property | Type | Description | Example |
---|---|---|---|
tenant | TenantBase | Business account identification | Contains phone_number_id, business_id |
user | UserBase | User who sent the message | Contains user_id, phone_number, profile_name |
message | BaseMessage | The actual message content | Text, media, interactive, location, etc. |
timestamp | datetime | When message was received | 2025-01-15T10:30:45Z |
platform | PlatformType | Source platform | PlatformType.WHATSAPP |
webhook_id | str | Unique webhook identifier | webhook_123456789 |
Essential Methods¶
Method | Returns | Description | Example |
---|---|---|---|
get_message_text() | str | Text content or empty string | "Hello there!" |
get_message_type_name() | str | Message type identifier | "text" , "image" , "interactive" |
get_interactive_selection() | str \| None | Button/list selection ID | "help_button" , "product_123" |
was_forwarded() | bool | Check if message was forwarded | True /False |
has_business_context() | bool | From business interaction | True /False |
has_ad_referral() | bool | From advertisement click | True /False |
get_sender_display_name() | str | User's display name | "John Doe" |
get_raw_webhook_data() | dict \| None | Original JSON payload | Full webhook dict |
User Information (webhook.user
)¶
Property | Type | Description | Example |
---|---|---|---|
user_id | str | WhatsApp user ID | "1234567890123" |
phone_number | str | User's phone number | "+1234567890" |
profile_name | str \| None | User's display name | "John Doe" |
get_display_name() | str | Name or phone fallback | "John Doe" |
Business Context (webhook.business_context
)¶
Available when message originated from business interactions (catalog, buttons).
Property | Type | Description |
---|---|---|
contextual_message_id | str | Original business message ID |
catalog_id | str \| None | Product catalog ID |
product_retailer_id | str \| None | Product ID from catalog |
has_product_context() | bool | Check if involves product |
Message Types & Data Access¶
Text Messages¶
# Basic text message
if webhook.get_message_type_name() == "text":
text = webhook.get_message_text() # "Hello!"
message_id = webhook.message.message_id # "wamid.abc123..."
# Reply context (if replying to another message)
if webhook.message.has_context():
context = webhook.message.get_context()
original_id = context.original_message_id # ID of replied message
Interactive Messages (Buttons & Lists)¶
# Button or list selection
if webhook.get_message_type_name() == "interactive":
selection = webhook.get_interactive_selection() # "help_button"
# Access interactive details
interactive_type = webhook.message.interactive_type # InteractiveType.BUTTON_REPLY
option_title = webhook.message.selected_option_title # "Get Help"
original_msg = webhook.message.original_message_id # Interactive message ID
# Check selection type
is_button = webhook.message.is_button_reply() # bool
is_list = webhook.message.is_list_reply() # bool
Media Messages¶
# Image, video, audio, document, sticker
if webhook.get_message_type_name() in ["image", "video", "audio", "document", "sticker"]:
media_id = webhook.message.media_id # "media_abc123"
file_size = webhook.message.file_size # 1024000 (bytes)
caption = webhook.message.caption # "Check this out!"
# Download information
download_info = webhook.message.get_download_info() # dict with URL/headers
# Type-specific properties
if webhook.message.is_image():
mime_type = webhook.message.media_type # MediaType.IMAGE_JPEG
elif webhook.message.is_audio():
duration = webhook.message.duration # 30 (seconds)
is_voice = webhook.message.is_voice_message() # bool
Location Messages¶
if webhook.get_message_type_name() == "location":
lat = webhook.message.latitude # 40.7589
lng = webhook.message.longitude # -73.9851
address = webhook.message.address # "New York, NY"
name = webhook.message.location_name # "Central Park"
Contact Messages¶
if webhook.get_message_type_name() == "contact":
name = webhook.message.contact_name # "John Doe"
phone = webhook.message.contact_phone # "+1234567890"
contact_data = webhook.message.contact_data # dict with all contact info
StatusWebhook Reference¶
Core Properties¶
Property | Type | Description | Example |
---|---|---|---|
tenant | TenantBase | Business account identification | Contains phone_number_id, business_id |
message_id | str | ID of message this status refers to | "wamid.abc123..." |
status | MessageStatus | Current message status | MessageStatus.DELIVERED |
recipient_id | str | User who received the message | "1234567890123" |
timestamp | datetime | When status update occurred | 2025-01-15T10:30:45Z |
platform | PlatformType | Source platform | PlatformType.WHATSAPP |
Status Check Methods¶
Method | Returns | Description |
---|---|---|
is_delivered_status() | bool | Message was delivered or read |
is_failed_status() | bool | Message delivery failed |
has_errors() | bool | Status includes error information |
get_primary_error() | ErrorDetailBase \| None | First error if failed |
is_billable_message() | bool | Message is billable |
Status Values¶
Status | Description | When It Happens |
---|---|---|
"sent" | Message sent to WhatsApp | Immediately after sending |
"delivered" | Message reached user's device | Within seconds |
"read" | User opened the message | When user reads it |
"failed" | Message delivery failed | Various error conditions |
ErrorWebhook Reference¶
Core Properties¶
Property | Type | Description | Example |
---|---|---|---|
tenant | TenantBase | Business account identification | Contains phone_number_id, business_id |
errors | list[ErrorDetailBase] | List of error details | Multiple errors possible |
timestamp | datetime | When errors occurred | 2025-01-15T10:30:45Z |
error_level | str | Error severity level | "system" , "app" , "account" |
platform | PlatformType | Source platform | PlatformType.WHATSAPP |
Error Analysis Methods¶
Method | Returns | Description |
---|---|---|
get_primary_error() | ErrorDetailBase | First/main error |
get_error_count() | int | Number of errors |
has_critical_errors() | bool | Any 5xx server errors |
has_retryable_errors() | bool | Any temporary errors |
get_error_codes() | list[int] | All error codes |
Error Detail Properties¶
Each error in the errors
list has these properties:
Property | Type | Description | Example |
---|---|---|---|
error_code | int | Numeric error code | 131051 |
error_title | str | Human-readable title | "Unsupported message type" |
error_message | str | Detailed error message | "Message type is not supported" |
is_temporary_error() | bool | Can be retried | Method to check retryability |
Raw vs Clean Interface¶
What WhatsApp Actually Sends (Raw)¶
{
"object": "whatsapp_business_account",
"entry": [{
"id": "987654321",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+1555123456",
"phone_number_id": "123456789"
},
"contacts": [{
"profile": {
"name": "John Doe"
},
"wa_id": "1234567890123"
}],
"messages": [{
"from": "1234567890123",
"id": "wamid.abc123def456",
"timestamp": "1642678845",
"text": {
"body": "Hello there!"
},
"type": "text"
}]
},
"field": "messages"
}]
}]
}
What Wappa Gives You (Clean)¶
# No JSON parsing needed!
async def process_message(self, webhook):
# Clean, typed access
user_id = webhook.user.user_id # "1234567890123"
user_name = webhook.user.profile_name # "John Doe"
message_text = webhook.get_message_text() # "Hello there!"
timestamp = webhook.timestamp # datetime(2022, 1, 20, 10, 0, 45)
# Helper methods work across all message types
message_type = webhook.get_message_type_name() # "text"
# Optional: access raw data for debugging
raw_json = webhook.get_raw_webhook_data() # Full original JSON
Common Patterns & Helper Methods¶
Message Type Detection¶
# Simple type checking
message_type = webhook.get_message_type_name()
if message_type == "text":
text = webhook.get_message_text()
elif message_type == "interactive":
selection = webhook.get_interactive_selection()
elif message_type in ["image", "video", "audio", "document"]:
media_id = webhook.message.media_id
caption = webhook.message.caption
elif message_type == "location":
lat, lng = webhook.message.latitude, webhook.message.longitude
Interactive Message Handling¶
# Handle button and list selections
if webhook.get_message_type_name() == "interactive":
selection = webhook.get_interactive_selection() # "help", "product_123"
# Pattern matching on selection IDs
if selection == "help":
await self.messenger.send_text("Here's help!", webhook.user.user_id)
elif selection.startswith("product_"):
product_id = selection.replace("product_", "")
await self.show_product_details(product_id, webhook.user.user_id)
Context Detection¶
# Check message context
if webhook.was_forwarded():
# Handle forwarded messages differently
await self.messenger.send_text("Thanks for forwarding that!", webhook.user.user_id)
if webhook.has_business_context():
# User interacted with catalog or business buttons
context = webhook.business_context
if context.has_product_context():
product_id = context.product_retailer_id
# Handle product inquiry
if webhook.has_ad_referral():
# User came from advertisement
referral = webhook.ad_referral
campaign_id = referral.source_id
# Track ad conversion
Error Handling Patterns¶
# Status webhook error handling
async def process_status(self, webhook):
if webhook.is_failed_status() and webhook.has_errors():
error = webhook.get_primary_error()
if error.error_code == 131051: # Unsupported message type
self.logger.info("User sent unsupported message - normal")
elif "rate limit" in error.error_title.lower():
self.logger.warning("Rate limited - implement backoff")
# Implement retry logic
else:
self.logger.error(f"Message delivery failed: {error.error_title}")
# Error webhook handling
async def process_error(self, webhook):
if webhook.has_critical_errors():
# 5xx server errors - escalate
await self.notify_admin(webhook.get_error_codes())
elif webhook.has_retryable_errors():
# Temporary issues - retry later
await self.schedule_retry(webhook)
Edge Cases & Gotchas¶
Message Type Edge Cases¶
# Some users send unsupported message types
if webhook.get_message_type_name() == "unsupported":
# WhatsApp couldn't process the message type
await self.messenger.send_text(
"Sorry, I can't process that type of message.",
webhook.user.user_id
)
# Empty text messages (rare but possible)
if webhook.get_message_type_name() == "text":
text = webhook.get_message_text()
if not text.strip():
# Handle empty message
return
Interactive Message Gotchas¶
# Interactive selections might be None
selection = webhook.get_interactive_selection()
if not selection:
self.logger.warning("Interactive message with no selection")
return
# Original message might not exist anymore
if webhook.get_message_type_name() == "interactive":
original_id = webhook.message.original_message_id
# This message ID might be from a different conversation
Media Message Considerations¶
# Media files have size limits and expiration
if webhook.message.is_image():
file_size = webhook.message.file_size
if file_size and file_size > 16 * 1024 * 1024: # 16MB
await self.messenger.send_text(
"Image too large. Please send smaller file.",
webhook.user.user_id
)
return
# Media URLs expire after some time
download_info = webhook.message.get_download_info()
# Download immediately or store media_id for later retrieval
Status Webhook Timing¶
# Status updates might arrive out of order
async def process_status(self, webhook):
# A "read" might arrive before "delivered" due to network timing
status = webhook.status.value
# Don't assume linear progression: sent → delivered → read
# Always check the actual status value
Debugging with Raw Data¶
Inspecting Raw Webhooks¶
async def process_message(self, webhook):
# Enable in development to see raw WhatsApp JSON
if settings.is_development:
raw_data = webhook.get_raw_webhook_data()
self.logger.debug(f"Raw webhook: {json.dumps(raw_data, indent=2)}")
# Your processing logic here
Common Debug Patterns¶
# Log webhook summaries
summary = webhook.get_summary()
self.logger.info(f"Processing: {summary}")
# Output example:
# {
# "webhook_type": "incoming_message",
# "platform": "whatsapp",
# "message_type": "text",
# "sender": "1234567890123",
# "tenant": "123456789",
# "timestamp": "2025-01-15T10:30:45Z"
# }
Webhook Validation¶
class MyApp(WappaEventHandler):
async def process_message(self, webhook):
# Always validate dependencies first
if not self.validate_dependencies():
self.logger.error("Dependencies not properly injected")
return
# Get dependency status for debugging
status = self.get_dependency_status()
self.logger.debug(f"Dependencies: {status}")
Next Steps¶
Now that you understand webhook data structures:
- Event System Guide - Practical event handling patterns
- Messaging Guide - Send any type of message WhatsApp supports
- State Management - Remember conversations between messages
- Event Handlers - See error handling examples in practice
Need working examples? Check our example applications to see webhook handling in action.