# TextMe Web App Reverse Engineering Report

**Date:** January 28, 2026  
**Target:** web.textme-app.com / api.textme-app.com

---

## Executive Summary

TextMe is a free calling/texting app with a web interface. The web app uses **QR code-based authentication** (similar to WhatsApp Web), making automated API access challenging without a valid mobile app session. The backend is Django-based with JWT authentication, running on AWS infrastructure.

**Viability Assessment:** ⚠️ **CHALLENGING** - Building an unofficial API wrapper is technically possible but requires:
1. A valid mobile app account to authenticate
2. Session token extraction from an authenticated session
3. Maintaining WebSocket connections for real-time events

---

## 1. Infrastructure Overview

### Domains Discovered
| Domain | Purpose |
|--------|---------|
| `web.textme-app.com` | Web client (AngularJS SPA) |
| `api.textme-app.com` | REST API server |
| `webauth.textme-app.com` | QR code WebSocket auth |
| `pubsub.textme-app.com` | Real-time messaging WebSocket |
| `sentry.go-text.me` | Error tracking |
| `ng.textme-app.com` | Offer wall / monetization |
| `go-text.me` | Marketing / assets |

### Tech Stack
- **Frontend:** AngularJS + Angular Material
- **Backend:** Django (evident from CSRF token naming)
- **Auth:** JWT tokens (`WWW-Authenticate: JWT realm="api"`)
- **Real-time:** NGINX PushStream + WebSockets
- **VoIP:** SIP.js for voice/video calls
- **Hosting:** AWS (ELB detected)
- **Analytics:** Google Analytics, Google Tag Manager
- **Error Tracking:** Sentry

---

## 2. Authentication System

### Primary Auth Method: QR Code Login
The web app uses a **WhatsApp Web-style QR code authentication**:

1. Web client generates a session UUID: `TMWEB-{uuid}`
2. Opens WebSocket: `wss://webauth.textme-app.com/ws/TMWEB-{uuid}/`
3. QR code encodes this session ID
4. Mobile app scans QR and authenticates the session
5. WebSocket receives auth token, web client stores JWT

### Secondary Auth Methods (in mobile app)
- Email/password via `/api/auth-token/`
- Social auth via `/api/auth-token-social/` (Google, Facebook)
- Token refresh via `/api/auth-token-refresh/`

### CSRF Protection
```
Cookie Name: csrftoken
Header Name: X-CSRFToken
```

### JWT Token Flow
1. After successful auth, JWT token issued
2. Token sent in `Authorization: JWT <token>` header
3. Token refresh endpoint available for session extension

---

## 3. API Endpoints Discovered

### Authentication
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/auth-token/` | POST | Username/password login |
| `/api/auth-token-social/` | POST | Social media OAuth login |
| `/api/auth-token-refresh/` | POST | Refresh JWT token |
| `/api/auth-token-voip-jwt/` | POST | Get VoIP-specific JWT |
| `/api/register-device/` | POST | Register mobile device |

### User Management
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/user-info/` | GET | Get current user info |
| `/api/users/` | GET | User lookup/search |
| `/api/profile-picture/` | POST | Upload profile picture |
| `/api/public-profile/` | GET | Public profile data |
| `/api/settings/` | GET/PUT | User settings |
| `/api/change-password/` | POST | Change password |
| `/api/reset-password/` | POST | Reset password |

### Phone Numbers
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/phone-number/available-countries/` | GET | List available countries |
| `/api/phone-number/choose/` | POST | Select a phone number |

### Messaging (Inferred)
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/event/conversation/` | WebSocket | Real-time conversation events |
| `/api/group/` | GET/POST | Group chat management |
| `/api/attachment/` | POST | Send attachments |
| `/api/attachment/get-upload-url/` | POST | Get presigned upload URL |

### Calls
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/call/` | POST | Initiate/manage calls |

### Store/Monetization
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/store/stickers/` | GET | Available stickers |
| `/api/store/stickers/packages/` | GET | Sticker packs |
| `/api/inapp/transactions/` | GET/POST | In-app purchases |
| `/api/pricing/` | GET | Pricing info |

### Misc
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/web/signup/` | POST | Web signup |
| `/api/opengraph-resolver/` | POST | Resolve OpenGraph metadata |

---

## 4. Real-Time Communication

### PushStream (NGINX Module)
Used for real-time messaging with multiple transport options:
- WebSocket: `wss://pubsub.textme-app.com/ws/textme`
- EventSource: `/ev`
- Long Polling: `/lp`
- Stream: `/sub`

### WebSocket Authentication
```
wss://webauth.textme-app.com/ws/TMWEB-{session-uuid}/
```

### VoIP (SIP.js)
Voice/video calls use SIP protocol over WebSocket:
- Codecs: OPUS, PCMA, PCMU
- Transport: TCP/WebSocket

---

## 5. Security Measures Observed

### Anti-Bot Measures
1. **Google reCAPTCHA** - Integrated but may not be active for all endpoints
2. **403 Forbidden on suspicious requests** - POST to auth endpoints blocked
3. **AWS WAF** - Detected blocking patterns

### Rate Limiting
- Not explicitly tested, but expected on auth endpoints

### CORS
- Strict CORS policy, likely whitelisting only official domains

---

## 6. Mobile App Details

- **Package:** `com.textmeinc.textme3`
- **Facebook App ID:** `288600224541553`
- **Available on:** iOS, Android
- **Company:** TextMe, Inc.

