Guides

How to Build an AI Assistant for iMessage Group Chats

Short answer: put a ZeroSettle number in the group and give it a brain. The hosted AI agent needs no code: it reads the group, answers only when a message is meant for it, and stays quiet otherwise. To run your own model, every group message reaches your webhook with a chat_id; your code decides whether to speak, reads the group with GET /api/imessage/messages?chat_id=…, and replies with POST /api/imessage/send and the same chat_id. A group holds up to 31 people, and every one of them must have agreed to hear from you.

Key takeaways

  • Two routes: the hosted agent answers in groups on its own, or your code receives each group message on a signed webhook and answers with your model and keys.
  • The hard part is silence. A good group assistant speaks when it is named, when someone replies to it, or when an open question is one it can answer. Everything else is the group talking to itself.
  • One id per group: chat_id names the group for reading, sending, typing and polls (a tapback names the message instead), and never changes when the group is renamed or its members change.
  • Never answer a bot. The hosted agent never replies to another ZeroSettle number; your own code needs the same rule, plus a cap on how often it speaks.
  • Group rules: up to 31 people, each new member counts as a new contact, and a STOP from any member blocks sends into every group they are in.

Plenty of useful assistants live in a 1:1 thread. Some jobs only make sense where several people already talk: a running club picking a start time, a team swapping shifts, friends planning a trip, a homeowner and a crew agreeing on paint colors. In those threads an assistant can answer the question everyone was about to ask, put up a poll, and otherwise stay out of the way.

This guide covers both ways to build one on ZeroSettle, with working TypeScript for the custom route: receiving group messages, deciding whether to answer, reading the group, replying into it, typing, polls, tapbacks, a queue per group, and the rule that keeps two bots from talking forever.

What is an iMessage group chat assistant?

An iMessage group chat assistant is an AI agent with its own number that sits in a group as one more member. People talk to each other as usual. When someone names it, replies to it, or asks something it can answer, it replies in the group, in the same thread everyone already has open.

On ZeroSettle the member is your dedicated iMessage number, run on Apple hardware we operate for you in a server facility. It can start a group with up to 31 people or be added to one someone else made; either way the group works the same.

How is a group different from a 1:1 thread?

A group is keyed by its chat_id instead of a person's handle, and the assistant has to share the room. Almost everything else in the API is the same call with one field swapped.

1:1 conversation vs group chat on a ZeroSettle number (ZeroSettle API docs, checked November 2, 2026)
What1:1Group
Conversation keyhandle (their number or email)chat_id, such as iMessage;+;chat123456789
Inbound handleThe personThe member who wrote it; the message also carries chat_id
Outbound handleThe personThe chat id
Send, typingtochat_id instead of to
Read history?handle=?chat_id=
Instant typingOn by defaultNever; raise "…" yourself
Renames, joins, leavesNoneA group.event, never a message.received
Who must have consentedThe personEvery member

The other difference isn't in the API at all. In a 1:1 thread every message is for the assistant. In a group most messages aren't, and an assistant that answers them all gets muted or removed fast. When should an AI speak in a group chat? goes deeper on that judgment; the code below gives you a working first version.

One thing a group is not: a broadcast. POST /api/imessage/broadcast sends each recipient their own 1:1 message, so nobody sees anyone else. Use a group only when the members should see each other.

Hosted agent or your own code?

Use the hosted agent when the assistant answers from things you can write down; use your own code when it must look things up in your systems, follow custom speaking rules, or run a specific model. A number is either Managed by AI Agent or Managed by API (with several numbers, per line group), and switching is one call.

The two routes in a group chat
QuestionHosted agentYour own code
When it speaksOnly when a message is meant for it or asks something it can help withWhatever rule you write
What it readsThe group's recent messages, each labeled with who wrote it, plus the group's name and membersAnything GET /api/imessage/messages?chat_id=… returns, plus your own data
Scheduled texts into the groupNever: its triggers don't fire into groupsYour own scheduler can send any time
Other botsNever answers another ZeroSettle numberYou write that rule (below)
Cost per reply1 credit per text it sendsYour model provider's bill; no credits for texts

How do you set up the hosted agent for a group?

Switch the number to the hosted agent, describe its role in the group in the business context, then put the number in the group. In the console that is the Messaging tab; with the API it is one call:

