{
  "id": "uwi2t0HnU6eolVHV",
  "meta": {
    "aiBuilderAssisted": true
  },
  "name": "iMessage <-> Slack Bridge (Blooio)",
  "tags": [
    {
      "id": "hlAdtsrDjDPDCITI",
      "name": "Orchestrate Academy",
      "createdAt": "2026-04-15T03:02:32.289Z",
      "updatedAt": "2026-04-24T03:04:42.054Z"
    }
  ],
  "nodes": [
    {
      "id": "ff32c311-e980-4e35-9fda-17b0ea4d9d5b",
      "name": "Blooio Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        528,
        528
      ],
      "parameters": {
        "path": "blooio-inbound",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "2cab0e8e-256d-4c17-a7cf-7e79ab9f2a90",
      "name": "Is Incoming Message",
      "type": "n8n-nodes-base.if",
      "position": [
        736,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-event-received",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.body.event }}",
              "rightValue": "message.received"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "214b9da7-bc3a-46a8-9d01-1a718b0f219c",
      "name": "Post iMessage to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        976,
        528
      ],
      "parameters": {
        "text": "=\ud83d\udcf1 *New iMessage from* `{{ $json.body.sender }}`\n\n{{ $json.body.text }}\n\n_Reply in thread to respond via iMessage._",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "<__PLACEHOLDER_VALUE__Slack channel ID \u2014 e.g. C08EF1BE8T1__>"
        },
        "otherOptions": {
          "mrkdwn": true,
          "includeLinkToWorkflow": false
        },
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "e7bf8ad3-2abb-4343-adf8-eb2c72ad24e4",
      "name": "Slack Thread Reply",
      "type": "n8n-nodes-base.slackTrigger",
      "position": [
        272,
        1184
      ],
      "parameters": {
        "options": {
          "resolveIds": false
        },
        "trigger": [
          "message"
        ],
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "<__PLACEHOLDER_VALUE__Same Slack channel ID as above \u2014 e.g. C08EF1BE8T1__>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "162128ff-853c-42fa-b410-9f4a08108310",
      "name": "Human Reply in Thread?",
      "type": "n8n-nodes-base.filter",
      "position": [
        512,
        1184
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-has-thread",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.thread_ts }}",
              "rightValue": ""
            },
            {
              "id": "cond-not-parent",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.thread_ts }}",
              "rightValue": "={{ $json.ts }}"
            },
            {
              "id": "cond-no-bot",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.bot_id }}",
              "rightValue": ""
            },
            {
              "id": "cond-no-subtype",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.subtype }}",
              "rightValue": "bot_message"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "bed37ccb-f8d0-4401-b474-1e63ed7e5ae4",
      "name": "Get Thread Parent",
      "type": "n8n-nodes-base.slack",
      "position": [
        752,
        1184
      ],
      "parameters": {
        "ts": "={{ $json.thread_ts }}",
        "limit": 1,
        "filters": {
          "inclusive": true
        },
        "resource": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.channel }}"
        },
        "operation": "replies",
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.4
    },
    {
      "id": "ecd1b93d-4c4f-4de5-9a1a-331a5dbea64f",
      "name": "Extract Phone + Reply",
      "type": "n8n-nodes-base.set",
      "position": [
        992,
        1184
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f-phone",
              "name": "phoneNumber",
              "type": "string",
              "value": "={{ ($json.text.match(/\\+\\d{7,15}/) || [\"\"])[0] }}"
            },
            {
              "id": "f-reply",
              "name": "replyText",
              "type": "string",
              "value": "={{ $('Slack Thread Reply').item.json.text }}"
            },
            {
              "id": "f-thread",
              "name": "threadTs",
              "type": "string",
              "value": "={{ $('Slack Thread Reply').item.json.thread_ts }}"
            },
            {
              "id": "f-user",
              "name": "slackUser",
              "type": "string",
              "value": "={{ $('Slack Thread Reply').item.json.user }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "81ff0bfd-3048-4dc3-9e0e-5cead5b20aa2",
      "name": "Send iMessage via Blooio",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1232,
        1184
      ],
      "parameters": {
        "url": "=https://backend.blooio.com/v2/api/chats/{{ encodeURIComponent($json.phoneNumber) }}/messages",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={ \"text\": {{ JSON.stringify($json.replyText) }} }",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.threadTs }}-{{ $json.slackUser }}-{{ Date.now() }}"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "98f2e4be-9f58-4b59-bac0-5fc73db6dfb3",
      "name": "Sticky Note 9a6d6867",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -656,
        160
      ],
      "parameters": {
        "color": "#FAFFE0",
        "width": 760,
        "height": 1692,
        "content": "## \ud83d\udcd6 README: iMessage \u2194 Slack Bridge\n\n**What This Does**\nA complete 2-branch bidirectional bridge that turns one Slack channel into a full business iMessage inbox. Inbound iMessages post as new parent messages. Reply in-thread, the reply goes back as iMessage to the original number. One channel, unlimited conversations, zero state store.\n\n**The Two Branches:**\n\n**Branch 1: Inbound iMessage \u2192 Slack** (Blooio Webhook)\n- Blooio webhook fires the moment an iMessage arrives at your business number\n- IF node filters to message.received events only (ignores delivery, read, sent receipts)\n- Slack node posts a new parent message with the sender's phone embedded in the text\n- Your team now has a threaded inbox they can reply to\n\n**Branch 2: Slack Reply \u2192 Outbound iMessage** (Slack Trigger)\n- Slack Trigger listens 24/7 for new messages in the bridge channel\n- Filter node drops self-posts (bot_message, bot_id) and non-threaded messages\n- Set node regexes the phone number out of the parent text\n- HTTP Request sends the reply as iMessage via Blooio \u2014 blue bubble delivered\n\n**Who This Is For**\n- Agencies triaging client iMessages\n- Law firms and real estate agents routing inbound texts\n- Dental and medical practices wanting blue-bubble premium feel\n- Solo operators keeping personal iPhone separate from work\n- Customer support teams replacing established brands at 1/10th the cost\n\n**What You Need**\n- Blooio account + warmed iMessage number (~$39/mo starting)\n- **!! NOTICE !!** Blooio is a P2P service \u2014 all iMessages must be opt-in. Perfect for replying to inbound traffic. \n- Slack workspace + a dedicated bridge channel (private recommended)\n- n8n (cloud or self-hosted)\n\n**Setup Steps**\n\n1. **Blooio Setup**\n   - Sign up at blooio.com and activate a warmed iMessage number\n   - Generate an API token from the dashboard\n   - Keep the webhook signing secret handy for HMAC verification\n\n2. **Slack Setup**\n   - Create a dedicated bridge channel (private recommended)\n   - Create a Slack app or use the built-in n8n OAuth\n   - Invite the bot to the bridge channel\n   - Grant scopes: channels:history, groups:history, chat:write, channels:read\n\n3. **Branch 1: Inbound iMessage \u2192 Slack**\n   - Open Blooio Webhook node and copy the Production URL\n   - In Blooio dashboard, register this URL and subscribe to message.received\n   - Connect your Slack credential on Post iMessage to Slack\n   - Select your bridge channel\n\n4. **Branch 2: Slack Reply \u2192 Outbound iMessage**\n   - Connect your Slack credential on Slack Thread Reply\n   - Select the SAME bridge channel as Branch 1 \u2014 they must match\n   - Connect your Slack credential on Get Thread Parent\n   - Create an HTTP Bearer Auth credential with your Blooio API token\n   - Attach it to Send iMessage via Blooio\n\n5. **Test & Activate**\n   - Text your Blooio number from your own phone\n   - Confirm it posts to the bridge channel as a new threaded message\n   - Reply in the thread from Slack\n   - Confirm the reply arrives as iMessage on your phone\n   - Toggle workflow Active\n\n**Self-Loop Protection**\nThe Filter node drops any message where bot_id exists or subtype equals bot_message. This prevents the bot from replying to its own posts \u2014 a critical safeguard when the trigger fires on ANY channel message.\n\n\n**HMAC Signature Verification (Production)**\nFor production, verify that incoming webhooks actually came from Blooio  (+1 code node) https://docs.blooio.com/guides/webhook-signatures\n\n\n**Troubleshooting**\n- Nothing posts to Slack \u2192 Blooio webhook not registered, or IF node filtered the event. Check $json.body.event value\n- Bot replies to itself \u2192 Filter not catching; inspect the Slack trigger payload for bot_id and subtype\n- Phone regex misses \u2192 Parent was edited and no longer contains +XXXXXXXXXXX\n- iMessage not sending \u2192 Verify Blooio token, check capability at /v1/api/contacts/{phone}/capabilities\n\n---\n\n\n## Learn more at [Orchestrate Academy](https://www.orchestrate.academy)"
      },
      "typeVersion": 1
    },
    {
      "id": "3eefdf86-7989-465c-bfe4-4422b6bf21af",
      "name": "Sticky Note d18f2fad",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        160
      ],
      "parameters": {
        "color": 6,
        "width": 1388,
        "height": 580,
        "content": "## \ud83c\udfaf Branch 1: Inbound iMessage \u2192 Slack\n\n**Trigger:** Blooio Webhook (message.received event)\n\n**What It Does:**\n1. Receives the webhook the moment an iMessage arrives at your business number\n2. Filters to message.received only \u2014 drops delivery, read, sent, reaction receipts\n3. Posts to your Slack bridge channel as a new parent message\n4. Embeds the sender's phone in the message text \u2014 this IS the routing key\n\n**Data Posted to Slack:**\n- Sender phone number (formatted as `+1XXXXXXXXXX` in a code span)\n- The full iMessage body\n- A hint asking the team to reply in thread\n\n**Key Point:** The phone number embedded in the Slack parent message is how Branch 2 knows who to reply to. No database. No lookup table. The thread IS the routing table."
      },
      "typeVersion": 1
    },
    {
      "id": "a2ad1f4c-95ff-4794-b444-90429318f810",
      "name": "Sticky Note 483a7fcb",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        768
      ],
      "parameters": {
        "color": 6,
        "width": 1388,
        "height": 692,
        "content": "## \ud83d\udcac Branch 2: Slack Reply \u2192 Outbound iMessage\n\n**Trigger:** Any new message in the Slack bridge channel\n\n**What It Does:**\n1. Listens 24/7 for new messages in the bridge channel\n2. Filter drops self-posts (bot_message, bot_id) and non-threaded messages \u2014 guards against loops\n3. Get Thread Parent fetches the original iMessage post via Slack's replies API\n4. Set node regexes the phone number from the parent text\n5. HTTP Request sends the team's reply as iMessage via Blooio\n\n**Why The Filter Matters:**\nWhen the Slack bot posts to the bridge channel, that post would itself fire the trigger \u2014 creating an infinite loop. The 4-condition filter (has thread_ts, not parent, no bot_id, no bot_message subtype) is the safety mechanism.\n\n**Key Point:** This is a real bidirectional bridge. Your team works in Slack like they always do. Customers get blue bubbles on their iPhone. Nobody has to learn a new inbox."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "e30c6ff3-182a-49fd-998a-af20382a7237",
  "connections": {
    "Blooio Webhook": {
      "main": [
        [
          {
            "node": "Is Incoming Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Thread Parent": {
      "main": [
        [
          {
            "node": "Extract Phone + Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Thread Reply": {
      "main": [
        [
          {
            "node": "Human Reply in Thread?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Incoming Message": {
      "main": [
        [
          {
            "node": "Post iMessage to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Phone + Reply": {
      "main": [
        [
          {
            "node": "Send iMessage via Blooio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Human Reply in Thread?": {
      "main": [
        [
          {
            "node": "Get Thread Parent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}