Package Information
Available Nodes
Documentation generating…
Documentation
n8n-nodes-bluebubbles
n8n community nodes for BlueBubbles, the self-hosted server that exposes macOS iMessage over HTTP. Use it to send and manage iMessages (messages, chats, attachments, contacts, scheduled messages) from n8n workflows, and to start workflows from incoming iMessages and other BlueBubbles server events.
n8n is a fair-code licensed workflow automation platform.
- Included nodes
- Credential setup
- BlueBubbles node
- BlueBubbles Trigger
- Networking
- Install from npm (recommended once published)
- Installing in self-hosted n8n (Docker)
- Development
- Contributing
- Compatibility
- License
Included nodes
| Name | Type | Purpose |
|---|---|---|
| BlueBubbles API | Credential | Server URL and password for a BlueBubbles server |
| BlueBubbles | Action node | Sends messages and manages chats, attachments, contacts, handles, scheduled messages and the server |
| BlueBubbles Trigger | Trigger node | Starts a workflow when BlueBubbles sends a webhook event |
Credential setup
Create a BlueBubbles API credential with:
| Field | Description |
|---|---|
| Server URL | Full base URL of your BlueBubbles server, including https:// and any path prefix, without /api/v1. Example: https://bluebubbles.example.com or http://192.168.1.50:1234. |
| Password | The BlueBubbles server password (as set in the BlueBubbles server app). |
The credential test calls GET /api/v1/ping.
BlueBubbles node
The BlueBubbles action node calls the BlueBubbles REST API (/api/v1) with the BlueBubbles API credential. It can be used as an AI agent tool.
Resources and operations
Operations marked PA need the Private API.
| Resource | Operation | API call | Notes |
|---|---|---|---|
| Message | Send | POST /message/text |
Send text to an existing chat. Options: Send Method, Subject (PA), Effect ID (PA), Reply to Message GUID (PA). |
| Message | Send to Address | POST /message/text, or POST /chat/new for SMS |
Send to a phone number or email. Service: iMessage or SMS (SMS needs PA). |
| Message | Get | GET /message/:guid |
Includes chats and attachments. |
| Message | Get Many | GET /chat/:guid/message |
Messages in one chat. Filters: Created After/Before, From Address, Sort. |
| Message | Search | POST /message/query |
Case-insensitive text search, optionally in one chat. Filters: Created After/Before, From Address. |
| Message | React (PA) | POST /message/react |
Add or remove a tapback (love, like, dislike, laugh, emphasize, question). |
| Message | Edit (PA) | POST /message/:guid/edit |
Edit a message you sent. |
| Message | Unsend (PA) | POST /message/:guid/unsend |
Retract a message you sent. |
| Chat | Get | GET /chat/:guid |
With participants and last message. |
| Chat | Get Many | POST /chat/query |
Most recent first. Options: Unread Only, Include Recent Messages. |
| Chat | Create (PA) | POST /chat/new |
Start a chat with one or more addresses and a first message. |
| Chat | Mark as Read / Mark as Unread (PA) | POST /chat/:guid/read, /unread |
Mark as Unread needs macOS 13+. |
| Chat | Start Typing / Stop Typing (PA) | POST / DELETE /chat/:guid/typing |
|
| Chat | Rename Group (PA) | PUT /chat/:guid |
|
| Chat | Add Participant / Remove Participant (PA) | POST /chat/:guid/participant/add, /remove |
|
| Chat | Leave (PA) | POST /chat/:guid/leave |
|
| Chat | Delete (PA) | DELETE /chat/:guid |
|
| Attachment | Get | GET /attachment/:guid |
Metadata only. |
| Attachment | Download | GET /attachment/:guid/download?original=true |
Writes the file to a binary field (default data). |
| Attachment | Send | POST /message/attachment (multipart) |
Sends a file from a binary field. |
| Contact | Get Many | GET /contact |
Optional case-insensitive search on name, phone numbers and emails. |
| Contact | Lookup by Address | POST /contact/query |
Addresses without a matching contact are left out. |
| Handle | Check iMessage Availability (PA) | GET /handle/availability/imessage |
Returns { available }. |
| Handle | Check FaceTime Availability (PA) | GET /handle/availability/facetime |
Returns { available }. |
| Scheduled Message | Create | POST /message/schedule |
Sends once at the given time. |
| Scheduled Message | Get Many | GET /message/schedule |
Each record has a status: pending, in-progress, complete or error (reason in error). |
| Scheduled Message | Delete | DELETE /message/schedule/:id |
|
| Server | Ping | GET /ping |
|
| Server | Get Info | GET /server/info |
Server and macOS versions, Private API status. |
| Server | Get My Addresses | GET /server/info, then GET /icloud/account |
Phone numbers and emails of the signed-in Apple account, primary first. The iCloud lookup is only tried with the Private API, and is best-effort. |
Choosing a chat
Operations that target a chat use a Chat picker with three modes:
- From List: searchable list of recent chats, shown by display name or participants.
- By GUID: a chat GUID such as
iMessage;+;chat123456. A leadingiMessage;-;is rewritten toany;-;so the server picks the service. - By Address: a phone number or email; targets the one-to-one chat
any;-;<address>.
Private API
Operations marked PA (and the Subject, Effect ID and Reply options) use the BlueBubbles Private API. It requires:
- Private API enabled in BlueBubbles Server.
- System Integrity Protection (SIP) disabled on the Mac.
- The BlueBubbles Private API helper connected (check with Server > Get Info:
private_apiandhelper_connected).
The node does not hide these operations when the Private API is off. The server rejects the request, and the error hint says to enable the Private API (SIP off, helper connected).
Send method
Text, attachment, chat-create and scheduled sends have a Send Method option: Auto (default), Private API or AppleScript.
- Auto calls
GET /server/infoonce per execution (cached for all items in that run) and uses the Private API only whenprivate_apiis on and the helper is connected; otherwise AppleScript. - Reply to Message GUID needs the Private API; the item fails if the method resolves to AppleScript.
- Send to Address with Service: SMS always needs the Private API and fails otherwise. It creates the chat with
POST /chat/new, so its output is a chat object (chatguid), not a message: there is no message GUID andtextisnull. - Send to Address with Service: iMessage sends to the chat
any;-;<address>and lets the server choose the service.
Scheduled messages
The send method of a scheduled message is resolved and frozen when it is scheduled. A message scheduled with the Private API fails when it fires if the helper is disconnected at that time, with no fallback to AppleScript and no retry. Scheduling is not delivery: check Scheduled Message > Get Many for records with status error afterwards.
Simplify
Message > Get / Get Many / Search and Chat > Get Many (for Include Recent Messages) have a Simplify toggle, on by default. Simplified messages keep only:
guid, text, handle (address, service), isFromMe, dateCreated, attachments, replyToGuid, associatedMessageGuid, associatedMessageType, dateEdited, dateRetracted, isAudioMessage, error, chats (guid, displayName, isArchived).
Turn it off to get the raw BlueBubbles objects.
Lists and pagination
Get Many and Search operations have Return All and Limit (default 50). Message Get Many, Message Search and Chat Get Many page through results with limit/offset, at most 1,000 rows per request (the server rejects larger pages). Contact Get Many and Scheduled Message Get Many fetch the full list in one request and apply the limit in n8n.
- Message > Get Many, From Address filter: applied after each page is fetched, so fewer results than the limit may come back. Use
mefor messages you sent. - Message > Search, From Address: matches the other party of the conversation; use
mefor your own messages. Searching a chat that does not exist returns no results rather than an error. Messages whose text is stored only in rich-text form may not be found. - Chat > Get Many, Unread Only: the limit applies after filtering, and "unread" is the read state of the Mac's user.
Visible and destructive actions
These change what other people see. Use them deliberately, especially from an AI agent:
- Visible to others: Message React, Edit and Unsend; Chat Mark as Read (read receipt), Start/Stop Typing, Rename Group, Add/Remove Participant.
- Destructive, cannot be undone: Chat Delete (removes the chat and all its messages from the Mac), Chat Leave (another member has to add you back), Chat Remove Participant, Message Unsend, Scheduled Message Delete.
Unverified
These are implemented from the BlueBubbles API reference but not yet confirmed against a live server. Test them before relying on them:
- The Subject and Effect ID send options.
- The request shape of Chat > Create (
POST /chat/newwithaddresses,message,method,service,tempGuid).
Errors
Errors name the request, HTTP status and BlueBubbles error message with a hint (for example "Chat does not exist; check the chat GUID" on a 404). The password is never included. With On Error > Continue, a failing item becomes { "error": "<message>" }, paired with its input item, and the rest of the items still run.
BlueBubbles Trigger
Webhook lifecycle
The trigger manages its BlueBubbles webhook for you; there is nothing to configure on the BlueBubbles side.
On workflow activation:
- Lists all webhooks with
GET /api/v1/webhookand matches the n8n webhook URL exactly. (The server's?url=filter is not used: on BlueBubbles Server 1.9.9 it returns HTTP 500 when no webhook has that URL.) If listing fails, activation continues and step 4 reports any problem. - If a webhook with the identical URL and events exists, it is reused.
- If a webhook with the same URL but different events exists, it is replaced. BlueBubbles keeps one webhook per URL and returns the existing one unchanged on
POST, so the trigger also checks the events the server returns and replaces the webhook if they differ. - Otherwise, a new webhook is created with
POST /api/v1/webhookand body{ "url": "...", "events": [...] }. - The webhook ID is stored in the workflow static data.
On workflow deactivation, the webhook is removed with DELETE /api/v1/webhook/:id.
"Listen for test event" in the editor temporarily registers the /webhook-test/ URL and removes it when listening stops.
Events
Select one or more events (* subscribes to all):
*, new-message, updated-message, message-send-error, group-name-change, group-icon-changed, group-icon-removed, participant-added, participant-removed, participant-left, chat-read-status-changed, typing-indicator, scheduled-message-error, server-update, new-server, new-findmy-location, incoming-facetime, ft-call-status-changed, imessage-alias-removed, hello-world, settings-backup-created, settings-backup-updated, settings-backup-deleted, theme-backup-created, theme-backup-updated, theme-backup-deleted
This is the server's own whitelist (webhookEventOptions in BlueBubbles Server); any other value is rejected with HTTP 400.
Output
Each item is the raw BlueBubbles payload:
{ "type": "new-message", "data": { "...": "..." } }
To branch on event type, add an IF or Switch node on {{ $json.type }}.
Notes
- BlueBubbles webhooks are not signed and are not retried. If n8n is unreachable when an event fires, that event is lost.
- BlueBubbles does not send a test event when a webhook is created.
hello-worldis only emitted when the BlueBubbles HTTP server starts; filter it out downstream if you subscribed to*. - The webhook API (
/api/v1/webhook) requires BlueBubbles Server 1.9.9 or newer. imessage-alias-removedis the name the server accepts for subscriptions, but BlueBubbles Server (1.9.9 / master) emits that event asimessage-aliases-removed, so only a*subscription receives it.- If activation fails ("Failed triggers"), open the trigger node: the error names the request, the server URL, the HTTP status or network error, the BlueBubbles error message and a hint. The password is never included.
Networking
BlueBubbles (on the Mac) calls n8n directly, so the webhook URL n8n generates must be reachable from that Mac. Set n8n's WEBHOOK_URL environment variable to an address the Mac can reach, such as a LAN IP, a Tailscale hostname, or a reverse-proxy URL. localhost will not work unless n8n runs on the same Mac.
Example Docker environment:
services:
n8n:
image: n8nio/n8n
environment:
- WEBHOOK_URL=http://192.168.1.20:5678/
# or: WEBHOOK_URL=https://n8n.your-tailnet.ts.net/
ports:
- "5678:5678"
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:
Install from npm (recommended once published)
The package is published to npm as n8n-nodes-bluebubbles, with npm provenance linking each version to the GitHub Actions run that built it.
n8n UI: go to Settings > Community Nodes > Install, enter n8n-nodes-bluebubbles, accept the risk prompt, and install.
Manual (e.g. Docker, or when the UI install is disabled):
docker exec -it n8n sh -c "mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes && npm i --omit=peer n8n-nodes-bluebubbles"
docker restart n8n
The --omit=peer and persistent-volume notes in the next section apply here too. To update, use Update in the Community Nodes settings, or re-run the command with n8n-nodes-bluebubbles@latest and restart.
Installing in self-hosted n8n (Docker)
Install the release tarball straight from GitHub. This works without npm, and every GitHub Release tarball is byte-identical to the npm package of the same version. Every release attaches n8n-nodes-bluebubbles-<version>.tgz at:
https://github.com/ejlevin1/n8n-nodes-bluebubbles/releases/download/v<version>/n8n-nodes-bluebubbles-<version>.tgz
VERSION below is kept at the latest release automatically:
VERSION=0.2.1 # x-release-please-version
docker exec -it n8n sh -c "mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes && npm i --omit=peer https://github.com/ejlevin1/n8n-nodes-bluebubbles/releases/download/v${VERSION}/n8n-nodes-bluebubbles-${VERSION}.tgz"
docker restart n8n
Then search for "BlueBubbles" in the node picker.
Notes:
--omit=peeris required. Without it npm installs then8n-workflowpeer dependency (hundreds of MB, plus a nativeisolated-vmbuild that fails in the official image). n8n providesn8n-workflowat runtime.~/.n8n(/home/node/.n8n) must be on a persistent volume, or the install is lost when the container is recreated.- Do not use
npm i github:ejlevin1/n8n-nodes-bluebubbles: a git install has to build from source, which pulls all dev dependencies and fails inside the official n8n image. - docker compose users: replace
docker exec -it n8nwithdocker compose exec n8n.
Local tarball (testing local changes or offline install)
npm run build && npm pack # on your machine, produces n8n-nodes-bluebubbles-<version>.tgz
docker cp n8n-nodes-bluebubbles-<version>.tgz n8n:/tmp/
docker exec -it n8n sh -c 'mkdir -p ~/.n8n/nodes && cd ~/.n8n/nodes && npm i --omit=peer /tmp/n8n-nodes-bluebubbles-<version>.tgz'
docker restart n8n
Updating
Re-run the install command with the new VERSION (or new tarball), then restart the container.
Development
Requires Node 24 (see .nvmrc).
npm install # also points git at the repo's hooks (see below)
npm run dev # starts n8n with hot reload of this node at http://localhost:5678
npm run lint
npm run build
npm test # builds, then runs test/*.test.cjs against a mock BlueBubbles server
Contributing
One-time setup after cloning
nvm use # Node 24 from .nvmrc
npm install
npm run hooks:install # git config core.hooksPath .githooks
npm install already runs hooks:install through npm's dependencies lifecycle script, which only runs in this repository, never when n8n installs the release tarball. Run npm run hooks:install yourself if you installed with --ignore-scripts or the hooks are not firing (check with git config core.hooksPath, which should print .githooks).
The hooks in .githooks/ are plain shell scripts with no extra dependencies:
| Hook | Runs | Purpose |
|---|---|---|
commit-msg |
npx --no -- commitlint --edit |
Rejects messages that are not Conventional Commits |
pre-commit |
npm run lint |
Same lint CI runs |
pre-push |
npm run build |
Same build CI runs |
They use node/npm from your PATH. If a GUI git client does not inherit your shell PATH, the hooks try to load nvm ($NVM_DIR, then ~/.nvm), and exit with a clear error if Node still cannot be found. git commit --no-verify skips them; CI checks the same rules anyway.
Commit conventions
Commit messages, and PR titles for squash merges, must follow Conventional Commits, using the rules in commitlint.config.mjs (@commitlint/config-conventional):
<type>(<optional scope>): <lower-case summary>
<optional body>
<optional footers, e.g. BREAKING CHANGE: ..., Refs: #12>
| Type | Release effect (while < 1.0.0) | Changelog section |
|---|---|---|
feat |
minor (0.1.0 -> 0.2.0) | Features |
fix, perf |
patch (0.1.0 -> 0.1.1) | Bug Fixes, Performance Improvements |
refactor, docs, revert |
patch | Code Refactoring, Documentation, Reverts |
chore, ci, test, build, style |
none on their own | hidden |
feat!: / BREAKING CHANGE: footer |
minor before 1.0.0, major after | Breaking changes |
Examples: feat(trigger): add chat filter option, fix: handle empty webhook list, docs: clarify WEBHOOK_URL.
CI enforces this: the PR Title workflow lints the PR title, and the commitlint job in CI lints every commit in the PR.
Releases
Releases are fully automated by release-please (.github/workflows/release-please.yml).
- Merge conventional commits into
main. - release-please opens or updates a release PR titled
chore(main): release X.Y.Z. It bumpspackage.json,.release-please-manifest.json,CHANGELOG.mdand theVERSIONin this README. - Merge the release PR when you want to ship. The same workflow then creates the
vX.Y.Ztag and GitHub Release, builds the package, uploadsn8n-nodes-bluebubbles-X.Y.Z.tgzto that release, and (when theNPM_PUBLISHrepository variable istrue) publishes that same tarball to npm.
Do not bump versions, edit CHANGELOG.md release entries, or push v* tags by hand. To force a specific version, add a Release-As: X.Y.Z footer to a commit on main.
Do not run npm publish from the working tree: the prepublishOnly script refuses it. Publishing a tarball (npm publish ./<file>.tgz) skips lifecycle scripts, which is how both CI and the one-time manual publish below work.
npm publishing (maintainers)
CI publishes to npm with trusted publishing (GitHub Actions OIDC), so there is no NPM_TOKEN secret. Provenance is generated automatically because the repository and package are public. The publish step is skipped unless the repository variable NPM_PUBLISH is true.
One-time setup, in order:
First publish by hand. The package must exist on npm before a trusted publisher can be attached. Publish the tarball from the first GitHub Release:
gh release download v0.1.0 --repo ejlevin1/n8n-nodes-bluebubbles --pattern '*.tgz' npm login npm publish ./n8n-nodes-bluebubbles-0.1.0.tgz --access publicAdd the trusted publisher. On npmjs.com, open the package's Settings > Trusted publishing, choose GitHub Actions, and enter:
- Organization or user:
ejlevin1 - Repository:
n8n-nodes-bluebubbles - Workflow filename:
release-please.yml(filename only; the publish must run in this file, not in a reusable workflow it calls) - Environment: leave empty
- Allow
npm publish.
- Organization or user:
Turn on CI publishing. In GitHub, Settings > Secrets and variables > Actions > Variables, add the repository variable
NPM_PUBLISHwith valuetrue.Lock down tokens. After the first CI publish succeeds, set the package's Settings > Publishing access to Require two-factor authentication and disallow tokens. Trusted publishing keeps working; classic and granular tokens can no longer publish.
The trusted publisher configuration must complete a publish within 2 days of being created, so do step 2 shortly before the next release PR is merged (or re-create it if that window passes). package.json repository.url must stay git+https://github.com/ejlevin1/n8n-nodes-bluebubbles.git, or npm rejects the provenance/OIDC publish.
Compatibility
Built with @n8n/node-cli 0.52.3. Tested against n8n 2.x.