bash
BASE=https://portal.zerosettle.io   # ZEROSETTLE_TOKEN is exported in your shell
jq -n --rawfile ctx business_context.txt '{managed_by: "zerosettle", business_context: $ctx, personality: "casual"}' |
curl -X PUT "$BASE/api/imessage/agent" \
  -H "Authorization: Bearer $ZEROSETTLE_TOKEN" -H "Content-Type: application/json" -d @-

Write the business context for the room it will sit in: who is in the group and why, what it knows (schedules, routes, prices, policies), what it should never do (take sides, share one member's 1:1 messages), and when to point people to a person.

Then start the group, or have someone add the number to an existing one:

bash
curl -X POST "$BASE/api/imessage/groups" \
  -H "Authorization: Bearer $ZEROSETTLE_TOKEN" -H "Content-Type: application/json" \
  -d '{"to": ["+13105550101", "+13105550102", "+13105550103"],
       "name": "Ridge Run Club",
       "text": "Hi all, this is Scout, the Ridge Run Club assistant. Ask me about routes and start times."}'

to takes 2 to 31 other members (never your own number), and the group is created by sending text. If the answer says "status": "group_unconfirmed", the message went out but the group's id isn't known yet: don't send it again, or you start a second group. It appears in GET /api/imessage/groups once someone replies.

From there the hosted agent does what the AI agent docs describe: it reads the group's recent messages, each labeled with the name you started that person's conversation with (else their number), keeps answers short, puts its typing bubble in the group, and stays silent when people are talking to each other. Tapbacks, poll votes, renames and members joining don't wake it. When several of your numbers with the agent on are in one group, only one answers. Each text it sends uses a credit, and the same limits apply in every group. Polls and the tapback replies in the example below come from your own code, not the hosted agent. How the hosted AI agent answers iMessages covers its settings in detail.

How do you build your own in TypeScript?

Point the number's signed webhook at a small server that filters group messages, reads the group, asks your model whether and what to say, and sends into the group.

You'll need Node 20 or later, a ZeroSettle number, its API token from the console, and a model provider key. The examples use Claude through the Anthropic SDK; any provider fits the same slot. If you haven't built a 1:1 agent before, Building iMessage agents walks through the same server shape in more depth.

Step 1: Install and hand the number to your code

bash
npm install @anthropic-ai/sdk express
npm install -D tsx typescript @types/express @types/node
export ZEROSETTLE_TOKEN=pt_mcp_…        # from the console
export ANTHROPIC_API_KEY=sk-ant-…      # your key, your bill

curl -X PUT "$BASE/api/imessage/agent" -H "Authorization: Bearer $ZEROSETTLE_TOKEN" \
  -H "Content-Type: application/json" -d '{"managed_by": "api"}'   # texts go to your webhook

Step 2: A thin ZeroSettle client

One fetch wrapper covers every call in this guide. It keeps the error's code and handle, because a STOP in a group tells you who through error.handle.

