Use cases

How clients text receipts to their bookkeeper over iMessage

ZeroSettle gives a bookkeeping or tax-prep firm an iMessage number that clients save and text receipt photos to as they happen; your webhook fetches each image, runs OCR or passes it to your expense tool, and confirms with a 👍 tapback. At month end the bookkeeper texts only what's missing. Keep W-2s, statements and Social Security numbers on your secure portal, because ZeroSettle's terms say message content may not be encrypted. On the $49-a-month Pro plan, the receipts clients send are never limited; only the firm's first text to a new client counts toward 5 a day.

Key takeaways

  • What it is: an iMessage number for the firm that clients text receipt photos to. Each photo reaches your server by signed webhook, and your code confirms it with a 👍 through ZeroSettle's REST API.
  • Who it fits: bookkeepers, fractional CFOs, small CPA firms and enrolled agents whose clients mostly use iPhone.
  • Price: Pro is $49/month, starting with one number. Growth, for several numbers working as one, is from $599/mo. No per-message fees.
  • Main limit: 5 new contacts a day per number on Pro, counting only the firm's first text to each client. Receipts clients send are never limited.
  • What it does not do: no PDFs, videos or other files (photos and voice notes only, one photo per text), no SMS fallback for Android clients, and no promise that message content is encrypted.

Who uses iMessage for receipt collection?

iMessage receipt collection fits bookkeepers, fractional CFOs, small CPA firms and enrolled agents whose small-business and freelance clients live on their iPhone but rarely open the client portal. 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 problem is timing. A receipt is easy to photograph at the register and hard to find three weeks later, when month-end close turns into chasing shoeboxes and tax season adds daily "is my return done?" calls. Receipt capture is one of the operations workflows in 50 ways businesses use iMessage.

How iMessage receipt collection works, step by step

Clients text receipts, your webhook files each photo and answers with a 👍, and at month end the bookkeeper texts only the charges still missing a receipt.

  1. The client opts in and saves the number. The engagement letter names the firm's number and asks for consent to texts about the books. ZeroSettle shares the firm's name and photo with the first message the firm sends. Leave the portal link out of that welcome: a link in a first message to someone who has never texted the number shows as plain text until the client replies.
  2. The client texts each receipt as it happens, one photo per text, with an optional caption ("lunch with the Acme client"). Each reaches your webhook as a signed message.received event with kind: "image" and the caption as text; HEIC arrives as JPEG.
  3. Your webhook fetches the image from GET /api/imessage/messages/{id}/image with the same bearer token, runs OCR or passes it to your expense tool, and files merchant, date and amount under the client.
  4. Your server confirms with a 👍 through POST /api/imessage/reactions with "reaction": "like". To the client, a receipt without one hasn't been logged.
  5. As charges post, your ledger matches them to filed receipts, and at month end the bookkeeper texts only the charges still missing one, from code (POST /api/imessage/send) or the ZeroSettle console. The 👍 means "filed", not "matched": a receipt texted at the register usually arrives before its charge reaches the bank feed.
  6. In tax season, the same thread carries the milestones (ready to sign, e-filed, accepted), with no daily cap on an ongoing conversation; on every plan, a third unanswered update waits for a reply. Sensitive documents go through your portal, whose link is tappable now that the client has texted.

A client with no paper receipt (a parking meter, a cash tip) can send a voice note; it arrives as an .m4a file for a transcription step like the one that turns technicians' voice notes into job reports. Insurance claim photos use the same inbound-photo pattern.

Example conversation: receipt collection

This thread shows a receipt filed mid-month, then the September close, as the firm sees them in the ZeroSettle console.

  1. Wed, Sep 9, 12:41 PM
  2. Client: [Photo: Staples receipt]👍
  3. Wed, Sep 30, 10:05 AM
  4. Business: Hi Carla, closing September. Just 2 missing: Home Depot $212.40 (9/14) and Shell $58 (9/22). Got photos?
  5. Client: [Photo: Home Depot receipt]👍
  6. Client: [Photo: Shell receipt]👍
  7. Business: Perfect, you're all set for September 🙌
Illustrative example with a fictional firm, Tallowmere Books. Blue bubbles are sent from the firm's number; gray bubbles are the client's, and bracketed lines stand for photos. Each 👍 is the firm's tapback, added by its webhook once the receipt was filed.

