Send a magic link via email for passwordless authentication. Users can sign in by clicking the link in their email without entering a password.
This endpoint does not require authentication and can create new users if create_user is set to true.
curl -X POST "http://localhost:8080/magiclink" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"create_user": true
}'
Request Body
Email address to send the magic link to.
Whether to create a new user if the email doesn’t exist. Defaults to false.
URL to redirect to after successful authentication. If not provided, uses the default redirect URL.
Additional user metadata to store if creating a new user.
Captcha token for verification if captcha is enabled.
Response
Unique identifier for the sent magic link email (when available)
{
"message_id" : "msg_1234567890abcdef"
}
Error Responses
400 - Invalid Email
404 - User Not Found
429 - Rate Limited
{
"code" : 400 ,
"msg" : "Invalid email format" ,
"details" : "Please provide a valid email address"
}
Magic Link Flow
Request Magic Link
User enters their email address and requests a magic link
Email Sent
A magic link email is sent to the user’s email address
User Clicks Link
User clicks the magic link in their email
Authentication
User is automatically authenticated and redirected to your application
Magic Link Email Template
<! DOCTYPE html >
< html >
< head >
< title > Sign in to Strike </ title >
</ head >
< body >
< div style = "max-width: 600px; margin: 0 auto; padding: 20px;" >
< h1 > Sign in to Strike </ h1 >
< p > Hi there, </ p >
< p > Click the button below to sign in to your Strike account: </ p >
< a href = "{{.MagicLinkURL}}"
style = "background-color: #0D9373; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;" >
Sign In
</ a >
< p > Or copy and paste this link into your browser: </ p >
< p > {{.MagicLinkURL}} </ p >
< p > This link will expire in 1 hour for security reasons. </ p >
< p > If you didn't request this sign-in link, you can safely ignore this email. </ p >
< p > Best regards, < br > The Strike Team </ p >
</ div >
</ body >
</ html >
Implementation Examples
import { useState } from 'react' ;
function MagicLinkForm () {
const [ email , setEmail ] = useState ( '' );
const [ loading , setLoading ] = useState ( false );
const [ sent , setSent ] = useState ( false );
const [ error , setError ] = useState ( '' );
const [ createUser , setCreateUser ] = useState ( true );
const handleSubmit = async ( e ) => {
e . preventDefault ();
setLoading ( true );
setError ( '' );
try {
const response = await fetch ( '/api/auth/magiclink' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
email ,
create_user: createUser ,
redirect_to: window . location . origin + '/dashboard'
}),
});
if ( ! response . ok ) {
const errorData = await response . json ();
throw new Error ( errorData . msg || 'Failed to send magic link' );
}
setSent ( true );
} catch ( err ) {
setError ( err . message );
} finally {
setLoading ( false );
}
};
if ( sent ) {
return (
< div className = "magic-link-success" >
< h2 > Check your email! </ h2 >
< p >
We've sent a magic link to < strong > { email } </ strong >
</ p >
< p >
Click the link in your email to sign in instantly.
</ p >
< p >
< small >
Didn't receive the email? Check your spam folder or { ' ' }
< button
onClick = { () => setSent ( false ) }
className = "link-button"
>
try again
</ button >
</ small >
</ p >
</ div >
);
}
return (
< form onSubmit = { handleSubmit } className = "magic-link-form" >
< h2 > Sign in with Magic Link </ h2 >
< p >
Enter your email address and we'll send you a link to sign in instantly.
</ p >
{ error && (
< div className = "error-message" >
{ error }
</ div >
) }
< div className = "form-group" >
< label htmlFor = "email" > Email Address </ label >
< input
type = "email"
id = "email"
value = { email }
onChange = { ( e ) => setEmail ( e . target . value ) }
placeholder = "Enter your email"
required
/>
</ div >
< div className = "form-group" >
< label className = "checkbox-label" >
< input
type = "checkbox"
checked = { createUser }
onChange = { ( e ) => setCreateUser ( e . target . checked ) }
/>
Create account if it doesn't exist
</ label >
</ div >
< button type = "submit" disabled = { loading || ! email } >
{ loading ? 'Sending...' : 'Send Magic Link' }
</ button >
< p >
Prefer a password? < a href = "/login" > Sign in with password </ a >
</ p >
</ form >
);
}
export default MagicLinkForm ;
Magic Link with User Creation
import { useState } from 'react' ;
function MagicLinkSignup () {
const [ formData , setFormData ] = useState ({
email: '' ,
firstName: '' ,
lastName: ''
});
const [ loading , setLoading ] = useState ( false );
const [ sent , setSent ] = useState ( false );
const [ error , setError ] = useState ( '' );
const handleSubmit = async ( e ) => {
e . preventDefault ();
setLoading ( true );
setError ( '' );
try {
const response = await fetch ( '/api/auth/magiclink' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
email: formData . email ,
create_user: true ,
data: {
first_name: formData . firstName ,
last_name: formData . lastName
},
redirect_to: window . location . origin + '/welcome'
}),
});
if ( ! response . ok ) {
const errorData = await response . json ();
throw new Error ( errorData . msg || 'Failed to send magic link' );
}
setSent ( true );
} catch ( err ) {
setError ( err . message );
} finally {
setLoading ( false );
}
};
const handleInputChange = ( field , value ) => {
setFormData ( prev => ({
... prev ,
[field]: value
}));
};
if ( sent ) {
return (
< div className = "signup-success" >
< h2 > Welcome to Strike! </ h2 >
< p >
We've sent a magic link to < strong > { formData . email } </ strong >
</ p >
< p >
Click the link to complete your account setup and sign in.
</ p >
</ div >
);
}
return (
< form onSubmit = { handleSubmit } className = "magic-signup-form" >
< h2 > Join Strike </ h2 >
< p >
Create your account and sign in instantly with a magic link.
</ p >
{ error && (
< div className = "error-message" >
{ error }
</ div >
) }
< div className = "form-row" >
< div className = "form-group" >
< label htmlFor = "firstName" > First Name </ label >
< input
type = "text"
id = "firstName"
value = { formData . firstName }
onChange = { ( e ) => handleInputChange ( 'firstName' , e . target . value ) }
required
/>
</ div >
< div className = "form-group" >
< label htmlFor = "lastName" > Last Name </ label >
< input
type = "text"
id = "lastName"
value = { formData . lastName }
onChange = { ( e ) => handleInputChange ( 'lastName' , e . target . value ) }
required
/>
</ div >
</ div >
< div className = "form-group" >
< label htmlFor = "email" > Email Address </ label >
< input
type = "email"
id = "email"
value = { formData . email }
onChange = { ( e ) => handleInputChange ( 'email' , e . target . value ) }
placeholder = "Enter your email"
required
/>
</ div >
< button type = "submit" disabled = { loading } >
{ loading ? 'Creating Account...' : 'Create Account & Send Magic Link' }
</ button >
< p >
Already have an account? < a href = "/login" > Sign in </ a >
</ p >
</ form >
);
}
export default MagicLinkSignup ;
Node.js Backend Handler
const express = require ( 'express' );
const { body , validationResult } = require ( 'express-validator' );
const router = express . Router ();
router . post ( '/magiclink' , [
body ( 'email' ). isEmail (). normalizeEmail (),
body ( 'create_user' ). optional (). isBoolean (),
body ( 'data' ). optional (). isObject ()
], async ( req , res ) => {
try {
// Check validation errors
const errors = validationResult ( req );
if ( ! errors . isEmpty ()) {
return res . status ( 400 ). json ({
code: 400 ,
msg: 'Invalid request data' ,
details: errors . array ()
});
}
const { email , create_user , redirect_to , data , captcha_token } = req . body ;
// Call Strike Auth Service
const response = await fetch ( ` ${ process . env . AUTH_SERVICE_URL } /magiclink` , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
email ,
create_user: create_user || false ,
redirect_to: redirect_to || ` ${ process . env . APP_URL } /dashboard` ,
data ,
captcha_token
}),
});
const result = await response . json ();
if ( ! response . ok ) {
return res . status ( response . status ). json ( result );
}
// Log magic link request
console . log ( `Magic link sent to: ${ email } , create_user: ${ create_user } ` );
res . json ( result );
} catch ( error ) {
console . error ( 'Magic link error:' , error );
res . status ( 500 ). json ({
code: 500 ,
msg: 'Internal server error' ,
details: 'Please try again later'
});
}
});
module . exports = router ;
Use Cases
Passwordless Login
Perfect for users who prefer not to remember passwords:
{
"email" : "user@example.com" ,
"create_user" : false ,
"redirect_to" : "https://yourapp.com/dashboard"
}
Quick Signup
Streamlined user registration without password requirements:
{
"email" : "newuser@example.com" ,
"create_user" : true ,
"data" : {
"first_name" : "John" ,
"last_name" : "Doe" ,
"marketing_consent" : true
},
"redirect_to" : "https://yourapp.com/welcome"
}
Account Recovery
Alternative to password reset for users who prefer magic links:
{
"email" : "user@example.com" ,
"create_user" : false ,
"redirect_to" : "https://yourapp.com/account-recovered"
}
Security Features
Token Expiration : Magic links expire after 1 hour
Single Use : Links can only be used once
Rate Limiting : Prevents spam and abuse
Secure Tokens : Cryptographically secure random tokens
Email Verification : Inherent email verification through link click
Rate Limiting
This endpoint is rate limited to prevent abuse:
Limit Type Limit Window Per Email 3 requests 5 minutes Per IP 10 requests 10 minutes
Best Practices
Provide clear instructions about checking email
Include troubleshooting tips for email delivery issues
Offer alternative sign-in methods
Show loading states during email sending
Implement resend functionality with cooldown
Use secure, random tokens with sufficient entropy
Implement proper token expiration (1 hour recommended)
Log magic link requests for security monitoring
Validate email addresses before sending
Implement rate limiting to prevent abuse
Use reputable email service providers
Implement proper email authentication (SPF, DKIM, DMARC)
Monitor delivery rates and bounce rates
Provide clear sender identification
Include plain text version of emails
Testing
Unit Tests
describe ( 'POST /magiclink' , () => {
test ( 'should send magic link for existing user' , async () => {
const response = await request ( app )
. post ( '/magiclink' )
. send ({
email: 'existing@example.com' ,
create_user: false
})
. expect ( 200 );
expect ( response . body . message_id ). toBeDefined ();
});
test ( 'should create user and send magic link' , async () => {
const response = await request ( app )
. post ( '/magiclink' )
. send ({
email: 'new@example.com' ,
create_user: true ,
data: {
first_name: 'Test' ,
last_name: 'User'
}
})
. expect ( 200 );
expect ( response . body . message_id ). toBeDefined ();
});
test ( 'should reject invalid email' , async () => {
await request ( app )
. post ( '/magiclink' )
. send ({
email: 'invalid-email' ,
create_user: true
})
. expect ( 400 );
});
test ( 'should return 404 for non-existent user when create_user is false' , async () => {
await request ( app )
. post ( '/magiclink' )
. send ({
email: 'nonexistent@example.com' ,
create_user: false
})
. expect ( 404 );
});
});