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 a second factor, the response is an MFA challenge instead of a session. TOTP returns { mfa_required: true, challenge_token }; complete it at /v1/totp/verify-signin. Till Authenticator returns method: "push" with a push block; show its match_number and poll /v1/push/signin/poll.
  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.