TypeScript
// src/zerosettle.ts
const BASE = "https://portal.zerosettle.io";
const headers = {
  Authorization: `Bearer ${process.env.ZEROSETTLE_TOKEN}`,
  "Content-Type": "application/json",
};
export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export async function zs(path: string, body?: unknown): Promise<any> {
  const res = await fetch(BASE + path, {
    method: body === undefined ? "GET" : "POST",
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const data = await res.json();
  if (!res.ok) {
    throw Object.assign(new Error(data.error?.message ?? res.statusText), {
      status: res.status, code: data.error?.code, handle: data.error?.handle,
    });
  }
  return data;
}

Step 3: Know who is in the room

The assistant needs three facts: which numbers are yours (so it never answers itself or another of your lines), which numbers are other bots you know about, and what to call each member. ZeroSettle gives you the first and the member list; names come from your own records, such as the signup form people used to join.

TypeScript
// src/room.ts
import { zs } from "./zerosettle.js";

let own: Set<string> | undefined;
export async function ownNumbers() {
  if (!own) {
    const line = await zs("/api/imessage/line");
    own = new Set<string>([line.handle, ...(line.lines ?? []).map((l: any) => l.number)]);
  }
  return own;
}

// Other automated numbers you know about, from your own config.
export const KNOWN_BOTS = new Set((process.env.KNOWN_BOT_HANDLES ?? "").split(",").filter(Boolean));

// Fill from your database; fall back to the handle.
const NAMES = new Map<string, string>();
export const nameOf = (handle: string) => NAMES.get(handle) ?? handle;

export async function groupInfo(chatId: string) {
  const { groups } = await zs("/api/imessage/groups");
  return groups.find((g: any) => g.chat_id === chatId); // {chat_id, name, participants, …}
}

Step 4: Decide whether to answer, read the group, reply

This is the heart of it. A cheap filter in code drops most group chatter before any model call: the message must name the assistant, be an inline reply, or end in a question. Then the model reads the recent group messages and makes the final call: reply, tapback, or stay silent. It sees what came after the message too, so it can tell when someone already answered.

TypeScript
// src/group-agent.ts
import Anthropic from "@anthropic-ai/sdk";
import { zs, sleep } from "./zerosettle.js";
import { ownNumbers, KNOWN_BOTS, nameOf, groupInfo } from "./room.js";

const NAME = "Scout";
const CALLED = new RegExp(`(^|\\W)@?${NAME}\\b`, "i");
const claude = new Anthropic(); // reads ANTHROPIC_API_KEY

const SYSTEM = `You are ${NAME}, an assistant added to an iMessage group chat.
You get the group's recent messages, one per line as "Name: text"; yours are "${NAME}".
Decide about the message marked >>.
Answer it if it is addressed to you, or if it asks something nobody has answered and you can help.
If people are talking to each other, or someone already answered, output exactly SILENT.
If it is a thank-you or an OK that needs no words, output exactly LIKE.
Otherwise write the reply: plain text, no markdown, at most two short sentences.
Never take sides, never speak for a member, never repeat what someone told you privately.`;

const spoke = new Map<string, number[]>(); // chat_id -> when the assistant sent

export async function onGroupMessage(m: any) {
  if ((await ownNumbers()).has(m.handle) || KNOWN_BOTS.has(m.handle)) return; // never answer a bot
  if (m.kind !== "message") return;                                          // photos, voice, polls: humans only, for now
  const text: string = m.text ?? "";
  if (!CALLED.test(text) && !m.reply_to && !text.trim().endsWith("?")) return; // most chatter: no model call
  if (tooChatty(m.chat_id)) return;

  await sleep(2000); // people finish a thought in two or three bubbles
  const q = (p: Record<string, string>) =>
    zs(`/api/imessage/messages?${new URLSearchParams({ chat_id: m.chat_id, ...p })}`);
  const before = (await q({ before_id: String(m.id + 1), limit: "40" })).messages;
  const after = (await q({ after_id: String(m.id), limit: "20" })).messages;
  const rows = [...before, ...after].filter((r: any) => r.kind === "message" || r.kind === "poll");

  const target = rows.find((r: any) => r.id === m.reply_to?.id);
  const addressed = CALLED.test(text) || target?.direction === "out"; // named, or replying to us
  if (addressed) await zs("/api/imessage/typing", { chat_id: m.chat_id, state: "start" });

  const group = await groupInfo(m.chat_id);
  const transcript = rows.map((r: any) =>
    `${r.id === m.id ? ">> " : ""}${r.direction === "out" ? NAME : nameOf(r.handle)}: ${r.text ?? ""}`).join("\n");

  const res = await claude.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 16000,
    output_config: { effort: "low" }, // chat: fast beats deep
    system: [{ type: "text", text: SYSTEM, cache_control: { type: "ephemeral" } }],
    messages: [{
      role: "user",
      content: `Group "${group?.name ?? "unnamed"}", ${group?.participants.length ?? "?"} members besides you.\n` +
        (addressed ? "The marked message is addressed to you.\n" : "") + "\n" + transcript,
    }],
  });
  const out = res.stop_reason === "refusal" ? "SILENT"
    : res.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("").trim();

  if (!out || out === "SILENT") {
    if (addressed) await zs("/api/imessage/typing", { chat_id: m.chat_id, state: "stop" });
    return;
  }
  if (out === "LIKE") {
    await zs("/api/imessage/reactions", { message_id: m.id, reaction: "like" });
    return;
  }
  try {
    await zs("/api/imessage/send", {
      chat_id: m.chat_id,
      text: out,
      reply_to: after.length ? m.id : undefined, // others spoke since: thread it under the question
    });
    spoke.get(m.chat_id)!.push(Date.now());
  } catch (e: any) {
    if (e.code === "recipient_opted_out") return notifyOrganizer(m.chat_id, e.handle); // a member texted STOP
    throw e;
  }
}

