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
urlverbatim. It points at the inert, signed confirmation page (aGETthat 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
UserNotInGuardExceptionif it does not match. With no$guardthe default guard is used; pass an allowed guard (the default plus any underguards) or get anUnknownGuardException. issueCodesupersedes the previous code for the same user and guard, so only the most recently issued code can be claimed. (issueLinkdoes not invalidate earlier links.)- The channel must be enabled. With
enabled = falsethe API throwsMagicLinkDisabledExceptionrather 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.
Building a link for another host
$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.
Beyond a single-use login link
- Multi-use links — hand out a link that may be redeemed a bounded number of times.
- Passphrase-gated links — require a shared secret on the confirmation page.
- Authorizing a gated resource without login — use the same token to authorize one specific resource without creating a session.