29 KiB
WhatsApp Business API Setup Guide
This guide will help you set up WhatsApp Business API credentials for use with WhatsHooked.
Common Error: "Object does not exist or missing permissions"
If you see this error:
Failed to connect client account_id=test error="API returned status 400:
{\"error\":{\"message\":\"Unsupported get request. Object with ID 'XXXXXXXXX' does not exist,
cannot be loaded due to missing permissions, or does not support this operation...\",
\"type\":\"GraphMethodException\",\"code\":100,\"error_subcode\":33...}}"
This means your access token lacks the required WhatsApp Business API permissions.
Prerequisites
Before you begin, ensure you have:
- A Meta Business Account
- WhatsApp Business API access (approved by Meta)
- A verified WhatsApp Business phone number
- Admin access to your Meta Business Manager
Step 1: Access Meta Business Manager
- Go to Meta Business Manager
- Select your business account
- Navigate to Business Settings (gear icon)
Step 2: Create a System User (Recommended for Production)
System Users provide permanent access tokens that don't expire with user sessions.
- In Business Settings, go to Users → System Users
- Click Add to create a new system user
- Enter a name (e.g., "WhatsHooked API Access")
- Select Admin role
- Click Create System User
Step 3: Assign the System User to WhatsApp
- In the System User details, scroll to Assign Assets
- Click Add Assets
- Select Apps
- Choose your WhatsApp Business app
- Grant Full Control
- Click Add People
- Select WhatsApp Accounts
- Choose your WhatsApp Business Account
- Grant Full Control
- Click Save Changes
Step 4: Generate Access Token with Required Permissions
- In the System User details, click Generate New Token
- Select your app from the dropdown
- IMPORTANT: Check these permissions:
- ✅
whatsapp_business_management - ✅
whatsapp_business_messaging
- ✅
- Set token expiration (choose "Never" for permanent tokens)
- Click Generate Token
- CRITICAL: Copy the token immediately - you won't see it again!
Verify Token Permissions
You can verify your token has the correct permissions:
# Replace YOUR_TOKEN with your actual access token
curl -X GET 'https://graph.facebook.com/v21.0/debug_token?input_token=YOUR_TOKEN' \
-H 'Authorization: Bearer YOUR_TOKEN'
Look for "scopes" in the response - it should include:
{
"data": {
"scopes": [
"whatsapp_business_management",
"whatsapp_business_messaging",
...
]
}
}
Step 5: Get Your Phone Number ID
The Phone Number ID is NOT your actual phone number - it's a unique identifier from Meta.
Method 1: Via WhatsApp Manager (Easiest)
- Go to WhatsApp Manager
- Select your WhatsApp Business Account
- Click API Setup in the left sidebar
- Copy the Phone Number ID (looks like:
123456789012345)
Method 2: Via API
# Replace YOUR_TOKEN and YOUR_BUSINESS_ACCOUNT_ID
curl -X GET 'https://graph.facebook.com/v21.0/YOUR_BUSINESS_ACCOUNT_ID/phone_numbers' \
-H 'Authorization: Bearer YOUR_TOKEN'
Response:
{
"data": [
{
"verified_name": "Your Business Name",
"display_phone_number": "+1 234-567-8900",
"id": "123456789012345", // <- This is your Phone Number ID
"quality_rating": "GREEN"
}
]
}
Step 6: Get Your Business Account ID (Optional)
# Get all WhatsApp Business Accounts you have access to
curl -X GET 'https://graph.facebook.com/v21.0/me/businesses' \
-H 'Authorization: Bearer YOUR_TOKEN'
Or find it in WhatsApp Manager:
- Go to WhatsApp Manager
- Click on Settings (gear icon)
- The Business Account ID is shown in the URL:
https://business.facebook.com/wa/manage/home/?waba_id=XXXXXXXXX
Step 7: Register Your Phone Number (Required for New Numbers)
If this is a new phone number that hasn't been used for WhatsApp Business API before, you need to register it first.
Why Registration is Needed
Phone numbers must be registered with WhatsApp to:
- Activate the number for sending messages
- Set up two-factor authentication (2FA)
- Verify ownership of the phone number
Registration Process
1. Get your 2FA PIN:
You should have received a 6-digit PIN when you set up your WhatsApp Business phone number. If you don't have it:
- Go to WhatsApp Manager
- Select your phone number
- Go to Settings → Two-step verification
- Note or reset your PIN
2. Register the phone number:
# Replace PHONE_NUMBER_ID with your Phone Number ID
# Replace YOUR_TOKEN with your access token
# Replace 890523 with your actual 2FA PIN
curl -X POST 'https://graph.facebook.com/v21.0/PHONE_NUMBER_ID/register' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"pin": "123123"
}'
Successful Response:
{
"success": true
}
Error Response (if already registered):
{
"error": {
"message": "(#131030) Phone number has already been registered",
"type": "OAuthException",
"code": 131030,
"fbtrace_id": "..."
}
}
Note: If you get error code 131030, it means your number is already registered and you can skip this step.
Important Notes
- ⚠️ You only need to register once - don't repeat this step unless you're setting up a new number
- ⚠️ Keep your PIN secure - it's used for two-factor authentication
- ⚠️ Registration can take a few minutes - wait before sending messages
- ⚠️ If registration fails, verify:
- Your PIN is correct (6 digits)
- Your access token has
whatsapp_business_managementpermission - The phone number is verified in Meta Business Manager
Step 8: Test Your Credentials
After registration, test your credentials to confirm everything is working:
# Replace PHONE_NUMBER_ID and YOUR_TOKEN
curl -X GET 'https://graph.facebook.com/v21.0/PHONE_NUMBER_ID' \
-H 'Authorization: Bearer YOUR_TOKEN'
If successful, you'll get a response like:
{
"verified_name": "Your Business Name",
"display_phone_number": "+1 234-567-8900",
"id": "123456789012345",
"quality_rating": "GREEN"
}
If you get an error like "error_subcode":33, your token lacks permissions - go back to Step 4.
Step 9: Configure WhatsHooked
Update your config.json with the Business API configuration:
{
"whatsapp": [
{
"id": "business",
"type": "business-api",
"phone_number": "+1234567890",
"business_api": {
"phone_number_id": "123456789012345",
"access_token": "EAAxxxxxxxxxxxx_your_permanent_token_here",
"business_account_id": "987654321098765",
"api_version": "v21.0",
"verify_token": "your_secure_random_token_here"
}
}
]
}
Configuration Fields
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier for this account in WhatsHooked |
type |
Yes | Must be "business-api" |
phone_number |
Yes | Your WhatsApp Business phone number (E.164 format) |
phone_number_id |
Yes | Phone Number ID from Meta (from Step 5) |
access_token |
Yes | Permanent access token (from Step 4) |
business_account_id |
No | WhatsApp Business Account ID (optional, for reference) |
api_version |
No | Graph API version (defaults to "v21.0") |
verify_token |
Yes | Random string for webhook verification (see Step 9a) |
Step 9a: Generate Verify Token
The verify token is used by Meta to verify your webhook endpoint. Generate a secure random string:
# Generate a random token
openssl rand -hex 32
# Or use any secure random string like:
# "my_secure_verify_token_abc123xyz789"
Add this token to your config.json (see above) and save it - you'll need it for webhook configuration in Step 11.
Step 10: Start WhatsHooked
./bin/whatshook-server -config config.json
You should see:
INFO Business API client connected account_id=business phone=+1234567890
INFO Hook manager started and subscribed to events event_types=13
If you see Failed to connect client, check the error message and verify:
- Phone Number ID is correct
- Access token has required permissions
- Access token hasn't expired
- Business Account has WhatsApp API access enabled
Step 11: Configure Webhook in Meta Developer Console
WhatsHooked provides a webhook endpoint to receive incoming messages and status updates from WhatsApp.
11.1: Webhook URL Format
Your webhook URL should be:
https://your-domain.com/webhooks/whatsapp/{account_id}
Where {account_id} matches the id field in your config (e.g., "business").
Example: If your domain is api.example.com and account ID is business:
https://api.example.com/webhooks/whatsapp/business
11.2: Configure in Meta Developer Console
- Go to Meta Developers
- Select your app
- Navigate to WhatsApp → Configuration
- Under "Webhook", click Edit
- Enter:
- Callback URL:
https://your-domain.com/webhooks/whatsapp/business - Verify Token: The same token from your
config.json(verify_tokenfield)
- Callback URL:
- Click Verify and Save
Meta will send a GET request to verify your endpoint. If verification succeeds, you'll see a green checkmark.
11.3: Subscribe to Webhook Events
After verification, subscribe to these webhook fields:
- ✅ messages - Incoming messages and message status updates
- ✅ message_template_status_update - Template approval/rejection (optional)
- ✅ account_update - Account changes (optional)
- ✅ phone_number_quality_update - Quality rating changes (optional)
Click Subscribe for each field you want to receive.
Supported Webhook Events
WhatsHooked supports all WhatsApp Business API webhook events and message types:
Message Types
| Type | Supported | Downloads Media | Description |
|---|---|---|---|
text |
✅ | N/A | Text messages |
image |
✅ | ✅ | Images with optional caption |
video |
✅ | ✅ | Videos with optional caption |
document |
✅ | ✅ | PDFs, docs, etc. with filename |
audio |
✅ | ✅ | Voice messages and audio files |
sticker |
✅ | ✅ | Animated and static stickers |
location |
✅ | N/A | GPS coordinates with name/address |
contacts |
✅ | N/A | Shared contact cards (vCard) |
interactive |
✅ | N/A | Button/list/flow replies |
button |
✅ | N/A | Quick reply button responses |
reaction |
✅ | N/A | Emoji reactions to messages |
order |
✅ | N/A | Catalog/commerce orders |
system |
✅ | N/A | System notifications |
Status Updates
| Status | Event | Description |
|---|---|---|
sent |
message.sent |
Message sent from your number |
delivered |
message.delivered |
Message delivered to recipient |
read |
message.read |
Message read by recipient |
failed |
message.failed |
Message delivery failed |
Webhook Notification Types
| Field | Description | Events Published |
|---|---|---|
messages |
Message events | message.received, message status updates |
message_template_status_update |
Template changes | Logged to console |
account_update |
Account config changes | Logged to console |
phone_number_quality_update |
Quality rating changes | Logged to console |
phone_number_name_update |
Display name changes | Logged to console |
account_alerts |
Important alerts | Logged to console |
Webhook Security
WhatsHooked implements proper webhook security:
- Verification: Uses the
verify_tokento verify Meta's webhook setup request - Account isolation: Each account has its own webhook endpoint path
- No authentication required: Meta's webhooks don't support custom auth headers
- Validation: Verifies webhook payload structure
Webhook Verification Flow
Meta sends: GET /webhooks/whatsapp/business?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=CHALLENGE
↓
WhatsHooked verifies token
↓
Returns CHALLENGE (200 OK) if valid
403 Forbidden if invalid
Receiving Messages
Meta sends: POST /webhooks/whatsapp/business
↓
WhatsHooked processes webhook
↓
Downloads media (if present)
↓
Publishes to event bus
↓
Triggers your configured hooks
↓
Returns 200 OK
Testing Webhooks
Test with Meta's Test Button
- In WhatsApp Configuration → Webhooks
- Click Test next to "messages"
- Select a sample event (e.g., "Text Message")
- Click Send to My Server
- Check WhatsHooked logs for the received event
Test with Real Messages
- Send a message to your WhatsApp Business number
- Check WhatsHooked logs (set
"log_level": "debug"for details):
DEBUG Publishing message received event account_id=business message_id=wamid.xxx from=1234567890 type=text
DEBUG Hook manager received event event_type=message.received
DEBUG Hook matches event hook_id=message_hook event_type=message.received
DEBUG Found relevant hooks for event event_type=message.received hook_count=1
DEBUG Sending to hook hook_id=message_hook url=https://your-webhook.com/messages
- Your webhook should receive the payload
Webhook Payload Example
{
"account_id": "business",
"message_id": "wamid.HBgNMTIzNDU2Nzg5MAUCABEYEjQyMzRGRDhENzk5MkY5OUFBMQA",
"from": "1234567890",
"to": "1234567890",
"text": "Hello World",
"timestamp": "2026-01-30T12:00:00Z",
"is_group": false,
"sender_name": "John Doe",
"message_type": "text"
}
Step 12: Configure Your Webhooks
WhatsHooked forwards events to your own webhook URLs. Configure them in config.json:
{
"hooks": [
{
"id": "message_hook",
"name": "Message Handler",
"url": "https://your-app.com/api/whatsapp/messages",
"method": "POST",
"headers": {
"Authorization": "Bearer your-app-token"
},
"active": true,
"events": [
"message.received",
"message.sent",
"message.delivered",
"message.read"
],
"description": "Receives all message events"
}
]
}
Hook Configuration Fields
| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier for this hook |
name |
Yes | Human-readable name |
url |
Yes | Your webhook URL to receive events |
method |
Yes | HTTP method (usually "POST") |
headers |
No | Custom headers (for authentication, etc.) |
active |
Yes | Enable/disable this hook |
events |
No | Event types to receive (empty = all events) |
description |
No | Description for documentation |
Available Event Types
Message Events:
message.received- Incoming messagesmessage.sent- Outgoing messagesmessage.delivered- Delivery confirmationsmessage.read- Read receiptsmessage.failed- Delivery failures
Connection Events:
whatsapp.connected- Account connectedwhatsapp.disconnected- Account disconnected
QR Code Events (whatsmeow only):
whatsapp.qr.code- QR code for pairingwhatsapp.qr.timeout- QR code expiredwhatsapp.qr.error- QR code error
Hook Events:
hook.triggered- Hook was calledhook.success- Hook responded successfullyhook.failed- Hook call failed
Query Parameters
WhatsHooked automatically adds query parameters to your webhook URL:
https://your-app.com/api/whatsapp/messages?event=message.received&account_id=business
event- The event typeaccount_id- The WhatsApp account that triggered the event
Message Cache System
WhatsHooked includes a message cache that stores events when no active webhooks are configured. This ensures zero message loss.
Enable Message Cache
Add to your config.json:
{
"message_cache": {
"enabled": true,
"data_path": "./data/message_cache",
"max_age_days": 7,
"max_events": 10000
}
}
When Events Are Cached
Events are automatically cached when:
- No webhooks are configured for the event type
- All webhooks are inactive (
"active": false) - No webhooks match the event in their
eventsarray
Cache Management API
List cached events:
curl -u username:password http://localhost:8825/api/cache
Get cache statistics:
curl -u username:password http://localhost:8825/api/cache/stats
Replay all cached events:
curl -X POST -u username:password http://localhost:8825/api/cache/replay
Replay specific event:
curl -X POST -u username:password \
"http://localhost:8825/api/cache/event/replay?id=EVENT_ID"
Delete cached event:
curl -X DELETE -u username:password \
"http://localhost:8825/api/cache/event/delete?id=EVENT_ID"
Clear all cache:
curl -X DELETE -u username:password \
"http://localhost:8825/api/cache/clear?confirm=true"
Cache Workflow Example
- Disable webhooks → New messages get cached
- Configure/enable webhooks → Future messages delivered immediately
- Call replay API → Cached messages delivered to webhooks
- Successful delivery → Events removed from cache automatically
Troubleshooting
Webhooks Not Receiving Events
Check these items:
- Verify token is correct in both
config.jsonand Meta Developer Console - Check webhook is active in Meta console (green checkmark)
- Verify URL is accessible from internet (Meta needs to reach it)
- Check logs with
"log_level": "debug":DEBUG Publishing message received event account_id=business DEBUG Hook manager received event event_type=message.received DEBUG Hook matches event hook_id=message_hook - Test with curl:
# Send test message to your WhatsApp Business number # Check if webhook receives it
Webhook Verification Fails
Error: "The callback URL or verify token couldn't be validated"
Causes:
verify_tokenmismatch between config.json and Meta console- WhatsHooked server not running
- Firewall blocking Meta's IP ranges
- Wrong webhook URL format
Fix:
- Ensure server is running:
./bin/whatshook-server -config config.json - Check logs for verification attempt
- Verify token matches exactly (case-sensitive)
- Test URL is accessible:
curl https://your-domain.com/webhooks/whatsapp/business
Messages Not Cached
Check:
message_cache.enabledistruein config- Hooks are actually inactive or not matching events
- Check cache stats:
curl -u user:pass http://localhost:8825/api/cache/stats
No Hooks Configured Error
If events are being cached but you have hooks configured, check:
- Hook
"active"istrue - Hook
"events"array includes the event type (or is empty for all events) - Hook URL is reachable and responding with 2xx status
Enable debug logging to trace the issue:
{
"log_level": "debug"
}
Webhook Payload Examples
Text Message
{
"account_id": "business",
"message_id": "wamid.HBgNMTIzNDU2Nzg5MAUCABEYEjQyMzRGRDhENzk5MkY5OUFBMQA",
"from": "1234567890",
"to": "1234567890",
"text": "Hello, how can I help?",
"timestamp": "2026-01-30T12:00:00Z",
"is_group": false,
"sender_name": "John Doe",
"message_type": "text"
}
Image Message (with media)
{
"account_id": "business",
"message_id": "wamid.xxx",
"from": "1234567890",
"to": "1234567890",
"text": "Check this out!",
"timestamp": "2026-01-30T12:00:00Z",
"is_group": false,
"sender_name": "John Doe",
"message_type": "image",
"media": {
"type": "image",
"mime_type": "image/jpeg",
"filename": "wamid.xxx_a1b2c3d4.jpg",
"url": "http://localhost:8825/api/media/business/wamid.xxx_a1b2c3d4.jpg",
"base64": "..." // Only if media.mode is "base64" or "both"
}
}
Location Message
{
"account_id": "business",
"message_id": "wamid.xxx",
"from": "1234567890",
"to": "1234567890",
"text": "Location: Office (123 Main St) - 40.712800, -74.006000",
"timestamp": "2026-01-30T12:00:00Z",
"is_group": false,
"sender_name": "John Doe",
"message_type": "location"
}
Button Reply (Interactive)
{
"account_id": "business",
"message_id": "wamid.xxx",
"from": "1234567890",
"to": "1234567890",
"text": "Yes, I'm interested",
"timestamp": "2026-01-30T12:00:00Z",
"is_group": false,
"sender_name": "John Doe",
"message_type": "interactive"
}
Delivery Status
{
"event_type": "message.delivered",
"timestamp": "2026-01-30T12:00:05Z",
"data": {
"account_id": "business",
"message_id": "wamid.xxx",
"from": "1234567890",
"timestamp": "2026-01-30T12:00:05Z"
}
}
Complete Configuration Example
Here's a complete config.json with all Business API features:
{
"server": {
"host": "0.0.0.0",
"port": 8825,
"default_country_code": "1",
"username": "admin",
"password": "secure_password",
"auth_key": "optional_api_key"
},
"whatsapp": [
{
"id": "business",
"type": "business-api",
"phone_number": "+1234567890",
"business_api": {
"phone_number_id": "123456789012345",
"access_token": "EAAxxxxxxxxxxxx",
"business_account_id": "987654321098765",
"api_version": "v21.0",
"verify_token": "my_secure_random_token_abc123"
}
}
],
"hooks": [
{
"id": "message_hook",
"name": "Message Handler",
"url": "https://your-app.com/api/whatsapp/messages",
"method": "POST",
"headers": {
"Authorization": "Bearer your-app-secret-token",
"X-Custom-Header": "value"
},
"active": true,
"events": [
"message.received",
"message.sent",
"message.delivered",
"message.read"
],
"description": "Handles all message events"
},
{
"id": "status_hook",
"name": "Connection Monitor",
"url": "https://your-app.com/api/whatsapp/status",
"method": "POST",
"active": true,
"events": ["whatsapp.connected", "whatsapp.disconnected"],
"description": "Monitors connection status"
}
],
"media": {
"data_path": "./data/media",
"mode": "link",
"base_url": "https://your-domain.com"
},
"message_cache": {
"enabled": true,
"data_path": "./data/message_cache",
"max_age_days": 7,
"max_events": 10000
},
"event_logger": {
"enabled": true,
"targets": ["file", "sqlite"],
"file_dir": "./data/events",
"table_name": "event_logs"
},
"log_level": "info"
}
Advanced Features
Media Handling Modes
WhatsHooked supports three media delivery modes:
1. Link Mode (default, recommended)
{
"media": {
"mode": "link",
"base_url": "https://your-domain.com"
}
}
- Downloads media and stores locally
- Webhooks receive URL:
https://your-domain.com/api/media/business/filename.jpg - Efficient for large media files
2. Base64 Mode
{
"media": {
"mode": "base64"
}
}
- Encodes media as base64 in webhook payload
- No separate download needed
- Good for small files, increases payload size
3. Both Mode
{
"media": {
"mode": "both"
}
}
- Provides both URL and base64
- Maximum flexibility, largest payloads
Event Logger
Track all events to file and/or database:
{
"event_logger": {
"enabled": true,
"targets": ["file", "sqlite", "postgres"],
"file_dir": "./data/events",
"table_name": "event_logs"
},
"database": {
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "whatshooked",
"password": "password",
"database": "whatshooked"
}
}
Logged events include:
- All message events
- Connection status changes
- Hook success/failure
- Webhook triggers
Two-Way Communication
Your webhooks can respond to trigger outgoing messages:
Webhook Response Format:
{
"send_message": true,
"to": "1234567890",
"text": "Thanks for your message!",
"account_id": "business"
}
This sends a reply immediately when your webhook receives an event.
Production Deployment Checklist
Before going live:
- Use a System User token (not personal user token)
- Set
verify_tokento a secure random string (32+ characters) - Configure webhooks in Meta Developer Console
- Subscribe to required webhook fields (messages, etc.)
- Test webhook verification succeeds
- Enable HTTPS for production (required by Meta)
- Set up firewall rules to allow Meta's webhook IPs
- Configure authentication (
username/passwordorauth_key) - Enable message cache for reliability
- Set up event logging for audit trail
- Test sending and receiving messages
- Monitor logs for errors
- Set up log rotation for production
- Document your webhook endpoints
- Set up monitoring/alerts for webhook failures
Troubleshooting Common Issues
Error: "Object with ID does not exist" (error_subcode: 33)
Cause: One of the following:
- Incorrect Phone Number ID
- Access token lacks permissions
- Access token expired
Fix:
- Verify token permissions (see Step 4)
- Double-check Phone Number ID (see Step 5)
- Generate a new token if needed
Error: "Invalid OAuth access token"
Cause: Token is invalid or expired
Fix: Generate a new access token (Step 4)
Error: "Application does not have permission"
Cause: App not added to WhatsApp Business Account
Fix: Complete Step 3 to assign System User to WhatsApp
Token Expires Too Quickly
Issue: Using a User Access Token instead of System User token
Fix:
- Use a System User (Step 2) for permanent tokens
- User Access Tokens expire in 60 days
- System User tokens can be set to "Never expire"
Security Best Practices
-
Never commit tokens to version control
- Add
config.jsonto.gitignore - Use environment variables for sensitive data
- Add
-
Rotate tokens regularly
- Even "permanent" tokens should be rotated periodically
- Revoke old tokens when generating new ones
-
Use System Users for production
- Don't use personal User Access Tokens
- System Users provide better security and permanence
-
Limit token permissions
- Only grant the minimum required permissions
- For WhatsHooked, you only need:
whatsapp_business_managementwhatsapp_business_messaging
-
Monitor token usage
- Check token status regularly via debug_token endpoint
- Watch for unexpected API calls
Additional Resources
- WhatsApp Business Platform Documentation
- Graph API Reference
- System Users Guide
- WhatsApp Business API Getting Started
Support
If you continue to have issues:
- Verify your Meta Business Account has WhatsApp API access
- Check that your phone number is verified in WhatsApp Manager
- Ensure you're using Graph API v21.0 or later
- Review the WhatsApp Business API changelog for updates