Use cases

Give your AI agent an iMessage number (bring your own model)

ZeroSettle gives an AI assistant its own iMessage number: someone texts it, your webhook fires, you call your own model with your own keys, and the answer goes back through POST /api/imessage/send, so the model, prompt and keys never touch ZeroSettle. Inbound texts are unlimited, anyone who texts the number first is never limited, and photos and voice messages arrive with files you can pass to vision and speech models. To skip the server, use ZeroSettle's hosted AI agent (Beta) instead. Pro is $49 a month for one number.

Key takeaways

  • What it is: an iMessage number for your assistant. ZeroSettle carries the messages; your server runs the model.
  • Who it fits: AI startups and developers building assistants, companions, tutors or concierge services on Claude, GPT or open-weight models, for users on iPhone.
  • Your keys stay yours: the model, prompt, provider account and API keys never touch ZeroSettle, and a line your own server answers uses no ZeroSettle AI credits.
  • Price: Pro is $49/month for one number. Growth, for several numbers working as one, is from $599/mo.
  • Main limit: replies to people who text first are never limited. A first text the agent sends to someone who never texted is a new contact: 5 a day per number on Pro, 50 on Growth.
  • What it does not do: group chats, SMS fallback for Android, or sending video and files.

Who uses iMessage for AI agents?

iMessage for AI agents fits assistants, companions, tutors and concierge services that people should reach by texting a number instead of installing an app. ZeroSettle is an iMessage API: a real phone number on real iMessage, running on Apple hardware we operate for you, that your code texts customers from and hears back on through a bearer token and signed webhooks.

The usual alternative to an app, US carrier SMS from a 10-digit number, needs A2P 10DLC brand and campaign registration (Twilio says registering lowers filtering) and shows green bubbles, while Apple lists typing indicators and Tapbacks among iMessage's features. For an assistant only you will text, see build a personal AI assistant you text on iMessage.

How iMessage for AI agents works, step by step

An AI agent on a ZeroSettle number is a webhook receiver that acknowledges each text at once, plus a worker per person that answers it.

  1. Register your receiver with PUT /api/imessage/webhook and store the signing secret (shown once).
  2. On message.received, verify X-ZeroSettle-Signature, dedupe on message.id and event, queue the message per handle and return 200 at once. The webhook times out after 10 seconds and is retried, so a slow model call in the handler can answer twice (webhook guide).
  3. The worker waits a few seconds for the rest of a burst (or, with "typing_events": true, for their typing.stopped), so “hey”, “quick q” and “can I change my delivery day” get one answer. It handles STOP before any model call.
  4. It calls POST /api/imessage/typing (ttl_s up to 60, re-sent about every 20 seconds) and rebuilds the thread from GET /api/imessage/messages?handle=…: inbound rows are user turns, outbound rows assistant turns.
  5. It calls your model with your key and sends one POST /api/imessage/send per bubble, in order; the first send clears the typing bubble.
  6. It ignores each send's message.sent echo, or it would answer itself.

To scaffold this from Claude Code or Cursor, run the ZeroSettle MCP connector's build-agent prompt (MCP guide). To skip the server, ZeroSettle's hosted AI agent (Beta) answers from a business context you write.

What your model receives from photos and voice messages

ZeroSettle converts inbound media first, so most files go straight to a model. Fetch each from its url with your bearer token.

Inbound media an AI agent receives from ZeroSettle (docs checked October 12, 2026)
The user sendsYour webhook getsPass it to
A photoJPEG for HEIC or oversized images; JPEG, PNG or GIF within 4 MB and 4032 px as sent; a Live Photo's stillA vision model; Claude takes JPEG, PNG, GIF and WebP, not HEIC
A voice messageAn .m4a (AAC) file with duration_msA speech model; OpenAI's transcription takes m4a up to 25 MB
Several photosOnly the firstAsk for the rest one at a time
A video or documentA message row with no fileA reply saying the agent can't open it

Example conversation: an AI travel assistant

