Use cases

Sign in with iMessage: passwordless login without SMS codes

Sign in with iMessage, built on ZeroSettle, lets a user log in by tapping Send on a prefilled iMessage: your app opens Messages with "SIGNIN <code>" addressed to your number, your webhook matches the code, and your auth provider issues the session. ZeroSettle's docs say the flow "verifies possession of an iMessage handle, the same guarantee as SMS OTP", with no SMS gateway, no OTP fee and nothing to type. Because the user texts first, there's no new-contact limit, and the cost is the flat plan ($49 a month on Pro); keep your existing sign-in for Android.

Key takeaways

  • What it is: the user sends one prefilled iMessage, "SIGNIN" plus a short code, to your ZeroSettle number; your backend matches the code from a signed webhook and your auth provider issues the session.
  • What it proves: possession of an iMessage handle, the same guarantee as an SMS code, plus the handle Apple attached to the message.
  • Limits: sign-ins use none of the new-contact allowance, because the user texts the line first, and inbound messages are unlimited.
  • Price: Pro is $49/month for one number; Growth is from $599/mo. No per-message or per-code fee.
  • What it does not do: reach Android users. There is no SMS fallback, so keep your existing sign-in beside it.

Who uses iMessage for sign-in?

Sign in with iMessage suits iOS and web developers whose users mostly carry Apple devices and who run Firebase, Supabase, Auth0, Clerk or their own auth. 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.

Sign in with iMessage replaces the outbound SMS one-time code. Firebase Phone Auth, for example, is "billed per SMS sent" (Firebase pricing), and every code goes out to a number nobody has verified yet. With Sign in with iMessage, ZeroSettle sends nothing until the user has texted your number, and then only a 👍 tapback. The trade-off is a trip to Messages: the user leaves your app to tap Send, then comes back signed in.

Only confirming a number on an existing account? Read phone verification without outbound SMS. More jobs are on the use-case hub.

How iMessage for sign-in works, step by step

Sign in with iMessage reverses the OTP direction: the user texts a code to your ZeroSettle number, and the signed webhook tells your backend which handle sent it.

  1. Start. Your backend creates an attempt with a single-use code. In the docs' reference implementation that's 6 characters from a 32-character alphabet, valid for 10 minutes.
  2. Send. The app opens sms:+18055550100&body=SIGNIN%204F7K2Q: Messages opens to your number with "SIGNIN 4F7K2Q" prefilled, and the user taps Send.
  3. Receive. ZeroSettle POSTs a signed message.received event with handle and text to your webhook, about 1–2 seconds after the line receives it.
  4. Match and mint. Your backend matches the code, marks the attempt verified with that handle, mints a session with Firebase, Supabase, Auth0, Clerk or your own auth, and puts a 👍 tapback on the text with POST /api/imessage/reactions.
  5. Sign in. The app polls the attempt every 2 seconds and receives the session, once, when it is verified.
  6. On a desktop that isn't a Mac, the page shows a QR code of the same sms: link, plus the number and code as text. The user sends it from their iPhone, and the browser tab signs in.

The handle in the webhook is what Apple attached to the message: an E.164 number for almost everyone, or an email address for iMessage-only devices. Store it exactly as returned, as the user's verified identifier. The iMessage webhook guide covers signing and retries, and sms: links and QR codes covers the link itself.

Example conversation: sign-in

One user's two sign-ins in a day, as the thread appears in the ZeroSettle console: first from the iPhone app, then from a Windows laptop through the QR code.

  1. Today 8:14 AM · Acme Notes iPhone app
  2. Customer: SIGNIN 4F7K2Q👍
  3. The app shows "Welcome back, Alex"
  4. Today 6:02 PM · Windows laptop, QR code scanned with Alex's iPhone
  5. Customer: SIGNIN 9QX2LM👍
  6. The desktop page shows "Signed in"
Illustrative example with a fictional notes app, Acme Notes. Gray bubbles are the user's prefilled texts; the blue 👍 is the tapback Acme Notes' backend sends from its number. Centered lines note what the app showed, not messages. Acme Notes never sends a text.

Setup: the docs' reference implementation

The docs' reference implementation is one backend you run, with three routes (start, webhook, status) and a provider-specific mint_session; ZeroSettle only carries the text. The Sign in with iMessage section of the docs adds the Swift and browser code and the Supabase, Auth0 and Clerk versions of mint_session.

Register the webhook

bash
BASE=https://portal.zerosettle.io
curl -s -X PUT -H "Authorization: Bearer $ZEROSETTLE_TOKEN" -H 'Content-Type: application/json' \
  -d '{"url":"https://auth.example.com/webhook"}' $BASE/api/imessage/webhook

Store the secret in the response as ZEROSETTLE_WEBHOOK_SECRET. It is shown once, and registering again rotates it.

The backend

python
# auth_imessage.py: run with uvicorn auth_imessage:app --port 8100
import hashlib, hmac, json, os, re, secrets, time
import httpx
from fastapi import FastAPI, Request, Response, HTTPException

