Magic Links
Magic links provide passwordless authentication — users click a link in their email to log in. No passwords to remember, no credentials to manage.
How It Works
1. User enters email ──▶ Your App
2. Your app calls send ──▶ Zyphr API ──▶ Email with magic link
3. User clicks link ──▶ Your App receives token from URL
4. Your app calls verify ──▶ Zyphr API ──▶ Returns user + tokens
If the email doesn't match an existing user, Zyphr automatically creates a new account during verification. Magic links work as both login and registration.
Send Magic Link
curl -X POST https://api.zyphr.dev/v1/auth/magic-link/send \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"redirect_url": "https://myapp.com/auth/magic-link"
}'
const response = await fetch('https://api.zyphr.dev/v1/auth/magic-link/send', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'jane@example.com',
redirect_url: 'https://myapp.com/auth/magic-link',
}),
});
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Recipient's email address |
redirect_url | string | No | URL to redirect to after clicking the link. The magic link token is appended as a query parameter. |
Response
{
"data": {
"success": true,
"message": "Magic link sent"
}
}
The response always indicates success, even if the email doesn't match an existing user. This prevents email enumeration attacks.
Verify Magic Link
When the user clicks the magic link, they're redirected to your redirect_url with a token. Extract the token and verify it:
curl -X POST https://api.zyphr.dev/v1/auth/magic-link/verify \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{ "token": "mlk_xxxx" }'
const response = await fetch('https://api.zyphr.dev/v1/auth/magic-link/verify', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: magicLinkToken,
}),
});
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The magic link token from the URL |
Response
{
"data": {
"user": {
"id": "usr_abc123",
"email": "jane@example.com",
"name": null,
"email_verified": true,
"created_at": "2025-01-15T10:00:00Z"
},
"tokens": {
"access_token": "eyJhbGci...",
"refresh_token": "zrt_xxxx",
"expires_in": 3600,
"token_type": "Bearer"
},
"is_new_user": true
}
}
The is_new_user field indicates whether the user was created during this verification. Use this to redirect new users to an onboarding flow.
Error Responses
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | Missing or empty token |
| 401 | invalid_token | Token is invalid, expired, or already used |
Complete Frontend Implementation
Here's a full implementation showing both the send and verify flows:
async function sendMagicLink(email) {
const response = await fetch('/api/auth/magic-link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (response.ok) {
// Show "check your email" message
showMessage('Check your email for a magic link!');
}
}
app.post('/api/auth/magic-link', async (req, res) => {
const { email } = req.body;
await fetch('https://api.zyphr.dev/v1/auth/magic-link/send', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
redirect_url: `${process.env.APP_URL}/auth/magic-link`,
}),
});
res.json({ success: true });
});
// Extract token from URL and verify
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
if (token) {
const response = await fetch('/api/auth/magic-link/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const { data } = await response.json();
// Store tokens
localStorage.setItem('access_token', data.tokens.access_token);
localStorage.setItem('refresh_token', data.tokens.refresh_token);
// Redirect based on user status
if (data.is_new_user) {
window.location.href = '/onboarding';
} else {
window.location.href = '/dashboard';
}
}
app.post('/api/auth/magic-link/verify', async (req, res) => {
const { token } = req.body;
const response = await fetch('https://api.zyphr.dev/v1/auth/magic-link/verify', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
});
const result = await response.json();
res.json(result);
});
Endpoint Reference
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /v1/auth/magic-link/send | App credentials | Send magic link email |
POST | /v1/auth/magic-link/verify | App credentials | Verify token and get auth tokens |