A ZeroSettle agent texts like a person: a typing bubble, short bubbles, and answers about photos.

  1. Today 8:14 PM
  2. Customer: can you plan me 3 days in Lisbon in March? I love food and hate crowds
  3. Pathfinch is typing
  4. Pathfinch: Love it. Day 1: Alfama early, before the tour groups, then lunch at a tasca in Graça.
  5. Pathfinch: Want me to do days 2 and 3 the same way?
  6. Customer: Photo: a restaurant storefront
  7. Customer: is this place any good?
  8. Pathfinch: It’s a local favorite. Go right at 12:30 when it opens, no reservations ❤️
Illustrative example with a fictional AI travel assistant, Pathfinch. Blue bubbles are its replies, sent by the developer's server; gray bubbles are the user's. The burst wait answers the photo and its question together.

Setup: the worker pattern for bursts, typing and STOP

This Python version adapts the AI agent section of the ZeroSettle docs with Claude as the model; another provider changes only the model call and its message format. Register the receiver, then put the returned secret in ZEROSETTLE_WEBHOOK_SECRET:

bash
curl -s -X PUT -H "Authorization: Bearer $ZEROSETTLE_TOKEN" -H 'Content-Type: application/json' \
  -d '{"url":"https://agent.example.com/webhook"}' https://portal.zerosettle.io/api/imessage/webhook
python
# agent.py: run with  uvicorn agent:app --port 8100
import asyncio, base64, hashlib, hmac, json, os, re, time
import anthropic, httpx
from fastapi import FastAPI, Request, Response

BASE = "https://portal.zerosettle.io"
TOKEN, SECRET = os.environ["ZEROSETTLE_TOKEN"], os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
SYSTEM = "You are Pathfinch, an AI travel planner. Plain text; one to three short bubbles split by blank lines."
app, llm = FastAPI(), anthropic.AsyncAnthropic()        # reads ANTHROPIC_API_KEY
zs = httpx.AsyncClient(base_url=BASE, headers={"Authorization": f"Bearer {TOKEN}"}, timeout=90)
seen, queues, muted = set(), {}, set()                  # keep seen and muted in your database
VISION = ("image/jpeg", "image/png", "image/gif", "image/webp")