---

## 7. Proof of Concept (Python)

```python
"""
TextMe Unofficial API Client - Proof of Concept

WARNING: This is for educational purposes only.
Requires a valid JWT token obtained from an authenticated session.
"""

import requests
import websocket
import json
import uuid
from typing import Optional, Dict, Any

class TextMeAPI:
    BASE_URL = "https://api.textme-app.com"
    WS_AUTH_URL = "wss://webauth.textme-app.com/ws"
    WS_PUBSUB_URL = "wss://pubsub.textme-app.com/ws/textme"
    
    def __init__(self, jwt_token: Optional[str] = None):
        self.jwt_token = jwt_token
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
            "Accept": "application/json",
            "Content-Type": "application/json",
        })
        if jwt_token:
            self.session.headers["Authorization"] = f"JWT {jwt_token}"
    
    def get_qr_session_id(self) -> str:
        """Generate a session ID for QR code authentication."""
        return f"TMWEB-{uuid.uuid4()}"
    
    def wait_for_qr_auth(self, session_id: str) -> Optional[str]:
        """
        Connect to WebSocket and wait for QR code scan.
        Returns JWT token on successful auth.
        """
        ws_url = f"{self.WS_AUTH_URL}/{session_id}/"
        
        def on_message(ws, message):
            data = json.loads(message)
            if "token" in data:
                self.jwt_token = data["token"]
                self.session.headers["Authorization"] = f"JWT {self.jwt_token}"
                ws.close()
                return data["token"]
        
        ws = websocket.WebSocketApp(
            ws_url,
            on_message=on_message,
        )
        ws.run_forever()
        return self.jwt_token
    
    def get_user_info(self) -> Dict[str, Any]:
        """Get current user information."""
        response = self.session.get(f"{self.BASE_URL}/api/user-info/")
        response.raise_for_status()
        return response.json()
    
    def get_settings(self) -> Dict[str, Any]:
        """Get user settings."""
        response = self.session.get(f"{self.BASE_URL}/api/settings/")
        response.raise_for_status()
        return response.json()
    
    def get_available_countries(self) -> Dict[str, Any]:
        """Get available countries for phone numbers."""
        response = self.session.get(f"{self.BASE_URL}/api/phone-number/available-countries/")
        response.raise_for_status()
        return response.json()
    
    def refresh_token(self) -> str:
        """Refresh the JWT token."""
        response = self.session.post(
            f"{self.BASE_URL}/api/auth-token-refresh/",
            json={"token": self.jwt_token}
        )
        response.raise_for_status()
        data = response.json()
        self.jwt_token = data["token"]
        self.session.headers["Authorization"] = f"JWT {self.jwt_token}"
        return self.jwt_token
    
    def send_message(self, conversation_id: str, text: str) -> Dict[str, Any]:
        """
        Send a text message.
        NOTE: Exact endpoint structure needs verification from network capture.
        """
        # This is speculative - actual endpoint needs verification
        response = self.session.post(
            f"{self.BASE_URL}/api/conversation/{conversation_id}/message/",
            json={"text": text}
        )
        response.raise_for_status()
        return response.json()


# Usage Example (requires manual token extraction)
if __name__ == "__main__":
    # Method 1: QR Code Auth (needs mobile app scan)
    api = TextMeAPI()
    session_id = api.get_qr_session_id()
    print(f"Scan this QR code with TextMe app:")
    print(f"Session: {session_id}")
    # token = api.wait_for_qr_auth(session_id)
    
    # Method 2: Use extracted token
    # token = "your_jwt_token_here"
    # api = TextMeAPI(jwt_token=token)
    # user = api.get_user_info()
    # print(user)
```

---

## 8. Obstacles for Unofficial API

### Critical Barriers
1. **QR Code Only Auth** - No username/password on web, requires mobile app
2. **AWS WAF** - Blocks suspicious POST requests
3. **CSRF Protection** - Django CSRF tokens required
4. **Device Registration** - May require registered device ID

### Potential Workarounds
1. **Mobile App Interception** - Proxy mobile traffic to capture tokens
2. **APK Reverse Engineering** - Decompile Android app for API secrets
3. **Selenium/Puppeteer** - Automate web login with real QR scan
4. **Browser Extension** - Inject into authenticated session

---

## 9. Recommendations

### For Building an Unofficial API
1. **Start with mobile app** - Proxy traffic with mitmproxy/Charles
2. **Extract tokens** - Get JWT from authenticated session
3. **Map conversation endpoints** - Need network capture for message send/receive
4. **Handle real-time** - Implement WebSocket client for pubsub
5. **Token management** - Implement refresh token rotation

### Legal Considerations
- Check TextMe Terms of Service before automated access
- Unofficial APIs may violate ToS
- Consider rate limiting to avoid account bans

---

## 10. Missing Information (Needs Further Research)

- [ ] Exact message send endpoint structure
- [ ] Conversation list endpoint
- [ ] Message history endpoint  
- [ ] Contact management endpoints
- [ ] Exact JWT payload structure
- [ ] Device registration requirements
- [ ] Rate limit thresholds

---

## Appendix: Raw Findings

### Sentry DSN
```
https://87d11b479de34e519af45bc5a47d4a9e@sentry.go-text.me/6
```

### Facebook App ID
```
288600224541553
```

### Detected Server Headers
```
Server: nginx
Server: awselb/2.0
WWW-Authenticate: JWT realm="api"
```