function tooChatty(chatId: string) {
  const now = Date.now();
  const recent = (spoke.get(chatId) ?? []).filter((t) => now - t < 10 * 60_000);
  spoke.set(chatId, recent);
  return recent.length >= 5; // at most five sends per group per ten minutes
}

async function notifyOrganizer(chatId: string, handle: string) {
  // Your alert (Slack, email): nothing was sent. Remove the member or leave the group.
  console.warn(`Group ${chatId} is blocked: ${handle} opted out`);
}

A few choices worth copying. The typing bubble goes up only when the assistant was addressed, because a "…" that vanishes without a message looks odd in a group. before_id returns the rows before an id in ascending order, so m.id + 1 gives the message and the 39 before it. And when other people spoke after the question, reply_to threads the answer under it so nobody wonders what it refers to; in a group, reply_to must name a message in that same group.

  1. Thu 7:12 PM
  2. Maya: anyone doing the long run saturday?
  3. Jordan: yes! 7?
  4. Priya: in, but I can't do before 8
  5. Sam: Scout what's the route this week and how far?
  6. Scout: Ridge Loop this Saturday, 10 miles from the Oak St trailhead.
  7. Jordan: thanks Scout👍
Illustrative example with a fictional running club, answered by the code above. Blue bubbles are the assistant, sent from the club's ZeroSettle number. It says nothing while Maya, Jordan and Priya sort things out, answers when Sam names it, and answers a thank-you with a tapback (the LIKE branch) instead of a text.

Step 5: Receive group messages and queue them per group

ZeroSettle POSTs every event to one URL, signed with HMAC-SHA256. Verify the raw bytes, answer 200 at once (the timeout is 10 seconds), drop retries you've seen, and run each group's work in order on its own queue so two members asking at once get answered one after the other.

TypeScript
// src/server.ts
import crypto from "node:crypto";
import express from "express";
import { onGroupMessage } from "./group-agent.js";
import { onPollVote } from "./polls.js";

const SECRET = process.env.ZEROSETTLE_WEBHOOK_SECRET!;
const seen = new Set<string>();
const queues = new Map<string, Promise<void>>(); // one chain per group

function verify(header: string | undefined, raw: Buffer) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const expected = crypto.createHmac("sha256", SECRET).update(`${m[1]}.`).update(raw).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}

const app = express();
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.get("X-ZeroSettle-Signature"), req.body)) return res.sendStatus(401);
  res.sendStatus(200); // answer first, think after

  const ev = JSON.parse(req.body.toString("utf8"));
  const m = ev.message;
  if (!m?.chat_id) return;                // 1:1 texts and typing events: not this assistant's job
  const key = `${ev.event}:${m.id}`;
  if (seen.has(key)) return;              // retries carry the same message id and event
  seen.add(key);

  const job =
    ev.event === "message.received" ? () => onGroupMessage(m) :
    ev.event === "poll.voted" ? () => onPollVote(m) :
    null;                                 // message.sent is your own send echoing back
  if (!job) return;
  const prev = queues.get(m.chat_id) ?? Promise.resolve();
  queues.set(m.chat_id, prev.then(job).catch((e) => console.error("group job failed", m.chat_id, e)));
});
app.listen(8100);

Only answer message.received

Every send you make comes back as message.sent, in groups too. Renames and members joining arrive as group.event, never as message.received, so a responder that acts only on message.received never answers one.

Step 6: Register the webhook and test with two phones

