Connecting iMessage to Everything
iMessage doesn't have an API. There's no webhook you can subscribe to, no OAuth flow, no developer portal. If you want to build automations around iMessage — get notified in Slack when a message arrives, log conversations to a CRM, auto-reply based on keywords — you're out of luck. The ecosystem just doesn't support it.
That's the problem I was trying to solve. I wanted to build a Zapier integration that connects iMessage to the thousands of apps people already use for work. Not a hacky workaround. A real integration, with real-time triggers, proper authentication, and the kind of reliability you'd expect from something you wire into your business.
This post is about what it took to make that work, and the problems I ran into along the way.
Why iMessage on Zapier Matters
Most business communication still happens over text. Sales teams text prospects. Support teams text customers. Real estate agents, healthcare providers, small business owners — they all live in iMessage. But none of that communication is connected to anything. Messages don't create CRM records. Replies don't trigger follow-ups. Nothing is logged, nothing is automated.
Zapier is the connective tissue for 7,000+ apps. If iMessage is on Zapier, suddenly a new message can create a HubSpot contact, a reply can update a Salesforce deal, an incoming photo can be saved to Google Drive, a keyword can trigger a workflow in Notion. The integration layer between iMessage and everything else just didn't exist. Now it does.
The Architecture
The Photon iMessage server gives you programmatic access to iMessage through an API. But Zapier needs events pushed to it — it needs webhooks. So there's a bridge. The Webhook Bridge sits between the Photon server and Zapier:
- It listens to iMessage events on the Photon server
- Normalizes payloads into a clean
{ event, data }format - Signs each payload with HMAC-SHA256
- Forwards it to the Zapier webhook URL in real-time
The signing matters. Without it, anyone who finds your Zapier webhook URL could inject fake events into your Zaps. The bridge generates a unique signing secret per webhook and sends two headers with every request — a signature and a timestamp. On the Zapier side, I verify the HMAC, check that the timestamp isn't stale for replay protection, and use timingSafeEqual to prevent timing attacks.
const sigBase = `v0:${timestamp}:${rawBody}`;
const expected = `v0=${createHmac("sha256", signingSecret)
.update(sigBase).digest("hex")}`;
return timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);Every trigger — new message, message updated, typing indicator, group name change, read receipts — shares this verification layer. The signature check happens before any event processing. If it doesn't pass, the payload is rejected.
Zapier's Validation System
Zapier has a strict validation suite that runs before you can push a new version to production. It checks every trigger, action, and search against platform rules. Some of these are documented. Most of them you discover by failing.
The ones that cost me the most time:
Every response needs an id field. Zapier uses it as a primary key for deduplication. If your API returns data without an id, you need to derive one or the push is rejected. Every action, every trigger sample, every search result — all of them.
Dynamic dropdowns are required for identifier fields. If a user needs to specify a chat, they shouldn't have to paste a GUID like iMessage;-;+1234567890. The integration needs a hidden trigger that fetches a list of chats and powers a dropdown picker. This means building infrastructure — a hidden trigger, a list endpoint — that the user never directly sees but that the Zap editor depends on.
The app description must follow a specific sentence structure. It has to start with "<App Name> is a...". If it doesn't, the push fails with a generic error. One-line fix once you know the rule, but nothing in the error message tells you what's wrong.
Sample data has to look real. Hardcoded samples with placeholder IDs get flagged during review. The triggers now call the user's actual server for sample data and fall back to realistic static samples only when no data exists yet.
These validation rules exist for good reasons — they enforce a consistent experience across Zapier's marketplace. But they're the kind of thing you can only learn by pushing early and reading the errors.
Authentication Flow
The setup requires three things: the server endpoint, an API key, and a signing secret. The signing secret is generated by the Webhook Bridge when you register a webhook. But to register a webhook, you need the Zapier webhook URL. And Zapier only generates that URL when the Zap is turned on. And the Zap can't turn on until the connection is complete.
I solved this by using Zapier's performSubscribe lifecycle. When a user turns on a Zap, the subscribe hook calls the Bridge API with the server credentials and Zapier's webhook URL, gets back the signing secret, and stores it. The webhook URL — bundle.targetUrl — is only available inside performSubscribe, so the subscribe step is the only place to wire everything together. Two auth fields, zero manual steps. The user connects their Photon server and the rest happens automatically.
URL Normalization
A small thing that broke everything. Users entering their server URL with a trailing slash — https://server.imsgd.photon.codes/ — caused every API call to construct URLs with double slashes: https://server.imsgd.photon.codes//api/v1/server/info. The server treated //api as a different path and returned 404.
The fix was a normalizeUrl function plugged into the beforeRequest middleware:
export const normalizeUrl = (url: string): string =>
(url || "")
.replace(/\/+$/, "")
.replace(/(https?:\/\/)\/+/g, "$1")
.replace(/([^:])\/\/+/g, "$1/");Every outbound request from every trigger, action, and search now passes through this. It strips trailing slashes and collapses doubled slashes while preserving the protocol. The kind of defensive code that seems unnecessary until you see how many ways users input URLs.
Multipart Attachments
Sending attachments through iMessage was one of the more useful actions — forward a file from email, save an image from a form submission, send a document from Google Drive. The original implementation sent the file as base64-encoded JSON. The correct approach turned out to be multipart/form-data.
The fix meant adding the form-data dependency and restructuring the request to use multipart encoding. A different HTTP content type for the same logical operation. This is the reality of building integrations: content types matter, and the error messages don't always point you in the right direction.
Focused Surface Area
The first version of the integration had everything. 20+ triggers, 15+ actions, 8 searches. Every event the server could emit, every operation the API supported. It was comprehensive and it was overwhelming.
Zapier's review guidance nudged toward focus. More actions means more things to maintain, more edge cases to handle, and more cognitive load for the user scrolling through a list of options. The question wasn't "what can we do?" but "what do people actually need?"
The final set: 6 triggers, 8 create actions, 4 searches. New message, message updated, typing indicator, group changes, read receipts. Send message, send attachment, schedule message, react, manage group chats. Search messages, search chats. The things people would actually build Zaps around.
Everything else got cut. Removing features is harder than adding them, but the integration is better for it.
What This Makes Possible
The point of all of this isn't the code. It's what people can do with it.
A sales team can get a Slack notification the moment a prospect replies to a text, with the message content right there in the notification. No checking phones, no missed follow-ups.
A support team can log every iMessage conversation to their helpdesk automatically. Messages come in, tickets get created, responses get tracked. The communication channel becomes part of the system instead of sitting outside it.
A small business owner can auto-reply to messages when they're away, triggered by a schedule in Google Calendar. Or forward photos sent via iMessage directly to a shared Google Drive folder.
A real estate agent can have new lead texts automatically create contacts in their CRM with the phone number, timestamp, and first message.
None of these workflows are complicated. They're the kind of thing that should have existed already. The hard part was building the infrastructure underneath — the real-time triggers, the webhook bridge, the signature verification, the Zapier integration layer — so that setting them up takes minutes, not engineering effort.
iMessage has always been powerful but closed. This cracks it open, just enough, to connect it to everything else.
Takeaways
Push to the platform early. Platform validation rules are impossible to predict locally. The earlier you hit them, the less rework. I could have saved hours by pushing a minimal version on day one instead of building out 20 actions first.
Build webhooks from the start. Polling triggers work for prototypes but they're a dead end. Users expect real-time. Zapier treats webhook triggers as first-class. If you're building an integration that involves events, start with the webhook architecture and work backwards.
Defensive input handling is non-negotiable. URL normalization, input validation, domain verification — these feel like polish but they prevent the most common support issues. Users will paste URLs with trailing slashes, forget protocols, and enter hostnames that point to GitHub repos instead of servers. Handle all of it in middleware so individual actions don't have to think about it.
If you're interested in iMessage automation, check out Photon and the Webhook Bridge.