Building iMessage Agents: The Complete Guide
Everything you need for building iMessage agents: how they work, a quickstart with working TypeScript, tools, approvals, native iMessage features, and launching on a real line.

Short answer: Building an iMessage agent takes three things: an iMessage-capable phone line, an AI agent with instructions and tools that call your APIs, and a runtime that delivers messages, keeps memory, and asks a person before risky actions. With Keiki you write the agent in TypeScript or the dashboard, run kit.sync(), and add an iMessage line; Keiki hosts the rest, so a first agent takes under 30 minutes.
An iMessage agent lives in the blue-bubble thread your customers already open dozens of times a day. There is no app to install and no chat widget to find. They text a number, and something useful happens.
This guide covers everything you need to build one: what the agent is made of, a quickstart with working TypeScript, tools that call your APIs, approvals for anything risky, the iMessage features worth using, and what to check before real customers text it.
What is an iMessage agent?
An iMessage agent is an AI agent with a phone number. A customer sends it a message, the agent reads the conversation, decides what to do, calls tools such as your order, booking, or account APIs, and replies in the same thread.
It is not a canned auto-responder. A good iMessage agent remembers who it is talking to, asks before doing anything it should not do alone, and hands off to a person with the whole conversation when a person is the right answer.
What building an iMessage agent involves
Every iMessage agent needs the same six parts:
- A line. An iMessage-capable number the agent sends and receives from.
- An agent loop. A model, instructions, and a loop that reads each message and decides the next step.
- Tools. Your APIs or MCP servers, described so the model knows when to call them.
- Identity. A way to know which customer is texting before the agent touches their data.
- Approvals. A person who decides before writes like refunds, cancellations, or charges run.
- Memory and history. Context about the customer that carries across conversations.
Building these yourself means message delivery, retries, queues, storage, and approval UI before a single customer gets an answer. On Keiki, you write the agent and Keiki runs the rest.
Quickstart: build an iMessage agent in TypeScript
You'll need a Keiki account, an API key from Settings → API keys, and Bun (kit.sync() bundles your tools with it).
Step 1: Install the SDK
bun add @onkeiki/agents effect
Set your API key as KEIKI_API_KEY in your environment.
Step 2: Describe the agent
Write the agent's instructions the way you would brief a new hire: who it serves, what it can do, and when it should hand off.
// src/agent.ts
import { create } from '@onkeiki/agents'
const kit = create({
apiKey: process.env.KEIKI_API_KEY!,
name: 'Acme Support',
agent: {
model: 'google/gemini-3.5-flash',
maxSteps: 10,
historyLimit: 40,
systemPrompt: ({ now }) => [
'You are the support agent for Acme, texting customers over iMessage.',
'Keep replies short, like a text from a helpful person.',
'Hand off to a human when a customer asks for one.',
`Current time: ${now.toISOString()}`,
].join('\n'),
tools: [],
},
})
await kit.sync()
Step 3: Give the agent tools
A tool is one of your API calls with a name, a description, and an input schema. The description is what the model reads to decide when to use it, so write it for the model. Scope every call to the person texting (ctx.phone), never only to what the model passes in, so one customer can't read another's order.
import { Effect, Schema } from 'effect'
import { defineTool } from '@onkeiki/agents'
const lookupOrder = defineTool({
name: 'lookup_order',
description: 'Look up an order by its number. Returns status, items, and delivery date.',
input: Schema.Struct({ orderNumber: Schema.String }),
execute: (input, ctx) =>
Effect.promise(async () => {
const customer = encodeURIComponent(ctx.phone)
const res = await fetch(`https://api.acme.com/customers/${customer}/orders/${encodeURIComponent(input.orderNumber)}`, {
headers: { Authorization: `Bearer ${process.env.ACME_API_KEY}` },
})
return await res.json()
}),
})
Already have an MCP server? Connect it from the agent's tools in the dashboard instead of wrapping each endpoint.
Step 4: Put risky actions behind approval
Reads can run on their own. Anything that changes money or data should ask first. Set approval: 'ask' and a person decides before the tool runs.
const cancelOrder = defineTool({
name: 'cancel_order',
description: 'Cancel an order that has not shipped yet.',
input: Schema.Struct({ orderNumber: Schema.String }),
policy: { approval: 'ask' },
execute: (input, ctx) =>
Effect.promise(async () => {
const customer = encodeURIComponent(ctx.phone)
const res = await fetch(`https://api.acme.com/customers/${customer}/orders/${encodeURIComponent(input.orderNumber)}/cancel`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ACME_API_KEY}` },
})
return await res.json()
}),
})
Then add both tools to the agent: tools: [lookupOrder, cancelOrder].
Step 5: Sync the agent
bun run src/agent.ts
kit.sync() pushes the config and tools to Keiki. From here Keiki runs the agent loop, delivery, memory, and approvals. You do not run a server.
Step 6: Add an iMessage line and text it
In the dashboard, open the agent's Channels and add an iMessage line: buy a dedicated line in Keiki or connect one you already have. Text the number from your phone and ask about an order.
You can also reach the same agent from code. Keiki speaks the OpenAI API, with the agent's name as the model:
import OpenAI from 'openai'
const client = new OpenAI({
baseURL: 'https://onkeiki.com/api/v1/openai',
apiKey: process.env.KEIKI_API_KEY,
})
const reply = await client.chat.completions.create({
model: 'Acme Support',
messages: [{ role: 'user', content: 'where is my order 1042?' }],
})
Make it feel native to iMessage
iMessage is more than text. A Keiki agent can use the parts that make a reply feel like it belongs in the thread:
- Tapbacks and threaded replies. The agent sees each message's id, so it can react to a message or answer it directly.
- Photos with context. A photo and the text sent with it arrive as one message, so the agent reads them together.
- Effects, polls, and voice notes. Screen and bubble effects, polls, and voice notes that play in a bubble.
- App cards. With the iMessage Apps add-on on a dedicated line bought in Keiki, a tool can open a web page as an app card in the conversation.
Use these when they help the customer, not because they exist. A tapback that says "got it" beats a sentence that says the same thing.
Know who's texting
A phone number is not a login. Before the agent reads or changes someone's account, connect an identity webhook so Keiki can match the sender to a customer in your system. Give each tool only the access it needs, and keep your own API keys on your side, never in the agent's prompt.
Before you go live
- Test the agent against real questions your support team gets every week.
- Keep every write on
approval: 'ask'for the first week, then turn on autonomy one tool at a time. - Read the conversation traces daily at first. They show every tool call and why it happened.
- Decide when the agent hands off to a person, and who that person is.
Step-by-step how-to guides
Each guide below takes one part of building an iMessage agent and walks through it end to end.
- How to Turn Your API into a WhatsApp and iMessage Agent: A step-by-step guide to building agents on your own APIs: turn your endpoints and MCP servers into a hosted WhatsApp and iMessage agent in under 30 minutes.
New how-to guides are added here as they are published.
FAQ
What do I need to build an iMessage agent? An iMessage-capable phone line, an agent with instructions and tools, and something to run it. With Keiki you write the agent in the dashboard or in TypeScript, add a line, and Keiki runs delivery, memory, and approvals.
Do I need to run my own server? No. Keiki hosts the agent and runs delivery, retries, memory, and approvals. You can buy a dedicated iMessage line in Keiki, and no iOS app is needed. Your APIs stay where they are.
Can an iMessage agent use my existing APIs?
Yes. Wrap an endpoint as a tool with defineTool, upload a webhook tool through the developer API, or connect an MCP server you already run.
How do I stop the agent from doing something it shouldn't?
Set approval: 'ask' on any tool that changes data or money. A person approves the action before it runs, and every call is recorded in the trace.
Does the same agent work on WhatsApp and SMS? Yes. Channels sit below the agent, so you can add WhatsApp, Slack, Telegram, and email to the same agent without rewriting it.
How long does it take to build an iMessage agent? Describing the agent, connecting a tool, and adding a line takes under 30 minutes. Testing it against real conversations is where the rest of your time should go.
Build your iMessage agent
Start building on Keiki, read the developer docs, or begin from an agent template.