Run the server, expose it (for local work, cloudflared tunnel --url http://localhost:8100 prints an HTTPS URL), and register it. The secret comes back once; store it as ZEROSETTLE_WEBHOOK_SECRET.

bash
npx tsx src/server.ts
curl -X PUT "$BASE/api/imessage/webhook" -H "Authorization: Bearer $ZEROSETTLE_TOKEN" \
  -H "Content-Type: application/json" -d '{"url": "https://YOUR-URL/webhook"}'

Then make a group with your number and two teammates. Chat about lunch: the assistant should say nothing. Ask "Scout, what time is the meeting?": you should see the "…" and a reply. If nothing happens, GET /api/imessage/line shows the last webhook delivery and its error. How to receive iMessages with a webhook covers signing and retries in depth.

How do polls, tapbacks and typing work in a group?

They work as in a 1:1 thread, with chat_id in place of to, and polls are where a group assistant earns its place. Sending one is a normal send with poll; every member can vote, and each vote arrives as poll.voted with the poll's current results, including the handles that chose each option.

TypeScript
// src/polls.ts
import { zs } from "./zerosettle.js";
import { groupInfo } from "./room.js";

export const askGroup = (chatId: string, question: string, options: string[]) =>
  zs("/api/imessage/send", { chat_id: chatId, poll: { question, options } }); // 2 to 12 options

const announced = new Set<number>();
export async function onPollVote(v: any) {
  const poll = v.poll; // results after this vote; null if the poll isn't stored
  if (!poll || announced.has(v.target_id)) return;
  const group = await groupInfo(v.chat_id);
  if (!group || poll.voter_count < group.participants.length) return; // wait for everyone
  announced.add(v.target_id);
  const top = [...poll.options].sort((a: any, b: any) => b.votes - a.votes)[0];
  await zs("/api/imessage/send", {
    chat_id: v.chat_id,
    text: `Everyone's in: ${top.text} it is.`,
    reply_to: v.target_id, // threaded under the poll
  });
}

Members on iOS 26 or later vote right in the thread; on older iPhones they see "Sent a poll" and can't vote, so a poll that waits for everyone may wait forever. Add a deadline in your own code, and handle ties instead of picking the first option.

  • Tapbacks. POST /api/imessage/reactions with the message_id of a group message sends the tapback into that group. Members' tapbacks arrive with chat_id and their own handle.
  • Typing. POST /api/imessage/typing with chat_id shows your "…" in the group. Instant typing never runs in a group, so this call is the only way. With typing_events: true on the webhook, a member typing arrives with chat_id and that member's handle.
  • Members coming and going. A group.event with group_event.type of member_added is a fair moment for a one-line hello. If member_removed has no handle, your number was removed: stop sending there.

The group chats docs list every field, and the iMessage group chat API guide covers creating, renaming and managing groups from code.

How do you keep it from answering other bots?

Refuse to answer any number you know is automated, and cap how often the assistant speaks so an unknown bot can't pull it into a loop.

The hosted agent has this built in: a message from any ZeroSettle number (another customer's line, or another of yours) is stored and shown as usual, but the agent never replies to it, in a group or 1:1, and no trigger fires to it. Your own code has to do the equivalent. The webhook doesn't flag other companies' bots, so the code above uses three layers:

  1. Never answer your own numbers. GET /api/imessage/line returns your number as handle and, with several numbers, each in lines.
  2. Never answer numbers on your own list of known bots.
  3. Never send more than a few messages per group in a short window, whoever is asking. That cap is what stops a loop with a bot you didn't know about.

What limits apply to group chats?

The same limits as 1:1 messaging, applied to every member: each new person counts as a new contact, any member's STOP blocks the group, and every member must have agreed to hear from you.

Group chat rules (ZeroSettle API docs, checked November 2, 2026)
RuleWhat it meansError
SizeUp to 31 people; to takes 2 to 31 other members422 invalid_request
New contactsEach member the number has never exchanged a message with counts as one when you start the group or add them. Pro: 5 a day. Growth: 50 a day per number, 3 per 15 minutes.429 new_contact_limit
STOPA member who texted STOP to any of your numbers blocks sends into every group they're in, until removed or they text START403 recipient_opted_out
On iMessageEach new member must be on iMessage422 not_on_imessage
Unanswered messagesThe 2-message awaiting_reply rule is a 1:1 rule; group sends skip itNone
CRMsGroup messages are not logged in Close or HubSpotNone

The new-contact rule shapes how you start groups. On Pro, a group of eight people the number has never talked to won't fit in one day. People who text the number first never count, so the easy path is to have each member text it first (an sms: link on your signup page does it; getting customers to text first shows how), then start the group. Every member is checked before anything goes out: if one of them fails a check, nothing is sent.

Consent is the rule with no error code. A group message reaches every member, so each of them must have given you their number for this purpose: a signup form, a club roster they agreed to, a job they booked. A friend adding your number to a group doesn't give you the other members' consent. The TCPA consent guide covers the US rules; this is not legal advice. How STOP works covers opt-outs in detail.

On price: Pro is $49 a month for one dedicated number with 5 new contacts a day and 500 free credits once. Growth is $199 a month per number (upgrade in the console), with 50 new contacts a day per number (and 3 per 15 minutes), team members and 1,000 free credits once, or $599 a month for 3 numbers from a Growth link plus $199 per extra number. Enterprise is custom pricing through sales for high volume and compliance needs (book a demo). The subscription has a 30-day money-back guarantee. Each number allows 10,000 messages a day, sent and received, with known contacts. Credits pay only for the hosted agent's texts (1 each) and videos; packs of 1,000 for $50 or 5,000 for $200 never expire. Your own code's texts use none.

What should you check before a real group uses it?

Run it in a test group of teammates for a few days and read every message it sent and every one it skipped.

  • Talk among yourselves for ten minutes. It should say nothing.
  • Name it, reply to one of its messages, and ask an open question it can answer. It should answer all three, once each.
  • Say "thanks Scout". You should get a tapback (your code) or nothing (the hosted agent), not a paragraph.
  • Ask it the same thing from two phones at once. The per-group queue should give one clear answer, not two.
  • Text STOP from a test member and confirm your organizer alert fires and nothing goes into the group.
  • Add a second bot (or another of your numbers) to the group and confirm they never trade messages.
  • Write its introduction: who runs it, what it's for, and that it's automated.

Your coding agent can also do the setup. ZeroSettle's MCP server has create_group_chat, list_group_chats and update_group_chat, and send_imessage, list_messages and show_typing take chat_id, so Claude Code or Cursor can create a test group and read it back while it builds the receiver.

Related guides

iMessage Group Chat API: Create, Send, Read and Manage Groups
Every group endpoint, with request shapes and errors.
When Should an AI Speak in a Group Chat?
The judgment behind the speaking rules above.
A Group Trip Planner That Lives in Your iMessage Group Chat
Polls, dates and a shared plan for friends traveling together.
An iMessage Group Chat Assistant for Teams, Clubs and Communities
Schedules, sign-ups and reminders for groups that meet regularly.
One iMessage Group per Job: Homeowner, Project Manager and Crew
A group for each job, with the assistant answering schedule questions.
Building iMessage Agents: The Complete Guide
The 1:1 agent: tools, guardrails and the full server.

Frequently asked questions

Can an AI assistant join an existing iMessage group chat?

Yes. A group your ZeroSettle number is added to by someone else works the same way as one it starts: its messages reach you with a chat_id, and the number can reply, react and send polls there. Every member still has to have agreed to hear from you, so add it only to groups where people expect it.

How does the assistant know a message is meant for it?

The hosted agent decides for itself: it answers when a message is meant for it or asks something it can help with, and stays silent when people are talking to each other. With your own code, every group message reaches your webhook and your code decides, usually by checking for the assistant's name, a reply to one of its messages, or an open question.

How many people can be in the group?

A group holds up to 31 people. Each member your number has never exchanged a message with counts as one new contact when you start the group or add them: 5 a day on Pro, 50 a day per number on Growth. People who texted the number first never count.

What happens if someone in the group texts STOP?

That person is blocked on every number on your account, and sends into any group they are in are refused with 403 recipient_opted_out until they are removed from it or text START. Nothing is sent, and you still receive what they write.

Does the hosted agent cost more in group chats?

No. Each text it sends uses one credit, in a group or 1:1. Pro includes 500 free credits once and Growth 1,000 once; packs of 1,000 for $50 or 5,000 for $200 never expire. Your own code uses no credits for texts.

Can the assistant send reminders into the group on a schedule?

Not the hosted agent: its triggers, such as follow-ups and daily texts, never fire into a group. Your own code can send into a group whenever it chooses with POST /api/imessage/send and the chat_id, run from your own scheduler.

Sources

  1. ZeroSettle iMessage API docs: “Group chats”, “AI agent” (in group chats, never answering another ZeroSettle number, credits), “Read messages”, “Typing indicators”, “Send a message” (polls, replies, reactions), “Webhooks, step by step”, “MCP” and “Errors, limits, and compliance”. Checked November 2, 2026.
  2. ZeroSettle iMessage API Terms of Service.

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

Product facts on this page come from the ZeroSettle iMessage API docs as of November 2, 2026. Ridge Run Club, Scout and the phone numbers shown are fictional.

Put an assistant in your group chats

Get a dedicated iMessage number, then switch on the hosted agent or point the webhook at your own model.

Pro is $49 a month for one number with a 30-day money-back guarantee. Growth is $199 a month per number, or $599 a month for 3 from a Growth link. Enterprise pricing is custom, for high volume and compliance needs.