Account Recovery & Device Rotation
TryMellon follows a strictly B2B posture: we never store or command your users’ personal data. Recovery is initiated from your server — you own the relationship with the user; we handle the cryptography.
Choosing a recovery flow
TryMellon ships two recovery flows. Pick one per integration:
| Flow | Endpoint | Who delivers the secret? | When to use |
|---|---|---|---|
| OTP recovery (this doc) | POST /v1/users/recovery/start | TryMellon (sends a 6-digit OTP by email). | Consumer apps where you’d rather TryMellon handle the channel. PII (email) is shared with TryMellon at recovery time. |
| B2B enrollment URL (b2b-recovery) | POST /v1/users/:external_user_id/recovery/enroll | You (push, SMS, in-app message). TryMellon never sees the destination. | Regulated fintechs, custodians, privacy-first apps where user contact data must stay in your systems. |
The two flows are mutually exclusive on a given recovery attempt — pick the one that matches your privacy posture and integration shape. Both end in a fresh passkey + atomic revocation of the previous credentials.
How it works (OTP flow)
- Your backend identifies the user and calls TryMellon to dispatch an OTP via email.
- The user receives a 6-digit code.
- Your frontend calls
client.passkey.recover()with the OTP — the SDK verifies the code, prompts the OS for a new passkey, and returns a fresh session token.
1. Server-to-Server: start recovery
When a user requests account recovery (e.g. via a “Lost phone” form), your backend must call TryMellon’s recovery endpoint. The email is used ephemerally for the OTP and not stored.
// Your backend — POST to TryMellon API
const response = await fetch('https://api.trymellonauth.com/v1/users/recovery/start', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_SECRET_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
external_id: 'usr_123A',
email: 'customer@domain.com',
}),
});
2. Client-side: rotate the device
Once the user receives the 6-digit OTP, call passkey.recover() from the SDK. This verifies the OTP, triggers a WebAuthn registration ceremony for a new passkey, and returns a session token.
import { TryMellon } from '@trymellon/js';
const clientResult = TryMellon.create({ appId: 'YOUR_APP_ID', publishableKey: 'cli_xxxx' });
if (!clientResult.ok) throw clientResult.error;
const client = clientResult.value;
// When the user submits the 6-digit OTP
const result = await client.passkey.recover({
externalUserId: 'usr_123A',
otp: '632145',
});
if (result.ok) {
console.log('Device rotated securely!');
// Send result.value.sessionToken to your backend to set session
await fetch('/api/auth/set-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionToken: result.value.sessionToken }),
});
}
if (!result.ok) {
console.error(result.error.code, result.error.message);
// Handle: INVALID_ARGUMENT (bad OTP format), NETWORK_FAILURE, etc.
}
RecoverAccountOptions
| Field | Type | Description |
|---|---|---|
externalUserId | string (required) | The external user ID of the account being recovered. |
otp | string (required) | The 6-digit OTP sent via email. |
RecoverAccountResult
| Field | Type | Description |
|---|---|---|
success | true | Always true on success. |
credentialId | string | The new passkey credential ID. |
status | string | Registration status. |
sessionToken | string | Fresh session token — send to your backend. |
user.userId | string | TryMellon internal user ID. |
user.externalUserId | string? | Your external user ID. |
user.email | string? | User email if available. |
redirectUrl | string? | Set when successUrl was passed and allowed. |
Security standards
- Rate limits: Maximum 3 OTPs per hour to prevent brute-force.
- Fresh code per request: Each call to start recovery issues a new OTP and invalidates any in-flight code for the same user. The outbound email always carries the latest code; previously emailed codes stop working the moment a new one is issued. Abuse is bounded by the 3/hour rate limit above.
- Hash at rest: OTPs are stored in Redis as versioned
HMAC-SHA256digests, never in plaintext. See Security → Data at rest. - Revocation: Previous cryptographic keys are immediately invalidated — lost devices cannot authenticate.