Use cases

Proof-of-delivery photos over iMessage for local delivery

ZeroSettle lets a florist, bakery or local courier text the buyer a proof-of-delivery photo with a caption (“Delivered to the front porch at 2:14 PM”) as one iMessage bubble from the business's own number, as soon as the driver marks the drop-off, provided the buyer has replied to that number at least once (ZeroSettle sends photos only to contacts who have replied). A JPEG under 5 MB and no more than 2,048 px on its long edge goes out exactly as uploaded; a photo over 2,048 px, or a HEIC, is re-encoded to JPEG, which strips EXIF and GPS location. Text the purchaser who opted in at checkout, never the gift recipient.

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 in a server facility, 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 and a caption in text; the buyer gets one photo bubble with the caption underneath.
  • 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: each first-time buyer counts once as a new contact, and Pro allows 5 a day per number. A photo can only go to a buyer who has replied; before that, the send returns 403 attachment_before_reply.
  • What it does not do: no SMS fallback for Android buyers, no video, no signature capture, and no delivered or read receipts in the API.

Who uses iMessage for proof of delivery?

Proof-of-delivery texts fit businesses that drive their own orders to a door: florists, bakeries, caterers, furniture and appliance stores, and local couriers. For a gift order, the photo is often the only way the buyer learns the flowers or the cake arrived, because the buyer is not at the address.

The photo replaces three things: “was it delivered?” calls on a busy afternoon; MMS photos, which above 600 KB “may be compressed and distorted due to carrier constraints”, per Klaviyo's MMS image guidance; and drivers texting from personal numbers, where buyers' replies never reach dispatch. Shipped parcels belong to “where is my order?” support, a photo of who is coming to the door to technician on-the-way texts, and photos sent during a service to pet care updates; more workflows are in 50 ways businesses use iMessage.

How iMessage proof-of-delivery texts work, step by step

Your delivery software calls ZeroSettle's send endpoint at the drop-off (and, if you like, when the driver heads out), and ZeroSettle carries the buyer's reply back. ZeroSettle does not watch your driver app.

  1. At checkout, a box reads “Text me delivery updates and a photo”. Store the buyer's number, the ticked box and the time with the order. Then get a reply before delivery day: send an order confirmation that asks for one (“Reply with any delivery notes”), or let the buyer text first from an sms: link on the order page. ZeroSettle refuses photos to anyone who has never replied with 403 attachment_before_reply.
  2. Optionally, when the driver heads to the stop, your backend sends an “on the way, about 15 minutes out” text with POST https://portal.zerosettle.io/api/imessage/send. The text names the shop, and ZeroSettle shares your brand name and photo with it (see what customers see).
  3. When the driver marks the drop-off, the app uploads the photo, and your backend calls the same endpoint with image_b64 and the caption as text. The call returns once the line hands the bubble to Apple, normally in 2–5 seconds (at most about 105 for a photo). If the buyer never replied, send the caption as a text and email the photo instead.
  4. The response carries the stored message id, and the same record reaches your webhook as message.sent. Store the id with the order as your record that the photo left the line.
  5. A reply such as “leave it at the side door” reaches your webhook as message.received about 1–2 seconds after the line receives it, and shows live in the ZeroSettle console for the dispatcher.

What happens to the driver's photo

ZeroSettle sends a delivery photo untouched only when it is a JPEG, PNG or GIF within 2,048 px and 5 MB, and that decides whether the photo's location data reaches the buyer.

How ZeroSettle handles a delivery photo (API docs, checked October 12, 2026)
The delivery app uploadsThe buyer receivesEXIF and GPS
JPEG, PNG or GIF within 2,048 px and 5 MBThe file exactly as uploadedKept
JPEG, PNG or GIF over 2,048 pxA re-encoded JPEGStripped
HEIC, WebP, TIFF or BMPA re-encoded JPEGStripped
Anything over 5 MB decodedNothing: 422 invalid_request—

Source: ZeroSettle iMessage API docs, “Attachments”.

The first row is the one to watch. When Location Services is on for the Camera app, the coordinates are “embedded into each photo”, per Apple, so a pass-through JPEG of a gift drop-off carries the recipient's location to anyone the buyer forwards it to. Resizing to 2,048 px and re-saving without EXIF, as the code below does, removes it and keeps the photo sharp. The full attachment rules are in what an iMessage API can send.

