Verified Modal

Who this is for: ISV partners embedding ProphetX in their own app or website.
This guide covers the two identity-verification surfaces the embed exposes:
1-Click Signup for brand-new users, and document verification for an
existing user whose automated KYC check came back inconclusive.

triangle-exclamation

This page has been corrected. A previous version of this page described a single
"Verified Modal" (useOnboarding/openOnboarding) that walked any user — new or
existing — through a phone number + SMS one-time-code flow. That flow is not
available
in the current embed: openOnboarding() opens a route that is
permanently gated off and shows an error screen, because the underlying Verified
Inc API integration was never approved for that integration type. There are now
two separate, real flows — described below — with different hooks and different
callback shapes. If your integration currently calls useOnboarding/openOnboarding,
switch to one of these two.

Everything below uses the public ProphetX SDK — you don't need access to any internal
ProphetX systems or verification API keys to integrate.


Two different flows, not one

You want to…Use thisVendor
Create a brand-new user from a phone number, no signup form of your ownuseOneClickSignupVerified Inc (their own hosted SDK modal)
Re-verify an existing user whose automated KYC came back FAILUREuseKyc (alias) or useIdUpload (preferred)IDComply (document photo upload)

These are not interchangeable, and they don't share a callback shape. 1-Click Signup ends where signup ends — KYC hasn't been asked for yet, and only starts once your backend redeems the identity it returns. Document verification, by contrast, IS the KYC decision for an already-existing user. That's why one uses onOneClick* callbacks and the other uses onKyc* callbacks: reporting a KYC verdict from 1-Click would claim a result for something nothing had been submitted to yet.

useKyc/openKyc is a historical alias — it now opens the exact same document-verification flow as useIdUpload/openIdUpload, kept only so partners already calling it keep working. New integrations should call useIdUpload directly.


Flow 1: 1-Click Signup (new users)

Create a ProphetX user from a phone number in about ten seconds. The user confirms their phone by one-time code inside Verified Inc's own hosted modal (not a ProphetX-built screen), reviews the identity Verified already holds for them, and shares it. Your backend redeems that identity and the user exists — but KYC has not run yet at that point; it starts asynchronously once your backend redeems the identity.

Building your own UI instead of embedding this modal? See Verified 1-Click API (server-to-server) for the same phone-verification-then-PII-lookup flow, driven entirely from your own backend.

This is the one flow that runs before there is a ProphetX user, so it carries no embed token at all — it takes a single-use sessionKey instead, minted by your backend.

The two backend legs

# 1. Mint the key. ISV JWT that omits `sub` — there is no user yet.
POST /private/v1/one-click/sessions
X-ProphetX-ISV: <your isv id>

  -> 200 { "sessionKey": "...", "environment": "sandbox" }
  -> 503 when Verified is unreachable

# 2. Redeem the identity your onOneClickSuccess callback received. Ordinary ISV JWT.
POST /private/v1/users/one-click
{
  "identityUuid": "...",
  "email": "[email protected]",          # Verified returns none — this is yours
  "emailVerifiedAt": "2026-08-25T12:00:00Z",
  "phoneVerifiedAt": "2026-08-25T12:00:00Z"
}

  -> 201 { "id": "...", "kycStatus": "...", "sharedSecret": "..." }
  -> 409 user_already_exists   (that identity already has a ProphetX user)
  -> 422 unknown identityUuid, or credentials too incomplete to build a user
  -> 503 when Verified is unreachable

A sessionKey is single-use — a spent or lapsed key must be re-minted, not reopened.

Client usage

// React — @prophetx/sdk-react
import { useOneClickSignup } from '@prophetx/sdk-react';

function SignUpButton() {
  const { open } = useOneClickSignup();

  async function startSignup() {
    // Your backend, not the browser — minting a key needs the Verified API key.
    const { sessionKey, environment } = await myBackend.mintOneClickSession();

    open({
      sessionKey,
      environment, // must match the environment the key was minted for

      // The only place you learn the identity. Redeem it server-side.
      onOneClickSuccess: (identityUuid) => myBackend.redeem(identityUuid),

      // The user chose not to share. Not an error — fall back to your own form.
      onOneClickDeclined: () => showManualSignupForm(),

      // `retryable` is true for exactly one case: a spent or lapsed key.
      onOneClickFailed: async (reason, retryable) => {
        if (!retryable) return showManualSignupForm();
        const next = await myBackend.mintOneClickSession();
        open({ sessionKey: next.sessionKey, environment: next.environment /* ...same callbacks */ });
      },
    });
  }

  return <button onClick={startSignup}>Sign up in one click</button>;
}

Vanilla JS (sdk.openOneClickSignup({...})), React Native, and Flutter all take the same options. There's no token, redirectUrl, or onSessionExpired on this flow — passing them is a type error, not a silently ignored option, because 1-Click has no embed session to expire.

Callbacks

CallbackWhen it firesWhat you should do
onOneClickSuccess(identityUuid)User shared their identityRedeem it server-side at POST /private/v1/users/one-click
onOneClickDeclined()User chose not to share — not an errorFall back to your own manual signup form
onOneClickFailed(reason, retryable)Terminal failure. retryable is only ever true for a spent/lapsed keyIf retryable, mint a fresh key and reopen; otherwise fall back to manual signup
onCancel()User closed the modalNo state change
onError(code, message)Something went wrong loading the flowShow a generic retry message

