Skip to content

Instantly share code, notes, and snippets.

@hmseeb
Last active March 8, 2026 21:36
Show Gist options
  • Select an option

  • Save hmseeb/e313cd954ad893b75433f2f2db0fb704 to your computer and use it in GitHub Desktop.

Select an option

Save hmseeb/e313cd954ad893b75433f2f2db0fb704 to your computer and use it in GitHub Desktop.
BlueBubbles Server — Complete Setup & API Guide (macOS iMessage bridge)

BlueBubbles Server — Complete Setup & API Guide

Self-hosted iMessage bridge with REST API & Webhooks. Send and receive iMessages programmatically from any device.


Requirements

  • 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)

1. Install BlueBubbles Server

brew install --cask bluebubbles

Then open the app:

open /Applications/BlueBubbles.app

Note: 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.


2. Initial Server Setup

When the app launches, follow the setup wizard:

Step 1: Permissions

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)

Step 2: Notifications (Firebase)

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

Step 3: Connection Settings

  1. Set a server password — this becomes your API key for all requests
  2. Click the save (floppy disk) icon to persist it
  3. 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
  4. Set the local port — default is 1234, change if needed

Step 4: Start the Server

  • 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.

3. Verify the Server

# 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.


4. REST API Reference

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": { ... }
}

4.1 Send a Text Message

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"
    }
  }
}

4.2 Query Chats (List Conversations)

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.


4.3 Get Messages from a Specific Chat

curl -s "$SERVER/api/v1/chat/any;-;+1234567890/message?password=$PASSWORD&limit=25&offset=0&sort=DESC"

4.4 Get All Recent Messages (Across All Chats)

curl -s "$SERVER/api/v1/message?password=$PASSWORD&limit=10&offset=0&sort=DESC"

4.5 Get Contacts

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/query to discover contacts from your conversation history instead.


4.6 Send an Attachment

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'

4.7 Server Info

curl -s "$SERVER/api/v1/server?password=$PASSWORD"

5. Webhooks — Actively Listen for Incoming Messages

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.).

5.1 How It Works

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

5.2 Register a Webhook via the UI

  1. Open BlueBubbles Server app
  2. Click API & Webhooks in the sidebar
  3. Under Manage, click Add Webhook
  4. Enter your receiver URL (must be HTTPS and publicly accessible)
  5. 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
  1. Click Save

5.3 Register a Webhook via API

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"]
  }
}

5.4 List Registered Webhooks

curl -s "$SERVER/api/v1/webhook?password=$PASSWORD"

5.5 Delete a Webhook

curl -s "$SERVER/api/v1/webhook/1?password=$PASSWORD" -X DELETE

(Replace 1 with the webhook ID from the list.)

5.6 Webhook Payload Format

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 content
  • data.handle.address — who sent it (phone or email)
  • data.isFromMetrue if you sent it, false if incoming
  • data.chats[0].guid — the chatGuid (use this to reply)
  • data.attachments — array of attached files (images, videos, etc.)

6. Webhook Receiver Examples

6.1 Python — Simple Webhook Listener

A minimal Flask server that listens for incoming iMessages and auto-replies:

pip install flask requests
from 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:5000

6.2 Node.js — Simple Webhook Listener

npm init -y && npm install express
const 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"));

6.3 n8n Workflow

  1. Create a Webhook node → set path to /webhook/imessage → copy the URL
  2. Register that URL as a webhook in BlueBubbles (UI or API)
  3. Add an IF node → condition: {{ $json.data.isFromMe }} equals false
  4. 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!"
      }

7. Standalone Python Client (No Webhook Server Needed)

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)

8. Tips & Gotchas

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

9. Keep the Mac Awake & Auto-Start

Prevent Sleep

# Run in Terminal (keeps Mac awake while on power)
caffeinate -s &

Or: System Settings → Energy → Prevent automatic sleeping when the display is off

Auto-Start BlueBubbles on Login

  1. System Settings → General → Login Items
  2. Click + → select BlueBubbles from Applications
  3. Now it starts automatically on boot/login

Auto-Start via Command Line

osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/BlueBubbles.app", hidden:true}'

10. API Endpoints Quick Reference

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.


11. Useful Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment