Use cases

How to fill open shifts by iMessage: offer one by one, first YES wins

ZeroSettle lets a staffing agency, restaurant or warehouse fill a call-out by texting available staff one at a time from the business's own iMessage number, and assigning the shift to the first person whose YES reaches your webhook. Each employee texts the number once at onboarding, so shift offers never count toward the new-contact limit (5 a day on the $49/month Pro plan), and nobody sees anyone else's number. The thread keeps a record of who accepted which shift, and when.

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 workflow: on a call-out, your code offers the shift to the best-matched worker, waits a few minutes, then asks the next. The first YES gets the shift; a later YES gets "Covered, thanks!".
  • Main limit: Pro allows 5 new contacts a day per number, but workers who texted the number at onboarding never count. On either plan, a worker who ignored two offers gets no third until they reply.
  • Price: Pro is $49/month, starting with one number, with unlimited messages with people who message you first; Growth is from $599/mo for several numbers working as one. No per-message fees.
  • What it does not do: no group chats, no SMS fallback for Android, and no shift scheduling of its own. Your code decides who is asked and when.

Who uses iMessage for shift coverage?

iMessage shift offers fit staffing agencies, restaurants, retail stores and warehouses with hourly staff on a roster, where one call-out leaves a shift short within hours. Without a system, a 6 am call-out means a manager phoning down the list, and a group text turns into noise: everyone sees everyone's number, and two people can each believe they took the shift.

Screening applicants with the hosted AI agent (text-to-apply on iMessage) and sending shift offers from your code need separate numbers: on a number the agent manages, your code's sends are refused with 403 managed_by_zerosettle. Paging one person to acknowledge an incident is a different job (on-call alerts over iMessage); more jobs are in the index of iMessage use cases.

How iMessage shift coverage works, step by step

Your scheduling system spots the call-out and ranks who can cover; ZeroSettle sends each offer from your number and posts each reply to your webhook.

  1. Each worker texts your number once at onboarding, from an sms: link or QR code in an onboarding packet that says the number sends shift offers. Per ZeroSettle's docs, texting first also gets past Screen Unknown Senders, which, Apple says, filters unknown senders' messages into another folder with no notification unless the worker turns one on.
  2. A call-out opens the shift. In 7shifts, for example, a shift's status can be set to Call Out, and its open field is "Set to true if the shift is not assigned to any user." Your code ranks eligible workers.
  3. The best match gets a one-to-one offer through POST https://portal.zerosettle.io/api/imessage/send, which normally returns in 2–5 seconds.
  4. A few minutes later, the next worker is asked, unless someone said yes. A "no" moves the queue on at once.
  5. The first YES wins. It reaches your webhook as message.received in about 1–2 seconds; your code assigns the shift through the scheduling API and sends check-in details and a photo. Because the worker texted first, links are tappable.
  6. A later YES gets "Covered, thanks!" Anything else goes to a manager, who can answer from the ZeroSettle console.
  7. On either plan, a worker who ignored two offers gets no third until they reply (403 awaiting_reply), so skip to the next person.

Nobody sees another worker's number or reply, and the thread records who accepted what and when (GET /api/imessage/messages). The same pattern fills cancelled appointments from a waitlist.

Example conversation: a Saturday forklift shift

A forklift operator called out of Saturday's 7 am shift at a fictional agency's client warehouse. Luis holds the certification and was free.

  1. Fri 7:42 PM
  2. Business: Hi Luis, open shift: Sat 7am–3pm, Quillbrook Distribution, $22/hr, forklift cert needed. Want it? Reply YES
  3. Worker: YES
  4. Business: It's yours ✅ Check in at Dock 4 with Sarah. Wear steel toes.
  5. Business: [Photo: Dock 4 entrance]👍
Illustrative example with a fictional agency, Fernhollow Staffing. Blue bubbles come from the agency's number; gray bubbles are Luis's. His YES reaches the agency's webhook as message.received, and his 👍 tapback as reaction.received.

Setup: a scheduling-system trigger and a first-YES handler

