Use cases

Tenant maintenance requests by iMessage, with photos

ZeroSettle gives a property manager an iMessage number that tenants text maintenance requests and photos to. Each message reaches your webhook about 1–2 seconds later, where your code opens a work order in your property-management software and confirms with a 👍 tapback. Because tenants text first, their requests never count toward the new-contact limit (5 a day on the $49-a-month Pro plan), however many units you manage. ZeroSettle's terms don't guarantee delivery of any particular message, so keep an emergency phone number for active leaks or no heat.

Key takeaways

  • What it is: an iMessage number tenants text a description and a photo to; your server gets each message as a signed webhook and turns it into a work order.
  • Who it fits: residential property managers, small landlords and HOA managers whose tenants mostly use iPhone.
  • Price: Pro is $49/month for one number. Growth, for several numbers working as one, is from $599/mo.
  • Main limit: Pro's 5 new contacts a day per number counts only texts you start to a tenant who has never texted the number, such as a first rent reminder. Requests never count.
  • What it does not do: no SMS fallback for Android tenants, no inbound video, only the first photo of a multi-photo message, and no promise that any one message arrives.

Who uses iMessage for maintenance requests?

iMessage maintenance requests suit residential property managers, small landlords and HOA managers with dozens to a few thousand units, whose tenants would rather text than log in to a 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 the blind dispatch: a tenant calls about a leak with no details or photo, a technician goes out without the right part, and nobody knows whether the request was seen. A text thread carries the photo, the 👍 and the work-order number. Text-a-photo quotes for trades and insurance claim photos use the same inbound-photo pattern; the use cases hub lists more.

How iMessage maintenance requests work, step by step

The tenant texts your number, ZeroSettle posts each message to your webhook, and your code opens the work order and answers in the same thread. ZeroSettle has no built-in property-management integration; the work-order calls are yours.

  1. Publish the number in the lease packet and on a laundry-room sign: “Text maintenance requests here,” with a QR code that encodes sms:+15555550100. Apple documents the sms: scheme as launching the Messages app; see how to get customers to text you first.
  2. The tenant texts a description and a photo. Each reaches your webhook as a signed message.received event in about 1–2 seconds. A photo carries kind: "image" and an image.url; HEIC photos arrive as JPEG.
  3. Match the sender to a tenant. The handle is usually the tenant's number, but it can be an iMessage email address (for example from an iPad with no number), so match on either.
  4. Open a work order and attach the photo. Buildium's Open API, for example, has “Create a work order” and “Create a resident request” endpoints and a task-history file upload. Fetch the photo with GET /api/imessage/messages/{id}/image and your bearer token.
  5. Confirm. POST /api/imessage/reactions with "reaction": "like" puts a 👍 on the tenant's message; a text with the work-order number follows.
  6. Keep the visit in the thread. Text the vendor's arrival window from the ZeroSettle console or your software; after the job, send the vendor's “after” photo as image_b64 (up to 5 MB) with a caption in the same bubble.

Ask tenants for one photo per text and no videos: of several photos, ZeroSettle delivers only the first, and a video arrives with no file.

Emergencies need a phone number

ZeroSettle's terms say “delivery of any particular message or feature is not guaranteed.” Print an emergency number on the same sign for active leaks or no heat.

Example conversation: a leak under the kitchen sink

One request, from the first text to the “after” photo.

  1. Tue 7:42 AM
  2. Tenant: Unit 3B, kitchen sink is leaking under the cabinet👍
  3. Tenant: [Photo of water under the sink]
  4. Business: Thanks, Aisha. Work order #2214 is open.
  5. Business: Did you turn off the valve under the sink?
  6. Tenant: yes
  7. Business: Perfect. A plumber can come tomorrow 10–12. Does that work?
  8. Tenant: yes
  9. Wed 11:58 AM
  10. Business: [Photo of the new P-trap]
    All fixed: new P-trap. Let us know if you see any more water.
Illustrative example with a fictional property manager. Blue bubbles are sent from Maple Property Co's number; gray bubbles are the tenant's. The 👍 is the property manager's tapback.

Aisha's text and photo are two message.received events: the text opens work order #2214 and gets the 👍, and the photo is attached to it. A coordinator typed the two questions and sent the “after” photo from the console.

Setup: webhook to work order

One receiver verifies ZeroSettle's signature, answers 2xx at once, then opens the work order, attaches the photo and confirms. It follows the webhook steps in the ZeroSettle docs; tenants, pms and notify_office stand in for your database, your property-management API and however your office gets alerts.

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

ZS = httpx.AsyncClient(base_url=os.environ.get("ZEROSETTLE_BASE_URL", "https://portal.zerosettle.io"),
                       headers={"Authorization": f"Bearer {os.environ['ZEROSETTLE_TOKEN']}"}, timeout=120)
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
app, seen, locks = FastAPI(), set(), defaultdict(asyncio.Lock)

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

async def handle(m: dict) -> None:
    tenant = tenants.by_handle(m["handle"])          # your DB: phone number or iMessage email
    body, is_image = (m.get("text") or "").strip(), m.get("kind") == "image"
    if tenant is None or not (body or is_image):     # unknown sender, or a video with no file
        return notify_office(m)                       # a person answers from the console
    wo = pms.open_work_order(tenant)                  # the tenant's unclosed order, if any
    if wo:
        pms.add_note(wo, body)                        # a later "yes" joins the open order
    else:
        wo = pms.create_work_order(tenant, description=body)
        r = await ZS.post("/api/imessage/reactions", json={"message_id": m["id"], "reaction": "like"})
        if r.status_code == 403 and r.json()["error"]["code"] == "recipient_opted_out":
            pms.flag_for_call(wo)                     # they texted "stop": see below
        else:
            await ZS.post("/api/imessage/send", json={"to": m["handle"],
                          "text": f"Thanks, {tenant.first_name}. Work order #{wo.number} is open."})
    if m.get("image"):                                # null if the download never finished
        img = await ZS.get(m["image"]["url"])         # /api/imessage/messages/{id}/image
        pms.attach_file(wo, img.content, img.headers["content-type"])