Every outcome here is terminal — there's no later signal to fall back on if you don't wire all three onOneClick* callbacks, so wire all of them.

Testing

Sandbox sends a real SMS — use a phone you actually hold. It returns a fixed test persona (Richard Hendricks, DOB 1989-08-01) regardless of the number.


Flow 2: Document verification (existing users)

For a user who already exists and whose automated KYC check came back FAILURE, the fallback is a government-ID photo upload reviewed by IDComply. This is the flow both useKyc/openKyc (historical alias) and useIdUpload/openIdUpload (preferred name) open — same implementation, same callbacks, with exactly one difference: only useIdUpload accepts redirectUrl. Prefer useIdUpload for new integrations, and definitely for native, where redirectUrl is effectively required.

This flow is narrow by design — it only proceeds to the document step for a user whose automated check actually came back FAILURE. Anyone already verified, still under review, or out of retry attempts sees the matching terminal screen ("Already Verified" / "Still Under Review" / "Not Eligible" / attempts-exhausted) within a second or two instead — that's the flow working correctly. Gate any "Verify ID" entry point on the user's KYC status rather than showing it unconditionally, or most users will see it close immediately.

How it runs

  1. The embed reads the user's KYC status first. If they're not eligible, it stops here and shows the matching terminal screen — no attempt is consumed.
  2. If eligible, it mints an IDComply session and opens the hosted form in a second browser tab — IDComply sets X-Frame-Options/frame-ancestors, so it cannot be shown in an iframe. There is no in-modal fallback.
  3. The modal stays on a "Finish Verifying Your Identity" waiting screen and polls while the user is in the form (document capture takes minutes, so this wait is deliberately generous).
  4. When the session finishes, the verdict resolves and the modal lands on a terminal screen. By the time your callback fires, the decision is final — unlike 1-Click, there's no further async step after this.

Client usage

// React — @prophetx/sdk-react
import { useIdUpload } from '@prophetx/sdk-react'; // useKyc opens the same flow but has no redirectUrl option

function VerifyIdButton() {
  const { open } = useIdUpload();

  return (
    <button
      onClick={() =>
        open({
          // redirectUrl only exists on useIdUpload, not the legacy useKyc alias.
          // Native apps must set this to a deep link (e.g. myapp://kyc/return) —
          // otherwise the OS browser can't hand control back to your app.
          redirectUrl: 'myapp://kyc/return',
          onKycSuccess: () => showVerifiedMessage(),
          onKycFailed: (reason, retryable) => {
            // `reason` is for logging/routing only — never display it, the embed
            // deliberately withholds specifics. Branch on `retryable`, not `reason`.
            if (retryable) showTryAgainMessage();
            else showContactSupportMessage();
          },
          onKycPending: () => showPendingReviewMessage(),
          onCancel: () => {},
        })
      }
    >
      Verify my identity
    </button>
  );
}

Callbacks

CallbackWhen it firesWhat you should do
onKycSuccess(identityUuid?)Document verified. No identityUuid — the IDPV API behind this route returns none.Treat the user as verified; you already have their UUID from POST /users.
onKycFailed(reason, retryable)Rejected. retryable is false for compliance outcomes and exhausted attempts — branch on it, not reason.If retryable, offer to try again; otherwise direct to support.
onKycPending()Document submitted, review not yet resolvedShow a "still under review" state
onCancel()User closed the modalNo state change
onError(code, message)Something went wrong loading the flowShow a generic retry message
onGeofenceBlocked(stateCode, reason)See Geofencing belowShow a "not available in your area" message

You cannot tell apart "already verified" / "still under review" / "compliance-failed" / "attempts-exhausted" from the API response alone — the backend answers 409 kyc_not_retryable identically for all four. The embed disambiguates by reading KYC status first, before asking to start; you just receive the resolved terminal outcome.


Geofencing

onGeofenceBlocked(stateCode, reason) and the PROPHETX_GEOFENCE_BLOCKED postMessage event are real and defined identically on every flow above, including 1-Click Signup — but it won't actually fire on 1-Click in practice, since that flow runs before a user exists and its tokenless session is treated as always-allowed rather than evaluated against the geo gate. In the current architecture, geo-eligibility for the embed is enforced at token issuance, not as a per-request check inside these flows — a user who isn't geo-eligible won't have your backend successfully mint them an embed token in the first place. Wire up onGeofenceBlocked for defensive completeness, but don't expect it to fire in normal operation with a validly-issued embed token.


Frequently asked

Do I ever see the user's personal data (name, DOB, SSN)?
No. For 1-Click Signup, all personal data stays inside Verified's own modal — you only receive an opaque identityUuid. For document verification, IDComply's hosted form runs in its own tab; you never receive PII at all, not even an identifier.

Do I need a verification provider account or API key?
No, for either flow. Session/API keys live server-side — your backend calls ProphetX, not Verified or IDComply directly.

What if the user closes the flow halfway?
You get onCancel(). Nothing is verified; they can reopen and start again (subject to 1-Click's single-use session key, or document verification's attempt cap).

Can I style either flow to match my brand?
Document verification runs inside the embed's own modal, so theme overrides on ProphetXProvider apply. 1-Click Signup renders Verified's own hosted modal, which the embed does not restyle.



Did this page help you?