Use cases

How to turn technicians' voice notes into job reports over iMessage

ZeroSettle gives a field-service company an iMessage number that technicians save and send a 30-second voice memo to after each job. Your webhook fetches the recording (converted to .m4a, with duration_ms), transcribes it, and a model pulls out the job number, parts used, time on site and follow-ups to write into Jobber, ServiceTitan or a spreadsheet; a 👍 tapback tells the tech the report is logged. Staff threads are ongoing conversations and inbound messages are never limited, so 30 techs sending 10 reports each a day never touch the new-contact limit on the $49-a-month Pro plan.

Key takeaways

  • What it is: one iMessage number for the whole company that techs send voice memos and job photos to. Each one reaches your webhook as a signed message.received event, and you fetch the file with your bearer token.
  • Who it fits: HVAC, plumbing, cleaning, landscaping and construction companies whose techs carry iPhones.
  • What you build: the transcription, the field extraction and the write to your job software. ZeroSettle delivers the audio file, not a transcript.
  • 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: none for the reports. Only the office's first text to a tech who has never texted the number counts toward Pro's 5 new contacts a day.
  • What it does not do: no SMS fallback for techs on Android, no video files, and no native Jobber or ServiceTitan integration.

Who uses iMessage for field reports?

iMessage field reports fit owners and operations managers of HVAC, plumbing, cleaning, landscaping and construction companies with crews in the field. 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. Here, the people texting the number are your own technicians.

The problem is the truck: a tech who just swapped a capacitor won't fill out a form in the driveway, so parts used, time on site and "they want a thermostat quote" never reach the office. Thirty seconds of talking into Messages is a smaller ask. Voice reports are one of the operations workflows in 50 ways businesses use iMessage.

How iMessage field reports work, step by step