async def serialized(m: dict) -> None:
    async with locks[m["handle"]]:                    # a text and a photo land concurrently
        await handle(m)

@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 m and ev["event"] == "message.received" and (m["id"], ev["event"]) not in seen:
        seen.add((m["id"], ev["event"]))              # retries repeat the id and event
        tasks.add_task(serialized, m)
    return {"ok": True}

The per-tenant lock matters because ZeroSettle can deliver a line's events concurrently, so a text and a photo could otherwise each open a work order. Failed deliveries are retried after 1, 5 and 20 seconds (delivery status, webhook retries and latency). Register the receiver with PUT /api/imessage/webhook; receiving iMessages with a webhook covers testing.

Limits and consent for maintenance requests

Maintenance requests use none of ZeroSettle's new-contact limit, because anyone who texted the line first is never limited. The limit counts only the first text, photo or voice message you send to a tenant who has never texted the number.

ZeroSettle limits for a maintenance line, per number (API docs, checked October 12, 2026)
MessageProGrowth
Tenant's request, photos and repliesUnlimitedUnlimited
Your replies in that threadAt most 2 messages until the tenant replies (texts within 5 minutes count as one)
A text you start to a tenant who never texted the number5 a day50 a day, 3 per 15 minutes

Source: ZeroSettle iMessage API docs, “Errors, limits, and compliance”. The day runs 3 AM to 3 AM Eastern; tapbacks never count.

For a 240-unit portfolio, requests from all 240 tenants use no allowance. A first rent reminder to 240 tenants who have never texted takes 48 days on one Pro number at 5 a day, and every tenant who has sent a request drops out of that count. On any plan, end the arrival-window text with a question. If the tenant answers neither it nor the work-order text, and the two went out more than 5 minutes apart, ZeroSettle refuses the “after” photo with 403 awaiting_reply. Any reply from the tenant, even a tapback, resets the count.

When a request contains the word “stop”

A text with the word STOP in it opts the sender out, and a maintenance request can contain it: “the toilet won't stop running” opts that tenant out of every number on your account. The request still reaches your webhook, but your tapback and replies return 403 recipient_opted_out, so the handler flags the order for a phone call. Texting START, as the whole message, turns texts back on; if the tenant asks on that call instead, ZeroSettle can clear the opt-out for you.

Rent reminders and community notices are texts you start, so they need the tenant's consent for that purpose and a record you can show, such as a lease clause or a separate form. Nothing promotional goes out before 8 am or after 9 pm in the tenant's time zone. Legally required notices still go through their required channels. See the compliance section of the docs. This isn't legal advice; check your notice and consent obligations with counsel.

When ZeroSettle is not a fit for maintenance requests

ZeroSettle is the wrong tool for a maintenance line in these cases:

  • You need an emergency channel. Delivery is not guaranteed, so keep a staffed phone number.
  • Tenants must send video. Inbound video arrives with no file; a noisy furnace has to be described.
  • Many tenants use Android. iMessage reaches only iPhone and other Apple users, ZeroSettle has no SMS fallback, and on Android the QR code opens the SMS app. Keep the portal and an office number for those tenants.
  • You need to reach a whole building today. A water-shutoff notice to tenants who have never texted the number goes out at 5 a day per Pro number (50 on Growth), so keep your existing notice channel for those.
  • You want tenant, vendor and office in one thread. Group chats are not supported.
  • You need to deliver legally required notices. Use the channel the law requires.

How much does iMessage for maintenance requests cost?

A maintenance line costs the plan price: ZeroSettle charges no per-message fees, and messages are unlimited (counted).

ZeroSettle plans for a maintenance line (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, 30-day money-back guarantee
GrowthFrom $599/moSeveral numbers working as one, set up after a demo

AI credits don't apply, because your code answers; on a number the hosted AI agent manages, inbound texts are not posted to your webhook, so nothing would open the work order. See pricing, the customer-support recipe in the docs, or get a number.

Frequently asked questions

Will tenants see the property manager's name?

Usually. Once you set a brand name and photo, ZeroSettle shares them with the first message your number sends each tenant, so iOS can show “Maybe: Maple Property Co” before they save the number. How it displays depends on each tenant's iPhone and contact settings.

Can several people in the office answer the same number?

Yes. Invite teammates to the ZeroSettle console. Replies sent from the console reach your webhook as message.sent, so your handler can log both sides of the thread on the work order.

Can we keep the thread as the record of the request?

Partly. ZeroSettle keeps messages “indefinitely for now”, and GET /api/imessage/messages with a handle filter returns one tenant's thread in both directions, with each photo's URL. Because that retention is only promised for now, copy the texts and photos into the work order too.

Sources

  1. ZeroSettle iMessage API docs: “Send a message”, “Attachments”, “Customer support”, “Read messages”, “Webhooks, step by step” and “Errors, limits, and compliance”. Checked October 12, 2026.
  2. ZeroSettle iMessage API Terms of Service, section 2. Last modified September 28, 2026.
  3. ZeroSettle homepage: plans and the FAQ on how a business name appears. Checked October 12, 2026.
  4. SMS Links, Apple URL Scheme Reference, Apple Developer. Checked October 8, 2026.
  5. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.
  6. Open API, powered by Buildium: Work Orders, Resident Requests and task history files, Buildium. 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 ZeroSettle's iMessage API docs, the zerosettle.io homepage, the iMessage API Terms and ZeroSettle's current opt-out handling 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.