Self-hosted iMessage bridge with REST API & Webhooks. Send and receive iMessages programmatically from any device.
- macOS 10.15+ (Catalina or later)
- Apple ID signed into iMessage on the Mac (open Messages.app and verify you can send/receive)
- Homebrew installed (https://brew.sh)
brew install --cask bluebubblesThen open the app:
open /Applications/BlueBubbles.appNote: The app is unsigned (Apple disabled the dev account). If macOS blocks it, right-click the app in Finder → Open → confirm. Do NOT open it from the Dock download area.
When the app launches, follow the setup wizard:
Go to System Settings → Privacy & Security and grant BlueBubbles:
- Full Disk Access — REQUIRED (reads the iMessage database at
~/Library/Messages/chat.db) - Accessibility — Optional but recommended (enables Private API features)
Firebase is needed for push notifications to mobile clients. If you only need the API, you can skip this — but it's recommended to set up.
Option A — Auto Setup (Recommended):
- Click "Connect" and sign in with a Google account
- BlueBubbles will auto-provision a Firebase project
Option B — Manual Setup:
- Go to Firebase Console
- Create a new project → Add an app → Download
google_services.json - Go to Project Settings → Service Accounts → Generate
firebase-adminsdk.json - Upload both files in the Manual Setup tab
- Set a server password — this becomes your API key for all requests
- Click the save (floppy disk) icon to persist it
- Select a proxy service:
- Cloudflare (default, recommended) — free, auto-generates a public HTTPS URL
- zrok — use if you need to transfer files >100MB (Cloudflare has a 100MB limit)
- Dynamic DNS / Manual Port Forwarding — for a stable URL you control
- Set the local port — default is
1234, change if needed
- Click Start — the server will generate a public URL like:
https://favourite-superintendent-inclusive-alberta.trycloudflare.com - Important: With Cloudflare free tier, this URL changes every time the server restarts. Note it down or use zrok/DDNS for a stable URL.
# Set these for all examples below
export SERVER="https://your-server-url.trycloudflare.com"
export PASSWORD="your-password"
# Ping test
curl -s "$SERVER/api/v1/ping?password=$PASSWORD"Expected response:
{"status": 200, "message": "Ping received!", "data": "pong"}If you get this, the server is live and ready.
Base URL: $SERVER/api/v1
Authentication: Every request needs password=YOUR_PASSWORD as a query parameter.
Response format: All responses return:
{
"status": 200,
"message": "Success",
"data": { ... }
}curl -s "$SERVER/api/v1/message/text?password=$PASSWORD" \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{
"chatGuid": "any;-;+1234567890",
"tempGuid": "temp-'"$(uuidgen)"'",
"message": "Hello from the BlueBubbles API!"
}'Required fields:
| Field | Description | Example |
|---|---|---|
chatGuid |
Chat identifier (see format below) | any;-;+1234567890 |
tempGuid |
Unique ID for deduplication | temp- + any UUID |
message |
The message text | Hello! |
chatGuid format:
- SMS/MMS to phone:
any;-;+<phone_number>(e.g.,any;-;+923710639259) - iMessage to email:
any;-;<email@icloud.com>(e.g.,any;-;user@icloud.com) - iMessage to phone:
any;-;+<phone_number>(same format, auto-routes via iMessage if available) - Group chat:
any;+;chat<id>(get the exact GUID from the chat query endpoint)
Success response:
{
"status": 200,
"message": "Message sent!",
"data": {
"guid": "8349E621-252B-4079-9A37-24238EDF8BDF",
"text": "Hello from the BlueBubbles API!",
"isFromMe": true,
"dateCreated": 1772642539012,
"handle": {
"address": "+1234567890",
"service": "SMS"
}
}
}curl -s "$SERVER/api/v1/chat/query?password=$PASSWORD" \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{"limit": 10}'With filters:
{
"limit": 10,
"offset": 0,
"with": ["lastMessage", "sms", "archived"]
}Each chat returns a guid (the chatGuid you need for sending), participants, and chatIdentifier.
curl -s "$SERVER/api/v1/chat/any;-;+1234567890/message?password=$PASSWORD&limit=25&offset=0&sort=DESC"curl -s "$SERVER/api/v1/message?password=$PASSWORD&limit=10&offset=0&sort=DESC"curl -s "$SERVER/api/v1/contact?password=$PASSWORD" \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{}'Note: This may return empty if iCloud contact sync isn't enabled on the Mac. Use
POST /api/v1/chat/queryto discover contacts from your conversation history instead.
curl -s "$SERVER/api/v1/message/attachment?password=$PASSWORD" \
-X POST \
-F 'chatGuid=any;-;+1234567890' \
-F 'tempGuid=temp-'"$(uuidgen)"'' \
-F 'message=Check out this file!' \
-F 'attachment=@/path/to/file.png'curl -s "$SERVER/api/v1/server?password=$PASSWORD"Webhooks are the primary way to receive messages in real-time. BlueBubbles will POST to your URL whenever an event occurs (new message, read receipt, typing, etc.).
Someone sends you an iMessage/SMS
↓
macOS Messages.app receives it
↓
BlueBubbles detects it (watches chat.db)
↓
BlueBubbles POSTs to your webhook URL(s)
↓
Your server/n8n/script processes the message
- Open BlueBubbles Server app
- Click API & Webhooks in the sidebar
- Under Manage, click Add Webhook
- Enter your receiver URL (must be HTTPS and publicly accessible)
- Select the events you want:
| Event | Description |
|---|---|
new-message |
A new message was received or sent |
updated-message |
Message status changed (delivered, read) |
message-send-error |
A message failed to send |
typing-indicator |
Someone started/stopped typing |
group-name-change |
A group chat was renamed |
participant-added |
Someone was added to a group |
participant-removed |
Someone was removed from a group |
participant-left |
Someone left a group |
chat-read-status-changed |
Chat was marked read/unread |
- Click Save
curl -s "$SERVER/api/v1/webhook?password=$PASSWORD" \
-X POST \
-H 'Content-Type: application/json' \
--data-raw '{
"url": "https://your-server.com/webhook/imessage",
"events": [
"new-message",
"updated-message",
"typing-indicator"
]
}'Response:
{
"status": 200,
"message": "Success",
"data": {
"id": 1,
"url": "https://your-server.com/webhook/imessage",
"events": ["new-message", "updated-message", "typing-indicator"]
}
}curl -s "$SERVER/api/v1/webhook?password=$PASSWORD"curl -s "$SERVER/api/v1/webhook/1?password=$PASSWORD" -X DELETE(Replace 1 with the webhook ID from the list.)
When a new message arrives, BlueBubbles sends a POST request to your webhook URL:
Headers:
Content-Type: application/json
Body (new-message event):
{
"type": "new-message",
"data": {
"guid": "ABC123-DEF456-...",
"text": "Hey, what's up?",
"isFromMe": false,
"dateCreated": 1772642539012,
"subject": null,
"handle": {
"originalROWID": 123,
"address": "+1234567890",
"service": "iMessage",
"country": "US"
},
"attachments": [],
"chats": [
{
"chatIdentifier": "+1234567890",
"guid": "any;-;+1234567890",
"displayName": "",
"participants": [
{
"address": "+1234567890",
"service": "iMessage"
}
]
}
],
"associatedMessageGuid": null,
"associatedMessageType": null,
"balloonBundleId": null,
"expressiveSendStyleId": null,
"isAudioMessage": false,
"hasPayloadData": false
}
}Key fields to extract:
data.text— the message contentdata.handle.address— who sent it (phone or email)data.isFromMe—trueif you sent it,falseif incomingdata.chats[0].guid— the chatGuid (use this to reply)data.attachments— array of attached files (images, videos, etc.)
A minimal Flask server that listens for incoming iMessages and auto-replies:
pip install flask requestsfrom flask import Flask, request, jsonify
import requests
import uuid
app = Flask(__name__)
SERVER = "https://your-server-url.trycloudflare.com"
PASSWORD = "your-password"
@app.route("/webhook/imessage", methods=["POST"])
def handle_webhook():
payload = request.json
event_type = payload.get("type")
data = payload.get("data", {})
if event_type == "new-message":
text = data.get("text", "")
is_from_me = data.get("isFromMe", True)
sender = data.get("handle", {}).get("address", "unknown")
chat_guid = data["chats"][0]["guid"] if data.get("chats") else None
print(f"[{event_type}] From: {sender} | Message: {text}")
# Only reply to messages from others (not your own)
if not is_from_me and chat_guid:
send_reply(chat_guid, f"Thanks for your message! You said: {text}")
elif event_type == "typing-indicator":
display = data.get("display", False)
print(f"[typing] {'Started' if display else 'Stopped'} typing")
return jsonify({"status": "ok"}), 200
def send_reply(chat_guid, message):
"""Send a reply back through BlueBubbles."""
resp = requests.post(
f"{SERVER}/api/v1/message/text",
params={"password": PASSWORD},
json={
"chatGuid": chat_guid,
"tempGuid": f"temp-{uuid.uuid4()}",
"message": message,
},
)
print(f"[reply] Status: {resp.json().get('status')} | To: {chat_guid}")
if __name__ == "__main__":
# Run on port 5000 — expose via ngrok, Cloudflare Tunnel, etc.
app.run(host="0.0.0.0", port=5000)To expose this publicly (so BlueBubbles can reach it):
# Option 1: ngrok
ngrok http 5000
# Then use the ngrok URL as your webhook: https://abc123.ngrok.io/webhook/imessage
# Option 2: Cloudflare Tunnel
cloudflared tunnel --url http://localhost:5000npm init -y && npm install expressconst express = require("express");
const crypto = require("crypto");
const app = express();
const SERVER = "https://your-server-url.trycloudflare.com";
const PASSWORD = "your-password";
app.use(express.json());
app.post("/webhook/imessage", async (req, res) => {
const { type, data } = req.body;
if (type === "new-message") {
const text = data.text || "";
const isFromMe = data.isFromMe;
const sender = data.handle?.address || "unknown";
const chatGuid = data.chats?.[0]?.guid;
console.log(`[${type}] From: ${sender} | Message: ${text}`);
// Auto-reply to incoming messages
if (!isFromMe && chatGuid) {
await sendReply(chatGuid, `Got your message: "${text}"`);
}
}
res.json({ status: "ok" });
});
async function sendReply(chatGuid, message) {
const res = await fetch(
`${SERVER}/api/v1/message/text?password=${PASSWORD}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chatGuid,
tempGuid: `temp-${crypto.randomUUID()}`,
message,
}),
}
);
const json = await res.json();
console.log(`[reply] Status: ${json.status} | To: ${chatGuid}`);
}
app.listen(5000, () => console.log("Webhook listener running on :5000"));- Create a Webhook node → set path to
/webhook/imessage→ copy the URL - Register that URL as a webhook in BlueBubbles (UI or API)
- Add an IF node → condition:
{{ $json.data.isFromMe }}equalsfalse - Add an HTTP Request node:
- Method:
POST - URL:
https://your-server-url.trycloudflare.com/api/v1/message/text?password=your-password - Body (JSON):
{ "chatGuid": "{{ $json.data.chats[0].guid }}", "tempGuid": "temp-{{ $now.toMillis() }}", "message": "Auto-reply: Got your message!" }
- Method:
If you just want to send messages and poll for new ones without setting up a webhook:
import requests
import uuid
import time
SERVER = "https://your-server-url.trycloudflare.com"
PASSWORD = "your-password"
def send_message(recipient, message):
"""Send a message. recipient = phone number or iCloud email."""
resp = requests.post(
f"{SERVER}/api/v1/message/text",
params={"password": PASSWORD},
json={
"chatGuid": f"any;-;{recipient}",
"tempGuid": f"temp-{uuid.uuid4()}",
"message": message,
},
)
result = resp.json()
print(f"[send] {result['status']}: {result['message']}")
return result
def get_recent_messages(limit=10):
"""Fetch most recent messages across all chats."""
resp = requests.get(
f"{SERVER}/api/v1/message",
params={"password": PASSWORD, "limit": limit, "sort": "DESC"},
)
return resp.json()
def get_chats(limit=20):
"""List recent conversations."""
resp = requests.post(
f"{SERVER}/api/v1/chat/query",
params={"password": PASSWORD},
json={"limit": limit},
)
return resp.json()
def poll_new_messages(interval=5):
"""Poll for new messages every N seconds."""
last_timestamp = int(time.time() * 1000)
print(f"Polling for new messages every {interval}s... (Ctrl+C to stop)")
while True:
resp = requests.get(
f"{SERVER}/api/v1/message",
params={
"password": PASSWORD,
"limit": 10,
"sort": "DESC",
"after": last_timestamp,
},
)
messages = resp.json().get("data", [])
for msg in messages:
if not msg.get("isFromMe"):
sender = msg.get("handle", {}).get("address", "unknown")
text = msg.get("text", "")
print(f" New message from {sender}: {text}")
ts = msg.get("dateCreated", 0)
if ts > last_timestamp:
last_timestamp = ts
time.sleep(interval)
# Example usage:
# send_message("+1234567890", "Hello!")
# poll_new_messages(interval=5)| Issue | Solution |
|---|---|
| Cloudflare URL changes on restart | Use zrok or Dynamic DNS for a stable URL, or re-read the URL from the BlueBubbles UI after each restart |
| Contacts endpoint returns empty | Use POST /api/v1/chat/query to discover contacts from conversation history |
| "tempGuid is required" error | Always include a unique tempGuid field when sending messages (any UUID works) |
| Server password = API key | Set in BlueBubbles → Settings → Connection → Password |
| Mac must stay awake | Disable sleep or run caffeinate -s & in Terminal |
| iMessage not working | Open Messages.app on the Mac, ensure you can send/receive normally first |
| App blocked by Gatekeeper | Right-click → Open in Finder (don't double-click or open from Dock) |
| Large file transfers fail on Cloudflare | Switch proxy to zrok (Cloudflare has 100MB limit) |
| Webhook not receiving events | Ensure your webhook URL is HTTPS and publicly reachable from the Mac |
| Messages showing as SMS instead of iMessage | The recipient may not have iMessage enabled, or Apple's servers can't verify their number |
# Run in Terminal (keeps Mac awake while on power)
caffeinate -s &Or: System Settings → Energy → Prevent automatic sleeping when the display is off
- System Settings → General → Login Items
- Click + → select BlueBubbles from Applications
- Now it starts automatically on boot/login
osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/BlueBubbles.app", hidden:true}'| Endpoint | Method | Description |
|---|---|---|
/api/v1/ping |
GET | Health check |
/api/v1/server |
GET | Server info |
/api/v1/message/text |
POST | Send a text message |
/api/v1/message/attachment |
POST | Send a file/image |
/api/v1/message |
GET | Get recent messages (all chats) |
/api/v1/chat/query |
POST | List/search conversations |
/api/v1/chat/:guid/message |
GET | Get messages from a specific chat |
/api/v1/contact |
POST | Get contacts |
/api/v1/webhook |
GET | List registered webhooks |
/api/v1/webhook |
POST | Register a new webhook |
/api/v1/webhook/:id |
DELETE | Remove a webhook |
All endpoints require ?password=YOUR_PASSWORD as a query parameter.
- BlueBubbles Docs: https://docs.bluebubbles.app
- API & Webhooks Guide: https://docs.bluebubbles.app/server/developer-guides/rest-api-and-webhooks
- Webhook Server Guide: https://docs.bluebubbles.app/server/developer-guides/simple-web-server-for-webhooks
- GitHub Repo: https://github.com/BlueBubblesApp/bluebubbles-server
- Private API Setup: https://docs.bluebubbles.app/private-api/installation
- Discord Community: https://discord.gg/bluebubbles