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_idnames 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.
| What | 1:1 | Group |
|---|---|---|
| Conversation key | handle (their number or email) | chat_id, such as iMessage;+;chat123456789 |
Inbound handle | The person | The member who wrote it; the message also carries chat_id |
Outbound handle | The person | The chat id |
| Send, typing | to | chat_id instead of to |
| Read history | ?handle= | ?chat_id= |
| Instant typing | On by default | Never; raise "…" yourself |
| Renames, joins, leaves | None | A group.event, never a message.received |
| Who must have consented | The person | Every 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.
| Question | Hosted agent | Your own code |
|---|---|---|
| When it speaks | Only when a message is meant for it or asks something it can help with | Whatever rule you write |
| What it reads | The group's recent messages, each labeled with who wrote it, plus the group's name and members | Anything GET /api/imessage/messages?chat_id=… returns, plus your own data |
| Scheduled texts into the group | Never: its triggers don't fire into groups | Your own scheduler can send any time |
| Other bots | Never answers another ZeroSettle number | You write that rule (below) |
| Cost per reply | 1 credit per text it sends | Your 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:
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:
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
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.
// 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.
// 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.
// 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.
- Thu 7:12 PM
- Maya: anyone doing the long run saturday?
- Jordan: yes! 7?
- Priya: in, but I can't do before 8
- Sam: Scout what's the route this week and how far?
- Scout: Ridge Loop this Saturday, 10 miles from the Oak St trailhead.
- Jordan: thanks Scout👍
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.
// 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.
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.
// 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/reactionswith themessage_idof a group message sends the tapback into that group. Members' tapbacks arrive withchat_idand their ownhandle. - Typing.
POST /api/imessage/typingwithchat_idshows your "…" in the group. Instant typing never runs in a group, so this call is the only way. Withtyping_events: trueon the webhook, a member typing arrives withchat_idand that member'shandle. - Members coming and going. A
group.eventwithgroup_event.typeofmember_addedis a fair moment for a one-line hello. Ifmember_removedhas nohandle, 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:
- Never answer your own numbers.
GET /api/imessage/linereturns your number ashandleand, with several numbers, each inlines. - Never answer numbers on your own list of known bots.
- 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.
| Rule | What it means | Error |
|---|---|---|
| Size | Up to 31 people; to takes 2 to 31 other members | 422 invalid_request |
| New contacts | Each 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 |
| STOP | A member who texted STOP to any of your numbers blocks sends into every group they're in, until removed or they text START | 403 recipient_opted_out |
| On iMessage | Each new member must be on iMessage | 422 not_on_imessage |
| Unanswered messages | The 2-message awaiting_reply rule is a 1:1 rule; group sends skip it | None |
| CRMs | Group messages are not logged in Close or HubSpot | None |
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
- 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.
- ZeroSettle iMessage API Terms of Service.