Example conversation: a delivery photo for a gift order

A buyer orders flowers for a friend and ticks the box at checkout. The buyer replies to the on-the-way text, so the photo can follow, and gets two texts and a tapback from the shop's number; the friend gets nothing.

  1. Today 1:59 PM
  2. Business: Petal & Stem here: your arrangement for Grace is on the way, about 15 minutes out.
  3. Customer: yay! she's home all afternoon
  4. Today 2:14 PM
  5. Business: [Photo: the arrangement on Grace's front porch]
    Delivered to the front porch at 2:14 PM 💐
  6. Customer: oh that's gorgeous, thank you!!❤️
Illustrative example with a fictional florist. Blue bubbles are sent from the shop's number through the API; the gray bubble is the buyer's reply, and the ❤️ is the shop's tapback (POST /api/imessage/reactions).

Setup: from a drop-off event to a photo send

The send, a resize that keeps the photo within ZeroSettle's pass-through size, and a signed webhook receiver fit in one file, following the send docs and the webhook steps. Your backend calls delivered() when the driver app uploads the photo, with the buyer's number, the photo's bytes and the caption.

python
# delivery_photos.py: run with  uvicorn delivery_photos:app --port 8100
import base64, hashlib, hmac, io, json, os, time
import requests
from fastapi import BackgroundTasks, FastAPI, Request, Response
from PIL import Image, ImageOps, UnidentifiedImageError   # pip install pillow

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(payload: dict) -> dict:                 # the on-the-way text is send({"to": ..., "text": ...})
    for _ in range(5):
        r = requests.post(f"{BASE}/api/imessage/send", headers=AUTH, json=payload, timeout=120)
        body = r.json()
        if r.ok or body["error"]["code"] != "line_busy":
            return body
        time.sleep(2)                            # one send at a time per line
    return body

def delivered(buyer: str, photo: bytes, caption: str) -> dict:   # driver marks the drop-off
    try:
        img = ImageOps.exif_transpose(Image.open(io.BytesIO(photo)))
    except UnidentifiedImageError:               # HEIC: ZeroSettle re-encodes it, dropping GPS
        return send({"to": buyer, "image_b64": base64.b64encode(photo).decode(), "text": caption})
    img.thumbnail((2048, 2048))                  # within 2,048 px: sent exactly as is
    out = io.BytesIO()
    img.convert("RGB").save(out, "JPEG", quality=85)             # saved without EXIF: no GPS
    return send({"to": buyer, "image_b64": base64.b64encode(out.getvalue()).decode(),
                 "image_format": "jpg", "text": caption})

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 note_for_driver(m: dict) -> None: ...        # your dispatch app: add m["text"] to the open order

@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" and m and (m["id"], ev["event"]) not in seen:
        seen.add((m["id"], ev["event"]))         # retries carry the same id and event
        tasks.add_task(note_for_driver, m)       # "leave it at the side door"
    return {"ok": True}

Register the receiver with PUT /api/imessage/webhook and keep the secret, which is shown once. When several drivers finish together, sends queue on the line, and one that waits about 12 seconds returns 429 line_busy, which send() retries after 2 seconds. Requeue a new_contact_limit text after error.retry_after_s, never retry recipient_opted_out, and on 403 attachment_before_reply (the buyer never replied) send the caption as a text and email the photo.

Limits and consent for proof-of-delivery texts

Each buyer counts once as a new contact, on the first text the line delivers to them (a photo can't be the first message); the photo after an on-the-way text belongs to an ongoing conversation and does not count again. The limits apply to the console as well as the API.

ZeroSettle limits that apply to delivery 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 minutes—3
Unanswered messages to one contact2, then 403 awaiting_reply2, then 403 awaiting_reply
Photo to a buyer who has never repliedRefused: 403 attachment_before_replyRefused: 403 attachment_before_reply
Messages sent and receivedUnlimited (counted)Unlimited (counted)

Source: ZeroSettle iMessage API docs, “Errors, limits, and compliance”. Messages sent within 5 minutes of the first count as one.

The limit math for Valentine's Day

Take a florist with 80 deliveries on February 14, all to buyers the shop's number has never texted.

  • First text on the day, one Pro number: 5 buyers hear from you; the other 75 sends return 429 new_contact_limit until the 3 AM Eastern reset, after the holiday.
  • Growth with three numbers: 150 a day fit, but only 9 per 15 minutes (3 per number), so if 15 buyers get their first text in the same quarter hour, 6 of those sends wait.
  • Text at checkout instead: the buyer then counts on the order day. If no single order day brings more than 5 new buyers, one Pro number covers them all, and the February 14 texts go to ongoing conversations.
  • On either plan, that makes three unanswered messages: confirmation, on-the-way text and photo, so the photo returns 403 awaiting_reply unless the buyer replied or tapbacked in between, and a buyer who has never replied can't receive the photo at all. Ask for a reply in the confirmation (“Reply with any delivery notes”).

Consent comes from the buyer, not the recipient

Text only the purchaser who ticked the box, keep the record, and name the shop in your first message. The buyer's checkbox is not the gift recipient's consent. Keep promotions, such as a Mother's Day pre-order offer, out of this thread unless the buyer separately agreed to them, and send any promotion only between 8 am and 9 pm in the buyer's time zone. A text with the word STOP in it, or a clear request like “take me off your list”, opts the buyer out of every number on your account. This isn’t legal advice.

When ZeroSettle is not a fit for proof of delivery

ZeroSettle is the wrong tool for delivery confirmations in these cases:

  • You would text gift recipients who didn't opt in. ZeroSettle suspends lines used for unsolicited messaging.
  • Peak days bring hundreds of first-time buyers to one Pro number. Pro allows 5 new contacts a day per number; move first texts to the order day or use Growth's several numbers.
  • Buyers won't reply before delivery. ZeroSettle sends photos only to contacts who have replied, so a buyer who never answers gets the caption as text, and the photo by email.
  • Many buyers use Android. iMessage reaches Apple users only; Apple's own Messages app sends to non-Apple phones as SMS, MMS or RCS, as Apple explains, and ZeroSettle has no such fallback. Email those buyers the photo. You can check whether a number is on iMessage first, and the check sends nothing.
  • You need signature capture or video proof. ZeroSettle sends photos and voice messages, not video or files; keep signatures in your delivery app.
  • You need proof the buyer saw the photo. The API reports message.sent and message.failed, not delivered or read status.

How much do iMessage proof-of-delivery texts cost?

Proof-of-delivery texts cost only the plan price: ZeroSettle charges no per-message fees, and messages are unlimited (counted).

ZeroSettle plans for delivery photos (zerosettle.io and API docs, checked October 12, 2026)
PlanPriceFits
Pro$49/monthOne number to start, 5 first-time buyers a day per number, 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, set up after a demo

Both plans carry a 30-day money-back guarantee on the subscription. AI credits are used only by the hosted AI agent, so this flow uses none. See ZeroSettle pricing, the iMessage API docs, or get a number.

Frequently asked questions

Can a shop without a driver app send the photo by hand?

Yes. The ZeroSettle console sends photos too: use the camera button next to the message box, or paste or drop a picture in, add a caption and send. The same rules apply as for the API: the new-contact limits, and photos only to buyers who have replied.

Can the caption include a tracking or review link?

It can, but the link is tappable only once the buyer has replied to your number or saved it as a contact. Before that, iPhone shows the link as plain text and the send response carries warning.code: "link_not_tappable" (see why first-message links aren't tappable).

Can I take back a photo sent to the wrong buyer?

No. Deleting the conversation removes every message with that buyer stored at ZeroSettle, but “nothing already delivered is unsent.” Match each photo to the order ID the driver marked, not to the driver's last stop.

Can the buyer text back a photo, such as where to leave the order?

Yes. It reaches your webhook as message.received with an image object, and you fetch the file with GET /api/imessage/messages/{id}/image and the same bearer token. A HEIC arrives as a JPEG, and if the buyer sends several photos in one message, only the first is delivered.

Sources

  1. ZeroSettle iMessage API docs: “Send a message”, “Attachments”, “Read messages”, “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. Understanding MMS image and GIF best practices, Klaviyo Help Center. 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 in a server facility, 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.

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.