Build an AI Sales Agent That Texts via iMessage (2026 Guide)
AI SDRs are replacing cold email. The next frontier is AI-powered iMessage outreach — blue bubbles that bypass spam filters, reach prospects in their personal inbox, and get 30–45% response rates. This guide walks through the complete architecture: an LLM connected to the Sendblue API, handling replies, and escalating to humans.
Why iMessage beats SMS for AI outreach
Carriers have built increasingly aggressive AI-detection filters on the SMS network. AI-generated messages are flagged and blocked at scale. iMessage is end-to-end encrypted, routes through Apple's servers, and carries none of the spam-filter baggage of A2P SMS.
The numbers tell the story:
- iMessage open rate: ~98% (it arrives in the native Messages app)
- iMessage response rate: 30–45% for personalized first messages
- Cold email response rate: 1–6%
- SMS response rate: 8–10% (and declining as filters tighten)
For B2B sales targeting professionals who use iPhones — which is the majority of enterprise buyers in North America — iMessage is the highest-engagement outbound channel available.
Architecture overview
The system has three components:
- LLM (Claude or GPT-4o) — generates personalized outreach messages and crafts replies to incoming responses
- Sendblue API — delivers and receives iMessages, returns delivery status
- Webhook handler (FastAPI) — receives inbound replies from Sendblue and feeds them back to the LLM
The flow for outbound:
- Pull prospect from CRM / CSV
- LLM generates a personalized opening message
- POST to
https://api.sendblue.co/api/send-message - Sendblue delivers as blue bubble
The flow for replies:
- Prospect replies → Sendblue POSTs to your webhook
- Retrieve conversation history from database
- LLM generates contextual response
- Send response via Sendblue
- If escalation signal detected → alert human rep
Outbound: AI generates and sends the first message
import os
import requests
import anthropic
from dotenv import load_dotenv
load_dotenv()
claude = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
SENDBLUE_URL = "https://api.sendblue.co/api/send-message"
SENDBLUE_HEADERS = {
"sb-api-key-id": os.environ["SENDBLUE_API_KEY_ID"],
"sb-api-secret-key": os.environ["SENDBLUE_API_SECRET_KEY"],
}
def generate_opening_message(prospect: dict) -> str:
"""Use Claude to write a personalized opening iMessage."""
message = claude.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=200,
messages=[{
"role": "user",
"content": (
f"Write a personalized iMessage outreach to {prospect['name']} "
f"who is {prospect['title']} at {prospect['company']}. "
f"Context: {prospect.get('context', 'They recently attended a conference on AI in sales.')} "
"Rules: max 2 sentences, casual tone, no emojis, end with a soft open-ended question. "
"Do NOT start with 'Hi' or 'Hello'. Output only the message text, nothing else."
)
}]
)
return message.content[0].text.strip()
def send_imessage(phone: str, content: str) -> dict:
"""Send a message via Sendblue and return the response."""
response = requests.post(
SENDBLUE_URL,
json={"number": phone, "content": content},
headers=SENDBLUE_HEADERS,
timeout=30,
)
return response.json()
# --- Run outbound campaign ---
prospects = [
{
"name": "Sarah",
"title": "VP of Sales",
"company": "TechCorp",
"phone": "+14155551234",
"context": "Her team doubled headcount last quarter."
},
{
"name": "Marcus",
"title": "Head of Growth",
"company": "ScaleUp Inc",
"phone": "+14155555678",
"context": "They just raised a Series B."
},
]
for prospect in prospects:
msg = generate_opening_message(prospect)
result = send_imessage(prospect["phone"], msg)
print(f"Sent to {prospect['name']}: {msg}")
print(f"Status: {result.get('status')} | Handle: {result.get('messageHandle')}\n")Inbound: handle replies and escalate to humans
When a prospect replies, Sendblue POSTs to your webhook. The agent generates a contextual response and checks whether to escalate:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
import sqlite3
app = FastAPI()
# Simple SQLite store for conversation history
# In production, use PostgreSQL + pgvector or Redis
def get_history(phone: str) -> list[dict]:
conn = sqlite3.connect("conversations.db")
conn.execute(
"CREATE TABLE IF NOT EXISTS messages "
"(phone TEXT, role TEXT, content TEXT, ts DATETIME DEFAULT CURRENT_TIMESTAMP)"
)
rows = conn.execute(
"SELECT role, content FROM messages WHERE phone=? ORDER BY ts",
(phone,)
).fetchall()
conn.close()
return [{"role": r[0], "content": r[1]} for r in rows]
def save_message(phone: str, role: str, content: str):
conn = sqlite3.connect("conversations.db")
conn.execute(
"INSERT INTO messages (phone, role, content) VALUES (?,?,?)",
(phone, role, content)
)
conn.commit()
conn.close()
ESCALATION_KEYWORDS = [
"pricing", "contract", "legal", "urgent", "not interested",
"stop", "remove", "unsubscribe", "lawsuit"
]
def needs_escalation(text: str) -> bool:
return any(kw in text.lower() for kw in ESCALATION_KEYWORDS)
def alert_human_rep(phone: str, reply: str):
"""In production: post to Slack, update CRM, send email to rep."""
print(f"[ESCALATION] Human needed for {phone}: {reply}")
class SendblueWebhook(BaseModel):
isOutbound: bool
status: str
messageHandle: str
fromNumber: str
number: str
content: Optional[str] = None
mediaUrl: Optional[str] = None
@app.post("/webhook/sendblue")
async def handle_reply(payload: SendblueWebhook):
if payload.isOutbound or not payload.content:
return {"status": "ok"}
phone = payload.fromNumber
reply = payload.content
save_message(phone, "user", reply)
if needs_escalation(reply):
alert_human_rep(phone, reply)
bridge = "Thanks for your message — I'm looping in a colleague who can help you directly."
send_imessage(phone, bridge)
save_message(phone, "assistant", bridge)
return {"status": "escalated"}
history = get_history(phone)
ai_response = claude.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=200,
system=(
"You are an AI sales assistant following up via iMessage. "
"Be helpful, concise, and human. "
"Goal: qualify interest and book a 15-minute call. "
"Max 2 sentences per reply. Never mention you are an AI unless directly asked."
),
messages=history,
).content[0].text.strip()
send_imessage(phone, ai_response)
save_message(phone, "assistant", ai_response)
return {"status": "ok"}
# Run: uvicorn agent:app --reload --port 8000Production considerations
- Rate limiting: Sendblue enforces per-account send rates. Spread outbound campaigns over time rather than sending all at once.
- Opt-out handling: The
ESCALATION_KEYWORDSlist above catches "stop" and "unsubscribe" — always honor these immediately and never re-contact that number. - Message review: For regulated industries, add a human-review step before the LLM message is sent. Store all prompts and outputs for compliance audits.
- Conversation memory: SQLite works for prototypes. For production use PostgreSQL with a conversations table indexed by phone number.
- Compliance: Sendblue is SOC 2 Type II and HIPAA compliant. Ensure your AI system prompt forbids protected health information in outbound messages.
Next steps
- API reference — Full Sendblue endpoint docs
- MCP integration — Use Sendblue as an MCP tool in Claude-powered agents
- LangChain iMessage tool — Integrate with LangChain agents
- Getting started — Sandbox setup and free trial