This document covers webhook configuration, environment variables, and operational procedures for all external platform integrations in Petunia.
Owner: Riley Chen
Version: 1.0.1
Last Updated: 2025-12-05
Verification
- Last verified: 2025-12-05 (manual-local-2025-12-05-integrations; GH Actions run ID pending)
- CI jobs:
ci.yml webhook + API stages (replace with GH Actions run ID once available)
- Commands (demo portal + sandbox keys only):
pnpm test:api --config jest.config.api.cjs --runTestsByPath tests/api/webhooks/yelp/messages.test.ts tests/api/webhooks/google/notifications.test.ts tests/app/api/webhooks/facebook/facebook-signature.test.ts tests/app/api/webhooks/instagram/instagram-signature.test.ts
pnpm test:api --config jest.config.api.cjs --runTestsByPath tests/api/communications/voice/status/callId.test.ts tests/api/communications/voice/elevenlabs/call.test.ts
pnpm cypress:run --spec "cypress/e2e/inbox/inbox-demo-production.cy.ts"
pnpm db:health
- Environment: CI/staging demo portal seeded; all external providers pointed to sandbox/test accounts; no production client data
- Next verification due: 2026-01-04 (attach GH Actions run link + updated coverage)
Table of Contents
- WhatsApp Business
- Twilio SMS/MMS
- SendGrid Email
- Facebook Messenger
- Instagram DM
- Yelp
- Google Business
- RingCentral SMS
WhatsApp Business
Overview
WhatsApp Business Cloud API integration for receiving and sending WhatsApp messages through the Unified Inbox.
Webhook Endpoint
POST /api/webhooks/whatsapp
GET /api/webhooks/whatsapp (verification challenge)
Environment Variables
| Variable | Required | Description |
|---|
WHATSAPP_APP_SECRET | Production | Meta App Secret for signature validation |
WHATSAPP_VERIFY_TOKEN | Yes | Custom token for webhook verification |
WHATSAPP_ACCESS_TOKEN | Yes | Graph API access token for sending messages |
WHATSAPP_PHONE_NUMBER_ID | Yes | WhatsApp Business phone number ID |
- Create a Meta Business App at developers.facebook.com
- Add WhatsApp product to your app
- Configure webhook URL:
https://your-domain.com/api/webhooks/whatsapp
- Set verify token to match
WHATSAPP_VERIFY_TOKEN
- Subscribe to events:
messages, message_echoes
Database Models
WhatsAppConnection - Stores portal-to-phone-number mappings
PhoneNumberMapping - Fallback phone number resolution
Message - Stores inbound/outbound messages
Conversation - Groups messages by sender
Supported Message Types
| Type | Inbound | Outbound |
|---|
| Text | ✅ | ✅ |
| Image | ✅ (media ID stored) | ✅ |
| Document | ✅ (media ID stored) | ✅ |
| Audio | ✅ (media ID stored) | ✅ |
| Video | ✅ (media ID stored) | ✅ |
| Location | ✅ | ❌ |
| Sticker | ✅ | ❌ |
Status Updates
The webhook processes delivery status updates:
sent - Message accepted by WhatsApp
delivered - Message delivered to recipient
read - Message read by recipient
failed - Message delivery failed (error details stored)
Troubleshooting
| Symptom | Likely Cause | Action |
|---|
| 403 on webhook verification | Wrong verify token | Check WHATSAPP_VERIFY_TOKEN matches Meta config |
| 401 on POST | Invalid signature | Verify WHATSAPP_APP_SECRET is correct |
| Messages not appearing | No portal mapped | Check WhatsAppConnection has matching phoneNumberId |
| Duplicate messages | Retry from Meta | System deduplicates via externalId - safe to ignore |
Twilio SMS/MMS
Overview
Twilio integration for inbound SMS/MMS with optional autoresponder.
Webhook Endpoint
POST /api/webhooks/twilio (inbound messages)
POST /api/webhooks/twilio/status (delivery status)
Environment Variables
| Variable | Required | Description |
|---|
TWILIO_ACCOUNT_SID | Yes | Twilio account identifier |
TWILIO_AUTH_TOKEN | Production | Auth token for signature validation |
TWILIO_WEBHOOK_URL | Optional | Override URL for signature validation |
TWILIO_PHONE_NUMBER | Yes | Your Twilio phone number |
Twilio Console Setup
- Go to Phone Numbers → Manage → Active Numbers
- Select your number
- Under Messaging, set webhook URL:
https://your-domain.com/api/webhooks/twilio
- Method: HTTP POST
- For status callbacks, configure:
https://your-domain.com/api/webhooks/twilio/status
Message Flow
- Inbound SMS received at Twilio
- Twilio POSTs to webhook with signature
- Webhook validates signature (production) or skips (development)
- Message stored in
Message table with channel: 'sms'
- Conversation created/updated via
conversationService
- AutoResponder generates TwiML response (if configured)
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Your auto-reply here</Message>
</Response>
Troubleshooting
| Symptom | Likely Cause | Action |
|---|
| 401 response | Invalid signature | Check TWILIO_AUTH_TOKEN, verify webhook URL |
| No portal found | Missing phone mapping | Add entry to PhoneNumberMapping |
| Auto-reply not sent | AutoResponder disabled | Check business settings |
| MMS attachments missing | Media URLs not stored | Check metadata.mediaUrls field |
SendGrid Email
Overview
SendGrid integration for inbound emails (Inbound Parse) and delivery tracking (Event Webhook).
Webhook Endpoint
POST /api/webhooks/sendgrid (inbound emails & events)
GET /api/webhooks/sendgrid (health check)
Environment Variables
| Variable | Required | Description |
|---|
SENDGRID_API_KEY | Yes | API key for sending emails |
SENDGRID_EVENT_WEBHOOK_SECRET | Production | Signing secret for event webhook |
SENDGRID_INBOUND_WEBHOOK_SECRET | Optional | Additional inbound security |
SendGrid Configuration
Inbound Parse Setup:
- Go to Settings → Inbound Parse
- Add Host & URL:
https://your-domain.com/api/webhooks/sendgrid
- Check "POST the raw, full MIME message"
- Add MX records for your domain pointing to
mx.sendgrid.net
Event Webhook Setup:
- Go to Settings → Mail Settings → Event Webhook
- HTTP Post URL:
https://your-domain.com/api/webhooks/sendgrid
- Select events: Delivered, Bounced, Opened, Clicked, Spam Report, Unsubscribe
Inbound Email Processing
- Portal matched by recipient email domain
- Contact created/found by sender email
- Conversation created with subject as title
- Message stored via
messagePersistence.storeInbound()
Event Types Processed
| Event | Action |
|---|
| delivered | Update message status to delivered |
| bounce | Mark as failed, store bounce reason |
| open | Track email opens |
| click | Track link clicks |
| spamreport | Flag for compliance |
| unsubscribe | Update contact preferences |
Troubleshooting
| Symptom | Likely Cause | Action |
|---|
| Inbound emails not received | MX records misconfigured | Verify DNS settings |
| No portal found | Email domain not configured | Check Business/Location email settings |
| Events not processing | Invalid signature | Verify SENDGRID_EVENT_WEBHOOK_SECRET |
Facebook Messenger
Overview
Facebook Messenger integration for business page messaging. Creates portal-scoped Conversations for unified inbox integration.
Webhook Endpoint
POST /api/webhooks/facebook
GET /api/webhooks/facebook (verification)
Environment Variables
| Variable | Required | Description |
|---|
FACEBOOK_CLIENT_SECRET | Production | App secret for signature validation |
FACEBOOK_WEBHOOK_VERIFY_TOKEN | Yes | Webhook verification token |
FACEBOOK_PAGE_ACCESS_TOKEN | Yes | Page access token for API calls |
Facebook App Setup
- Create app at developers.facebook.com
- Add Messenger product
- Generate page access token
- Configure webhook:
https://your-domain.com/api/webhooks/facebook
- Subscribe to:
messages, messaging_postbacks
Database Models
FacebookConnection - Links pageId to portal via ConnectionIntegration → Client → Business
Message - Stores inbound messages with portalId, conversationId
Conversation - Groups messages by sender
Contact - Created/found per sender
Message Flow
- Webhook receives message/comment from Facebook
- Portal resolved via
FacebookConnection.pageId
- Contact created/found by sender ID
- Conversation created via
conversationService.findOrCreateFromChannel()
- Message stored with full portal and conversation linking
- Conversation aggregates updated
Event Types Handled
| Event | Action |
|---|
message | Store as incoming DM, create/update conversation |
postback | Log button click payload |
read | Log read receipt |
delivery | Log delivery confirmation |
comments | Store as comment message with separate conversation |
posts | Log new posts |
ratings | Log page ratings |
Troubleshooting
| Symptom | Likely Cause | Action |
|---|
| 403 on verification | Wrong verify token | Check FACEBOOK_WEBHOOK_VERIFY_TOKEN matches app config |
| 401 on POST | Invalid signature | Verify FACEBOOK_CLIENT_SECRET is correct |
| Messages not appearing | No portal mapped | Check FacebookConnection has matching pageId |
| Duplicate messages | Retry from Meta | System deduplicates via externalId (message.mid) |
Instagram DM
Overview
Instagram Direct Messages, comments, and mentions integration. Uses Facebook Graph API and creates portal-scoped Conversations for unified inbox integration.
Webhook Endpoint
POST /api/webhooks/instagram
GET /api/webhooks/instagram (verification)
Environment Variables
| Variable | Required | Description |
|---|
FACEBOOK_CLIENT_SECRET | Production | App secret for signature validation (same as Facebook) |
FACEBOOK_WEBHOOK_VERIFY_TOKEN | Yes | Webhook verification token (same as Facebook) |
INSTAGRAM_ACCESS_TOKEN | Yes | Instagram Business account token |
Requirements
- Instagram Business or Creator account
- Facebook Page linked to Instagram account
- Instagram Graph API access
Database Models
FacebookConnection - Instagram uses Facebook's connection model (pageId = Instagram Business Account ID)
Message - Stores DMs, comments, mentions with portalId, conversationId
Conversation - Separate conversations for DMs vs comments vs mentions
Contact - Created per unique sender
Message Flow
- Webhook receives Instagram event (DM, comment, or mention)
- Portal resolved via
FacebookConnection (Instagram uses Facebook's Graph API)
- Contact created/found by sender ID
- Conversation created with type-specific externalId pattern:
- DMs: sender's Instagram ID
- Comments:
ig-comment-{senderId}
- Mentions:
ig-mention-{senderId}
- Message stored with metadata including platform, type, username
- Conversation aggregates updated
Event Types Handled
| Event | Action |
|---|
messaging | Store as Instagram DM |
comments | Store as comment with post ID reference |
mentions | Store as mention with media/comment ID |
Troubleshooting
| Symptom | Likely Cause | Action |
|---|
| 403 on verification | Wrong verify token | Check FACEBOOK_WEBHOOK_VERIFY_TOKEN |
| No portal found | Instagram not linked to FacebookConnection | Check connection configuration |
| Comments not appearing | Wrong Instagram Business Account ID | Verify pageId in FacebookConnection |
Yelp
Overview
See INTEGRATIONS_YELP.md for comprehensive Yelp documentation.
Webhook Endpoint
POST /api/webhooks/yelp (incoming messages)
Key Environment Variables
| Variable | Required | Description |
|---|
YELP_CLIENT_ID | Yes | OAuth client ID |
YELP_CLIENT_SECRET | Yes | OAuth client secret |
YELP_API_KEY | Yes | API key for Fusion API |
Google Business
Overview
Google Business Profile integration for reviews, questions, and answers via Pub/Sub. Creates portal-scoped Conversations for unified inbox integration.
Webhook Endpoint
POST /api/webhooks/google (Pub/Sub push endpoint)
Environment Variables
| Variable | Required | Description |
|---|
GOOGLE_CLIENT_ID | Yes | OAuth 2.0 client ID |
GOOGLE_CLIENT_SECRET | Yes | OAuth 2.0 client secret |
GOOGLE_REDIRECT_URI | Yes | OAuth callback URL |
GOOGLE_PUBSUB_AUDIENCE | Optional | Expected JWT audience for Pub/Sub auth |
Database Models
GoogleConnection - Links businessId (locationName) to portal via ConnectionIntegration → Client → Business
Message - Stores reviews/questions/answers with portalId, conversationId
Conversation - Groups interactions by reviewer/author name
Contact - Created per unique reviewer/author
Message Flow
- Google Cloud Pub/Sub pushes notification to webhook
- Webhook verifies JWT auth token (production)
- Decodes base64 message payload
- Portal resolved via
GoogleConnection.businessId (locationName)
- Contact created/found by reviewer/author display name
- Conversation created with type-specific externalId pattern:
- Reviews:
google-review-{reviewerName}
- Q&A:
google-qa-{authorName}
- Message stored with metadata including starRating, type, questionId
- Conversation aggregates updated
Event Types Handled
| Event | Action |
|---|
NEW_REVIEW | Store review with star rating |
UPDATED_REVIEW | Store review update (skip business replies) |
NEW_QUESTION | Store Q&A question |
NEW_ANSWER | Store Q&A answer (customer/local guide only) |
Troubleshooting
| Symptom | Likely Cause | Action |
|---|
| 401 on POST | Invalid Pub/Sub JWT | Check GOOGLE_PUBSUB_AUDIENCE matches |
| No portal found | LocationName not matched | Verify GoogleConnection.businessId |
| Reviews not appearing | Wrong location name format | Check Pub/Sub message payload |
| Duplicate reviews | Retry from Pub/Sub | System deduplicates via reviewId |
RingCentral SMS
Overview
RingCentral integration for inbound SMS with conversation creation for unified inbox integration.
Webhook Endpoint
POST /api/webhooks/ringcentral (inbound SMS and status updates)
GET /api/webhooks/ringcentral (health check)
Environment Variables
| Variable | Required | Description |
|---|
RINGCENTRAL_CLIENT_ID | Yes | OAuth client ID |
RINGCENTRAL_CLIENT_SECRET | Yes | OAuth client secret |
RINGCENTRAL_SERVER | Yes | API server URL |
RINGCENTRAL_WEBHOOK_SECRET | Production | Webhook validation secret |
Database Models
PhoneNumberMapping - Links phone numbers to portals
PhoneNumber (fallback) - Business phone number records
Message - Stores inbound SMS with portalId, conversationId
Conversation - Groups messages by sender phone
Contact - Created per unique sender phone
Message Flow
- RingCentral sends webhook with
Validation-Token header for subscription setup
- Webhook validates signature (production)
- Portal resolved via
PhoneNumberMapping or PhoneNumber tables
- Contact created/found by sender phone number
- Conversation created via
conversationService.findOrCreateFromChannel()
- Message stored with full portal and conversation linking
- Conversation aggregates updated
- Status updates processed via
messagePersistence.updateStatusByProviderMessageId()
Event Types Handled
| Event | Action |
|---|
| Validation request | Return Validation-Token header for subscription |
| Inbound SMS | Store message, create/update conversation |
| Status update | Update message delivery status |
Troubleshooting
| Symptom | Likely Cause | Action |
|---|
| Subscription failing | Validation-Token not returned | Check webhook returns header correctly |
| 401 on POST | Invalid signature | Verify RINGCENTRAL_WEBHOOK_SECRET |
| No portal found | Phone number not mapped | Add to PhoneNumberMapping |
| Duplicate messages | Retry from RingCentral | System deduplicates via providerMessageId |
Common Patterns
Portal Resolution
All webhooks resolve the target portal using:
- Platform-specific connection table (e.g.,
WhatsAppConnection)
- Fallback to
PhoneNumberMapping for phone-based channels
- Email domain matching for email channels
Conversation Creation
All inbound messages use conversationService.findOrCreateFromChannel():
const conversation = await conversationService.findOrCreateFromChannel({
portalId: portal.id,
channel: 'sms' | 'email' | 'whatsapp' | etc,
externalId: senderIdentifier, // Phone, email, or platform ID
title: conversationTitle,
participant: {
contactId,
name,
phone?,
email?,
},
});
Message Storage
All channels store messages with consistent provider tracking:
await prisma.message.create({
data: {
id: nanoid(),
externalId: platformMessageId,
content,
channel,
contactId,
portalId,
conversationId,
direction: 'incoming',
status: 'received',
providerId: platformName,
providerMsgId: platformMessageId,
providerStatus: 'received',
},
});
Signature Validation
Production environments validate webhook signatures:
- WhatsApp: HMAC-SHA256 with
x-hub-signature-256 header
- Twilio: HMAC-SHA1 with
x-twilio-signature header
- SendGrid: ECDSA signature validation
- Development environments skip validation for easier testing
Monitoring & Alerts
Health Checks
All webhooks support health check via GET:
curl https://your-domain.com/api/webhooks/{platform}
# Returns: { "status": "ok", "webhook": "{platform}", "supports": [...] }
Key Metrics to Monitor
- Webhook response times (p95 < 500ms)
- Error rates by platform
- Message processing latency
- Dead letter queue sizes (if applicable)
- Signature validation failures
Recommended Alerts
- Signature failures > 5/min - Possible secret rotation needed
- No messages in 1 hour - Webhook may be misconfigured
- Error rate > 5% - Platform API issues or code bugs
- p95 latency > 2s - Performance degradation
Testing
Local Development
Use ngrok or similar for local webhook testing:
ngrok http 3000
# Configure platform with ngrok URL
Test Endpoints
# Health check
curl http://localhost:3000/api/webhooks/whatsapp
# Simulate inbound (dev only, no signature)
curl -X POST http://localhost:3000/api/webhooks/twilio \
-d "From=+1234567890&To=+0987654321&Body=Test&MessageSid=SM123"
Integration Tests
pnpm test -- tests/app/api/webhooks/