TryMellon
Navigation

B2B account recovery

Recover users without giving TryMellon their PII. You issue a one-time enrollment link over your own channel; users register a new passkey, their old credentials are revoked.

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

SituationWhy this flow fits
Regulated fintech / CeFiPII stays in your PCI/KYC-compliant store. TryMellon only sees opaque IDs.
Mobile banking with push infraYou already have a trusted delivery channel. Reuse it.
Privacy-first consumer appsNo email addresses in TryMellon means no secondary attack surface.
Internal tools behind SSOYou 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 ─────────────────│                        │
  1. Your backend calls POST /v1/users/:external_user_id/recovery/enroll with Basic Auth.
  2. TryMellon returns a one-time enrollment_url with a configurable TTL (15 min – 7 days).
  3. You deliver the URL to the user through your own channel.
  4. When the user opens it, TryMellon walks them through passkey registration.
  5. On success, all previously registered credentials are revoked and a fresh session token is issued.
  6. TryMellon emits recovery.enrollment.completed to 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,
})

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.

{
  "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

NameTypeDescription
external_user_idstringYour user ID — the same one you used at registration.

Body

FieldTypeRequiredDescription
ttl_secondsnumbernoEnrollment URL lifetime. Default 3600 (1 h). Min 900 (15 min). Max 604800 (7 d).

Response — 201 Created

FieldTypeDescription
ticket_idstringOpaque one-time ticket. Store for audit.
enrollment_urlstringDeliver this to the user. Consumed on first successful registration.
expires_atstring (ISO 8601)Absolute expiry. After this the URL returns 410 Gone.
context_hashstringSHA-256 of the ticket context. Compare in audit to detect tampering.

Webhook events

EventWhen emitted
recovery.enrollment.issuedA ticket was issued (call-and-response with your /enroll call).
recovery.enrollment.completedThe user completed enrollment; old credentials revoked.

See Webhook events for the envelope.


Error codes

HTTPCodeCause
404RECOVERY_USER_NOT_FOUNDexternal_user_id does not exist in your tenant.
409RECOVERY_TICKET_LIMIT_EXCEEDEDUser already has an active recovery ticket. Wait for it to expire or consume it.
401unauthorizedBad or missing Basic Auth.
403forbiddenclient_secret is valid but the application is disabled or lacks the recovery capability.
429rate_limitedMore 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.