This is the full developer documentation for Beeper Developer Docs
# Build on Beeper
> Build applications with access to all chats across networks, extend Beeper with new capabilities.
import { Card, LinkCard, CardGrid } from "@astrojs/starlight/components";
{/* */}
# Integrate with Beeper Android
import { CardGrid, LinkCard } from "@astrojs/starlight/components";
There are two ways to integrate with Beeper Android:
# Content Providers
> Query chats, messages, and contacts from Beeper using Android Content Provider APIs
import { Aside, Tabs, TabItem, Steps, Badge, LinkButton } from '@astrojs/starlight/components';
import { Callout } from '@stainless-api/docs/components';
Build Android apps and connected devices that integrate with Beeper using standard Android [content provider](https://developer.android.com/guide/topics/providers/content-providers) APIs. Content providers encapsulate data and provide mechanisms for defining data security, serving as the standard interface between processes.
Experimental
Content providers are experimental and APIs are subject to change.
### What you can build
- **Universal Search** - Search across all chats and messages
- **Widgets & Dashboards** - Display chat summaries and unread counts
- **Automation** - Send messages and react to changes
- **Wearables** - Integrate with watches and IoT devices
### Key features
- **Authority**: `com.beeper.api`
- **Permissions**: Runtime (request at first use)
- **Data Access**: Chats, messages, contacts
- **Operations**: Query, insert, observe changes
- **Protocol Support**: WhatsApp, Telegram, Signal, and more
## Quick start
1. **Add permissions to your manifest**
```xml title="AndroidManifest.xml"
```
These are custom Beeper permissions. Declare them in the manifest and request them at runtime with a permission prompt.
```kotlin
// Request at runtime (e.g., in an Activity or Fragment)
val requestPermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
val hasRead = results["com.beeper.android.permission.READ_PERMISSION"] == true
val hasSend = results["com.beeper.android.permission.SEND_PERMISSION"] == true
// Handle granted/denied states
}
requestPermissions.launch(arrayOf(
"com.beeper.android.permission.READ_PERMISSION",
"com.beeper.android.permission.SEND_PERMISSION"
))
```
2. **Query recent chats**
```kotlin
import androidx.core.net.toUri
val uri = "content://com.beeper.api/chats?limit=50".toUri()
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val titleIdx = cursor.getColumnIndexOrThrow("title")
val previewIdx = cursor.getColumnIndexOrThrow("messagePreview")
val unreadIdx = cursor.getColumnIndexOrThrow("unreadCount")
while (cursor.moveToNext()) {
val title = cursor.getString(titleIdx)
val preview = cursor.getString(previewIdx)
val unread = cursor.getInt(unreadIdx)
// Display chat info in your UI
}
}
```
3. **Send a message**
```kotlin
import android.net.Uri
import androidx.core.net.toUri
val message = "Hello from my app!"
val roomId = "!room:server.com"
val result = contentResolver.insert(
("content://com.beeper.api/messages?" +
"roomId=$roomId&text=${Uri.encode(message)}").toUri(),
null
)
result?.let {
val messageId = it.getQueryParameter("messageId")
// Message sent successfully
}
```
4. **Observe changes**
```kotlin
import android.os.Handler
import android.os.Looper
import android.database.ContentObserver
import androidx.core.net.toUri
contentResolver.registerContentObserver(
"content://com.beeper.api/chats".toUri(),
true,
object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
// Re-query and refresh your UI
}
}
)
```
Only `content://com.beeper.api/chats` currently emits change notifications. Observers for `messages` or `contacts` will not fire.
## Common use cases
```kotlin
// Get total unread count across all chats
val uri = "content://com.beeper.api/chats/count?isUnread=1".toUri()
val cursor = contentResolver.query(uri, null, null, null, null)
val unreadTotal = cursor?.use {
if (it.moveToFirst()) {
it.getInt(it.getColumnIndexOrThrow("count"))
} else 0
} ?: 0
```
```kotlin
// Search with surrounding context
val searchTerm = "meeting"
val uri = ("content://com.beeper.api/messages?" +
"query=${Uri.encode(searchTerm)}" +
"&contextBefore=2&contextAfter=2").toUri()
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val textIdx = cursor.getColumnIndexOrThrow("text_content")
val matchIdx = cursor.getColumnIndexOrThrow("is_search_match")
while (cursor.moveToNext()) {
val text = cursor.getString(textIdx)
val isMatch = cursor.getInt(matchIdx) == 1
// Highlight matches in UI
}
}
```
```kotlin
// Get WhatsApp chats only
val uri = "content://com.beeper.api/chats?protocol=whatsapp".toUri()
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
// Process WhatsApp-specific chats
}
```
## Performance tips
Best practices
1. **Always Set Limits** - Default limit is 100, but always specify `limit` to avoid large queries
2. **Use Filters** - Filter by `roomIds`, `protocol`, or other parameters to reduce data
3. **Batch Operations** - Combine multiple filters in one query instead of multiple queries
4. **Lifecycle Awareness** - Register/unregister observers with your component lifecycle
## Supported protocols
## Learn more
- **[API Reference โ](/android/content-providers/api-reference)** - Complete API documentation for all endpoints
- **[Integration Guide โ](/android/content-providers/integration-guide)** - Permissions, notifications, and troubleshooting
- **[Android Docs โ](https://developer.android.com/guide/topics/providers/content-provider-basics)** - Official content provider documentation
# API Reference
> Complete reference for Beeper Content Provider APIs
import { Aside, Tabs, TabItem, Card, Badge } from '@astrojs/starlight/components';
import { Callout } from '@stainless-api/docs/components';
## Base configuration
| Setting | Value |
|---------|-------|
| **Authority** | `com.beeper.api` |
| **Default Limit** | 100 rows (if not specified) |
| **Required Permissions** | `READ_PERMISSION`, `SEND_PERMISSION` |
`READ_PERMISSION` and `SEND_PERMISSION` are runtime permissions and must be requested via an in-app prompt.
## Chats API
### List chats
Retrieves a list of chats/conversations.
```
content://com.beeper.api/chats
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | int | Max chats to return |
| `offset` | int | Pagination offset |
| `roomIds` | string | Comma-separated room IDs |
| `isLowPriority` | 0/1 | Filter low priority chats |
| `isArchived` | 0/1 | Filter archived chats |
| `isUnread` | 0/1 | Filter unread chats |
| `showInAllChats` | 0/1 | Filter visibility |
| `protocol` | string | Filter by protocol (e.g., `whatsapp`) |
| Column | Type | Description |
|--------|------|-------------|
| `roomId` | string | Unique chat identifier |
| `title` | string | Chat display name |
| `messagePreview` | string | Last message preview |
| `senderEntityId` | string | Last message sender |
| `protocol` | string | Network protocol |
| `isMuted` | boolean | Mute status |
| `unreadCount` | int | Number of unread messages |
| `timestamp` | long | Last activity timestamp |
| `oneToOne` | boolean | Direct message flag |
```kotlin
import androidx.core.net.toUri
val cursor = contentResolver.query(
"content://com.beeper.api/chats?limit=50&isUnread=1".toUri(),
null, null, null, null
)
cursor?.use { c ->
val roomIdIdx = c.getColumnIndexOrThrow("roomId")
val titleIdx = c.getColumnIndexOrThrow("title")
val unreadIdx = c.getColumnIndexOrThrow("unreadCount")
while (c.moveToNext()) {
val roomId = c.getString(roomIdIdx)
val title = c.getString(titleIdx)
val unread = c.getInt(unreadIdx)
println("$title: $unread unread")
}
}
```
### Count chats
Get the total count of chats matching filters.
```
content://com.beeper.api/chats/count
```
Same filters as List Chats
```kotlin
val cursor = contentResolver.query(
"content://com.beeper.api/chats/count?isUnread=1".toUri(),
null, null, null, null
)
val count = cursor?.use {
if (it.moveToFirst()) {
it.getInt(it.getColumnIndexOrThrow("count"))
} else 0
} ?: 0
```
## Messages API
### List messages
Retrieves messages with optional filtering and search.
```
content://com.beeper.api/messages
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | int | Max messages |
| `offset` | int | Pagination offset |
| `roomIds` | string | Comma-separated room IDs |
| `senderId` | string | Filter by sender contact ID |
| `query` | string | Full-text search |
| `contextBefore` | int | Messages before match |
| `contextAfter` | int | Messages after match |
| `openAtUnread` | boolean | Center on first unread |
`openAtUnread=true` requires exactly one `roomId` and no other filters
When `openAtUnread=true` is used, additional columns `paging_offset` and `last_read` are returned to provide a paging window centered around the first unread message. These columns are not returned otherwise.
| Column | Type | Description |
|--------|------|-------------|
| `roomId` | string | Chat identifier |
| `originalId` | string | Message ID |
| `senderContactId` | string | Sender ID |
| `timestamp` | long | Message timestamp |
| `isSentByMe` | boolean | Sent by current user |
| `isDeleted` | boolean | Deletion status |
| `type` | string | Message type |
| `text_content` | string | Message text |
| `reactions` | string | Encoded reactions |
| `displayName` | string | Sender name |
| `is_search_match` | boolean | Search match flag |
| `paging_offset` | int | Pagination helper |
| `last_read` | boolean | Read marker |
```kotlin
// Search with context
val searchTerm = "important"
val uri = ("content://com.beeper.api/messages?" +
"query=${Uri.encode(searchTerm)}" +
"&contextBefore=3&contextAfter=2").toUri()
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val textIdx = cursor.getColumnIndexOrThrow("text_content")
val matchIdx = cursor.getColumnIndexOrThrow("is_search_match")
while (cursor.moveToNext()) {
val text = cursor.getString(textIdx)
val isMatch = cursor.getInt(matchIdx) == 1
if (isMatch) {
// This is the matching message
println("MATCH: $text")
} else {
// This is context
println("Context: $text")
}
}
}
```
```kotlin
import androidx.core.net.toUri
val roomId = "!room:server.com"
val uri = ("content://com.beeper.api/messages?" +
"roomIds=$roomId&openAtUnread=true&limit=50").toUri()
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val textIdx = cursor.getColumnIndexOrThrow("text_content")
val offsetIdx = cursor.getColumnIndexOrThrow("paging_offset")
val lastReadIdx = cursor.getColumnIndexOrThrow("last_read")
while (cursor.moveToNext()) {
val text = cursor.getString(textIdx)
val offset = cursor.getInt(offsetIdx)
val isLastRead = cursor.getInt(lastReadIdx) == 1
// Center UI around first unread using offset/last_read
}
}
```
### Count messages
Get total message count with filters.
```
content://com.beeper.api/messages/count
```
```kotlin
val count = contentResolver.query(
"content://com.beeper.api/messages/count?roomIds=!room:server.com".toUri(),
null, null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getInt(cursor.getColumnIndexOrThrow("count"))
} else 0
} ?: 0
```
### Reactions format
The `reactions` column encodes reactions as comma-separated entries:
`emoji|senderId|isSentByMe|order`
Example: `๐|@alice:server.com|0|123,๐|@me:server.com|1|124`
```kotlin
// Parse reactions
val reactionsRaw = cursor.getString(cursor.getColumnIndexOrThrow("reactions")) ?: ""
val reactions = reactionsRaw.split(',')
.filter { it.isNotEmpty() }
.map { part ->
val fields = part.split('|')
Reaction(
emoji = fields.getOrNull(0) ?: "",
senderId = fields.getOrNull(1) ?: "",
isSentByMe = fields.getOrNull(2) == "1",
order = fields.getOrNull(3)?.toLongOrNull() ?: 0
)
}
```
### Send message
Send a text message to a chat.
```
content://com.beeper.api/messages
```
This is an `insert()` operation, not a query
| Parameter | Type | Description |
|-----------|------|-------------|
| `roomId` | string | Target room ID |
| `text` | string | Message body (URL-encoded) |
- **Success**: URI with `roomId` and `messageId` query parameters
- **Failure**: `null`
```kotlin
import android.net.Uri
import androidx.core.net.toUri
val message = "Hello World!"
val roomId = "!room:server.com"
val resultUri = contentResolver.insert(
("content://com.beeper.api/messages?" +
"roomId=$roomId&text=${Uri.encode(message)}").toUri(),
null
)
if (resultUri != null) {
val sentRoomId = resultUri.getQueryParameter("roomId")
val messageId = resultUri.getQueryParameter("messageId")
println("Sent message $messageId to $sentRoomId")
} else {
println("Failed to send message")
}
```
## Contacts API
### List contacts
Retrieve contacts with optional filtering.
```
content://com.beeper.api/contacts
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | int | Max contacts |
| `offset` | int | Pagination offset |
| `senderIds` | string | Comma-separated sender IDs |
| `roomIds` | string | Comma-separated room IDs |
| `query` | string | Search display names |
| `protocol` | string | Filter by protocol |
| Column | Type | Description |
|--------|------|-------------|
| `id` | string | Contact identifier |
| `roomIds` | string | Associated rooms (CSV) |
| `displayName` | string | Display name |
| `contactDisplayName` | string | Alternative name |
| `linkedContactId` | string | Linked contact |
| `itsMe` | boolean | Current user flag |
| `protocol` | string | Network protocol |
```kotlin
val cursor = contentResolver.query(
"content://com.beeper.api/contacts?query=John".toUri(),
null, null, null, null
)
cursor?.use { c ->
val idIdx = c.getColumnIndexOrThrow("id")
val nameIdx = c.getColumnIndexOrThrow("displayName")
val roomsIdx = c.getColumnIndexOrThrow("roomIds")
while (c.moveToNext()) {
val contactId = c.getString(idIdx)
val name = c.getString(nameIdx)
val rooms = c.getString(roomsIdx).split(",")
println("$name is in ${rooms.size} rooms")
}
}
```
### Count contacts
Get total contact count with filters.
```
content://com.beeper.api/contacts/count
```
```kotlin
val count = contentResolver.query(
"content://com.beeper.api/contacts/count?protocol=whatsapp".toUri(),
null, null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getInt(cursor.getColumnIndexOrThrow("count"))
} else 0
} ?: 0
```
## Common URI examples
```text
# Recent unread chats
content://com.beeper.api/chats?isUnread=1&limit=10
# WhatsApp chats only
content://com.beeper.api/chats?protocol=whatsapp
# Search messages for "meeting"
content://com.beeper.api/messages?query=meeting
# Active chats (not archived or low priority)
content://com.beeper.api/chats?isArchived=0&isLowPriority=0
```
## Performance guidelines
Best Practices
1. **Always specify `limit`** - Avoid unbounded queries
2. **Use filters** - Reduce data with `roomIds`, `protocol`, etc.
3. **Close cursors** - Use try-with-resources or `.use{}` in Kotlin
4. **Batch reads** - Combine filters instead of multiple queries
5. **URL encode** - Always encode text parameters with `Uri.encode()`
# Integration Guide
> Permissions, notifications, troubleshooting, and best practices for Beeper Content Provider integration
import { Steps, Tabs, TabItem } from '@astrojs/starlight/components';
import { Callout } from '@stainless-api/docs/components';
## Permissions
### Required permissions
Beeper uses custom Android permissions that must be declared in your app's manifest:
```xml title="AndroidManifest.xml"
```
| Permission | Type | Purpose |
|------------|------|---------|
| `READ_PERMISSION` | Runtime | Query chats, messages, contacts |
| `SEND_PERMISSION` | Runtime | Send messages |
These are runtime permissions. Declare them in the manifest and request them via an in-app permission prompt.
```kotlin
// Check and request at runtime
val hasRead = ContextCompat.checkSelfPermission(
this, "com.beeper.android.permission.READ_PERMISSION"
) == PackageManager.PERMISSION_GRANTED
val hasSend = ContextCompat.checkSelfPermission(
this, "com.beeper.android.permission.SEND_PERMISSION"
) == PackageManager.PERMISSION_GRANTED
if (!hasRead || !hasSend) {
val launcher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
val grantedRead = results["com.beeper.android.permission.READ_PERMISSION"] == true
val grantedSend = results["com.beeper.android.permission.SEND_PERMISSION"] == true
// Handle granted/denied states
}
launcher.launch(arrayOf(
"com.beeper.android.permission.READ_PERMISSION",
"com.beeper.android.permission.SEND_PERMISSION"
))
}
```
Users can deny permissions; handle gracefully and explain limited functionality if not granted.
See Android's [uses-permission](https://developer.android.com/guide/topics/manifest/uses-permission-element)
and [permission](https://developer.android.com/guide/topics/manifest/permission-element) documentation.
## Change notifications
### ContentObserver setup
Use `ContentObserver` to react to data changes in real-time:
1. **Create an observer**
```kotlin
class ChatObserver(handler: Handler) : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
// Data changed - refresh your UI
refreshChatList()
}
}
```
2. **Register the observer**
```kotlin
import androidx.core.net.toUri
val observer = ChatObserver(Handler(Looper.getMainLooper()))
contentResolver.registerContentObserver(
"content://com.beeper.api/chats".toUri(),
true, // Notify for descendant URIs
observer
)
```
3. **Unregister when done**
```kotlin
override fun onDestroy() {
super.onDestroy()
contentResolver.unregisterContentObserver(observer)
}
```
### Common observer patterns
```kotlin
class ChatViewModel : ViewModel() {
private var observer: ContentObserver? = null
fun startObserving(contentResolver: ContentResolver) {
observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
loadChats()
}
}
contentResolver.registerContentObserver(
"content://com.beeper.api/chats".toUri(),
true,
observer!!
)
}
override fun onCleared() {
observer?.let {
getApplication()
.contentResolver
.unregisterContentObserver(it)
}
}
}
```
Only the content://com.beeper.api/chats URI currently emits change notifications. Observers on messages and contacts are not implemented yet.
Some Cursor implementations auto-update, but explicit observers ensure consistent behavior across Android versions.
## Troubleshooting
### Common issues and solutions
#### No rows returned
**Causes:**
- Beeper not installed/logged in
- Missing permissions
- Too restrictive filters
**Solutions:**
- Verify Beeper installation
- Check manifest permissions
- Add `limit` parameter
- Adjust filters
#### Permission denied
**Causes:**
- Permissions not declared in manifest
- Runtime permissions not granted
**Solutions:**
- Add permissions to manifest
- Request permissions at runtime
#### Message send fails
**Causes:**
- Invalid room ID
- Text not URL-encoded
- Network issues
**Solutions:**
- Verify `roomId` format
- Use `Uri.encode()` for text
- Check Beeper connectivity
#### UI not updating
**Causes:**
- No ContentObserver registered
- Wrong Handler/Looper
- Observer not on main thread
**Solutions:**
- Register ContentObserver
- Use `Handler(Looper.getMainLooper())`
- Refresh on main thread
### Debug checklist
1. **Verify Beeper installation**
```kotlin
val packageManager = context.packageManager
try {
packageManager.getPackageInfo("com.beeper.android", 0)
// Beeper is installed
} catch (e: PackageManager.NameNotFoundException) {
// Beeper not installed
}
```
2. **Check permissions**
```kotlin
val permissions = listOf(
"com.beeper.android.permission.READ_PERMISSION",
"com.beeper.android.permission.SEND_PERMISSION"
)
permissions.forEach { permission ->
val granted = checkSelfPermission(permission) ==
PackageManager.PERMISSION_GRANTED
Log.d("Permissions", "$permission: $granted")
}
```
3. **Test basic query**
```kotlin
try {
val cursor = contentResolver.query(
"content://com.beeper.api/chats?limit=1".toUri(),
null, null, null, null
)
Log.d("Debug", "Query returned ${cursor?.count ?: 0} rows")
cursor?.close()
} catch (e: Exception) {
Log.e("Debug", "Query failed", e)
}
```
## Frequently asked questions
**What's the authority?**
`com.beeper.api`
**Do I need runtime permissions?**
Yes. Request READ_PERMISSION and SEND_PERMISSION at runtime in addition to declaring them in the manifest.
**What's the default limit?**
Messages and contacts default to 100 if not specified. Always specify `limit` for chats to avoid large queries.
**Can I use this from a service?**
Yes, content providers work from any Android component with a Context.
**How do I open at the first unread message?**
Use `openAtUnread=true` with exactly one `roomId` and no other filters.
**Can I send media messages?**
Currently only text messages are supported via the content provider API.
**Is pagination cursor-based or offset-based?**
Offset-based. Use `limit` and `offset` parameters.
**Are queries case-sensitive?**
Text searches are case-insensitive. Room/sender IDs are case-sensitive.
**What's the maximum limit?**
No hard limit, but use reasonable values (50-200) for UI responsiveness.
**Can I cache query results?**
Yes, but use ContentObserver to invalidate cache on changes.
**How often do observers trigger?**
Immediately on data change. Consider debouncing for high-frequency updates.
**Should I close Cursors?**
Always. Use try-with-resources or Kotlin's `.use{}` extension.
## Best practices
### Always URL encode
```kotlin
// โ Good
Uri.encode("Hello & welcome!")
// โ Bad
"Hello & welcome!"
```
### Use lifecycle components
```kotlin
// Register in onStart
override fun onStart() {
super.onStart()
registerObserver()
}
// Unregister in onStop
override fun onStop() {
super.onStop()
unregisterObserver()
}
```
### Handle null results
```kotlin
// Always check for null
val result = contentResolver.insert(uri, null)
if (result != null) {
// Success
} else {
// Handle failure
}
```
### Batch operations
```kotlin
// โ Single query with multiple IDs
"roomIds=!room1:server,!room2:server"
// โ Multiple queries
query("roomIds=!room1:server")
query("roomIds=!room2:server")
```
## Testing Tips
Development testing
1. **Use Android Studio's Database Inspector** to view ContentProvider data
2. **Test with different Beeper account states** (logged out, no chats, many chats)
3. **Verify permission handling** by testing without declaring permissions
4. **Test observer behavior** by sending messages from another device
5. **Check edge cases** like empty results, special characters, long text
## Need help?
- Check the [API Reference](/android/content-providers/api-reference) for detailed endpoint documentation
- Review the [Quick start](/android/content-providers#quick-start) for basic examples
- Contact [info@beeper.com](mailto:info@beeper.com) for integration support
# Android Intents
> Control Beeper Android through Android Intents from other apps, Tasker, or command-line
import { Steps, Tabs, TabItem, Code } from '@astrojs/starlight/components';
import { Callout } from '@stainless-api/docs/components';
Beeper Android supports Android Intents that allow other apps, automation tools like Tasker, or command-line tools to interact with and control certain Beeper features. This enables powerful automation and integration possibilities.
## Supported intents
### Toggle incognito mode
Control Beeper's incognito mode programmatically from external sources.
- Requires Beeper Android version 4.31.1 or later
- Requires a Beeper Plus or PlusPlus subscription to enable Incognito Mode
**Intent details:**
- **Action**: `com.beeper.android.TOGGLE_INCOGNITO_MODE`
- **Component**: `com.beeper.android/com.beeper.ext.ExternalBroadcastReceiver`
- **Extra**: `enabled` (boolean) - `true` to enable, `false` to disable incognito mode
## Usage examples
```kotlin
import android.content.Intent
import android.content.ComponentName
// Enable incognito mode
fun enableIncognitoMode() {
val intent = Intent().apply {
action = "com.beeper.android.TOGGLE_INCOGNITO_MODE"
component = ComponentName(
"com.beeper.android",
"com.beeper.ext.ExternalBroadcastReceiver"
)
putExtra("enabled", true)
}
sendBroadcast(intent)
}
// Disable incognito mode
fun disableIncognitoMode() {
val intent = Intent().apply {
action = "com.beeper.android.TOGGLE_INCOGNITO_MODE"
component = ComponentName(
"com.beeper.android",
"com.beeper.ext.ExternalBroadcastReceiver"
)
putExtra("enabled", false)
}
sendBroadcast(intent)
}
```
**Task setup:**
1. Create a new Task in Tasker
2. Add Action โ System โ Send Intent
3. Configure the intent:
- **Action**: `com.beeper.android.TOGGLE_INCOGNITO_MODE`
- **Package**: `com.beeper.android`
- **Class**: `com.beeper.ext.ExternalBroadcastReceiver`
- **Extra**: `enabled:true` (or `enabled:false`)
- **Target**: Broadcast Receiver
```bash
# Enable incognito mode
adb shell am broadcast \
-a com.beeper.android.TOGGLE_INCOGNITO_MODE \
--ez enabled true \
-n com.beeper.android/com.beeper.ext.ExternalBroadcastReceiver
# Disable incognito mode
adb shell am broadcast \
-a com.beeper.android.TOGGLE_INCOGNITO_MODE \
--ez enabled false \
-n com.beeper.android/com.beeper.ext.ExternalBroadcastReceiver
```
Requires USB debugging to be enabled on the target device.
## Troubleshooting
Common issues
**Intent not received:**
- Verify Beeper Android is installed and updated
- Check that the component name is exactly: `com.beeper.android/com.beeper.ext.ExternalBroadcastReceiver`
- Ensure the action string is correct: `com.beeper.android.TOGGLE_INCOGNITO_MODE`
**ADB command fails:**
- Confirm ADB is properly installed and device is connected
- Check that the device is authorized for ADB access
### Verification
To verify the intent was received, check Beeper's incognito mode status in the app UI after sending the intent.
# Bridges & Self-Hosting
> Understanding Matrix bridges and how Beeper uses them
import { CardGrid, LinkCard, Icon } from '@astrojs/starlight/components';
import { Callout } from '@stainless-api/docs/components';
Beeper is built on top of the Matrix protocol. When you use Beeper to message someone on WhatsApp, Telegram, or any other supported network, you're using a Matrix bridge - a piece of software that translates messages between Matrix and the other chat network.
Beeper natively supports 11 networks out of the box with integrated bridges. They either run on your device (when using On-Device Connections) or in our cloud (when using Beeper Cloud).
All of our bridges are open source and we maintain [a library](https://github.com/mautrix/go) to easily build new ones.
Any user can run their own bridge, connected to any network they wish, and integrate it with Beeper seamlessly with everything syncing across all devices.
What is Matrix?
[Matrix](https://matrix.org) is an open standard for secure, decentralized, real-time communication. It provides an open network where users can communicate across different service providers, similar to how email works.
## Community & support
- Join [#self-hosting:beeper.com](https://matrix.to/#/#self-hosting:beeper.com) for self-hosting help
- Email [info@beeper.com](mailto:info@beeper.com) for sponsorship opportunities
# Self-Hosting Bridges
> Run your own bridges with Beeper Bridge Manager
import { Aside, CardGrid, LinkCard, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
import { Callout } from '@stainless-api/docs/components';
Support notice
Self-hosting bridges are not entitled to the usual level of customer support.
For help with self-hosting, join [#self-hosting:beeper.com](https://matrix.to/#/#self-hosting:beeper.com).
## Overview
The easiest way to self-host bridges is using **Beeper Bridge Manager** (`bbctl`). It's a tool for running self-hosted bridges with your Beeper account.
### Key benefits
- **Security** - Run official Beeper bridges on your own hardware securely
- **Flexibility** - Connect custom or third-party bridges without self-hosting a full Matrix server
- **Control** - Maintain complete control over message processing and encryption
Accounts bridged with self-hosting are **free** and do not count against your account limits.
## Prerequisites
Before you begin, ensure you have:
- **Operating System**: Linux or macOS (amd64 or arm64)
Windows users should use WSL
- **Python**: Python 3 with `venv` module for Python-based bridges
- **ffmpeg**: Required for media conversion in some bridges
- **Beeper account**: You'll need an active Beeper account
## Installation
### Step 1: Download bbctl
Download the appropriate binary for your system:
You can check the [GitHub releases](https://github.com/beeper/bridge-manager/releases) for the latest version, checksum(s) and source code.
```bash
git clone https://github.com/beeper/bridge-manager.git
cd bridge-manager
./build.sh
```
### Step 2: Install dependencies
```bash
sudo apt install python3 python3-venv ffmpeg
```
```bash
brew install python ffmpeg
```
### Step 3: Log in to Beeper
```bash
bbctl login
```
## Running official bridges
1. **Start a bridge**
```bash
bbctl run sh-
```
Example: `bbctl run sh-whatsapp`
2. **Configure the bridge**
- Send a DM to the bridge bot: `@sh-bot:beeper.local`
- Follow the bot's instructions to log in to the remote network
3. **Keep it running**
- Currently runs in foreground (use `tmux` or `screen` for persistence)
- Service mode for automatic startup coming soon
Data storage
Bridge data is stored in `~/.local/share/bbctl` by default.
You can change this location in `~/.config/bbctl.json`.
## Supported official bridges
## Running third-party bridges
### Bridge v2-based bridges
For bridges built with mautrix-go's Bridge v2 framework:
1. **Generate config**
```bash
bbctl config --type bridgev2 sh-
```
2. **Add network configuration**
Add the `network` section to the generated config with bridge-specific settings
3. **Run the bridge**
Run the bridge with your config file
### Custom bridges
For other third-party bridges:
1. **Generate registration**
```bash
bbctl register sh-
```
2. **Configure the bridge**
Configure the bridge according to its documentation
3. **Run the proxy**
```bash
bbctl proxy -r registration.yaml
```
This connects your bridge to Beeper
## Managing bridges
### Delete a bridge
This action is permanent and cannot be undone.
```bash
bbctl delete sh-
```
This permanently removes the bridge and all associated data from Beeper servers.
# Deploy to Fly.io
> Deploy Beeper self-hosted bridges to Fly.io from your browser
import DeployToFlyApp from '../../../../components/DeployToFlyApp';
Use this tool to deploy and manage Beeper self-hosted bridges on Fly.io.
# Beeper Desktop API
> Local API & MCP for Beeper Desktop
import {
Card,
CardGrid,
Steps,
} from "@astrojs/starlight/components";
import { Callout, Button } from "@stainless-api/docs/components";
{/* # Overview :badge[Experimental]{variant=note} */}
Beeper Desktop API is a fully local API for all your chats across WhatsApp, Instagram, Telegram, Google Messages, Google Voice, Google Chat, Messenger, Signal, LinkedIn, X, Discord, Slack, and more. It comes with an API and SDKs for JavaScript, Python, Go, and PHP that allow you to search chats, send messages, and control Beeper Desktop.
You can search your message history, send or draft messages, and integrate your desktop client with Beeper Desktop.
**Personal use recommended**
We recommend Beeper Desktop API for personal use only. Sending too many messages might result in account suspension by the networks.
Actions like searching or fetching existing chats or messages are always local and can be used without limitations.
**Limitations**
- Beeper Desktop API runs inside Beeper Desktop and requires Beeper Desktop to be running to be accessible. By default, it can't be accessed from other devices.
- Message history might be limited. Beeper indexes your messages from the networks in the background, when you first add an account, only recent messages might be available. For best results, prefer using On-Device Connections instead of Beeper Cloud when connecting accounts.
- iMessage is only supported on macOS.
## Get started
If you are looking for the MCP to integrate into apps like Claude Desktop or Codex, see the [MCP documentation](/desktop-api/mcp).
The easiest way to start building with the Desktop API directly is to ask your agent to use our SDKs.
```
I want to build an integration with Beeper Desktop API.
Please visit https://developers.beeper.com/desktop-api/index.md
Use the official SDKs to build it.
```
You can also use the [REST API](https://developers.beeper.com/desktop-api-reference/) directly or use the [built-in MCP server](/desktop-api/mcp). Support for more languages coming soon!
## Projects built on Desktop API
- **[f/deeper](https://github.com/f/deeper)** (Swift) - A macOS messaging analytics app for Beeper that connects to the local Beeper Desktop API and visualizes conversations across platforms.
- **[blqke/beepctl](https://github.com/blqke/beepctl)** (TypeScript) - CLI for Beeper Desktop API; unified messaging from your terminal, including AI-agent workflows across platforms.
- **[cameronaaron/beeper-go-sdk](https://github.com/cameronaaron/beeper-go-sdk)** (Rust) - Pure Go SDK for the Beeper Desktop API with a chat-archiver CLI tool.
- **[foeken/beeper-cli](https://github.com/foeken/beeper-cli)** (Go) - CLI for Beeper Desktop API.
- **[mimen/beeper-messaging-tools](https://github.com/mimen/beeper-messaging-tools)** (TypeScript) - A web app for interacting with the Beeper Desktop API.
- **[adamanz/omnichannel-messenger](https://github.com/adamanz/omnichannel-messenger)** (TypeScript) - Multi-platform messaging app using Beeper Desktop API to send messages to WhatsApp, LinkedIn, Slack, and more from one interface.
- **[BosTheCoder/beeper-wsl-proxy](https://github.com/BosTheCoder/beeper-wsl-proxy)** (Python) - Beeper WSL proxy that allows access to the Beeper Desktop API from WSL.
- **[ErdemGKSL/beeper-desktop-api](https://github.com/ErdemGKSL/beeper-desktop-api)** (Rust) - Beeper Desktop API project (no repo description provided).
- **[nerveband/beeper-api-cli](https://github.com/nerveband/beeper-api-cli)** (Go) - Cross-platform CLI for using your Beeper client from the command line via the Beeper Desktop API.
- **[raycast/extensions/extensions/beeper](https://github.com/raycast/extensions/tree/main/extensions/beeper)** (TypeScript) - Raycast extension built using the Desktop API JS SDK.
## Questions and feedback
- Join our community room on Beeper. In Beeper Desktop, go to **Settings** โ **Integrations** and click "**Join Community**". Or join with [Matrix](https://matrix.to/#/#beeper-developers:beeper.com).
- You can always reach out to us on [help@beeper.com](mailto:help@beeper.com) or DM us on [X](https://x.com/beeper) with any questions or feedback.
# Advanced
> Advanced guides for Beeper Desktop API
import { CardGrid, LinkCard } from "@astrojs/starlight/components";
# Remote Access
> Connect Beeper Desktop API to external services like ChatGPT, n8n, Claude.ai, and more.
import { Aside, CardGrid, LinkCard } from "@astrojs/starlight/components";
import { Callout } from "@stainless-api/docs/components";
Understand the risks
Beeper Desktop API is designed to be used locally on your computer. Exposing it to the internet can expose chat history and might allow others to send messages on your behalf. Please only continue if you understand the risks.
Beeper is not responsible for any consequences arising from exposing Beeper Desktop API to the internet.
By default, Beeper Desktop API and MCP server come with limitations for security:
- The server binds to `localhost`, so only the local machine is allowed to connect.
- URLs in the OpenAPI spec and OAuth discovery endpoints use `http://localhost:23373` (or the custom port you set)
- CORS headers also limit access to the local machine
You can enable **Remote Access** in **Settings** โ **Integrations** โ **Advanced** to expose the API to the internet. This will change the following:
- The server binds to `0.0.0.0`, listening on every network interface, including VPNs and tunnel adapters.
- Base URL is computed from `X-Forwarded-Host`, `X-Forwarded-Proto`, and forwarded port headers when traffic arrives via a proxy or tunnel. If those headers are missing, it falls back to the direct host that reached your machine.
- OAuth discovery endpoints (`/.well-known/...`) allow cross-origin `GET` requests so they can be accessed and used by external services.
This allows you to tunnel/proxy traffic to your local machine and make sure everything works as expected. For example, this is required for PKCE authentication to work properly with external services like ChatGPT, n8n, Claude.ai, and more.
## Tunneling
Beeper does not provide any tunneling services. The easiest way to expose the API to the internet is to use a third-party tunneling service like Cloudflare or Tailscale.
# Cloudflare Quick Tunnels
> Use Cloudflare's TryCloudflare tunnels to share the Beeper Desktop API or MCP server over HTTPS in a few commands.
import { Steps, Tabs, TabItem, Code, LinkButton } from "@astrojs/starlight/components";
import { Callout, Button } from "@stainless-api/docs/components";
Understand the risks
Beeper Desktop API is designed to be used locally on your computer. Exposing it to the internet can expose chat history and might allow others to send messages on your behalf. Please only continue if you understand the risks.
Beeper is not responsible for any consequences arising from exposing Beeper Desktop API to the internet.
Also note that Beeper has no affiliation with Cloudflare and this guide is for educational purposes only.
Cloudflare's Quick Tunnels spin up an HTTPS endpoint that forwards traffic straight to your local Beeper Desktop API. The tunnel lives only as long as the `cloudflared` process runs, making it ideal for testing and personal usage.
Before you begin, make sure **Remote Access** is enabled in **Settings** โ **Integrations** โ **Advanced** so the server listens on all interfaces.
Cloudflare Quick Tunnels do not support Server-Sent Events (SSE) (`/v0/sse`), so if you are using the MCP server you must use the Streamable HTTP transport (`/v0/mcp`).
## Install `cloudflared`
Install via Homebrew:
Use Windows Package Manager:
Download the latest release and install it manually:
Make sure to check Cloudflare's [Downloads page](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) for the most up-to-date instructions.
## Launch a quick tunnel
1. **Start the tunnel**
2. **Copy the forwarding URL**
`cloudflared` prints a line similar to `https://example.trycloudflare.com`. Append `/v0/mcp` for the MCP server.
3. **Authenticate from your remote tool**
Use the standard OAuth or token flow. Cloudflare preserves forwarded headers, so the Desktop API generates redirect URLs based on the public hostname automatically.
### Keep the tunnel online
The URL is only valid while the `cloudflared` process runs. Use a session manager such as `screen`, `tmux`, or a background service if you need to keep it available. Press Ctrl + C to close the tunnel immediately.
## Harden for stable deployments
Cloudflare Quick Tunnels has request limits and no authentication. For persistent deployments on Cloudflare, you can use Cloudflare Zero Trust:
Zero Trust tunnels let you pin a hostname, enforce identity checks, and restrict IP rangesโall without changing your local Beeper Desktop configuration.
# Authentication
> Authentication methods for Beeper Desktop API
import { Aside, Steps, Tabs, TabItem, Badge } from '@astrojs/starlight/components';
Beeper Desktop API requires an access token for all endpoints.
## Create a token
1. **[Make sure Beeper Desktop API is enabled and running](/desktop-api#get-started)**
2. **Open Settings** โ **Integrations** in Beeper Desktop
3. **Click the "+" button** next to "Approved connections"
4. **Follow the instructions to create your token**
If you want to allow your users to interactively allow your application to access their Beeper Desktop, you can use OAuth 2.0 with [PKCE](https://oauth.net/2/pkce/) for authentication.
[RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) compliant metadata is available at `http://localhost:23373/.well-known/oauth-authorization-server`
Built-in MCP server uses OAuth natively for authentication. See the [authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) page of the MCP specification for more details.
[Get in touch with us](mailto:help@beeper.com) for help, questions, or feedback.
## Use your token
All requests to Beeper Desktop API, including requests to the MCP endpoints (`/v0/mcp`, `/v0/sse`), support the `Authorization` header with the `Bearer` token for authentication.
When used with the MCP server, MCP authentication is bypassed and the token is used directly.
```
Authorization: Bearer
```
## Desktop API discovery and introspection
- `GET /v1/info` returns metadata about the active Desktop API server and exposes endpoint URLs for discovery.
OAuth token introspection is available via `POST /oauth/introspect` and accepts form-encoded payloads:
```text
Content-Type: application/x-www-form-urlencoded
token=&token_type_hint=access_token
```
The endpoint returns `active: true` with token metadata (when active), or `active: false` for inactive/invalid tokens.
# Beeper Desktop MCP
> Connect Beeper Desktop with AI assistants via Model Context Protocol
import {
Code,
Aside,
Steps,
LinkButton,
Tabs,
TabItem,
} from "@astrojs/starlight/components";
import DetailsHashHandler from "../../../components/DetailsHashHandler.astro";
import NodeInstallInstructions from "../../../components/NodeInstallInstructions.astro";
import { CardGrid, LinkCard } from "@astrojs/starlight/components";
import { generateCursorDeeplink, generateVSCodeDeeplink, generateRaycastDeeplink } from "../../../components/mcp-deeplink-helper";
import { Callout, Button } from "@stainless-api/docs/components";
Beeper Desktop API includes a built-in MCP (Model Context Protocol) server, enabling seamless integration with AI assistants like Claude Desktop, Claude Code, Codex, Cursor, and more.
What is Model Context Protocol?
MCP is an open protocol that standardizes how applications provide context to large language models (LLMs). Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools. MCP enables you to build agents and complex workflows on top of LLMs and connects your models with the world.
**Beeper MCP runs on your device and requires Beeper Desktop to be running.**
## Quick start
1. **[Install Beeper Desktop](https://www.beeper.com/download)** and make sure it's running.
2. **Connect your MCP client**
Additional options like Claude Code, Codex, Streamable HTTP, and stdio are available in **Settings** โ **Integrations**.
## Manual setup
As long as your MCP client supports OAuth and Streamable HTTP, you just need to add the MCP server URL to your client and the authentication will be handled automatically.
Manually setting the token
If you are having issues with authentication, or want to skip the authentication process, you can create an access token manually. See the
Authentication Guide for details on how to obtain tokens.
When used with the MCP server, MCP authentication is bypassed and the token is used directly.
Streamable HTTP with custom token
stdio via mcp-remote with custom token
### Client-specific instructions
Cursor
If you are running on different port, you can setup manually:
**Configuration file:** `~/.cursor/mcp.json`
**Streamable HTTP**
Claude Code
**Install globally:**
**Install within a directory:**
**With manual token:**
Read more on Claude Code's official documentation.
Visual Studio Code
You can use this link to install globally in VS Code.
Workspace or user configuration (mcp.json)
Create .vscode/mcp.json in your workspace, or open MCP: Open User Configuration to edit your profile mcp.json:
Command line (code --add-mcp)
Read more on Visual Studio Code's official documentation.
Codex
**Add via CLI:**
**With manual token:**
Read more on Codex's official documentation.
stdio
Prerequisite
Using Beeper MCP server with stdio requires `node` to be installed on your system. See instructions below. **We are working on lifting this restriction.**
If your MCP client supports Streamable HTTP, prefer those instead of stdio.
# WebSocket (Experimental)
> Experimental live event stream for Beeper Desktop API
import { Callout } from "@stainless-api/docs/components";
This WebSocket interface is experimental and may change between desktop releases. We would love to hear your feedback on [X](https://x.com/beeper) or [help@beeper.com](mailto:help@beeper.com).
Beeper Desktop API exposes a live WebSocket endpoint at:
```text
ws://localhost:23373/v1/ws
```
Use the same token as HTTP requests:
```text
Authorization: Bearer
```
## Subscription model
Use a single command, `subscriptions.set`, to fully replace current subscriptions.
- `chatIDs: ["*"]` subscribes to all chats.
- `chatIDs: ["!tdvY9_XjNZ6F5P8DzBxDiqBwla0:ba_aBcD1234EfGhIjKlMnOpQrStUvWxYz.local-ai.localhost", "!tdvY9_XjNZ6F5P8DzBxDiqBwla0:ba_ZxYwVuTsRqPoNmLkJiHgFeDcBa9876.local-whatsapp.localhost"]` subscribes to specific chats.
- `chatIDs: []` pauses events.
- `"*"` cannot be mixed with specific chat IDs.
### Set all chats
```json
{
"type": "subscriptions.set",
"requestID": "r1",
"chatIDs": ["*"]
}
```
### Set specific chats
```json
{
"type": "subscriptions.set",
"requestID": "r2",
"chatIDs": ["!tdvY9_XjNZ6F5P8DzBxDiqBwla0:ba_aBcD1234EfGhIjKlMnOpQrStUvWxYz.local-ai.localhost", "!tdvY9_XjNZ6F5P8DzBxDiqBwla0:ba_ZxYwVuTsRqPoNmLkJiHgFeDcBa9876.local-whatsapp.localhost"]
}
```
## Control messages
### `ready`
```json
{
"type": "ready",
"version": 1,
"chatIDs": []
}
```
### `subscriptions.updated`
```json
{
"type": "subscriptions.updated",
"requestID": "r2",
"chatIDs": ["!tdvY9_XjNZ6F5P8DzBxDiqBwla0:ba_aBcD1234EfGhIjKlMnOpQrStUvWxYz.local-ai.localhost", "!tdvY9_XjNZ6F5P8DzBxDiqBwla0:ba_ZxYwVuTsRqPoNmLkJiHgFeDcBa9876.local-whatsapp.localhost"]
}
```
### `error`
```json
{
"type": "error",
"requestID": "r2",
"code": "INVALID_PAYLOAD",
"message": "chatIDs cannot combine '*' with specific IDs"
}
```
## Domain events
All live domain events use the same flat shape:
```json
{
"type": "message.upserted",
"seq": 42,
"ts": 1739320000000,
"chatID": "chat_a",
"ids": ["m1", "m2"],
"entries": [{ "id": "m1", "reactions": [] }],
}
```
Field meanings:
- `type`: domain event name.
- `seq`: monotonic per-connection sequence number.
- `ts`: server timestamp in milliseconds.
- `chatID`: canonical chat identifier.
- `ids`: changed entity IDs.
- `entries`: optional best-effort full payload objects for `message.upserted` events.
Mutation types are normalized so both `upsert` and `update` sync mutations are delivered as `.upserted` events. Deletion mutations are delivered as `.deleted`.
`message.upserted` events are hydrated from the local message endpoint before emission. This means:
- `entries` will usually include a full message payload (including attachments when available).
- If the message is not yet retrievable, the event is skipped.
- `message.deleted` and `chat.deleted` are emitted as IDs-only payloads (no `entries`).
## Event names
Current event names:
- `chat.upserted`
- `chat.deleted`
- `message.upserted`
- `message.deleted`
# Open Source at Beeper
> Beeper is built on open source. Explore our open source projects, from bridges and bots to libraries and tools.
import { Card, LinkCard, CardGrid } from "@astrojs/starlight/components";
## Bridges
### Official bridges
These bridges are the same bridges we run with On-Device Connections and also on Beeper Cloud.
ยน Not available in the app.
### Community-maintained bridges
These are bridges that are maintained by the community and sponsored by Beeper. They are only available for self-hosting.
### Tools
## Example projects
{/* */}
## Libraries
{/* ## Matrix Bots
*/}
{/* ## Forks
*/}
## Deprecated Texts.com integrations
JavaScript integrations for chat networks built for the legacy Texts.com Desktop app. These are no longer maintained.
This page represents a selection of our open source work. To see more you can, visit our GitHub organizations: [beeper](https://github.com/beeper) and [mautrix](https://github.com/mautrix). Beeper is part of [Automattic](https://github.com/automattic).