Developer guideiMessage API7 min read
iMessage API for Node.js and TypeScript
Send and receive iMessage from Node.js or TypeScript with Flow. Install the SDK, get a test key, stream inbound messages, verify signed webhooks in Express and reply.

The short answer
With Flow, install one Node.js package, get a test key with no account and loop over message.received events. Verify the signed webhook, use an idempotency key and run the same handler on iMessage, WhatsApp and Telegram.
Building on the iMessage API? Build on iMessage with Flow: one agent for iMessage, WhatsApp and Telegram. New to it? Read the iMessage API guide.
Install and get a key
npm install @flow-engineer/messaging
npx @flow-engineer/messaging init
init creates a test key with no account, writes FLOW_MESSAGING_KEY to .env, adds .env to .gitignore, and prints a sandbox link. Open the link on your phone and tap Start. The sandbox runs on Telegram. The handler is the same on iMessage, so build here first. The iMessage API guide explains why Apple has no official API.
A test key allows one contact and 50 messages and expires after seven days. Sign in with a Flow account to keep the app.
The first reply
Save this as agent.ts and run node --env-file=.env agent.ts. It is the example from Flow’s quickstart:
import { FlowMessaging, contentText } from "@flow-engineer/messaging";
const flow = new FlowMessaging();
for await (const event of flow.events.stream({ types: ["message.received"] })) {
const said = contentText(event.data.message.content);
await event.conversation.reply("You said: " + said, {
idempotencyKey: event.id,
});
}
The stream reconnects and resumes for you, and needs no public URL, so it is the easiest way to develop on a laptop. Replace the echo with your model call. reply() also accepts a model stream and sends complete chat bubbles as they are ready. See iMessage agent with the Vercel AI SDK for a worked example.
Receive events in production
In production, register an HTTPS endpoint with POST /v1/webhook_endpoints and verify every delivery. This is the Express example from Flow’s docs:
import express from "express";
import { FlowMessaging, contentText } from "@flow-engineer/messaging";
const flow = new FlowMessaging();
const app = express();
// The raw body is required: a parsed and re-serialized body will not match.
app.post("/flow", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = await flow.webhooks.constructEvent(req.body, req.header("Flow-Signature"), process.env.FLOW_MESSAGING_WEBHOOK_SECRET!);
} catch {
return res.status(400).end();
}
if (event.type === "message.received") {
return res.json(flow.webhooks.reply(`You said: ${contentText(event.data.message.content)}`));
}
res.json({});
});
app.listen(3000);
Answering in the webhook response is the fastest path, and Flow waits 10 seconds for it. For slower work, return {} at once and send the reply later. iMessage webhooks: signing, retries and ordering explains what happens when your endpoint is down.
To try the handler on localhost with no tunnel, run npx @flow-engineer/messaging listen --forward-to http://localhost:3000/flow. It forwards each event from the live stream, signed like a real delivery.
No SDK: use fetch
The API is plain HTTP, so fetch works. This is the reply call from the docs:
await fetch(`https://api.flow.engineer/v1/conversations/${conversationId}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FLOW_MESSAGING_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": event.id,
},
body: JSON.stringify({ content: { type: "text", text: "Hello from my agent" } }),
});
Send iMessage programmatically lists the errors to handle, such as rate_limited and outside_window.
Other languages: Python and Go.
Frequently asked questions
Is there an iMessage API for Node.js?
Yes. Flow publishes @flow-engineer/messaging on npm. The underlying API is HTTP, so plain fetch also works.
How do I get an API key?
With Flow, run npx @flow-engineer/messaging init or call POST /v1/sandbox/keys. You get a test key with no account. It allows one contact and 50 messages on the Telegram sandbox and expires after seven days.
Can I test iMessage from Node.js without an iPhone?
You can test your logic with simulated contacts on a test key, and test on a real phone through the Telegram sandbox. For iMessage itself, Flow has a free shared line for up to 25 people a month.
How do I verify a webhook in Express?
Use express.raw so you keep the raw body, then call flow.webhooks.constructEvent with the body, the Flow-Signature header and your webhook secret. Return 400 if it throws.
How do I avoid sending a reply twice?
Pass the event ID as the idempotency key. Flow keeps keys for 24 hours and returns the first answer for a repeat.
Do I need TypeScript?
No. The SDK works from JavaScript, and the API is plain HTTP. TypeScript gives you typed events and content helpers.
Keep reading
All posts: the Flow blog. The Flow docs have the reference.
From reading to building
Your agent, on iMessage and everywhere else.
Flow gives your agent an iMessage line, and the same code answers on WhatsApp and Telegram and works as an MCP server and CLI. See how it works on iMessage.
Build on iMessage with Flow