The tech talks, ZeroSettle delivers the recording to your webhook, and your server files the job note and answers with a 👍. The tech types only when a required field is missing.

  1. At onboarding, each tech opts in and texts the number once, then saves it. Your staff table maps that handle to the tech's user in your job software.
  2. After each job, the tech sends a voice memo: job number, work done, parts, time on site, follow-ups. Once the line receives it, ZeroSettle POSTs a signed message.received event with kind: "audio" and an audio object whose duration_ms is filled in.
  3. Your webhook answers 2xx and queues the work. The worker fetches GET /api/imessage/messages/{id}/audio (the audio object's url) with the same bearer token and gets an .m4a (AAC) file.
  4. Speech-to-text makes a transcript, and a model extracts job_number, parts, minutes_on_site and follow_up as JSON. No job number in the memo? Match the tech to today's schedule.
  5. Your code writes the note through Jobber's or ServiceTitan's API, or to a sheet, then calls POST /api/imessage/reactions with "reaction": "like" on the memo.
  6. A missing field gets one question from POST /api/imessage/send, and the answer returns as another message.received.
  7. Photos of finished work arrive as kind: "image", one per text (only the first photo of a message is delivered), and attach to the same job.

Attachment formats and sizes are in what an iMessage API can send and receive. The customer-facing side of the visit, technician on-the-way texts, runs on the same API.

Example conversation: field reports

One HVAC report from the company's side: a voice memo and its 👍, one missing field, and a photo that gets a 👍 once it is attached.

  1. Today 3:42 PM
  2. Technician: [Voice message, 0:27]👍
  3. Business: Logged to job 4471: replaced run capacitor, 1 hr on site. Did you change the filter too?
  4. Technician: yes
  5. Business: Added ✅
  6. Technician: [Photo: finished install]👍
Illustrative example with a fictional HVAC company. Gray bubbles are the technician's (bracketed lines stand for a voice memo and a photo); blue bubbles are sent from the company's number by its server, through the API. Both 👍 tapbacks are the company's: one once job 4471 had its note, one once the photo was attached. The filter question comes from the maintenance checklist.

How do you turn a voice note into a transcript and structured fields?

One webhook route does it: verify ZeroSettle's signature, fetch the audio, transcribe, extract, write to the job and react with like. The verifier is copied from the ZeroSettle webhook docs; tech_for(), transcribe(), extract() and the job-software calls are yours.

python
# reports.py: run with uvicorn reports: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
one_at_a_time = asyncio.Lock()                # the line sends one message at a time

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 post(zs: httpx.AsyncClient, path: str, body: dict) -> httpx.Response:
    async with one_at_a_time:                 # queue here, not on the line
        for _ in range(5):
            r = await zs.post(path, json=body)
            if r.status_code != 429 or r.json()["error"]["code"] != "line_busy":
                break
            await asyncio.sleep(2)            # line_busy: retry after 2 s
    return r                                  # 403 recipient_opted_out is final

async def handle(m: dict) -> None:
    tech = tech_for(m["handle"])              # your staff table
    if tech is None:
        return                                # not one of your techs
    like = {"message_id": m["id"], "reaction": "like"}
    async with httpx.AsyncClient(base_url=BASE, timeout=90,
                                 headers={"Authorization": f"Bearer {TOKEN}"}) as zs:
        if m.get("audio"):                    # a voice memo, already .m4a (AAC)
            if (m["audio"].get("duration_ms") or 0) < 3000:
                return                        # a pocket tap, not a report
            audio = (await zs.get(m["audio"]["url"])).raise_for_status().content
            report = await extract(await transcribe(audio), tech.jobs_today())
            job = await save_report(tech, report)       # Jobber, ServiceTitan or a sheet
            await post(zs, "/api/imessage/reactions", like)
            if question := missing_fields_question(job, report):
                await post(zs, "/api/imessage/send", {"to": m["handle"], "text": question})
        elif m.get("image"):                  # a photo of the finished work
            photo = (await zs.get(m["image"]["url"])).raise_for_status().content
            await attach_photo(tech.last_job(), photo)
            await post(zs, "/api/imessage/reactions", like)
        elif m.get("kind") in ("audio", "image") or not (m.get("text") or "").strip():
            await post(zs, "/api/imessage/send", {"to": m["handle"],   # a video, or a failed download
                       "text": "That one didn't come through. Can you resend it as a voice memo or photo?"})
        else:
            await answer_or_typed_report(tech, m["text"])   # "yes", or a typed report

@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; transcription runs after
    return {"ok": True}

Register the route with PUT /api/imessage/webhook and store the signing secret, which is shown once. ZeroSettle expects a 2xx within 10 seconds and retries after 1, 5 and 20 seconds with the same message.id and event, so the dedupe keeps one memo from filing two notes. The post() helper hands the line one tapback or question at a time and retries line_busy after 2 seconds; it never retries 403 recipient_opted_out. The attachments docs describe the audio and image objects, and how webhook events, signing and retries work covers the rest.

Where a voice report goes wrong

  • No job number in the memo: match the tech to today's schedule, and ask only when two open jobs fit.
  • The recording didn't finish downloading: the event arrives with audio set to null, and the handler asks the tech to resend it.
  • A video of the install: it arrives with no file, so the handler asks for a photo or a voice memo instead.
  • Thirty techs finishing at 5 PM: the line sends one message at a time, and a tapback or question that waits about 12 seconds behind others returns 429 line_busy. That is why post() queues the handler's sends behind one lock and retries after 2 seconds.

The same fetch-then-tapback pattern files receipt photos for bookkeepers and customers' voice answers in iMessage feedback surveys.

Which limits and consent rules apply to field reports?

Field reports don't hit ZeroSettle's limits: inbound messages are never limited, tapbacks never count as new contacts, and a tech who texted the number first is an ongoing conversation. Consent is narrow: employees only, for internal use, with an opt-in at onboarding.

ZeroSettle limits that apply to field reports, per number (API docs, checked October 12, 2026)
What the company's number handlesProGrowth
Voice memos, photos and texts techs sendUnlimitedUnlimited
The line's 👍 tapbacksNever a new contactNever a new contact
Questions to a tech who has textedUnlimited, but 2 unanswered, then 403 awaiting_reply; the tech's next memo resets itUnlimited, but 2 unanswered, then 403 awaiting_reply; the tech's next memo resets it
First text to a tech who hasn't texted yet5 a day50 a day, 3 per rolling 15 minutes

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

The math: 30 techs sending 10 reports each a day is 300 voice memos and 300 tapbacks a day, and none of it counts against a plan limit. Only onboarding can: if the office texts 30 new techs first on Pro, that is 5 a day over six days. If each tech texts the number first, it is zero.

Keep the opt-in, and watch the word "stop"

Keep each tech's opt-in record. A text with the word STOP in it, or a clear request like "take me off your list", opts the sender out across every number on your account, even "have to stop at the supply house first"; saying it in a voice memo doesn't count. Their memos still arrive, but tapbacks and questions to them return 403 recipient_opted_out until they text START as the whole message. More in how STOP and opt-outs work. Texting employees can carry its own rules where you operate; this isn't legal advice.

When ZeroSettle is not a fit for field reports

ZeroSettle is the wrong tool when techs carry Android phones, when you expect a ready-made Jobber or ServiceTitan app, when reports need video, or when nobody can run the code.

  • Techs on Android: iMessage works between iPhone and other Apple products, and other phones use SMS or RCS, as Apple explains. ZeroSettle has no SMS fallback, so this workflow needs techs on iPhone. Keep your field app's form for the rest.
  • You expect a native Jobber or ServiceTitan app: ZeroSettle has none; you build the write through their APIs. Jobber's pricing page mentions its Open API ("API Tour with API Access"), and ServiceTitan's help center says customers on The Works or Enterprise Plus package can create Customer-Built Apps, so check what your plan allows before you build.
  • You need video reports: inbound video arrives with no file.
  • You have no developer: transcription, extraction and the job write are code you run. ZeroSettle's hosted AI agent (Beta) answers texts but doesn't file job notes, and on a number it manages, inbound texts aren't sent to your webhook.

How much does iMessage for field reports cost?

Field reports cost the plan price only: ZeroSettle has no per-message fees, and messages are unlimited (counted) in both directions.

ZeroSettle plans for field reports (zerosettle.io, checked October 12, 2026)
PlanPriceFits a company that
Pro$49/month, no setup feewants one number for the whole crew, self-serve, with unlimited messages with people who message you first and a 30-day money-back guarantee
GrowthFrom $599/moneeds several numbers working as one, with dedicated support, set up after a demo

Speech-to-text and model costs come from the providers you pick. 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

Which transcription service should I use?

Any speech-to-text API that accepts .m4a; ZeroSettle only carries the file. OpenAI's file transcription and Amazon Transcribe's batch jobs both list M4A as an input format.

What should techs say in the memo?

A five-item card in the truck works: job number, what you did, parts, time on site, anything to follow up. Your code asks for whatever the memo leaves out.

Can the office answer a tech by hand?

Yes. Report threads show live in the ZeroSettle console, where a dispatcher can reply in the same thread, and the account owner can invite teammates to the console.

How long does ZeroSettle keep the recordings?

Messages are kept indefinitely for now, and GET /api/imessage/messages/{id}/audio returns a recording later. Store a copy with the job anyway, because deleting a tech's conversation removes its media too.

Sources

  1. ZeroSettle iMessage API docs: "Send a message" (reactions), "Attachments", "Read messages", "Webhooks, step by step" and "Errors, limits, and compliance". Checked October 8, 2026.
  2. ZeroSettle homepage: plans and prices. Checked October 8, 2026.
  3. File transcription, OpenAI API docs (supported input formats include m4a). Checked October 8, 2026.
  4. Data input and output, Amazon Transcribe Developer Guide (batch formats include M4A). Checked October 8, 2026.
  5. Understand approved integration paths in ServiceTitan, ServiceTitan Help Center. Checked October 8, 2026.
  6. Jobber pricing, Jobber ("API Tour with API Access"). Checked October 8, 2026.
  7. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. 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. 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.