Home
/
Blog
/
AI Sales Agent via iMessage
April 5, 2026
12 min read
Nikita Jerschow

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:

  1. Pull prospect from CRM / CSV
  2. LLM generates a personalized opening message
  3. POST to https://api.sendblue.co/api/send-message
  4. Sendblue delivers as blue bubble

The flow for replies:

  1. Prospect replies → Sendblue POSTs to your webhook
  2. Retrieve conversation history from database
  3. LLM generates contextual response
  4. Send response via Sendblue
  5. 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 8000

Production 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_KEYWORDS list 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

Ready to build your AI sales agent?

Free sandbox, no credit card required. Get your API keys in minutes.

Get API Access