Skip to main content

Minting links and codes yourself

Sometimes you want to deliver the link or code over a channel the bundled email flow does not cover — an SMS, a chat message, an existing transactional email, or a queued job. The Mint API issues a credential and hands it back without sending anything.

The facade

use EmailMagicLink\Facades\EmailMagicLink;

// A single-use magic link for the default guard.
$link = EmailMagicLink::issueLink($user);
$link->url; // signed, single-use confirmation URL — deliver this verbatim
$link->expiresAt; // Carbon\CarbonInterface
$link->expiresInMinutes; // e.g. 15 — handy for your own copy

// A one-time code instead.
$code = EmailMagicLink::issueCode($user);
$code->code; // the code to deliver
$code->expiresAt;
$code->expiresInMinutes;

expiresAt is typed as Carbon\CarbonInterface, which is what lets the same value work whether or not your application calls Date::use(CarbonImmutable::class). You get back whatever your app's date factory produces: the mutable Illuminate\Support\Carbon by default, a CarbonImmutable if you have switched. Type your own parameters against CarbonInterface too, and either setting fits.

The contract

Prefer dependency injection? Depend on the EmailMagicLink\Contracts\MagicLinkIssuer contract; the facade is a thin wrapper over it and takes exactly the same arguments, including the optional maxUses, passphrase and baseUrl shown below.

use EmailMagicLink\Contracts\MagicLinkIssuer;

public function __construct(private MagicLinkIssuer $issuer) {}

The full signatures, including the optional per-link parameters, are in the contract reference.

The rules the API enforces

The minted credential is hashed at rest, single-use, and consumed through the exact same flow as an emailed one — only nothing is sent. A few rules the API enforces or expects:

  • Deliver url verbatim. It points at the inert, signed confirmation page (a GET that changes nothing); the token is spent only when the user submits it. Never send or prefetch the consume endpoint.
  • Pass a user that belongs to the guard. Issuing re-resolves the user through the guard's own provider — the same provider the consume step uses — and throws UserNotInGuardException if it does not match. With no $guard the default guard is used; pass an allowed guard (the default plus any under guards) or get an UnknownGuardException.
  • issueCode supersedes the previous code for the same user and guard, so only the most recently issued code can be claimed. (issueLink does not invalidate earlier links.)
  • The channel must be enabled. With enabled = false the API throws MagicLinkDisabledException rather than minting a credential that could never be consumed.
  • Look a user up by email first by injecting EmailMagicLink\Contracts\UserLookup — that is the supported email-to-user path; the Mint API deliberately takes an already-resolved user.
$link = $issuer->issueLink($user, baseUrl: 'https://tenant-a.example.com');

Handy for multi-domain or multi-tenant applications. The signature is computed over that final host, so the link verifies only when visited there.

Without a base URL the host comes from the request that asked for the link, which is the framework's default for every URL it builds. Read the host in the emailed link before you deploy: an application that answers any Host header should either restrict the hosts it trusts or force its origin.

A base URL that carries no scheme — tenant-a.example.com or //tenant-a.example.com — is completed with the scheme the application itself is served over, so the link in the email is always absolute. A base URL that states its own scheme is used exactly as given, including one that differs from the application's.