B2B account recovery
Not every product can — or should — hand over user email to its auth provider. Regulated fintechs, crypto custodians, and privacy-first apps often need recovery that keeps PII inside their own systems.
TryMellon’s enrollment URL flow fits that shape. Your backend asks TryMellon for a one-time recovery link over S2S. You deliver the link through your own channel (push notification, SMS, secure inbox). The user opens it, registers a new passkey, and all previous credentials are revoked atomically.
No PII leaves your system. No inbound email from TryMellon. One endpoint to call, one ticket to deliver.
When to use this flow
| Situation | Why this flow fits |
|---|---|
| Regulated fintech / CeFi | PII stays in your PCI/KYC-compliant store. TryMellon only sees opaque IDs. |
| Mobile banking with push infra | You already have a trusted delivery channel. Reuse it. |
| Privacy-first consumer apps | No email addresses in TryMellon means no secondary attack surface. |
| Internal tools behind SSO | You already know how to reach the user; TryMellon just handles the cryptography. |
If you prefer TryMellon to send the OTP email directly, use the OTP recovery flow instead.
How it works
Your backend TryMellon API Your user
│ │ │
│── POST /v1/users/:id/ ─────>│ issue enrollment ticket
│ recovery/enroll │ + context hash + TTL
│<── { enrollment_url, ───────│
│ ticket_id, expires_at } │
│ │
│── deliver URL via ─────────────────────────────────>│
│ push / SMS / secure inbox │
│ │ │
│ │<── open URL ──────────│
│ │ │
│ │── WebAuthn register ──>│
│ │<── new passkey ───────│
│ │ │
│ │ revoke all existing credentials atomically
│ │ emit `recovery.enrollment.completed` webhook
│ │ │
│<── webhook ─────────────────│ │
- Your backend calls
POST /v1/users/:external_user_id/recovery/enrollwith Basic Auth. - TryMellon returns a one-time
enrollment_urlwith a configurable TTL (15 min – 7 days). - You deliver the URL to the user through your own channel.
- When the user opens it, TryMellon walks them through passkey registration.
- On success, all previously registered credentials are revoked and a fresh session token is issued.
- TryMellon emits
recovery.enrollment.completedto your configured webhook.
Quick start
1. Request an enrollment ticket from your backend
POST https://api.trymellonauth.com/v1/users/usr_123A/recovery/enroll
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/json
{
"ttl_seconds": 3600
}
Response — 201 Created:
{
"ok": true,
"data": {
"ticket_id": "tkt_01HXYZ…",
"enrollment_url": "https://auth.trymellonauth.com/enroll?ticket=tkt_01HXYZ…",
"expires_at": "2026-04-17T15:30:00.000Z",
"context_hash": "a1b2c3d4…"
}
}
2. Deliver the URL to the user
Use the channel you trust. TryMellon deliberately does not do this step — so your user data never leaves your system.
await pushNotifications.send({
userId: 'usr_123A',
title: 'Recover your account',
body: 'Tap to register a new passkey.',
url: enrollmentUrl,
})
3. User opens the link
TryMellon serves the enrollment page, runs a WebAuthn registration ceremony, and redirects back to your app with a fresh session token. Your SDK handles this automatically on return — no extra client code needed for the happy path.
4. Listen for the webhook (optional but recommended)
{
"id": "evt_…",
"type": "recovery.enrollment.completed",
"created_at": "2026-04-17T15:12:34.000Z",
"application_id": "app_…",
"tenant_id": "ten_…",
"data": {
"ticket_id": "tkt_01HXYZ…",
"user_id": "user_…",
"external_user_id": "usr_123A",
"new_credential_id": "cred_…",
"revoked_credential_ids": ["cred_old1", "cred_old2"],
"completed_at": "2026-04-17T15:12:34.000Z"
}
}
Use this to log the recovery in your audit trail, clear security flags, or notify the user on a second channel.
API reference
POST /v1/users/:external_user_id/recovery/enroll
Authentication: Basic Auth (client_id:client_secret from dashboard).
Rate limit: 5 requests per minute per application. Returns 429 with Retry-After.
Path parameters
| Name | Type | Description |
|---|---|---|
external_user_id | string | Your user ID — the same one you used at registration. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
ttl_seconds | number | no | Enrollment URL lifetime. Default 3600 (1 h). Min 900 (15 min). Max 604800 (7 d). |
Response — 201 Created
| Field | Type | Description |
|---|---|---|
ticket_id | string | Opaque one-time ticket. Store for audit. |
enrollment_url | string | Deliver this to the user. Consumed on first successful registration. |
expires_at | string (ISO 8601) | Absolute expiry. After this the URL returns 410 Gone. |
context_hash | string | SHA-256 of the ticket context. Compare in audit to detect tampering. |
Webhook events
| Event | When emitted |
|---|---|
recovery.enrollment.issued | A ticket was issued (call-and-response with your /enroll call). |
recovery.enrollment.completed | The user completed enrollment; old credentials revoked. |
See Webhook events for the envelope.
Error codes
| HTTP | Code | Cause |
|---|---|---|
404 | RECOVERY_USER_NOT_FOUND | external_user_id does not exist in your tenant. |
409 | RECOVERY_TICKET_LIMIT_EXCEEDED | User already has an active recovery ticket. Wait for it to expire or consume it. |
401 | unauthorized | Bad or missing Basic Auth. |
403 | forbidden | client_secret is valid but the application is disabled or lacks the recovery capability. |
429 | rate_limited | More than 5 enroll calls per minute for this app. |
Security model
- Single-use: the ticket is invalidated on the first successful registration. Retries after success return
410 Gone. - Atomic revocation: new passkey registration and old-credential revocation happen in a single transaction. Partial states are impossible.
- Bounded TTL: you pick
ttl_seconds; TryMellon enforces the 15 min – 7 d window. Longer is convenient, shorter is safer — tune to your recovery UX. - No PII: TryMellon never sees the destination channel, the message body, or the user’s contact details. You control delivery.
- Phishing-resistant registration: the enrollment URL is bound to your
rpId. A cloned page cannot produce a valid passkey for your app. - Audit by default: every issuance and completion is logged in your audit log and emitted as a webhook — non-repudiable by design.
Operational guidance
- Idempotency on the integrator side. If your user taps “recover” twice, your backend should check whether there’s an active ticket (catch
RECOVERY_TICKET_LIMIT_EXCEEDED) and either resend the existing URL or show a “link already sent” message. - Short TTL for high-value accounts. For admin or treasury users, use
ttl_seconds: 900(15 min) and pair with a second-channel confirmation. - Combine with step-up. If you run Action Signing on sensitive operations, the recovered passkey is immediately usable for action approvals on the new device.
- Don’t log the URL. The enrollment URL is a bearer credential until consumed. Redact it in application logs.
Related
- Account Recovery (OTP flow) — TryMellon sends the OTP email for you.
- Webhook event catalog
- Admin REST API