TILLAUTH · FLOWS

Magic links — bound, brief, single-use.

GA

Magic links are the most-stolen credential on the internet because they live in inboxes. TillAuth's links are bound to the requesting network, expire in 15 minutes, and are single-use. Forwarding the email gets you nowhere.

02Request

Send

POST /v1/magic/send with { email, redirect_uri? }:

  1. Rate limit per IP and per (app, email) — no email-bombing surface.
  2. Mint the token, store only its hash bound to the requesting network and the redirect_uri, with a 15-minute TTL.
  3. Send the email via your configured provider (Resend by default), using the per-app branding template.
  4. Always answer success-shaped ({ ok: true }) — never reveal whether the email matched a real user. Audit row: signin.magic.sent.
03Redeem

Consume

POST /v1/magic/consume with { token }:

  1. Find the token by its hash.
  2. Check expiry — if expired, audit signin.magic.expired, 400 Invalid or expired link.
  3. Check the network — if it doesn't match, audit signin.magic.ip_mismatch, 403 with a message telling the user to request a fresh link. The token is consumed regardless — the original link no longer works either, by design.
  4. Mark the token used, mint the session inside the same transaction, audit signin.magic.ok.
  5. If the account has TOTP enrolled, the response is an MFA challenge ({ mfa_required: true, challenge_token }) instead of a session — complete it at /v1/totp/verify-signin.
  6. If redirect_uri was bound and it passes the allow-list, return it in the response so your SDK can navigate the user there.
Password-less is a start, not a sentence
An account born from a magic link has no password — and never needs one. But if the user wants one, POST /v1/password/set adds it from their signed-in session, no mailbox round-trip. See embedded account settings.
04Hosted

Hosted-login flow

On <slug>.tilldev.app the user clicks the magic-link button, types their email, gets the mail, clicks the link. The hosted-login page handles the consume side and redirects back to the app — your app never has to embed the magic-link UI itself.

05SDK

React SDK

From the React SDK, hosting your own flow is two hooks:

ts
// Send page
const { send, sent, pending } = useMagicLink()
await send({ email, redirect_uri: '/' })

// Callback page — read ?t= from the URL on mount
const { consume } = useMagicLink()
const r = await consume(token)
if (r.ok) router.push(r.redirect ?? '/')
06Fit

When magic links are wrong

  • High-value workflows (admin dashboards, treasury, anything regulated) should use passkeys or OAuth. Mailbox access shouldn't grant superuser.
  • Shared inboxes (support@, team@) — the link works for whoever gets it first. Use email + password with MFA.