Two pieces: cover(), which your scheduling system's webhook (or a manager's script) starts in the background on a call-out, and a receiver that verifies ZeroSettle's signature and gives the shift to the first YES. The verifier is copied from the ZeroSettle webhook docs. Undefined functions are your roster and scheduling system; in 7shifts, assignment is Update Shift with the worker's user_id and open set to false.

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

BASE = os.environ.get("ZEROSETTLE_BASE_URL", "https://portal.zerosettle.io")
TOKEN = os.environ["ZEROSETTLE_TOKEN"]
SECRET = os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
app = FastAPI()
seen, offers = set(), {}               # your database in production

def verify(header, raw):
    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) > 300:
        return False
    expected = hmac.new(SECRET.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

async def send(to, **body):            # text=... or image_b64=...
    async with httpx.AsyncClient(timeout=120) as c:
        for _ in range(5):
            r = await c.post(f"{BASE}/api/imessage/send", json={"to": to, **body},
                             headers={"Authorization": f"Bearer {TOKEN}"})
            if r.status_code != 429 or r.json()["error"]["code"] != "line_busy":
                return r.status_code == 200    # False: awaiting_reply, opted out...
            await asyncio.sleep(2)
    return False

async def cover(shift):                # shift["taken_by"] = None, shift["declined"] = set()
    for w in rank_workers(shift):      # your roster: role, certs, hours this week
        if shift["taken_by"]:
            return
        if not await send(w["handle"], text=f"Hi {w['first']}, open shift: {shift['label']}. Want it? Reply YES"):
            continue                   # skip to the next worker
        offers[w["handle"]] = shift
        for _ in range(60):            # five minutes, unless taken or declined
            if shift["taken_by"] or w["handle"] in shift["declined"]:
                break
            await asyncio.sleep(5)
    if not shift["taken_by"]:
        page_manager(shift)            # nobody took it: call down the list

async def handle(m):
    who, text, shift = m["handle"], (m.get("text") or "").strip(), offers.get(m["handle"])
    yes = re.match(r"yes\b", text, re.I)
    if re.search(r"\bstop\b", text, re.I):  # opted out of every number, even "can't stop by"
        if shift:
            shift["declined"].add(who)
        notify_manager(m)              # a person reads every STOP
    elif shift and re.match(r"(no|can['’]?t)\b", text, re.I):
        shift["declined"].add(who)     # ask the next worker now
    elif shift and yes and not shift["taken_by"]:
        shift["taken_by"] = who        # production: UPDATE ... WHERE taken_by IS NULL
        assign_shift(shift, who)       # your scheduling API
        await send(who, text=f"It's yours ✅ {shift['check_in']}")
        with open(shift["photo"], "rb") as f:
            await send(who, image_b64=base64.b64encode(f.read()).decode(), image_format="jpg")
    elif shift and yes and shift["taken_by"] != who:
        await send(who, text="Covered, thanks!")
    else:
        notify_manager(m)              # questions, swaps: a person reads them

@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)
    if ev["event"] != "message.received" or ev["message"]["id"] in seen:
        return {"ok": True}            # echoes of your own sends, or a retry
    seen.add(ev["message"]["id"])
    tasks.add_task(handle, ev["message"])
    return {"ok": True}

Register the receiver with PUT /api/imessage/webhook and keep the signing secret, shown once. Retries after 1, 5 and 20 seconds repeat the message.id, hence the dedupe (how iMessage webhooks are signed and retried). To settle a dispute, fetch one worker's thread; each row has direction, text and a ts_ms timestamp:

bash
BASE=https://portal.zerosettle.io
curl -s -H "Authorization: Bearer $ZEROSETTLE_TOKEN" \
  "$BASE/api/imessage/messages?after_id=0&limit=100&handle=%2B15555550123"

Limits and consent for shift coverage

Shift offers rarely touch ZeroSettle's new-contact limits: every worker texted the number at onboarding, so each offer continues an ongoing conversation. The rule that shapes the queue is the cap on unanswered messages, which applies on both plans.

ZeroSettle limits as they apply to shift offers (API docs, checked October 12, 2026)
LimitProGrowthFor shift offers
New contacts per day, per number (from 3 AM Eastern)550Only workers who never texted the number
New contacts per rolling 15 minutes, per number—3Same
Unanswered messages to one worker2, then 403 awaiting_reply2, then 403 awaiting_replySkip to the next worker