app = FastAPI()
BASE = os.environ.get("ZEROSETTLE_BASE_URL", "https://portal.zerosettle.io")
TOKEN, SECRET = os.environ["ZEROSETTLE_TOKEN"], os.environ["ZEROSETTLE_WEBHOOK_SECRET"]
ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"     # no 0/O/1/I
attempts: dict[str, dict] = {}                     # use Redis in production
by_code: dict[str, str] = {}

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)

@app.post("/auth/imessage/start")
def start():
    aid, code = secrets.token_urlsafe(16), "".join(secrets.choice(ALPHABET) for _ in range(6))
    attempts[aid] = {"code": code, "exp": time.time() + 600, "state": "pending", "handle": None, "session": None}
    by_code[code] = aid
    return {"id": aid, "code": code}

@app.post("/webhook")
async def webhook(req: Request):
    raw = await req.body()
    if not verify(SECRET, req.headers.get("X-ZeroSettle-Signature", ""), raw):
        return Response(status_code=401)
    ev = json.loads(raw)
    if ev["event"] != "message.received":
        return {"ok": True}
    m = ev["message"]
    match = re.search(r"\bSIGNIN\s+([A-Z2-9]{6})\b", (m.get("text") or "").upper())
    aid = match and by_code.pop(match.group(1), None)
    a = aid and attempts.get(aid)
    if not a or a["state"] != "pending" or a["exp"] < time.time():
        return {"ok": True}                                    # ignore junk, expired, or replayed codes
    a.update(state="verified", handle=m["handle"], session=await mint_session(m["handle"]))
    async with httpx.AsyncClient() as c:                       # acknowledge with a tapback; no text needed
        await c.post(f"{BASE}/api/imessage/reactions", headers={"Authorization": f"Bearer {TOKEN}"},
                     json={"message_id": m["id"], "reaction": "like"})
    return {"ok": True}

@app.get("/auth/imessage/status")
def status(id: str):
    a = attempts.get(id)
    if not a:
        raise HTTPException(404)
    if a["state"] == "pending" and a["exp"] < time.time():
        a["state"] = "expired"
    out = {"state": a["state"]}
    if a["state"] == "verified":
        out["session"] = a.pop("session")                      # one-time read
        attempts.pop(id, None)
    return out

The webhook calls mint_session, so add one to the same file. For Firebase, it finds or creates the user by handle and returns a custom token for the app's signInWithCustomToken:

python
# pip install firebase-admin
import firebase_admin
from firebase_admin import auth
firebase_admin.initialize_app()          # GOOGLE_APPLICATION_CREDENTIALS points at a service account

async def mint_session(handle: str) -> dict:
    try:
        user = auth.get_user_by_phone_number(handle) if handle.startswith("+") else auth.get_user_by_email(handle)
    except auth.UserNotFoundError:
        user = auth.create_user(phone_number=handle) if handle.startswith("+") else auth.create_user(email=handle, email_verified=True)
    token = auth.create_custom_token(user.uid, {"imessage_verified": True})
    return {"provider": "firebase", "custom_token": token.decode()}

How each auth provider issues the session

Session minting for Sign in with iMessage, per ZeroSettle's docs and each provider's documentation (checked October 9, 2026)
AuthHow the session is issuedEmail handles
FirebaseAdmin SDK custom token, exchanged in the app; it expires after one hourYes
SupabaseThe Send SMS hook hands you the OTP instead of sending it; the app calls verifyOTPNo (E.164 only)
Auth0Passwordless SMS with a custom phone provider; its Action posts the code to your backend instead of sending itNot in ZeroSettle's docs
ClerkPOST /sign_in_tokens; the app redeems the ticketYes
Your own authYour usual cookie or JWT for that handleYes

Provider docs: Firebase custom tokens, Supabase Send SMS Hook, Auth0 passwordless SMS, Clerk sign-in tokens.

What breaks sign-in, and the fix

Sign in with iMessage breaks in six known places, each with a fix. For tokens and signing, see how ZeroSettle secures its iMessage API.

Sign in with iMessage failure modes (ZeroSettle docs, checked October 8, 2026)
SituationWhat to do
Safari ignores the buttonSafari drops sms: navigations that aren't the direct result of a tap: set location.href in the click handler, not window.open after an await.
The code expiresAfter 10 minutes the status is expired; offer your usual sign-in.
Someone else sees the QR codeThe code alone fetches nothing. Keep the attempt id out of the QR and return the session once.
The line is Managed by AI AgentThe hosted agent takes inbound texts and your webhook isn't called. Keep the sign-in number on Managed by API.
The visitor is on Androidsms: opens the SMS app. Show the QR so they can send from an iPhone, or your fallback sign-in.
Many users sign in at onceThe line sends one thing at a time, so a tapback can come back 429 line_busy. The attempt is already verified; send the tapback after answering the webhook, which must respond within 10 seconds.