Setup: a receipt-capture webhook

A receipt-capture webhook is one route that checks ZeroSettle's signature, answers 2xx at once, then fetches each inbound photo, files it and reacts with like. The verifier is the one in the ZeroSettle webhook docs; how webhook events, signing and retries work covers the rest.

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

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

def file_receipt(handle: str, img: bytes, caption: str) -> bool:
    return False   # your OCR or expense tool: True once it's filed

def verify(header: str, raw: 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, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

async def call(zs: httpx.AsyncClient, path: str, body: dict) -> None:
    r = await zs.post(path, json=body)
    while r.status_code == 429 and r.json()["error"]["code"] == "line_busy":
        await asyncio.sleep(2)                # one send at a time per number
        r = await zs.post(path, json=body)

async def handle(m: dict) -> None:
    text = (m.get("text") or "").strip()
    async with httpx.AsyncClient(base_url=BASE, timeout=90,
                                 headers={"Authorization": f"Bearer {TOKEN}"}) as zs:
        if m.get("image"):                    # a receipt photo
            img = (await zs.get(m["image"]["url"])).content
            caption = "" if text == "📷 Photo" else text
            if await asyncio.to_thread(file_receipt, m["handle"], img, caption):
                await call(zs, "/api/imessage/reactions",
                           {"message_id": m["id"], "reaction": "like"})
        elif m.get("kind") == "image" or not text:   # no file: failed photo, lone PDF or video
            await call(zs, "/api/imessage/send", {"to": m["handle"], "text":
                "That one didn't come through. Could you send it as a photo or a screenshot?"})

@app.post("/webhook")
async def webhook(req: Request, tasks: BackgroundTasks):
    raw = await req.body()
    if not verify(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 or (m["id"], ev["event"]) in seen:
        return {"ok": True}                   # echoes of your own sends, and retries
    seen.add((m["id"], ev["event"]))
    tasks.add_task(handle, m)                 # answer 2xx now; OCR runs after
    return {"ok": True}

Register the route with PUT /api/imessage/webhook and store the signing secret, which is shown once. ZeroSettle retries a failed delivery after 1, 5 and 20 seconds with the same message.id and event, hence the dedupe. The call helper covers a client who texts ten receipts at once: ZeroSettle sends one message at a time per number, a 👍 queued about 12 seconds returns 429 line_busy, and the docs say to retry after 2 seconds. A photo your OCR can't read gets no 👍 and waits in the console, like any plain question.

Also file each image in your own document system: ZeroSettle says only that messages are kept "indefinitely for now", and the IRS lists receipts among a business's supporting documents and says "all requirements that apply to hard copy books and records also apply to electronic records."

Three ways a receipt goes missing

  • Several photos in one message: ZeroSettle delivers only the first, so clients send one receipt per text.
  • A PDF or a video: no file arrives. Sent alone it comes through empty, and the handler asks for a photo or a screenshot; sent with words, it reads as a plain text and waits for the bookkeeper.
  • A photo whose download never finished: image is null, and the same reply asks for it again.

Limits and consent for receipt collection

Receipt collection rarely meets ZeroSettle's limits: inbound messages are never limited, and only the firm's first message to a client who has never texted counts as a new contact.

ZeroSettle limits that apply to receipt collection, per number (API docs, checked October 12, 2026)
What the firm's number handlesProGrowth
Receipt photos and texts clients sendUnlimitedUnlimited
The firm's 👍 tapbacksNot countedNot counted
First text to a client who hasn't texted yet5 a day50 a day, 3 per rolling 15 minutes
Unanswered texts to one client2, then 403 awaiting_reply2, then 403 awaiting_reply

Source: ZeroSettle iMessage API docs, "Errors, limits, and compliance". The day runs 3 AM to 3 AM Eastern.

The math: on Pro, a firm moving 60 clients onto the number can welcome 5 a day, so all 60 within 12 days. Clients who text first, from a link in the engagement letter, never count (see how to get customers to text you first), and the receipts, milestones and tapbacks that follow use none of the allowance. On every plan, a quiet client gets the missing-receipts text and one nudge, then nothing until they send something back (a tapback counts).

Keep the signed engagement letter as the consent record, and have the first text say who is writing. A pitch for a tax-planning package is likely promotional: treat it as needing marketing consent and, per ZeroSettle's docs, never send it before 8 am or after 9 pm in the client's time zone. This isn't legal advice.

Never ask for tax forms or SSNs by text

Keep Social Security numbers, full account numbers, W-2s and statements out of the thread. The FTC's Safeguards Rule guide names tax preparation firms among the financial institutions it may cover and lists "Encrypt customer information on your system and when it's in transit" among its requirements, while ZeroSettle's terms say message content may not be encrypted. More in what ZeroSettle secures, and what it doesn't claim. This isn't legal advice; ask counsel how the rule applies to your firm.

A text with the word STOP in it, even "I'll stop by Friday with the folder", or a clear request like "take me off your list", opts the client out of every number on the firm's account. Their receipts still arrive, but each 👍 and text to them returns 403 recipient_opted_out until they text START as the whole message; flag that error for a phone call. See how STOP works on a ZeroSettle line.

When ZeroSettle is not a fit for receipt collection

ZeroSettle is the wrong tool for receipt collection when documents are sensitive, clients use Android, or paperwork arrives as files.

  • Tax forms, SSNs or account numbers: use your secure portal; ZeroSettle's terms say message content may not be encrypted.
  • Mostly Android clients: there is no SMS fallback, so keep email or portal upload for them. ZeroSettle checks whether a number is on iMessage before the first message and sends nothing if it isn't.
  • Multi-page PDFs and statements: files don't arrive, only photos and voice notes.
  • A native integration with your accounting software: ZeroSettle has none. Build it on the webhook, in code or with Zapier, n8n or Make.
  • Proof a client read the missing-receipts text: the API reports message.sent (the message left the line), not delivered or read.

How much does iMessage receipt collection cost?

On ZeroSettle's side, receipt collection costs the plan price: there are no per-message fees and messages are unlimited on both plans, so tax season costs the same as August.

ZeroSettle plans for receipt collection (zerosettle.io, checked October 8, 2026)
PlanPriceFits a firm that
Pro$49/month, no setup feewants one number for every client, self-serve, with 5 new contacts a day per number and unlimited messages with people who message you first
GrowthFrom $599/moneeds several numbers working as one, with dedicated support, set up after a demo

Pro has a 30-day money-back guarantee and can be cancelled anytime; AI credits apply only to the hosted AI agent, which this workflow doesn't use. See ZeroSettle plans and pricing, read the iMessage API docs, or get a number.

Frequently asked questions

Can clients send emailed e-receipts?

Not as PDFs, which arrive with no file. A screenshot of the e-receipt is a photo, so it arrives and gets its 👍 like any other receipt.

What if a client texts a photo of a W-2 anyway?

ZeroSettle stores it like any photo, and messages are kept indefinitely for now. Reply with the portal link and delete your webhook's copy. DELETE /api/imessage/conversations/{handle} removes the photo from ZeroSettle only with the client's whole stored thread; it stays in their own Messages, and your next text to them counts as a new contact.

Can two bookkeepers work from the same number?

Yes. The account owner can invite teammates to the ZeroSettle console, where they answer the same conversations from the same number.

Can the hosted AI agent answer "is my return done?" texts?

Not on the number your receipt webhook uses. On a line group managed by the AI agent (in Beta), inbound messages go to the agent instead of your webhook, and your code and console can't send (403 managed_by_zerosettle). The agent also knows only the business context you write, not whether a client's return is filed.

Sources

  1. ZeroSettle iMessage API docs: "Send a message" (reactions), "Attachments", "Read messages", "AI agent", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 8, 2026.
  2. ZeroSettle iMessage API Terms of Service, section 13, "Compliance and Client Responsibility" (engagement privacy disclaimer). Last modified September 28, 2026.
  3. FTC Safeguards Rule: What Your Business Needs to Know, Federal Trade Commission. Checked October 8, 2026.
  4. What kind of records should I keep, Internal Revenue Service. 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, the zerosettle.io plans and the iMessage API Terms as of October 8, 2026. Outside facts come from the pages listed under Sources, checked the same day. Tallowmere Books is a fictional firm. 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.