TryMellon
Navigation

Events & Error handling

SDK events and error handling with Result and error codes.

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

CodeDescription
NOT_SUPPORTEDWebAuthn not available in this environment
USER_CANCELLEDUser cancelled the operation
PASSKEY_NOT_FOUNDNo passkey found for this user
SESSION_EXPIREDSession or QR session expired
NETWORK_FAILURENetwork error (SDK retries with backoff)
INVALID_ARGUMENTInvalid argument in config, options, or origin not allowed
TIMEOUTOperation timed out
ABORT_ERROROperation aborted via AbortSignal or browser AbortError
CHALLENGE_MISMATCHLink already used, replayed, or context hash mismatch
RATE_LIMIT_EXCEEDEDToo many requests — SDK backs off and retries automatically
TICKET_NOT_FOUNDEnrollment ticket not found or invalid
TICKET_EXPIREDEnrollment ticket has expired
TICKET_ALREADY_USEDEnrollment ticket was already consumed
PIN_MISMATCHBridge presence PIN does not match
PIN_LOCKEDBridge PIN locked after too many failed attempts
BRIDGE_SESSION_EXPIREDBridge session expired or not found
OTP_INVALID_OR_EXPIREDEmail OTP (fallback or recovery) is invalid or has expired — request a new one
ACTION_CHALLENGE_EXPIREDAction signing challenge TTL exceeded (default 5 min) — issue a new challenge
ACTION_ALREADY_CLAIMEDAction challenge already verified — anti-replay; each challenge is single-use
ACTION_PAYLOAD_MISMATCHPayload hash signed by the device doesn’t match what the server issued
FORBIDDENAccess denied — missing required role or scope
SECRET_ROTATION_FORBIDDENCaller is not allowed to rotate this application’s client secret (not creator / tenant owner / account owner)
JWT_KID_MISMATCHJWT kid does not match any key in the JWKS — signing key may have rotated; verifyOffline auto-refreshes on next call
INTROSPECTION_FAILEDToken introspection rejected — check credentials and token format
CUSTOM_CLAIM_NOT_ALLOWEDA customClaims key was rejected by the application’s whitelist schema
CUSTOM_CLAIMS_TOO_LARGEcustomClaims payload exceeds the allowed size cap
SERVER_ERRORUnrecoverable server-side error
UNKNOWN_ERRORUnexpected error — check error.details for context

Webhook event types

The full catalog with payload examples lives in Webhook events. Quick reference:

EventTrigger
application.secret_rotatedApplication client_secret rotated.
session.revokedSession revoked (admin or DELETE /v1/sessions/:id).
session.logoutUser-initiated logout.
user.lockedHard-lockout after 10 failed auth attempts in 24h.
credential.revokedPasskey 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:

  1. Call POST /v1/applications/:id/rotate-secret with grace_period_seconds: 1800 (or whatever exceeds your worst-case rollout time).
  2. Push the new client_secret to your secret store (Vault, GitHub Secrets, AWS SSM).
  3. Trigger your deploy. All replicas eventually pick up the new secret.
  4. 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')
}