Limits and consent for sign-in

Sign in with iMessage never uses ZeroSettle's new-contact allowance, because every sign-in starts with the user texting the line; the cost is the flat plan ($49 a month on Pro).

What a sign-in counts against on a ZeroSettle line (API docs, checked October 8, 2026)
Part of the sign-inCounted asLimit
The user's SIGNIN textInbound messageUnlimited
Your 👍 tapbackReaction, never a new contactNone
A later text to that userAn ongoing conversation (they texted first)No new-contact limit (2 unanswered messages per contact, on every plan)
Calls to your /start routeYour server, not ZeroSettleYours to set: rate-limit per IP and device

So 300 sign-ins on one Pro number in a day add 300 inbound texts and no new contacts; all 5 of the number's new contacts for the day stay free.

Signing in isn't marketing consent

The user texts to prove a handle, not to hear offers, so ZeroSettle's docs advise asking separately before sending anything promotional, and sending it only between 8 am and 9 pm in their time zone. Consent rules vary by country and use; this isn't legal advice, so check with counsel. A user who texted STOP earlier can still sign in, because they're the one texting, but the confirming tapback to them is blocked with 403 recipient_opted_out, so don't depend on it. See how STOP works on a ZeroSettle line.

When ZeroSettle is not a fit for sign-in

Sign in with iMessage is the wrong primary login in these cases:

  • Most of your users are on Android. The user has to send from iMessage, and ZeroSettle has no SMS or email fallback.
  • You need phishing-resistant sign-in, such as hardware security keys. NIST SP 800-63B covers this pattern, a code carried from the session to an out-of-band device by typing or a QR code, and says "Out-of-band authentication is not phishing-resistant"; AAL3 requires an authenticator that is. A user tricked into texting someone else's code signs that person in to the user's account. Use Sign in with iMessage where an SMS code would do.
  • Users sign in on a desktop with no iPhone nearby. The QR flow needs an Apple device with iMessage to send from.
  • You can't run a webhook (or a poller) to match codes. Matching the text to an attempt and minting the session happen on your server.

How much does iMessage for sign-in cost?

Sign in with iMessage costs the ZeroSettle plan price, with no per-message or per-code fee: messages are unlimited (counted). See the plans.

ZeroSettle plans for sign-in (zerosettle.io, checked October 8, 2026)
PlanPriceFits
Pro$49/month, no setup feeOne number, self-serve; unlimited messages with people who message you first, and its 5 new contacts a day per number don't apply to sign-ins
GrowthFrom $599/moSeveral numbers working as one; ZeroSettle sets it up after a demo

Both plans are billed monthly with a 30-day money-back guarantee on the subscription, and sign-ins use no AI credits. Get a number to start.

Frequently asked questions

Is Sign in with iMessage more secure than SMS codes?

About the same. It proves possession of the handle, as an SMS code does, and adds the handle Apple attached to the message, which ZeroSettle's docs call a stronger identifier than a number the user typed. Neither is phishing-resistant under NIST SP 800-63B.

Can users sign in from an iPad or a Mac?

Yes. On an iPad, or a Mac with Messages, the button opens Messages directly. From an iMessage-only device the handle can be an email address, which Firebase, Clerk and your own auth accept but Supabase phone auth does not. That email is a different handle from the user's number, so link the two to one account rather than letting mint_session create a second.

What about users who already have an account, such as through Sign in with Apple?

Attach the verified handle to the existing account instead of creating a second one. Most providers have a link or update-user call; in Firebase, auth.update_user sets phone_number on the existing record.

Do I need a webhook for Sign in with iMessage?

No. You can poll GET /api/imessage/messages?after_id=… instead; inbound messages appear there within about 1–2 seconds. ZeroSettle's docs prefer the webhook because it is pushed and signed.

Sources

  1. ZeroSettle iMessage API docs: "Sign in with iMessage", "Read messages", "Webhooks, step by step", "AI agent" and "Errors, limits, and compliance". Checked October 8, 2026.
  2. ZeroSettle homepage: Plans. Checked October 8, 2026.
  3. Firebase pricing, Google: Phone Auth "Billed per SMS sent". Checked October 8, 2026.
  4. Create Custom Tokens, Firebase Authentication documentation. Checked October 8, 2026.
  5. Send SMS Hook, Supabase Auth documentation. Checked October 8, 2026.
  6. Passwordless Authentication with SMS, Auth0 documentation: "Auth0 supports using Twilio and a custom phone provider to send OTPs." Checked October 9, 2026.
  7. createSignInToken(), Clerk Backend SDK reference. Checked October 8, 2026.
  8. NIST SP 800-63B, Authentication and Authenticator Management, National Institute of Standards and Technology: section 3.1.3 (out-of-band devices) and section 2.3.2 (AAL3). 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 8, 2026; plan limits were updated October 12, 2026. Nothing here is legal advice. Firebase, Supabase, Auth0, Clerk and NIST details come from their own documentation, linked under Sources.

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.