Skip to main content

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

  1. You click Dispatch on a work order.
  2. A SHA-256-hashed token is generated server-side. Plaintext is sent once to the contractor's email or SMS.
  3. The contractor taps the link. No login screen — the URL itself is the auth.
  4. They see the WO: address, scope, PM contact, tenant contact, cost ceiling.
  5. Buttons: Accept / Schedule (date picker) / On site now / Complete (mark done + upload invoice) / Raise a question.
  6. 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 with aiUsed: false, and the page says so rather than faking a translation.
  • Rate-limited per IP (jobs-translate scope: 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