Use cases

Technician on-the-way texts with a photo and ETA over iMessage

ZeroSettle lets a home-service company send an “on the way” iMessage with the technician's photo and arrival time in a single bubble, from the company's own number, when its dispatch software marks a job en route. The customer's reply, such as a gate code, reaches your webhook about 1–2 seconds after it lands on the line, so your code can push it to the tech. If the customer already has a thread with your number, from a booking confirmation or an earlier text, the on-the-way text doesn't count against Pro's 5 new contacts a day. The photo goes only to customers who have replied to your number before; anyone else gets the caption alone.

Key takeaways

  • What it is: 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 send: one POST /api/imessage/send with image_b64 (the tech's photo) and a caption; both arrive in one bubble. Photos go only to customers who have replied to you; otherwise send the caption alone.
  • Price: Pro is $49/month for one number; Growth, for several numbers working as one, is from $599/mo. No per-message fees.
  • Main limit: on Pro, 5 new contacts a day per number; on Growth, 50 a day and 3 per rolling 15 minutes. Texts to customers already in a thread with you don't count.
  • What it does not do: no SMS fallback for Android, no live map, and ZeroSettle doesn't detect dispatch events or schedule sends.

Who uses iMessage for on-the-way texts?

On-the-way texts suit companies that send staff into customers' homes: home-service, cleaning, pest control, appliance repair and moving companies. The customer sees a face and a first name in the same bubble as the arrival time, so they know who is about to knock instead of missing the window or leaving a stranger at the door.

The reply matters as much as the photo: a door code or “park on the side street” tends to reach the office, not the tech at the gate. ZeroSettle is not a fit for companies whose customers mostly use Android or who need a live map (see when ZeroSettle is not a fit). More service workflows are in 50 ways businesses use iMessage.

How iMessage on-the-way texts work, step by step

Your dispatch system makes one API call when a job goes en route; ZeroSettle carries the captioned photo out and the reply back, but does not watch your dispatch board.

  1. At booking, the customer gives a mobile number for job updates; store that consent with the job. If the booking confirmation or reminder goes out from the same number, the thread exists before the job day.
  2. When dispatch marks the job en route, your field-service software (or an automation step you build) calls POST https://portal.zerosettle.io/api/imessage/send with to, the tech's photo as image_b64, and the caption as text. If the customer has never replied to your number, the photo is refused with 403 attachment_before_reply, so send the caption alone. The call returns once the line hands the message to Apple: normally 2–5 seconds, at most about 105 seconds for a photo.
  3. The photo and caption arrive as one bubble, “exactly as Messages shows a photo with a caption”. With the first message of a conversation, ZeroSettle also shares the brand name and photo you set (see what customers see).
  4. The customer replies, and ZeroSettle posts a signed message.received event to your webhook about 1–2 seconds after the line receives it. A photo of the gate arrives with the file.
  5. Your handler pushes the reply to the tech's job screen and sends a 👍 tapback, or a dispatcher answers in the ZeroSettle console.

Example conversation: an on-the-way text and a door code

The loop as the company sees it in the ZeroSettle console:

  1. Today 9:52 AM
  2. Business: [Photo of Ana]
    Ana from Sparkle Home Cleaning is on her way and will arrive around 10:15.
  3. Customer: thanks! door code is 4471, dog is friendly👍
  4. Business: Passed along to Ana.❤️
Illustrative example with a fictional cleaning company. Blue bubbles are sent from the company's number through the API; gray bubbles are the customer's. Rachel had replied to her booking confirmation, so the photo could go. The 👍 is the company's tapback; the ❤️ is Rachel's.

The door code reaches Ana's job screen while she is still driving. The first bubble has no tracking link on purpose: a first-time customer's iPhone would show it as plain text.

How do you send the photo and pass replies to the tech?

One small service does both: a send with image_b64 and a caption, and a receiver that verifies ZeroSettle's signature and forwards replies, following the send docs and webhook steps.

Prepare each headshot once. A JPEG within 5 MB and 2,048 px on its long edge goes out exactly as stored; HEIC, WebP, TIFF, BMP or anything over 2,048 px is re-encoded to JPEG, which strips EXIF and GPS, and an image over 5 MB is refused (more in what an iMessage API can send). A pass-through JPEG keeps its metadata, and an iPhone photo taken while the Camera has location access embeds where it was shot, per Apple's guide to photo location metadata, so strip it when you store the file.

python
# on_the_way.py: run with uvicorn on_the_way:app --port 8100
import base64, hashlib, hmac, json, os, time
import requests
from fastapi import BackgroundTasks, FastAPI, Request, Response

BASE = "https://portal.zerosettle.io"
AUTH = {"Authorization": f"Bearer {os.environ['ZEROSETTLE_TOKEN']}"}
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
app = FastAPI()
seen: set[tuple[int, str]] = set()             # use your database in production

def send_on_the_way(to: str, caption: str, photo_path: str) -> dict:   # call from your en-route hook
    with open(photo_path, "rb") as f:
        image = base64.b64encode(f.read()).decode()
    r = requests.post(f"{BASE}/api/imessage/send", headers=AUTH, timeout=120,
                      json={"to": to, "image_b64": image, "image_format": "jpg", "text": caption})
    if r.status_code == 403 and r.json()["error"]["code"] == "attachment_before_reply":
        r = requests.post(f"{BASE}/api/imessage/send", headers=AUTH, timeout=120,
                          json={"to": to, "text": caption})   # never replied: caption alone
    return r.json()

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

def open_job_for(handle: str) -> dict | None: ...   # your dispatch DB
def push_to_tech(job: dict, m: dict) -> None: ...    # your field-service app's job notes

def forward(m: dict) -> None:
    job = open_job_for(m["handle"])
    if job is None:
        return                                 # a dispatcher answers in the console
    push_to_tech(job, m)
    requests.post(f"{BASE}/api/imessage/reactions", headers=AUTH, timeout=30,
                  json={"message_id": m["id"], "reaction": "like"})

@app.post("/webhook")
async def webhook(req: Request, tasks: BackgroundTasks):
    raw = await req.body()
    if not verify(SECRET, req.headers.get("X-ZeroSettle-Signature", ""), raw):
        return Response(status_code=401)
    ev = json.loads(raw)
    m = ev.get("message")
    if ev["event"] != "message.received" or m is None:
        return {"ok": True}                    # your own sends echo back as message.sent
    if (m["id"], ev["event"]) in seen:
        return {"ok": True}                    # retries carry the same id and event
    seen.add((m["id"], ev["event"]))
    tasks.add_task(forward, m)
    return {"ok": True}

Register the receiver with PUT /api/imessage/webhook and store the secret, which is shown once. ZeroSettle expects a 2xx within 10 seconds and retries after 1, 5 and 20 seconds, so forwarding runs after the response. On a failed send, wait retry_after_s on new_contact_limit, retry line_busy after 2 seconds, wait for the customer's reply on awaiting_reply, send the caption alone on attachment_before_reply, and never retry recipient_opted_out.

Which limits and consent rules apply to on-the-way texts?

An on-the-way text costs no new-contact allowance when the customer already has a thread with your number; a first-time customer's first message counts once. The pinch point is the morning rush.

ZeroSettle limits that apply to on-the-way texts, per number (API docs, checked October 12, 2026)
LimitProGrowth
New contacts per day (3 AM to 3 AM Eastern)550
New contacts per rolling 15 minutesNot stated; the daily 5 is the limit3
Unanswered messages to one contact2, then 403 awaiting_reply (texts within 5 minutes of the first count as one)
Photo to a customer who never repliedRefused with 403 attachment_before_reply; send the caption alone
Tapbacks, typing, texts in an existing threadNever a new contactNever a new contact

Source: ZeroSettle iMessage API docs, “Errors, limits, and compliance”. A new contact is a handle the line delivers a text, voice message or photo to first.

The limit math for a morning dispatch

Take 12 techs whose first jobs go en route between 7:45 and 8:00 AM, each to a first-time customer.

  • Confirmation sent at booking: every customer already has a thread, so all 12 texts go out as dispatched at no new-contact cost; customers who replied to the confirmation get the photo, the rest the caption alone. On Growth, each comes from the number the confirmation came from.
  • On-the-way text is the first message, on Pro: it goes as the caption alone, since the customer has never replied. 5 go out; the 6th to 12th return 429 new_contact_limit with retry_after_s and wait for the 3 AM Eastern reset, so Pro can't cover this case.
  • Same case on Growth with 3 numbers: each number takes 3 new contacts per 15 minutes, so the first 9 captions go out as dispatched and the last 3 wait until about 15 minutes after the first send. ZeroSettle refuses a new contact only when every number is full.
  • Across a day on Pro: the 6th first-time customer waits for the 3 AM Eastern reset.

So send the confirmation at booking time: it still counts once, but on the day the customer books, not in the dispatch window. Ask for a reply in it (“Reply with any gate code or parking note”): a reply lets the tech's photo go later, and on every plan an unanswered confirmation plus an unanswered reminder makes the on-the-way text return 403 awaiting_reply, until the customer sends anything back, a tapback included.

Consent comes first

Collect text consent at booking and keep the record; the caption names the company because your first message must say who you are. Keep promotions out of this thread unless the customer separately agreed: the FCC's TCPA rule requires prior express written consent for autodialed advertising and telemarketing calls to mobile numbers, and ZeroSettle's docs say “Marketing texts need written consent.” Send nothing promotional before 8 am or after 9 pm in the recipient's time zone. This isn't legal advice; have counsel review your program.

One trap is specific to arrival texts: a text with the word STOP in it opts the customer out, and directions use it (“stop at the second gate”). Texts and tapbacks to that customer then return 403 recipient_opted_out from every number on your account, and the console disables the conversation, until they text START. Their messages still reach your webhook, so the tech still gets the directions; your number just can't text back.

When ZeroSettle is not a fit for on-the-way texts

ZeroSettle is the wrong tool for arrival texts in these cases:

  • You need a live GPS map. ZeroSettle sends a photo and text, not a map. Send your field-service app's tracking link once the customer has replied or saved your number; before that, their iPhone shows it as plain text and the API returns warning.code: "link_not_tappable".
  • Many customers use Android. There is no SMS fallback. iMessage only reaches iPhone and other Apple products, and texts to other phones go as SMS or RCS, as Apple explains. You can check whether a number is on iMessage before texting it, and the check sends nothing.
  • You want texts to go out with no integration. ZeroSettle doesn't watch your dispatch board or schedule sends; without code or an automation step, a dispatcher sends each photo and caption from the console by hand.
  • Techs insist on texting from their own phones. Replies to personal numbers never reach dispatch.
  • You need proof the customer saw the ETA. The API reports message.sent and message.failed, not delivered or read status.

Related patterns: proof-of-delivery photos and technicians' voice notes as job reports.

How much do on-the-way texts cost?

On-the-way texts cost only the plan price: ZeroSettle charges no per-message fees and doesn't cap how many messages you send; only new contacts, unanswered messages and photos to customers who never replied are limited.

ZeroSettle plans (zerosettle.io, checked October 12, 2026)
PlanPriceFits
Pro$49/monthOne number, 5 new contacts a day, unlimited messages with people who message you first, self-serve, no setup fee
GrowthFrom $599/mo3+ numbers working as one, 50 new contacts a day per number, after a demo

Both include a 30-day money-back guarantee on the subscription; this flow uses no AI credits. See ZeroSettle pricing, get a number, or read the API docs.

Frequently asked questions

What if the tech is running late after the text goes out?

Send a second text in the same thread (“Running about 15 minutes late, now around 10:30”). It is an ongoing conversation, so it costs no new-contact allowance; it is refused only if the customer hasn't answered your last 2 messages.

Can the customer text the technician directly?

Not through ZeroSettle. Replies come to the company number, and your webhook or a dispatcher passes them on, so the tech's personal number stays private.

Can the customer send a photo of the gate or parking spot?

Yes. It arrives in message.received with an image object; fetch it with GET /api/imessage/messages/{id}/image and the same bearer token. A HEIC photo is converted to JPEG first, and if one message holds several photos, only the first is delivered.

Sources

  1. ZeroSettle iMessage API docs: “Send a message”, “Name and photo”, “Attachments”, “Webhooks, step by step” and “Errors, limits, and compliance”. Checked October 12, 2026.
  2. ZeroSettle homepage: plans and prices. Checked October 12, 2026.
  3. Manage location metadata in Photos, Apple Personal Safety User Guide, Apple Support. Checked October 8, 2026.
  4. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.
  5. 47 CFR § 64.1200, Delivery restrictions, paragraph (a)(2), the FCC's TCPA rule (Legal Information Institute, Cornell Law School). 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 the zerosettle.io homepage as of October 12, 2026. Outside facts come from the pages listed under Sources, checked October 8, 2026. Nothing here is 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.