Source: ZeroSettle iMessage API docs, "Errors, limits, and compliance". Anyone who texted the line first is never limited, and neither is an ongoing conversation.

  • Roster size: workers who texted at onboarding never count, however large the roster. Current staff who never texted the number do: on Pro, the 6th cold offer in a day returns 429 new_contact_limit, and a cold text can land in Screen Unknown Senders. Ask current staff to text the number before the first call-out.
  • Unanswered offers: on either plan, a worker who ignored Tuesday's and Thursday's offers gets none on Saturday until they send anything back, even a tapback. ZeroSettle's docs warn that "repeated messages to people who never answer are the fastest way to get a number flagged by Apple", so the cap protects your number too.

The worker's onboarding text is the consent record, so keep it, and keep shift texts separate from anything promotional. ZeroSettle's docs bar promotional texts before 8 am or after 9 pm in the recipient's time zone; a 6 am call-out will tempt you to text earlier, so put the hours offers can arrive in the onboarding packet.

A text with the word STOP, or a clear request like "take me off your list", opts a worker out of every number on your account, shift offers included, until they text START; sends to them return 403 recipient_opted_out. Even "can't stop by today" counts, which is why the sample hands every STOP to a manager (how STOP works on a ZeroSettle line).

Seattle's Secure Scheduling Ordinance, for hourly employees at retail and food-service establishments with 500+ employees worldwide (full-service restaurants also need 40+ full-service locations worldwide), tells employers to "respect employees’ right to decline any hours not on originally posted schedules" and to keep records for three years. Check the rules where you operate; this isn't legal advice.

When ZeroSettle is not a fit for shift coverage

ZeroSettle is the wrong tool for shift coverage in four cases.

  • You want a team group chat. ZeroSettle has none, and that noise is what this workflow replaces.
  • Many workers use Android. Apple describes iMessage as messaging to "another iPhone or another Apple device", and ZeroSettle has no SMS fallback, so those workers never see an offer.
  • Staffing is safety-critical. ZeroSettle's terms say "delivery of any particular message or feature is not guaranteed", so keep a phone tree.
  • A worker texted STOP. No number on your account can send them shift offers until they text START, so they need a call.

How much does iMessage shift coverage cost?

Shift coverage costs the plan price and nothing per message: ZeroSettle's messages are unlimited (counted) on both plans.

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

Both bill monthly with a 30-day money-back guarantee on the subscription. Compare ZeroSettle's plans, read the iMessage API docs, or get a number.

Frequently asked questions

What if a worker answers with a tapback instead of YES?

A tapback reaches your webhook as reaction.received, not message.received, so the sample ignores it and the offer stays open until its window ends; that is why the offer asks for a typed YES. On either plan, the tapback still counts as a reply and resets the unanswered-message count.

What if two workers reply YES at nearly the same time?

Whichever YES your handler claims first, with an atomic database update, wins; the other gets "Covered, thanks!". Webhook events can arrive out of order under retries, so order by message.id if strict first-come matters.

How long does ZeroSettle keep the record of who accepted?

ZeroSettle's docs say messages are kept indefinitely for now, but DELETE /api/imessage/conversations/{handle} removes every stored row for that worker. Copy acceptances into your own records and keep them as long as local rules such as Seattle's require.

How fast can I go down the list?

Each send normally returns in 2–5 seconds, so the pace is the answer window you set: five-minute windows reach the fourth worker 15 minutes after the first.

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 iMessage API Terms of Service, section 2. Last modified September 28, 2026.
  3. Update Shift, 7shifts API Reference. Checked October 8, 2026.
  4. Screen and filter text messages on iPhone, iPhone User Guide, Apple Support. Checked October 8, 2026.
  5. What is the difference between iMessage, RCS, and SMS/MMS?, Apple Support. Checked October 8, 2026.
  6. Secure Scheduling, Seattle Office of Labor Standards. 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 zerosettle.io as of October 12, 2026. Fernhollow Staffing, Quillbrook Distribution and Luis are fictional, and 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.