Magic-Link Dispatch
Tradies do not log in to software. They won't install your app. They won't remember your password. PMFriend's magic-link dispatch is built around this truth.
The contractor experience
- You click Dispatch on a work order.
- A SHA-256-hashed token is generated server-side. Plaintext is sent once to the contractor's email or SMS.
- The contractor taps the link. No login screen — the URL itself is the auth.
- They see the WO: address, scope, PM contact, tenant contact, cost ceiling.
- Buttons: Accept / Schedule (date picker) / On site now / Complete (mark done + upload invoice) / Raise a question.
- Each button transitions the WO status. Transitions push back to the PM inbox in real-time.
Token security
- 14-day TTL. After that the link 404s; you can regenerate with a fresh dispatch.
- SHA-256 hashed at rest — the server stores a hash, not the token. If an attacker dumps the database, they still can't forge links.
- Single-use transition safety — a completed WO can't be re-opened via magic link. The PM has to re-dispatch.
- Rate-limited per IP — 30 req/min on the public magic-link endpoints.
Why not SSO / password / OTP?
We piloted with 12 tradies across 4 agencies. Summary:
- SSO — most tradies don't have a work email; they use their personal Gmail from 2011.
- Password — "I'll write it in my ute book" → forgotten → support ticket → PM dispatches via SMS anyway.
- OTP — works but adds a 15-second pause before accepting. 85% of dispatches are accepted within 2 minutes; friction matters.
Magic-link + 14-day TTL won. We'd revisit if a security concern surfaced, but so far the SHA-256 hash + one-time plaintext distribution has held up.
The flow the PM sees
[ASSIGNED] PM dispatched to Harbour Plumbing at 14:22
└─ status chip: "Magic link sent (14d valid)"
[ACCEPTED] Contractor opened the link at 14:31 and clicked Accept
└─ status chip: "Accepted 14:31"
[SCHEDULED] Contractor scheduled for 2026-04-26 10:00
└─ status chip: "Scheduled Fri 10:00"
[ON_SITE] Contractor clicked On site at 09:58
└─ status chip: "On site since 09:58"
[COMPLETED] Contractor clicked Complete + uploaded invoice PDF at 11:42
└─ status chip: "Completed 11:42 · invoice attached"
Translate this job into the worker's language
Trades crews are multilingual — acutely so in the Gulf, where the on-site worker often understands English only partially and is far more confident in Hindi, Malayalam, Urdu, Tamil, Bengali, Nepali, Tagalog, Sinhala or Arabic — so today someone at reception re-explains or hand-translates each task before work starts. The job page carries a one-click fix: pick a language, tap Translate, and the scope re-renders in that language, with a toggle back to English. Arabic and Urdu render right-to-left automatically.
- Translation is content-only (the job scope), not UI-chrome localization — the page frame stays English.
- Backed by the same Claude adapter pattern as triage: active when
app.triage.provider=claude, and best-effort — if the model is unavailable the endpoint returns the original English text withaiUsed: false, and the page says so rather than faking a translation. - Rate-limited per IP (
jobs-translatescope: 20/hour, 100/day) so a leaked link can't loop the button and run up an AI bill.
This ships in both the AU product and the Qatar variant — it belongs in the core, not a fork.
API
The magic-link endpoints are on /api/v1/jobs/* (public, no auth
required beyond the token in the URL):
GET /api/v1/jobs/{token} # WO details
POST /api/v1/jobs/{token}/status # body: { target: SCHEDULED | IN_PROGRESS | COMPLETED, ... }
POST /api/v1/jobs/{token}/translate # body: { targetLanguage: "Malayalam" } → { translated, language, aiUsed }
POST /api/v1/jobs/{token}/identity-confirm # contractor confirms details on file
The internal PM-facing endpoints sit at /api/v1/work-orders/*.
See also
- Ranked Picker — the step before dispatch
- Contractors Overview