Native Authentication
Build custom login UI with Transactional Auth as a headless backend.
Overview
Native authentication lets you build your own login, signup, and account management UI while using Transactional Auth as the backend. Users never see Transactional's hosted pages - they interact entirely with your branded interface.
When to Use Native Auth
| Use Case | Recommended Approach |
|---|---|
| Custom branded login UI | Native Auth |
| Mobile apps (React Native, Swift, Kotlin) | Native Auth |
| Desktop apps (Electron, Tauri) | Native Auth |
| CLI tools | Native Auth |
| Standard web apps | OAuth or Better Auth |
| Enterprise SSO required | SSO |
How It Works
┌─────────────────────────────────────┐
│ Your Application │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Your Login │ │ Your API │ │
│ │ Form │──┼──────────────┤ │
│ └─────────────┘ │ │ │
│ │ Validates │ │
│ │ JWT tokens │ │
│ └──────────────┘ │
└────────────────────┬────────────────┘
│
│ HTTPS
▼
┌─────────────────────────────────────┐
│ Transactional Auth (Headless API) │
│ your-app.auth.usetransactional.com │
│ │
│ /api/auth/signup │
│ /api/auth/login │
│ /api/auth/refresh │
│ /api/auth/logout │
│ /api/auth/me │
└─────────────────────────────────────┘
Prerequisites
- A Transactional account with an Auth application
- Your application's Auth Domain (e.g.,
your-app-abc123.auth.usetransactional.com) - Your Client ID and Client Secret
API Endpoints
All endpoints are available at your application's Auth Domain:
https://your-app-abc123.auth.usetransactional.com/api/auth/
| Endpoint | Method | Description |
|---|---|---|
/api/auth/signup | POST | Create new user account |
/api/auth/login | POST | Authenticate and get tokens |
/api/auth/refresh | POST | Refresh access token |
/api/auth/logout | POST | Revoke refresh token |
/api/auth/me | GET | Get current user profile |
/api/auth/forgot-password | POST | Request password reset |
Authentication
All requests must include your client credentials:
// Option 1: In request body
{
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"email": "user@example.com",
"password": "..."
}
// Option 2: In headers
headers: {
"x-client-id": "your_client_id",
"x-client-secret": "your_client_secret",
"Content-Type": "application/json"
}Signup
Create a new user account:
const response = await fetch(
'https://your-app.auth.usetransactional.com/api/auth/signup',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'your_client_id',
client_secret: 'your_client_secret',
email: 'user@example.com',
password: 'securepassword123',
name: 'John Doe',
given_name: 'John',
family_name: 'Doe',
}),
}
);
const { data } = await response.json();
// {
// user: { id, email, name, email_verified, ... },
// access_token: "eyJ...",
// refresh_token: "...",
// expires_in: 3600,
// token_type: "Bearer"
// }Signup Errors
| Error Code | Description |
|---|---|
invalid_client | Invalid client_id or client_secret |
invalid_request | Missing email or password |
invalid_email | Email format invalid |
weak_password | Password must be at least 8 characters |
user_exists | Account with this email already exists |
Login
Authenticate an existing user:
const response = await fetch(
'https://your-app.auth.usetransactional.com/api/auth/login',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'your_client_id',
client_secret: 'your_client_secret',
email: 'user@example.com',
password: 'securepassword123',
}),
}
);
const { data } = await response.json();
// {
// user: { id, email, name, ... },
// access_token: "eyJ...",
// refresh_token: "...",
// expires_in: 3600,
// token_type: "Bearer"
// }Login Errors
| Error Code | Description |
|---|---|
invalid_credentials | Wrong email or password |
user_blocked | Account has been blocked |
user_deactivated | Account has been deactivated |
Token Refresh
Access tokens expire after 1 hour. Use refresh tokens to get new access tokens:
const response = await fetch(
'https://your-app.auth.usetransactional.com/api/auth/refresh',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'your_client_id',
refresh_token: storedRefreshToken,
}),
}
);
const { data } = await response.json();
// {
// access_token: "eyJ...", // New access token
// refresh_token: "...", // NEW refresh token (rotation)
// expires_in: 3600,
// token_type: "Bearer"
// }
// IMPORTANT: Always store the new refresh token!Token Rotation
Refresh tokens are rotated on each use for security. Always store the new refresh token returned from the refresh endpoint.
If a refresh token is reused (indicating possible theft), all tokens for that user are automatically revoked.
Refresh Errors
| Error Code | Description |
|---|---|
invalid_token | Refresh token not found |
token_expired | Refresh token has expired (30 days) |
token_reused | Token already used (possible theft) |
Get Current User
Retrieve the authenticated user's profile:
const response = await fetch(
'https://your-app.auth.usetransactional.com/api/auth/me',
{
headers: {
'Authorization': `Bearer ${accessToken}`,
},
}
);
const { data } = await response.json();
// {
// id: "usr_...",
// email: "user@example.com",
// name: "John Doe",
// given_name: "John",
// family_name: "Doe",
// email_verified: true,
// picture: "https://...",
// created_at: "2024-01-15T...",
// updated_at: "2024-01-20T..."
// }Logout
Revoke a refresh token to end the session:
await fetch(
'https://your-app.auth.usetransactional.com/api/auth/logout',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: storedRefreshToken,
}),
}
);Password Reset
Request a password reset email:
await fetch(
'https://your-app.auth.usetransactional.com/api/auth/forgot-password',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: 'your_client_id',
email: 'user@example.com',
}),
}
);
// Always returns success to prevent email enumerationAccess Token Structure
Access tokens are JWTs with these claims:
{
"sub": "usr_abc123",
"email": "user@example.com",
"email_verified": true,
"name": "John Doe",
"org_id": 123,
"app_id": 456,
"iss": "https://your-app.auth.usetransactional.com",
"aud": "your_client_id",
"iat": 1704067200,
"exp": 1704070800
}Token Validation
Validate tokens in your backend:
import jwt from 'jsonwebtoken';
function validateToken(token: string) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
audience: process.env.TRANSACTIONAL_CLIENT_ID,
issuer: `https://${process.env.TRANSACTIONAL_AUTH_SUBDOMAIN}.auth.usetransactional.com`,
});
return { valid: true, user: decoded };
} catch (error) {
return { valid: false, error: error.message };
}
}React Example
Complete React hook for native auth:
// hooks/useAuth.ts
import { useState, useEffect, createContext, useContext } from 'react';
const AUTH_URL = process.env.NEXT_PUBLIC_AUTH_URL;
const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID;
const CLIENT_SECRET = process.env.NEXT_PUBLIC_CLIENT_SECRET;
interface User {
id: string;
email: string;
name: string;
email_verified: boolean;
}
interface AuthContext {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
signup: (email: string, password: string, name: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContext | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// Check for existing session on mount
useEffect(() => {
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
fetchUser(accessToken);
} else {
setLoading(false);
}
}, []);
async function fetchUser(token: string) {
try {
const res = await fetch(`${AUTH_URL}/api/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const { data } = await res.json();
setUser(data);
} else {
// Token invalid, try refresh
await refreshTokens();
}
} catch {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
} finally {
setLoading(false);
}
}
async function refreshTokens() {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) return;
const res = await fetch(`${AUTH_URL}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: CLIENT_ID,
refresh_token: refreshToken,
}),
});
if (res.ok) {
const { data } = await res.json();
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
await fetchUser(data.access_token);
} else {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
setUser(null);
}
}
async function login(email: string, password: string) {
const res = await fetch(`${AUTH_URL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
email,
password,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(error.message);
}
const { data } = await res.json();
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
setUser(data.user);
}
async function signup(email: string, password: string, name: string) {
const res = await fetch(`${AUTH_URL}/api/auth/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
email,
password,
name,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(error.message);
}
const { data } = await res.json();
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
setUser(data.user);
}
async function logout() {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
await fetch(`${AUTH_URL}/api/auth/logout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
}
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
setUser(null);
}
return (
<AuthContext.Provider value={{ user, loading, login, signup, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}React Native Example
// services/auth.ts
import * as SecureStore from 'expo-secure-store';
const AUTH_URL = 'https://your-app.auth.usetransactional.com';
const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
export async function login(email: string, password: string) {
const res = await fetch(`${AUTH_URL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
email,
password,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(error.message);
}
const { data } = await res.json();
// Store tokens securely
await SecureStore.setItemAsync('access_token', data.access_token);
await SecureStore.setItemAsync('refresh_token', data.refresh_token);
return data.user;
}
export async function getAccessToken() {
const token = await SecureStore.getItemAsync('access_token');
if (!token) return null;
// Check if expired and refresh if needed
// ... token validation logic
return token;
}Security Considerations
-
Store Client Secret Securely
- For web apps: Store server-side only, never expose to client
- For mobile/desktop apps: Use secure storage (Keychain, Keystore)
- For public clients: Consider using PKCE OAuth flow instead
-
Token Storage
- Web: Use httpOnly cookies or memory (avoid localStorage in production)
- Mobile: Use platform secure storage (SecureStore, Keychain)
- Desktop: Use OS credential manager
-
HTTPS Only
- All API calls must use HTTPS in production
- Never send credentials over unencrypted connections
-
Rate Limiting
- Built-in rate limiting on all auth endpoints
- Implement client-side throttling for better UX
Next Steps
On This Page
- Overview
- When to Use Native Auth
- How It Works
- Prerequisites
- API Endpoints
- Authentication
- Signup
- Signup Errors
- Login
- Login Errors
- Token Refresh
- Token Rotation
- Refresh Errors
- Get Current User
- Logout
- Password Reset
- Access Token Structure
- Token Validation
- React Example
- React Native Example
- Security Considerations
- Next Steps