Events & Error handling
Events
The SDK emits events so you can show spinners and handle analytics:
client.on('start', (payload) => {
console.log('Started:', payload.operation) // 'signUp' | 'signIn' | 'enroll'
showSpinner()
})
client.on('success', (payload) => {
console.log('Success:', payload.operation)
hideSpinner()
})
client.on('error', (payload) => {
console.error('Error:', payload.error)
hideSpinner()
showError(payload.error.message)
})
client.on('cancelled', (payload) => {
console.log('Cancelled:', payload.operation) // 'signUp' | 'signIn'
hideSpinner()
})
// Unsubscribe
const unsubscribe = client.on('start', handler)
unsubscribe()
Events: 'start', 'success', 'error', 'cancelled'.
Error handling
All methods return Result<T, TryMellonError>. Check result.ok and use result.error.code:
import { isTryMellonError } from '@trymellon/js'
const result = await client.signIn({ externalUserId: 'user_123' })
if (!result.ok) {
const error = result.error
switch (error.code) {
case 'USER_CANCELLED':
console.log('User cancelled')
break
case 'NOT_SUPPORTED':
await client.otp.send({
userId: 'user_123',
email: 'user@example.com',
})
break
case 'PASSKEY_NOT_FOUND':
await client.signUp({ externalUserId: 'user_123' })
break
case 'NETWORK_FAILURE':
// Often caused by unallowed Origin in TryMellon dashboard (CORS rejection)
console.error('Network error:', error.details)
break
case 'TIMEOUT':
console.error('Operation timed out')
break
default:
console.error(error.code, error.message)
}
return
}
// result.value has sessionToken, user, etc.
Error codes
| Code | Description |
|---|---|
NOT_SUPPORTED | WebAuthn not available in this environment |
USER_CANCELLED | User cancelled the operation |
PASSKEY_NOT_FOUND | No passkey found for this user |
SESSION_EXPIRED | Session or QR session expired |
NETWORK_FAILURE | Network error (SDK retries with backoff) |
INVALID_ARGUMENT | Invalid argument in config, options, or origin not allowed |
TIMEOUT | Operation timed out |
ABORT_ERROR | Operation aborted via AbortSignal or browser AbortError |
CHALLENGE_MISMATCH | Link already used, replayed, or context hash mismatch |
RATE_LIMIT_EXCEEDED | Too many requests — SDK backs off and retries automatically |
TICKET_NOT_FOUND | Enrollment ticket not found or invalid |
TICKET_EXPIRED | Enrollment ticket has expired |
TICKET_ALREADY_USED | Enrollment ticket was already consumed |
PIN_MISMATCH | Bridge presence PIN does not match |
PIN_LOCKED | Bridge PIN locked after too many failed attempts |
BRIDGE_SESSION_EXPIRED | Bridge session expired or not found |
OTP_INVALID_OR_EXPIRED | Email OTP (fallback or recovery) is invalid or has expired — request a new one |
ACTION_CHALLENGE_EXPIRED | Action signing challenge TTL exceeded (default 5 min) — issue a new challenge |
ACTION_ALREADY_CLAIMED | Action challenge already verified — anti-replay; each challenge is single-use |
ACTION_PAYLOAD_MISMATCH | Payload hash signed by the device doesn’t match what the server issued |
FORBIDDEN | Access denied — missing required role or scope |
SECRET_ROTATION_FORBIDDEN | Caller is not allowed to rotate this application’s client secret (not creator / tenant owner / account owner) |
JWT_KID_MISMATCH | JWT kid does not match any key in the JWKS — signing key may have rotated; verifyOffline auto-refreshes on next call |
INTROSPECTION_FAILED | Token introspection rejected — check credentials and token format |
CUSTOM_CLAIM_NOT_ALLOWED | A customClaims key was rejected by the application’s whitelist schema |
CUSTOM_CLAIMS_TOO_LARGE | customClaims payload exceeds the allowed size cap |
SERVER_ERROR | Unrecoverable server-side error |
UNKNOWN_ERROR | Unexpected error — check error.details for context |
Webhook event types
The full catalog with payload examples lives in Webhook events. Quick reference:
| Event | Trigger |
|---|---|
application.secret_rotated | Application client_secret rotated. |
session.revoked | Session revoked (admin or DELETE /v1/sessions/:id). |
session.logout | User-initiated logout. |
user.locked | Hard-lockout after 10 failed auth attempts in 24h. |
credential.revoked | Passkey credential revoked by user or admin. |
For HMAC verification and replay protection see Signature verification. For invalidating local session caches see Handling revocation.
Troubleshooting CORS & Network Errors
If your users experience silent failures or you see NETWORK_FAILURE in the SDK with a (null) status in your browser’s Network tab for OPTIONS preflight requests, this typically means CORS rejection.
Ensure that the domain from which the SDK is running (e.g., https://your-app.com) has been added to your Application’s Allowed Origins list in the TryMellon dashboard. TryMellon enforces strict dynamic origin validation to protect your users.
Troubleshooting: rotating secrets without downtime
Rotation returns a previous_secret_expires_at timestamp; both the old and new secret are accepted during that window (default 15 min, max 60 min). Pattern for zero-downtime CI/CD:
- Call
POST /v1/applications/:id/rotate-secretwithgrace_period_seconds: 1800(or whatever exceeds your worst-case rollout time). - Push the new
client_secretto your secret store (Vault, GitHub Secrets, AWS SSM). - Trigger your deploy. All replicas eventually pick up the new secret.
- The previous secret keeps working until the grace window closes — old replicas can drain naturally.
Full example (GitHub Actions): API key rotation.
If you see SECRET_ROTATION_FORBIDDEN, the calling user is not the application creator, tenant owner, or account owner — only those roles can rotate.
Cancel with AbortSignal
const controller = new AbortController()
setTimeout(() => controller.abort(), 10000)
const result = await client.signUp({
externalUserId: 'user_123',
signal: controller.signal,
})
if (!result.ok && result.error.code === 'ABORT_ERROR') {
console.log('Cancelled')
}