diff --git a/README.md b/README.md
index 0bdb246..5aaec0a 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,33 @@ https://your-domain.com/test%40example.com/github
- Clean, scannable layout
- Relevant emojis for quick identification
+#### Webex Interact (`/wxinteract`)
+Formats Webex Interact SMS API webhook events into clean, readable emails.
+
+**Supported events:**
+- 📤 Outbound SMS - Submitted
+- ✅ Outbound SMS - Delivered
+- ❌ Outbound SMS - Failed
+- 🔗 Shortlink Clicked
+- 📥 Inbound SMS Received
+- 🚫 SMS Opt Out Received
+- 👤 Contacts Callback
+
+**Example URL:**
+```
+https://your-domain.com/test%40example.com/wxinteract
+```
+
+**What you get:**
+- Event-specific formatting with relevant emojis
+- Message content displayed in a clean message box
+- Contact and campaign information
+- Error details for failed messages
+- Click tracking details for shortlinks
+- Custom fields support for contact callbacks
+
+**Test samples available in:** `wxinteract_test_samples.json`
+
### Creating Your Own Parser
Want to add support for Stripe, GitLab, or other services? Check out the [**Parser Development Guide**](../PARSER_PROMPT.md) for detailed instructions on creating custom parsers.
@@ -160,6 +187,49 @@ State: open
Branch: feature-auth → main
```
+### With Webex Interact Parser
+
+Configure your Webex Interact API callback URL with:
+```
+https://your-domain.com/yourname%40company.com/wxinteract
+```
+
+When SMS events occur, you'll receive formatted emails like:
+
+**Outbound SMS - Submitted:**
+```
+📤 Webex Interact: Outbound SMS - Submitted
+
+Message ID: msg_123456789
+To: +61412345678
+From: +61387654321
+Campaign ID: camp_987654321
+Submitted At: 2026-01-22T16:45:00Z
+
+Message Content:
+Hello! Your appointment is confirmed for tomorrow at 2 PM.
+```
+
+**SMS Opt Out:**
+```
+🚫 Webex Interact: SMS Opt Out Received
+
+Phone Number: +61412345678
+Keyword: STOP
+Opt Out At: 2026-01-22T18:15:00Z
+```
+
+**Shortlink Clicked:**
+```
+🔗 Webex Interact: Shortlink Clicked
+
+Shortlink: https://wxint.co/abc123
+Original URL: https://example.com/special-offer
+Phone Number: +61412345678
+Clicked At: 2026-01-22T17:30:45Z
+IP Address: 203.0.113.45
+```
+
## 📦 Installation
1. Clone the repository:
diff --git a/index.php b/index.php
index 805795b..c2f9f76 100755
--- a/index.php
+++ b/index.php
@@ -787,6 +787,610 @@ HTML;
];
}
+/**
+ * Webex Interact Webhook Parser
+ * Parses Webex Interact SMS webhook events into a formatted email
+ *
+ * @param string $rawBody The raw request body
+ * @param array $headers The request headers
+ * @param array $metadata Request metadata (date, ip, method, contentType)
+ * @return array|false Array with 'html' and 'subject' keys, or false on failure
+ */
+function wxinteractParse($rawBody, $headers, $metadata) {
+ $payload = json_decode($rawBody, true);
+ if (!$payload || !isset($payload['data'])) {
+ return false;
+ }
+
+ $data = $payload['data'];
+ $status = $data['status'] ?? 'Unknown';
+
+ // Sanitize common fields
+ $statusSafe = htmlspecialchars($status, ENT_QUOTES, 'UTF-8');
+ $dateSafe = htmlspecialchars($metadata['date'], ENT_QUOTES, 'UTF-8');
+ $ipSafe = htmlspecialchars($metadata['ip'], ENT_QUOTES, 'UTF-8');
+
+ // Determine event type and icon
+ $eventIcon = '📱';
+ $eventTitle = 'SMS Event';
+ $eventContent = '';
+
+ switch ($status) {
+ case 'submitted':
+ $eventIcon = '📤';
+ $eventTitle = 'Outbound SMS - Submitted';
+ $eventContent = buildOutboundSmsSubmittedContent($data);
+ break;
+
+ case 'delivered':
+ $eventIcon = '✅';
+ $eventTitle = 'Outbound SMS - Delivered';
+ $eventContent = buildOutboundSmsDeliveredContent($data);
+ break;
+
+ case 'failed':
+ $eventIcon = '❌';
+ $eventTitle = 'Outbound SMS - Failed';
+ $eventContent = buildOutboundSmsFailedContent($data);
+ break;
+
+ case 'shortlink_clicked':
+ $eventIcon = '🔗';
+ $eventTitle = 'Shortlink Clicked';
+ $eventContent = buildShortlinkClickedContent($data);
+ break;
+
+ case 'received':
+ $eventIcon = '📥';
+ $eventTitle = 'Inbound SMS Received';
+ $eventContent = buildInboundSmsContent($data);
+ break;
+
+ case 'opt_out':
+ $eventIcon = '🚫';
+ $eventTitle = 'SMS Opt Out Received';
+ $eventContent = buildOptOutContent($data);
+ break;
+
+ case 'contact_created':
+ case 'contact_updated':
+ $eventIcon = '👤';
+ $eventTitle = 'Contacts Callback';
+ $eventContent = buildContactsCallbackContent($data);
+ break;
+
+ default:
+ // Unknown event type - display all data
+ $eventIcon = '📱';
+ $eventTitle = "SMS Event: $statusSafe";
+ $eventContent = '
Event Details
';
+ $eventContent .= renderDataAsTable($data);
+ $eventContent .= '
';
+ }
+
+ $eventTitleSafe = htmlspecialchars($eventTitle, ENT_QUOTES, 'UTF-8');
+
+ $html = <<
+
+
+
+
+ Webex Interact - $eventTitleSafe
+
+
+
+
+
+
+
+
📋 Event Information
+
+
+
+ $eventContent
+
+
+
+
+
+HTML;
+
+ $subject = "$eventIcon Webex Interact: $eventTitle";
+
+ return [
+ 'html' => $html,
+ 'subject' => $subject
+ ];
+}
+
+/**
+ * Build content for Outbound SMS Submitted event
+ */
+function buildOutboundSmsSubmittedContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $submittedAt = htmlspecialchars($data['submitted_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 📤 SMS Details
+
+
+
+
+
💬 Message Content
+
$message
+
+HTML;
+}
+
+/**
+ * Build content for Outbound SMS Delivered event
+ */
+function buildOutboundSmsDeliveredContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $deliveredAt = htmlspecialchars($data['delivered_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ ✅ Delivery Details
+
+
+HTML;
+}
+
+/**
+ * Build content for Outbound SMS Failed event
+ */
+function buildOutboundSmsFailedContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $failedAt = htmlspecialchars($data['failed_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $errorCode = htmlspecialchars($data['error_code'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $errorMessage = htmlspecialchars($data['error_message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ ❌ Failure Details
+
+
+
+
+
⚠️ Error Information
+
+ Error Code: $errorCode
+ Error Message: $errorMessage
+
+
+HTML;
+}
+
+/**
+ * Build content for Shortlink Clicked event
+ */
+function buildShortlinkClickedContent($data) {
+ $shortlink = htmlspecialchars($data['shortlink'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $originalUrl = htmlspecialchars($data['original_url'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $clickedAt = htmlspecialchars($data['clicked_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $campaignId = htmlspecialchars($data['campaign_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $userAgent = htmlspecialchars($data['user_agent'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $ipAddress = htmlspecialchars($data['ip_address'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 🔗 Click Details
+
+
+
+
+
🌐 Browser Information
+
$userAgent
+
+HTML;
+}
+
+/**
+ * Build content for Inbound SMS Received event
+ */
+function buildInboundSmsContent($data) {
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $to = htmlspecialchars($data['to'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $message = htmlspecialchars($data['message'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $receivedAt = htmlspecialchars($data['received_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 📥 Inbound SMS Details
+
+
+
+
+
💬 Message Content
+
$message
+
+HTML;
+}
+
+/**
+ * Build content for SMS Opt Out event
+ */
+function buildOptOutContent($data) {
+ $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $optOutAt = htmlspecialchars($data['opt_out_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $keyword = htmlspecialchars($data['keyword'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $messageId = htmlspecialchars($data['message_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $from = htmlspecialchars($data['from'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ return <<
+ 🚫 Opt Out Details
+
+
+HTML;
+}
+
+/**
+ * Build content for Contacts Callback event
+ */
+function buildContactsCallbackContent($data) {
+ $contactId = htmlspecialchars($data['contact_id'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $phoneNumber = htmlspecialchars($data['phone_number'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $firstName = htmlspecialchars($data['first_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $lastName = htmlspecialchars($data['last_name'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $email = htmlspecialchars($data['email'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $status = htmlspecialchars($data['status'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $createdAt = htmlspecialchars($data['created_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+ $updatedAt = htmlspecialchars($data['updated_at'] ?? 'N/A', ENT_QUOTES, 'UTF-8');
+
+ $customFieldsHtml = '';
+ if (isset($data['custom_fields']) && is_array($data['custom_fields']) && !empty($data['custom_fields'])) {
+ $customFieldsHtml = '🏷️ Custom Fields
+HTML;
+ }
+ $customFieldsHtml .= '
';
+ }
+
+ return <<
+ 👤 Contact Details
+
+
+
+ $customFieldsHtml
+HTML;
+}
+
// Build email headers to ensure proper HTML rendering and sender information
// MIME-Version declares email format capabilities
$emailHeaders = "MIME-Version: 1.0\r\n";
diff --git a/wxinteract_test_samples.json b/wxinteract_test_samples.json
new file mode 100644
index 0000000..ed0b823
--- /dev/null
+++ b/wxinteract_test_samples.json
@@ -0,0 +1,86 @@
+{
+ "outbound_sms_submitted": {
+ "data": {
+ "status": "submitted",
+ "message_id": "msg_123456789",
+ "to": "+61412345678",
+ "from": "+61387654321",
+ "message": "Hello! Your appointment is confirmed for tomorrow at 2 PM.",
+ "submitted_at": "2026-01-22T16:45:00Z",
+ "campaign_id": "camp_987654321"
+ }
+ },
+ "outbound_sms_delivered": {
+ "data": {
+ "status": "delivered",
+ "message_id": "msg_123456789",
+ "to": "+61412345678",
+ "from": "+61387654321",
+ "delivered_at": "2026-01-22T16:46:15Z",
+ "campaign_id": "camp_987654321"
+ }
+ },
+ "outbound_sms_failed": {
+ "data": {
+ "status": "failed",
+ "message_id": "msg_987654321",
+ "to": "+61499999999",
+ "from": "+61387654321",
+ "failed_at": "2026-01-22T16:47:00Z",
+ "error_code": "ERR_INVALID_NUMBER",
+ "error_message": "The destination number is invalid or not reachable",
+ "campaign_id": "camp_987654321"
+ }
+ },
+ "shortlink_clicked": {
+ "data": {
+ "status": "shortlink_clicked",
+ "shortlink": "https://wxint.co/abc123",
+ "original_url": "https://example.com/special-offer",
+ "clicked_at": "2026-01-22T17:30:45Z",
+ "phone_number": "+61412345678",
+ "message_id": "msg_123456789",
+ "campaign_id": "camp_987654321",
+ "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15",
+ "ip_address": "203.0.113.45"
+ }
+ },
+ "inbound_sms_received": {
+ "data": {
+ "status": "received",
+ "message_id": "msg_inbound_001",
+ "from": "+61412345678",
+ "to": "+61387654321",
+ "message": "YES I would like to receive updates",
+ "received_at": "2026-01-22T18:00:00Z",
+ "keyword": "YES"
+ }
+ },
+ "sms_opt_out": {
+ "data": {
+ "status": "opt_out",
+ "phone_number": "+61412345678",
+ "opt_out_at": "2026-01-22T18:15:00Z",
+ "keyword": "STOP",
+ "message_id": "msg_optout_001",
+ "from": "+61387654321"
+ }
+ },
+ "contact_created": {
+ "data": {
+ "status": "contact_created",
+ "contact_id": "contact_abc123",
+ "phone_number": "+61412345678",
+ "first_name": "John",
+ "last_name": "Smith",
+ "email": "john.smith@example.com",
+ "created_at": "2026-01-22T19:00:00Z",
+ "updated_at": "2026-01-22T19:00:00Z",
+ "custom_fields": {
+ "company": "Acme Corporation",
+ "department": "Sales",
+ "customer_id": "CUST_12345"
+ }
+ }
+ }
+}