def verify(header: str, raw: bytes) -> bool:
    try:
        p = dict(x.split("=", 1) for x in header.split(",")); t = int(p["t"])
    except Exception:
        return False
    if abs(time.time() - t) > 300:
        return False
    exp = hmac.new(SECRET.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(exp, p["v1"])

@app.post("/webhook")
async def webhook(req: Request):
    raw = await req.body()
    if not verify(req.headers.get("X-ZeroSettle-Signature", ""), raw):
        return Response(status_code=401)
    ev = json.loads(raw)
    if ev["event"] != "message.received" or (ev["message"]["id"], ev["event"]) in seen:
        return {"ok": True}                             # message.sent echoes and retries
    m = ev["message"]; seen.add((m["id"], ev["event"]))
    if m["handle"] not in queues:
        queues[m["handle"]] = asyncio.Queue(); asyncio.create_task(worker(m["handle"]))
    queues[m["handle"]].put_nowait(m)
    return {"ok": True}

async def worker(handle: str):
    while True:
        burst = [await queues[handle].get()]
        try:                                            # wait for 3 s of quiet
            while True: burst.append(await asyncio.wait_for(queues[handle].get(), timeout=3))
        except asyncio.TimeoutError:
            pass
        try: await reply(handle, burst)
        except Exception as e: print("reply failed", handle, repr(e))

async def reply(handle: str, burst: list[dict]):
    burst.sort(key=lambda m: m["id"])                   # retries can arrive out of order
    for m in burst:                                     # STOP before any model call
        t = (m.get("text") or "").strip().lower()
        if re.search(r"\bstop\b", t): muted.add(handle)
        elif t in ("start", "unstop"): muted.discard(handle)
    if handle in muted:
        return
    typing = asyncio.create_task(keep_typing(handle))
    try:
        r = await llm.beta.messages.create(
            model="claude-opus-5-5", max_tokens=16000, output_config={"effort": "low"},
            betas=["server-side-fallback-2026-07-01"], extra_body={"fallbacks": "default"},
            system=[{"type": "text", "text": SYSTEM, "cache_control": {"type": "ephemeral"}}],
            messages=await thread(handle, burst[0]["id"], burst[-1]["id"]))
    finally:
        typing.cancel()
    text = "".join(b.text for b in r.content if b.type == "text").strip()
    if r.stop_reason == "refusal" or not text:
        await zs.post("/api/imessage/typing", json={"to": handle, "state": "stop"}); return
    for bubble in [b.strip() for b in text.split("\n\n") if b.strip()][:3]:
        await send(handle, bubble)

async def keep_typing(handle: str):
    while True:
        await zs.post("/api/imessage/typing", json={"to": handle, "state": "start", "ttl_s": 60})
        await asyncio.sleep(20)

async def thread(handle: str, first: int, last: int) -> list[dict]:
    rows = (await zs.get("/api/imessage/messages", params={
        "handle": handle, "before_id": last + 1, "limit": 40})).json()["messages"]
    out = [{"role": "user", "content": [{"type": "text", "text": "(conversation so far)"}]}]
    for r in rows:
        if r["status"] == "failed" or r["kind"] == "reaction":
            continue
        role = "user" if r["direction"] == "in" else "assistant"
        text = "[voice message]" if r["kind"] == "audio" else r["text"] or "[video or file]"
        blocks = [{"type": "text", "text": text}]
        if (r["kind"] == "image" and r.get("image") and role == "user" and r["id"] >= first
                and r["image"]["content_type"] in VISION):
            img = (await zs.get(r["image"]["url"])).content
            blocks.insert(0, {"type": "image", "source": {"type": "base64",
                "media_type": r["image"]["content_type"], "data": base64.b64encode(img).decode()}})
        if out[-1]["role"] == role: out[-1]["content"] += blocks
        else: out.append({"role": role, "content": blocks})
    return out

async def send(handle: str, text: str):
    for attempt in range(4):
        r = await zs.post("/api/imessage/send", json={"to": handle, "text": text[:4000]})
        if r.status_code == 200: return
        err = r.json().get("error", {})
        if r.status_code not in (429, 503): raise RuntimeError(f"send failed: {err.get('code')}")
        await asyncio.sleep(err.get("retry_after_s") or 2 * (attempt + 1))
    raise RuntimeError(f"send failed after retries: {err.get('code')}")

fallbacks retries a declined request on another Claude model. Voice messages reach the model as “[voice message]”; to let it hear one, transcribe the file at audio.url first. To test, text the line from your own phone: the “…” should appear 3–5 seconds after your last text, then the reply. If nothing appears, check last_delivery in GET /api/imessage/line.

Limits and consent for AI agents

Replies to people who text the agent first are never capped, and links in them are tappable. Limits apply when the agent texts someone first, which needs their opt-in, and, on every plan, after two unanswered messages.

ZeroSettle limits for a bring-your-own-model agent (API docs, checked October 12, 2026)
LimitProGrowth
Replies to people who texted firstUnlimitedUnlimited
New contacts per number per day (3 AM to 3 AM Eastern)550
New contacts per number, rolling 15 minutes—3
Unanswered messages to one contact2, then 403 awaiting_reply2, then 403 awaiting_reply

A new contact is a handle the line delivers a text, voice message or photo to first.

The limit math for an AI agent

  • A daily check-in to 50 opted-in users who never texted: on one Pro number the first texts take ten days (5 a day); then each is an ongoing conversation. A link in a first text stays plain text until they reply or save the number.
  • The same check-in on one Growth number: all 50 first texts go out in about four hours (3 per 15 minutes). On any plan, a third check-in in a row with no answer returns 403 awaiting_reply until they send anything, a tapback included.
  • Many people at once: a line sends one message at a time, normally in 2–5 seconds, so a three-bubble answer holds it for about 6–15 seconds; a send that waits about 12 seconds gets 429 line_busy.

Disclosure, opt-in and STOP

ZeroSettle's terms require “transparent disclosure of AI interaction in systems like chatbots, unless it is contextually evident”: say it's an AI in the name you share or in its first reply. Text first only people who opted in, and send nothing promotional before 8 am or after 9 pm in their time zone. This isn't legal advice.

A text with the word STOP in it, or a clear request like “take me off your list”, opts the sender out of every number on your account; sends, typing and tapbacks to them return 403 recipient_opted_out. “Can we stop in Sintra on day 2?” counts too, so tell users that texting START turns the assistant back on (how STOP works).

When ZeroSettle is not a fit for AI agents

ZeroSettle is the wrong transport for an AI agent when:

  • You need group chats. Conversations are one to one; group chats are on the roadmap.
  • Your users are on Android. There is no SMS fallback, and an sms: link opens their SMS app, so keep email or web chat. WhatsApp is no easy swap: as of October 2026, Meta's published terms bar AI providers from its business platform when AI is the primary functionality, as Meta decides, except in some countries (channels compared).
  • The agent must send video or files. It sends text, photos (5 MB) and voice messages (6 MB).
  • You want proactive outreach at scale. New contacts are capped per number per day, and the terms prohibit high-volume unsolicited messaging.
  • You need read status. For your sends the API reports message.sent (handed to Apple) or message.failed, never delivered or read.

How much does iMessage for AI agents cost?

A bring-your-own-model agent costs the ZeroSettle plan plus your model provider's bill; ZeroSettle doesn't charge per message, and AI credits apply only to the hosted agent.

ZeroSettle plans for an AI agent (zerosettle.io and API docs, checked October 12, 2026)
PlanPriceWhat it includes
Pro$49/monthOne number, 5 new contacts a day, unlimited messages with people who message you first, no setup fee, 30-day money-back guarantee; 1,000 AI credits a month
GrowthFrom $599/mo3+ numbers working as one, dedicated support, set up after a demo; 5,000 AI credits a month
AI credit packs1,000 for $50 or 5,000 for $200Hosted agent only; never expire

See the plans on zerosettle.io, read the ZeroSettle docs, or get a number. More workflows are in 50 ways businesses use iMessage.

Frequently asked questions

Can I switch between my own model and the hosted agent?

Yes, per line group. Under Managed by AI Agent, inbound texts no longer reach your webhook and your sends return 403 managed_by_zerosettle; setting managed_by back to "api" hands the conversations back to your server.

Can the agent reply with a photo or a voice message?

Yes. POST /api/imessage/send takes image_b64 (up to 5 MB, optional caption) or audio_b64 (up to 6 MB, no caption), and text-to-speech output works as-is.

Can the agent answer with a tapback instead of a text?

Yes. Call POST /api/imessage/reactions with the message_id of their text and love, like, dislike, laugh, emphasize or question. The docs suggest giving the model a react tool, so “thanks!” gets a ❤️, not a paragraph.

Sources

  1. ZeroSettle iMessage API docs: “AI agent”, “Send a message”, “Typing indicators”, “Attachments”, “Read messages”, “Webhooks, step by step”, “Connect your AI agent” and “Errors, limits, and compliance”. Checked October 12, 2026.
  2. ZeroSettle iMessage API Terms of Service, section 4 (AI disclosure) and section 13 (prohibited uses). Last modified September 28, 2026.
  3. Meta Terms for WhatsApp Business Platform, section 4.7 (AI Providers), Meta. Last modified September 23, 2026; checked October 8, 2026.
  4. Vision, Claude API docs, Anthropic: supported image formats. Checked October 8, 2026.
  5. File transcription, OpenAI API docs: supported input formats and file size. Checked October 8, 2026.
  6. Programmable Messaging and A2P 10DLC, Twilio. Checked October 8, 2026.
  7. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.

Written by the ZeroSettle team. ZeroSettle is an iMessage API: a real phone number on real iMessage, running on Apple hardware we operate for you, that your code texts customers from and hears back on through a bearer token and signed webhooks.

Product facts on this page come from the ZeroSettle iMessage API docs and iMessage API Terms as of October 12, 2026. Pathfinch is a fictional assistant, and this page is not legal advice.

Text your customers from your own iMessage number

ZeroSettle gives your business its own phone number on real iMessage, running on Apple hardware we operate for you in a server facility. Your code sends with one API call and hears every reply on a signed webhook.

Pro is $49 a month with a 30-day money-back guarantee. Growth, for several numbers working as one, is from $599/mo.