Sessions
A successful otp/verify call — whether the code arrived over WhatsApp or SMS, MiniMoth's delivery channel doesn't affect sessions at all — returns two tokens. Here's what they do and why both exist.
access_token
Short-lived — expires in 5 minutes. Send this with every authenticated request to your backend. Validate it locally as a JWT (zero network round trip — the Node.js SDK does this by default), or call session/validate for a server-checked result. See local vs. API validation for the trade-off.
refresh_token
Long-lived — absolute expiry set per project (default 10 days, max 30 days). Store this securely. Use it to get a new access_token when the old one expires, without asking the user to re-enter their OTP. Every refresh also rotates the refresh_token itself — replace both, see Refresh a session.
The access_token is intentionally short-lived. If it leaks — intercepted in a log, accidentally exposed in an error — the damage is limited to 5 minutes. After that it's useless.
But you can't ask your user to re-verify their phone number every 5 minutes. That's where the refresh_token comes in. It lives on your server only, never travels in API responses to end users, and silently exchanges for a new access_token in the background.
Think of it like this: the access_token is the door key (short-lived, used constantly), and the refresh_token is the key-cutting machine (long-lived, stored safely, used only when the door key expires).
OTP verified → two tokens issued
Store access_token in memory (or a short-lived cookie). Store refresh_token in an httpOnly cookie or secure server-side store.
Every request → validate access_token
Call session/validate. Returns valid: true + the session's expiry. Fast and lightweight — safe to call on every request. To resolve which end-user this session belongs to, look up the session_id against your own records — see the note below.
access_token expires (5 min) → call refresh
Call session/refresh with the refresh_token. Both tokens are rotated — you get a new access_token and a new refresh_token. Replace both in storage.
refresh_token reaches absolute expiry
The session is over. The user must re-verify their phone to get new tokens. The absolute expiry never extends — using the refresh token does not reset the clock.
User logs out → both tokens revoked immediately
Call session/logout. The access_token is immediately invalidated and all refresh tokens for the session are removed.
Every refresh rotates both tokens. The old refresh_token is immediately invalid. But there's one common scenario where this causes problems without a grace window:
The SSR hydration problem
In Next.js or similar frameworks, the server renders the page and the browser hydrates it almost simultaneously. If both try to refresh the token at the same instant — within milliseconds of each other — the first call succeeds and rotates the token. The second call arrives a few milliseconds later with the now-invalidated old token and gets a 401, logging the user out unexpectedly.
The 10-second grace window solves this: if the same refresh_token is used a second time within 10 seconds, MiniMoth treats it as a duplicate of the first call and returns a new access_token without invalidating anything.
After 10 seconds, the grace window closes. If an already-rotated refresh_token is used again, MiniMoth treats it as a stolen token and immediately revokes all tokens for that session, forcing the user to re-verify.
Within 10 seconds
Same token used again → treated as a duplicate. Returns new access_token safely. No lockout.
After 10 seconds
Same token used again → theft detected. All session tokens revoked. User must re-verify.
Note: when theft is detected, any access_token already issued for that session remains valid until it expires naturally (up to 5 minutes). Revocation applies to refresh tokens immediately — access tokens expire on their own TTL.
Validate a session
Call this on every authenticated request from your app's users. Fast and lightweight — safe to call on every request without latency concerns. Returns { valid, expires_at } — no session_id or phone number; see the note below on resolving those.
const res = await fetch('https://api.minimoth.dev/v1/session/validate', {
method: 'POST',
headers: { 'X-Api-Key': 'mm_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({ access_token }),
})
const { valid, expires_at } = await res.json()
// valid: false on any invalid/expired token — never throws The access_token is a signed JWT (RS256). Calling session/validate as above is API validation — every call checks MiniMoth's server-side cache, so revocation (logout, theft detection) is caught immediately.
The Node.js SDK also offers local JWT validation: it fetches MiniMoth's public signing key once from /.well-known/jwks.json (cached 1 hour) and verifies the token's signature and expiry entirely on your server — zero network round trip per request. This is the SDK's default validateMode: 'instant'.
Local (instant)
Fastest — no network call. Trade-off: blind to server-side revocation until the token's own 5-minute expiry. Fine for most apps.
API (or strict mode)
Always current — catches logout and theft detection immediately. Costs one network round trip per validate call.
The SDK also has recheck_1m / recheck_3m modes that balance the two — see validateMode for the full comparison.
Using the Node.js SDK? mm.session.safeValidate() does local JWT validation by default and auto-refreshes expired tokens for you:
import { MiniMoth } from '@minimoth/sdk-node'
const mm = new MiniMoth({ apiKey: process.env.MINIMOTH_API_KEY })
// Local JWT verify only, zero network round trip (default validateMode: 'instant')
const result = await mm.session.safeValidate(accessToken)
if (!result.valid) return res.status(401).json({ error: result.code })
// result.session.sessionId, result.session.expiresAt
// If the access token had expired, safeValidate already refreshed it —
// forward result.session.newTokens to the client if present Refresh a session
const res = await fetch('https://api.minimoth.dev/v1/session/refresh', {
method: 'POST',
headers: { 'X-Api-Key': 'mm_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token }),
})
const { access_token, refresh_token: new_refresh_token, expires_at } = await res.json()
// Always replace the stored refresh_token with new_refresh_token.
// The old one is immediately invalidated.
// 401 if the refresh token is expired or was reused outside the 10-second grace window. Always replace both tokens after a refresh — store the new access_token and the new refresh_token. The old refresh_token is immediately invalid.
Log out
await fetch('https://api.minimoth.dev/v1/session/logout', {
method: 'POST',
headers: { 'X-Api-Key': 'mm_live_...', 'Content-Type': 'application/json' },
body: JSON.stringify({ access_token }),
})
// 401 INVALID_ACCESS_TOKEN if the access_token is expired — refresh first, then log out Don't need session management? You can use MiniMoth purely to verify a phone number and manage sessions yourself. See the OTP-only recipe.
Resolving a session to a phone number. session/validate returns only { valid, expires_at } — no session_id and no phone number, by design (JWT payloads are unencrypted and travel client-side). The session_id lives in the access_token's own JWT payload — decode it yourself (the Node.js SDK's safeValidate() does this for you, exposing it as result.session.sessionId). You already have the phone number at the moment you call otp/verify (you submitted it yourself); store a session_id → phone mapping in your own database at that point, and look it up whenever a validated session needs to be tied back to a specific user.
Try the full OTP flow interactively in the sandbox — no SMS sent, no credits consumed. The same Playground also has a live test to send a real OTP to your phone (uses your credits).
Test in Playground →