# Feedback If you encounter incorrect, outdated, or confusing documentation on any page, submit feedback: POST https://developer.prove.com/feedback ```json { "path": "/current-page-path", "feedback": "Description of the issue" } ``` Only submit feedback when you have something specific and actionable to report. # Account Opening Manage Source: https://developer.prove.com/explanation/account-opening-manage Learn how the Prove Platform manages identity information and provides continuous monitoring of phone number changes. ## Receive Prove Manage webhooks Use this guide to **register an HTTPS endpoint** in the Prove Portal, **verify** each delivery with the signed JWT in **`X-Prove-Authorization`**, and **process** phone-change notifications from the JSON body. Webhook events are available in the **US** only. ### Prerequisites * **Public HTTPS URL** — Prove must be able to `POST` to your endpoint from the internet. Configure the URL in the [Prove Portal](https://portal.prove.com). * **Portal access** — Sign-in to the [Prove Portal](https://portal.prove.com/en/login) with permission to create or open an **Identity Manager** project. * **JWT verification** — Your service can validate **HS256** JWTs using the **shared secret** from the Portal (before you accept webhook JSON). * **Delivery expectations** — Prove emits events as changes are detected; delivery is **best-effort** and failed deliveries are re-queued for up to **3 delivery attempts**. Design your endpoint to respond quickly and return a **2xx** status when you accept a delivery. * **No retroactive history** — Events that occurred **before** you saved the webhook configuration are not sent later. * **Per-identity stream** — After certain outcomes (**disconnect**, **moved out of coverage**, or some **phone number change** situations), Prove may send **no further** notifications for that identity until you **re-verify** and re-enroll an updated number if the customer supplies one. ### Register the endpoint in the Portal Open the [Prove Portal](https://portal.prove.com/en/login) and authenticate. Go to **Projects**. Create a project (**Create Project**) and choose **Prove Identity Manager**, or open an existing Identity Manager project. In the project, open the **Configure** tab and locate **Sandbox** webhook settings (or the environment you are integrating). For a throwaway test URL, use [Webhook.site](https://webhook.site/) to mint a unique HTTPS endpoint. Enter your **HTTPS** listener URL in the webhook field, then use **Save and Test Webhook** so Prove persists the configuration and issues a **test** `POST` to your endpoint. For each request, treat the body as **untrusted** until the JWT checks pass. 1. Read **`Authorization`** from the **`X-Prove-Authorization`** header (`Bearer `). 2. Verify the JWT with **HS256** and the **shared secret** from the Portal. 3. Validate standard time claims (`iat`, `nbf`, `exp`), allowing about **±60 seconds** clock skew. Reject tokens past **`exp`** (Prove uses about **5 minutes** of validity from issuance). 4. Confirm **`iss`** equals the literal string **`Prove Identity`**. 5. **Replay protection** — Reject any **`jti`** you have already accepted within your replay window (each delivery uses a **new** UUID **`jti`**). 6. **Integrity** — Compute **SHA-256** over the **raw request body bytes** (before JSON parsing). Compare a **constant-time** hex string equality against the **`body_hash`** claim inside the JWT payload. If it fails, reject the request. Prove also sends **`X-Correlation-ID`** on each attempt—log it for support and tracing. Example decoded payload (middle segment of the JWT): ```json Example JWT payload theme={"dark"} { "iss": "Prove Identity", "iat": 1710864000, "nbf": 1710864000, "exp": 1710864300, "jti": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "body_hash": "8af324cfdeb52d549a2504f7ba20ea51950ee4593ce6182d2b1cea927e41944d" } ``` After JWT and **`body_hash`** succeed, parse the JSON. Expect a top-level **`notifications`** array (Prove may send **up to 100** objects per call; batches can be smaller). Handle each element using the fields below. Optional keys such as **`newCarrier`** or **`newLineType`** appear only for some event types. | Field | Use | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `eventId` | Store for idempotency and support references. | | `event` | Human-readable description. | | `eventType` | Branch logic. Values include `PHONE_NUMBER_CHANGE`, `DISCONNECT`, `PORT`, `LINE_TYPE_CHANGE`, `MOVED_OUT_OF_COVERAGE`. | | `eventTimestamp` | ISO 8601 time from Prove. | | `clientCustomerId` | Your customer key, if supplied on enrollment. | | `proveId` | Prove identifier for the enrolled identity. | ```json Example payload theme={"dark"} { "notifications": [ { "eventId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "event": "phone number change detected", "eventType": "PHONE_NUMBER_CHANGE", "eventTimestamp": "2025-01-23T10:11:12Z", "clientCustomerId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "proveId": "81d3829a-7207-4fd7-9a78-2dbf33fd54ad" } ] } ``` * Trigger **Save and Test Webhook** and confirm your handler receives a **test** request, returns **2xx**, and passes JWT + **`body_hash`** checks. * Force an **invalid** signature or body tamper in a dev environment and confirm your service **rejects** the call. * In Sandbox, exercise at least one real notification path you plan to support and confirm your persistence and idempotency logic behave as expected. ### Stop monitoring an identity Use the following endpoints when you need to stop webhook notifications for a specific enrolled identity: * [`POST /v3/identity/{identityId}/deactivate`](https://developer.prove.com/reference/identity-manager-deactivate-identity) stops webhook notifications without disenrolling the identity. * [`DELETE /v3/identity/{identityId}`](https://developer.prove.com/reference/identity-manager-delete-identity) disenrolls the identity from Identity Manager. To monitor that identity again later, you must re-enroll it. # Account Opening Source: https://developer.prove.com/explanation/account-opening-overview How Prove Account Opening's instant, phone-anchored verification works as a server-side integration—from /v3/verify through optional continuous monitoring. Prove Account Opening banner: instant, phone-anchored identity verification at signup ## Prove Account Opening solution Prove Account Opening verifies new customers using as little as a name and phone number, enabling instant onboarding and immediate activation at one of the highest-intent moments in the customer lifecycle. Where possible, identity is confirmed silently—via passive authentication using a cryptographic key or SIM validation—so legitimate users move forward without OTPs or extra steps, while suspicious signups can be challenged or blocked. This guide is where that pitch becomes an integration. Account Opening is delivered as a **server-side API** your backend calls during signup. It returns a structured **verification result** that your application uses to decide whether to **approve**, **step up**, or **decline**, plus identifiers (such as `proveId`) you persist to link the customer to later Prove calls and ongoing monitoring. Account Opening is built around **the phone as an anchor**: you establish **who is behind the device** at account creation so subsequent checks and records can rely on **Prove-backed identity** rather than thin, self-typed application data alone. Adaptive rules from Prove's **Global Fraud Policy** then shape each decision. Account Opening is **not** a full replacement for every compliance or risk policy you operate under. It gives you **strong phone-centric identity signals** and structured outcomes so your application can **approve**, **step up**, or **decline** according to your own rules and any other checks you run in parallel. Account Opening is available only in the **United States**. Flowchart: Account Opening from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success ## How fits together At a high level, rests on **Verify** and optionally **Manage**. They describe **how the real-time product is structured**, not a rigid checklist: your integration may combine or revisit them depending on channel, risk, and whether the customer already has a bound **Prove Key**. ### Verify **Verify** is your back end calling [`POST /v3/verify`](https://developer.prove.com/reference/verify) with the verification type and the inputs your flow requires to obtain a **verification result** and, when successful, identifiers such as **`proveId`** that you can persist for later calls, support, and linking to other Prove capabilities. End-to-end setup, Sandbox testing, and response handling are in the Verify guide. ### Manage **Manage** applies when a verified identity meets Prove’s [**Global Fraud Policy**](https://developer.prove.com/reference/global-fraud-policy): it can be **enrolled for continuous monitoring** of phone and related identity changes. Number changes, disconnects, and coverage moves are signals you want **after** day one, not only at signup. That path is documented under the Manage guide. **Webhook** notifications for those phone and identity change events are **available in the US**. # Build Prove with LLMs Source: https://developer.prove.com/explanation/build-with-llm How Prove’s documentation is structured for machine readability and LLM-assisted integration work. ## Concepts and definitions To better understand the technical infrastructure of Prove’s documentation, here are the key concepts and terms used: * **Machine Readability:** The design of content in a format that can be processed and "understood" by computer programs or AI, rather than just being optimized for human visual consumption. * **LLM (Large Language Model):** AI systems that process and generate human-like text, commonly reached through chat interfaces or APIs. ## Plain text docs Prove designs its documentation for AI consumption. Every page maintains a parallel Markdown representation accessible via the `.md` extension—for example, [this page as Markdown](https://developer.prove.com/explanation/build-with-llm.md). That helps AI tools and agents consume Prove content. | Feature | Markdown (.md) | HTML/JS Rendered Pages | | :----------------------- | :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | | **Token Efficiency** | **High.** Minimal syntax means more actual content fits in the context window. | **Low.** Dense with tags (`
`, ``) and scripts that waste tokens. | | **Data Extraction** | **Direct.** Content is ready to be parsed as-is without extra processing. | **Complex.** Requires a browser engine to execute JS before content is visible. | | **Content Visibility** | **Complete.** Includes all text, including content hidden in UI tabs or toggles. | **Partial.** Hidden or "lazy-loaded" content is often missing from the initial scrape. | | **Contextual Hierarchy** | **Explicit.** Headers (`#`, `##`) signal the importance and relationship of data. | **Inferred.** AI must guess hierarchy based on nested tags or CSS classes. | | **Formatting Noise** | **Minimal.** Focuses on the data, reducing the risk of the AI getting distracted. | **Significant.** Inline styles and attributes add "noise" to the signal. | Prove hosts /llms.txt and /llms-full.txt files which instruct AI tools and agents how to retrieve the plain text versions of Prove pages. The /llms.txt file is an [emerging standard for making websites and content more accessible to LLMs](https://llmstxt.org/). ## Contextual menu Plain Markdown and `/llms.txt` help tools *find* content. The harder part is getting the **page you are reading** into an assistant or agent **without fragile copy-paste**. The **contextual menu** at the top of each page is where you turn the current doc into **grounding context**: copy text, open Markdown, or connect to assistants and editor tooling. ## Model Context Protocol (MCP) The Prove [Model Context Protocol (MCP)](https://developer.prove.com/explanation/model-context-protocol) exposes tools that AI agents can use to search Prove’s documentation and read full pages from a virtual documentation filesystem. # Human Assurance Authenticate Source: https://developer.prove.com/explanation/human-assurance-authenticate Learn how the Prove Platform authenticates users through many integration methods. Prove simplifies and strengthens your authentication security by automatically choosing the right authenticator based on trusted device recognition and authenticator availability—reducing friction while protecting against fraud. Architecturally, Unify is **one coordinated session** across **client SDKs** (Web, iOS, Android) and **Unify platform APIs** such as [`/v3/unify`](https://developer.prove.com/reference/unify-request), with **channel fallbacks**—Instant Link, SMS OTP, Mobile Auth where enabled—so you are not maintaining a separate integration path for every surface and channel. Unify focuses on **phone possession**, **device trust**, and **Prove Key** lifecycle for that session. It does **not** replace your full **identity provider**, **authorization** (scopes, roles, entitlements), or **every** downstream fraud policy—you still map Prove outcomes into your own accounts, sessions, and risk stack. **Possession** is coordinated work: your app runs a **client-side SDK** (Web, iOS, or Android) while Prove’s **Unify platform APIs** advance the session and bind outcomes. Which endpoints apply depends on the integration variant. ## Concepts and definitions * **Prove Key:** Non-extractable cryptographic key used for **authentication** by Prove. * **Bind:** Establishes possession of the phone number to register the Prove Key. * **Customer-Initiated Bind:** Registry of a Prove Key based on customer-supplied possession of the phone number. * **Key Persistence:** Browser fingerprinting technology for the Web SDK that helps recover device registration when the Prove Key is unavailable, eliminating the need for SMS OTP or Instant Link re-authentication. ### What the Prove Key is associated with On Prove’s systems, each Prove Key is **correlated** with: * **`phoneNumber`:** the one used when the key is bound or authenticated * **`proveId`:** Prove’s identifier for the consumer when it is known * **`clientCustomerId`** and **`clientHumanId`:** when passed in the API request, these client-supplied identifiers are correlated with the same Prove Key Pass optional identifiers on your API requests when you want that linkage recorded for the session. ### Prove Key validity and expiration The Prove Key doesn't expire on a fixed calendar date. It stays valid with normal use, subject to the following: * **Inactivity:** Prove **deactivates the key on our servers** if the device remains inactive for an extended period. Each time the customer completes a **new authentication**, that inactivity period resets from that activity. * **App uninstall and reinstall for native apps:** If the user **deletes the app and reinstalls it**, behavior depends on platform: * **iOS:** The Prove Key **remains on the device**, so it is still available after reinstall. **Fingerprint technology is not needed** to reconstitute the key in that scenario. * **Android:** Key Persistence for recovering registration when the Prove Key is unavailable isn't available for the Android SDK. * **Forcing a possession check:** You can require an **active possession check** even when a key may still be valid; the Unify request supports signaling that intent (see the [`/v3/unify`](https://developer.prove.com/reference/unify-request) reference and [implementation guide](https://developer.prove.com/how-to/unify-implementation-guide)). ## Geographic availability **Prove Key** is available in **all countries**. **Mobile Auth** isn't supported in every country. Coverage varies by region—confirm availability for your markets when your flow depends on Mobile Auth. ## Integration variants Prove supports several **integration models**. They differ in **who runs possession first**, **which channels are in play**, and **when the Prove Key is considered bound**, but the **stages** below describe the same mental model for all of them. ### Stages every variant shares * **Open a protected session** — Typically scoped to the phone number so possession and downstream checks stay tied to one line. * **Possession or key reuse** — The SDK and server work to either **reuse a bound Prove Key** or complete a **possession** step through the channels your configuration allows. * **Status** — [`/v3/unify-status`](https://developer.prove.com/reference/unify-status-request) (and your app logic) resolve whether authentication finished, possession is still required, or the customer used an existing key. * **Bind when needed** — [`/v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) finalizes registration of the key when your flow requires an explicit bind after possession. * **Return visits** — A bound key often allows **lighter** re-authentication until policy or **rebind** forces a full possession ritual again. ### Choosing an integration model Use this as a **decision lens**, not a rigid rule: * **[Prove’s possession flow](https://developer.prove.com/how-to/human-assurance-prove-possession-web-sdk)** — Prefer when you want **Prove-led channels** (Instant Link, OTP, Mobile Auth) to carry **primary** possession and you do not need your own vendor in the critical path. * **[Customer-supplied possession with force bind](https://developer.prove.com/how-to/human-assurance-customer-possession)** — Prefer when you already run **your own possession** (or must) and still want Prove to **issue and register** the Prove Key after your step succeeds. * **[Prove passive authentication with customer-supplied possession fallback](https://developer.prove.com/how-to/human-assurance-prove-auth-then-customer)** — Prefer when you want **passive Mobile Auth first** on supported mobile networks, with **your** possession channel only when passive paths cannot complete binding. ### Prove’s possession flow **In scope:** Prove Key; **Mobile Auth** and **Prove OTP** on mobile; **Instant Link** on desktop. **Out of scope:** Your own possession channel as the primary path. Diagram illustrating Prove's unified authentication possession flow In this model, **Prove’s channels** carry possession on each surface. The flow opens a **protected session** scoped to the phone number. The **client SDK** then tries the lightest path first: if a **bound Prove Key** already exists, the customer may authenticate without repeating a full possession ritual. If no usable key is present, **possession** depends on context. On **desktop**, that typically means **Instant Link** so the user can complete proof in the mobile environment. On **mobile**, Prove-led channels apply—most often **SMS OTP**, and when **Mobile Auth** is enabled, silent network authentication may run ahead of OTP with OTP as fallback. The key may not be **fully bound** until **status** confirms the session, depending on how channels stack in your configuration. The **status** stage tells you whether authentication succeeded, whether possession is still outstanding, or whether the customer authenticated with an existing key—so your application knows what to surface next. After a successful bind or authentication, the same customer can usually return through **Prove Key–based** authentication on later visits. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/human-assurance-prove-possession-web-sdk). ### Customer-supplied possession with force bind **Scope:** Mobile channels where Prove still issues the **Prove Key**, but **your possession vendor** performs the primary possession step. Diagram illustrating the Unify flow using customer-supplied possession Here the session is shaped so **Prove does not lead possession first**; instead, your integration drives the primary possession experience. The SDK still checks for an existing key and may stage key state, but **binding** completes only after **your** possession succeeds. An early **status** outcome distinguishes **“already authenticated with the key”** from **“possession still required.”** When your customer completes your possession step, a **bind** phase registers the key in Prove’s registry. Later visits behave like other Prove Key flows. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/human-assurance-customer-possession). ### Prove passive authentication with customer-supplied possession fallback **In scope:** **Prove Key**; passive **Mobile Auth** on mobile where available; **Prove OTP** where applicable; **your** possession when passive paths do not finish binding; a **bind** step when your possession is required to close the loop. Diagram illustrating the Unify flow using customer-supplied possession This variant **layers** strategies: Prove attempts **passive** mobile authentication first to minimize friction. If the session cannot complete on passive paths alone, **status** indicates that **additional possession** is needed; **your** channel can supply that proof, and **bind** finalizes registration when required. The same customer can then authenticate with the **Prove Key** on return visits. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/human-assurance-prove-auth-then-customer). # Human Assurance Manage Source: https://developer.prove.com/explanation/human-assurance-manage Learn how the Prove Platform manages identity information and provides continuous monitoring of phone number changes. ## Receive Prove Manage webhooks Use this guide to **register an HTTPS endpoint** in the Prove Portal, **verify** each delivery with the signed JWT in **`X-Prove-Authorization`**, and **process** phone-change notifications from the JSON body. Webhook events are available in the **US** only. ### Prerequisites * **Public HTTPS URL** — Prove must be able to `POST` to your endpoint from the internet. Configure the URL in the [Prove Portal](https://portal.prove.com). * **Portal access** — Sign-in to the [Prove Portal](https://portal.prove.com/en/login) with permission to create or open an **Identity Manager** project. * **JWT verification** — Your service can validate **HS256** JWTs using the **shared secret** from the Portal (before you accept webhook JSON). * **Delivery expectations** — Prove emits events as changes are detected; delivery is **best-effort** and failed deliveries are re-queued for up to **3 delivery attempts**. Design your endpoint to respond quickly and return a **2xx** status when you accept a delivery. * **No retroactive history** — Events that occurred **before** you saved the webhook configuration are not sent later. * **Per-identity stream** — After certain outcomes (**disconnect**, **moved out of coverage**, or some **phone number change** situations), Prove may send **no further** notifications for that identity until you **re-verify** and re-enroll an updated number if the customer supplies one. ### Register the endpoint in the Portal Open the [Prove Portal](https://portal.prove.com/en/login) and authenticate. Go to **Projects**. Create a project (**Create Project**) and choose **Prove Identity Manager**, or open an existing Identity Manager project. In the project, open the **Configure** tab and locate **Sandbox** webhook settings (or the environment you are integrating). For a throwaway test URL, use [Webhook.site](https://webhook.site/) to mint a unique HTTPS endpoint. Enter your **HTTPS** listener URL in the webhook field, then use **Save and Test Webhook** so Prove persists the configuration and issues a **test** `POST` to your endpoint. For each request, treat the body as **untrusted** until the JWT checks pass. 1. Read **`Authorization`** from the **`X-Prove-Authorization`** header (`Bearer `). 2. Verify the JWT with **HS256** and the **shared secret** from the Portal. 3. Validate standard time claims (`iat`, `nbf`, `exp`), allowing about **±60 seconds** clock skew. Reject tokens past **`exp`** (Prove uses about **5 minutes** of validity from issuance). 4. Confirm **`iss`** equals the literal string **`Prove Identity`**. 5. **Replay protection** — Reject any **`jti`** you have already accepted within your replay window (each delivery uses a **new** UUID **`jti`**). 6. **Integrity** — Compute **SHA-256** over the **raw request body bytes** (before JSON parsing). Compare a **constant-time** hex string equality against the **`body_hash`** claim inside the JWT payload. If it fails, reject the request. Prove also sends **`X-Correlation-ID`** on each attempt—log it for support and tracing. Example decoded payload (middle segment of the JWT): ```json Example JWT payload theme={"dark"} { "iss": "Prove Identity", "iat": 1710864000, "nbf": 1710864000, "exp": 1710864300, "jti": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "body_hash": "8af324cfdeb52d549a2504f7ba20ea51950ee4593ce6182d2b1cea927e41944d" } ``` After JWT and **`body_hash`** succeed, parse the JSON. Expect a top-level **`notifications`** array (Prove may send **up to 100** objects per call; batches can be smaller). Handle each element using the fields below. Optional keys such as **`newCarrier`** or **`newLineType`** appear only for some event types. | Field | Use | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `eventId` | Store for idempotency and support references. | | `event` | Human-readable description. | | `eventType` | Branch logic. Values include `PHONE_NUMBER_CHANGE`, `DISCONNECT`, `PORT`, `LINE_TYPE_CHANGE`, `MOVED_OUT_OF_COVERAGE`. | | `eventTimestamp` | ISO 8601 time from Prove. | | `clientCustomerId` | Your customer key, if supplied on enrollment. | | `proveId` | Prove identifier for the enrolled identity. | ```json Example payload theme={"dark"} { "notifications": [ { "eventId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "event": "phone number change detected", "eventType": "PHONE_NUMBER_CHANGE", "eventTimestamp": "2025-01-23T10:11:12Z", "clientCustomerId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "proveId": "81d3829a-7207-4fd7-9a78-2dbf33fd54ad" } ] } ``` * Trigger **Save and Test Webhook** and confirm your handler receives a **test** request, returns **2xx**, and passes JWT + **`body_hash`** checks. * Force an **invalid** signature or body tamper in a dev environment and confirm your service **rejects** the call. * In Sandbox, exercise at least one real notification path you plan to support and confirm your persistence and idempotency logic behave as expected. ### Stop monitoring an identity Use the following endpoints when you need to stop webhook notifications for a specific enrolled identity: * [`POST /v3/identity/{identityId}/deactivate`](https://developer.prove.com/reference/identity-manager-deactivate-identity) stops webhook notifications without disenrolling the identity. * [`DELETE /v3/identity/{identityId}`](https://developer.prove.com/reference/identity-manager-delete-identity) disenrolls the identity from Identity Manager. To monitor that identity again later, you must re-enroll it. # Human Assurance Source: https://developer.prove.com/explanation/human-assurance-overview What Prove Human Assurance is—verifiable human presence at high-intent moments using phone-anchored signals, POST /v3/verify, and optional monitoring, with minimal friction for real users. ## Prove Human Assurance solution Prove Human Assurance helps businesses treat **human presence as a first-class signal**: in real time, it separates genuine people from malicious automation **before** downstream cost and fraud. It passively evaluates whether a **phone number** behaves like a **human authenticator** tied to a credible device, contrasting typical consumer-phone behavior with patterns common to **virtual machines, emulators, and IoT SIM–class** endpoints that often show up in scripted or tolling abuse. Signals draw on **phone reputation and activity**, **longitudinal tenure and authentication history**, and **device-oriented intelligence** from Prove’s identity graph. It is not a substitute for your own controls, and it does not treat **IP address or browser fingerprint alone** as proof of a real human behind the interaction. **Typical program goals** teams pair with Human Assurance include: * Catching **SMS pumping and toll-style** abuse before you send large volumes of OTP or challenge traffic * Slowing **scripted signups and developer-style** account abuse at the top of the funnel * Surfacing **low-tenure or suspicious** numbers that lack credible human usage before you fully onboard or entitle an account The product is aimed at **moments that matter**—sign-in, signup, or high-risk steps—where you need a **timely human-vs-automation** read **before** an attacker racks up abuse or burns trust downstream. Human Assurance is **not** a complete anti-abuse or bot-management program on its own. It gives you **strong phone-centric signals** (with supporting device and tenure context) and structured **`/v3/verify` outcomes**—including [**assurance level**](https://developer.prove.com/reference/assurance-levels) and policy-related fields interpreted through [**Global Fraud Policy**](https://developer.prove.com/reference/global-fraud-policy)—so your application can **allow**, **step up**, or **block**. Human Assurance is **available globally**. Flowchart: Human Assurance from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success ## How fits together At a high level, rests on **Authenticate**, **Verify**, and optionally **Manage**. They describe **how the real-time product is structured**, not a rigid checklist: your integration may combine or revisit them depending on channel, risk, and whether the customer already has a bound **Prove Key**. ### Authenticate (possession) **Authenticate** means proving possession of the phone number. Your app integrates **Prove’s Authenticate SDK** (Web, iOS, or Android) so the customer can complete possession on the device. That establishes trust in the **device and number** before or alongside your server-side checks. End-to-end setup—including how your back end coordinates with Prove—is covered in the Authenticate guide. ### Verify **Verify** is your back end calling [`POST /v3/verify`](https://developer.prove.com/reference/verify) with the verification type and the inputs your flow requires to obtain a **verification result** and, when successful, identifiers such as **`proveId`** that you can persist for later calls, support, and linking to other Prove capabilities. End-to-end setup, Sandbox testing, and response handling are in the Verify guide. ### Manage **Manage** applies when a verified identity meets Prove’s [**Global Fraud Policy**](https://developer.prove.com/reference/global-fraud-policy): it can be **enrolled for continuous monitoring** of phone and related identity changes. Number changes, disconnects, and coverage moves are signals you want **after** day one, not only at signup. That path is documented under the Manage guide. **Webhook** notifications for those phone and identity change events are **available in the US**. # Model Context Protocol (MCP) Source: https://developer.prove.com/explanation/model-context-protocol What Prove’s hosted Model Context Protocol server is—how assistants search documentation and read pages—and how to connect using your own MCP-capable client. ## Prove MCP overview The Prove **Model Context Protocol (MCP)** server exposes tools to connected AI clients so assistants can **search** Prove documentation and **read** full pages from a virtual documentation filesystem. It complements other doc access patterns such as [plain Markdown and `llms.txt`](https://developer.prove.com/explanation/build-with-llm). If your team uses AI-powered editors, you can point your client at Prove’s **hosted** MCP endpoint—no separate Prove package to run on your infrastructure for this service. ## Hosted endpoint Prove hosts an [MCP server](https://developer.prove.com/mcp). Register this URL in your MCP client using that product’s workflow for **HTTP** MCP servers. Exact steps depend on the vendor; follow their documentation for adding or enabling MCP servers, ensure your network allows HTTPS to `developer.prove.com`, then confirm the **prove** (or equivalent) server appears connected in the client’s MCP or tools UI. ## MCP resources (`skill.md`) The MCP server exposes **[`skill.md` files](https://mintlify.com/docs/ai/skillmd) as MCP resources** so agents discover capability descriptions without installing those files separately. Resources appear in the MCP resource list alongside the tools below. ## Tools available to assistants Tools are exposed to connected AI clients as follows. ### `search_prove` Search across the Prove knowledge base to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about Prove, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages. If you need the full content of a specific page, use the `query_docs_filesystem_prove` tool to `head` or `cat` the page path (append `.mdx` to the path returned from search — for example, `head -200 /api-reference/create-customer.mdx`). Optional parameters for `search_prove` (when the client passes them through) include: * **`pageSize`** — Number of results to return, between **1** and **50**; defaults to **10**. * **`scoreThreshold`** — Minimum relevance score between **0** and **1**; filters out lower-confidence matches. * **`version`** — Restrict results to a documentation version tag. ### `query_docs_filesystem_prove` Run a read-only shell-like query against a virtualized, in-memory filesystem rooted at `/` that contains **only** the Prove documentation pages and OpenAPI specs. This is **not** a shell on any real machine — nothing runs on the user's computer, the server host, or any network. The filesystem is a sandbox backed by documentation chunks. This is how you read documentation pages: there is no separate “get page” tool. To read a page, pass its `.mdx` path (for example, `/quickstart.mdx`, `/api-reference/create-customer.mdx`) to `head` or `cat`. To search the docs with exact keyword or regex matches, use `rg`. To understand the docs structure, use `tree` or `ls`. **Workflow:** Start with the search tool for broad or conceptual queries like “how to authenticate” or “rate limiting”. Use `query_docs_filesystem_prove` when you need exact keyword or regex matching, structural exploration, or to read the full content of a specific page by path. **Supported commands:** `rg` (ripgrep), `grep`, `find`, `tree`, `ls`, `cat`, `head`, `tail`, `stat`, `wc`, `sort`, `uniq`, `cut`, `sed`, `awk`, `jq`, plus basic text utilities. No writes, no network, no process control. Run `--help` on any command for usage. **Stateless calls:** Each call is stateless: the working directory always resets to `/` and no shell variables, aliases, or history carry over between calls. If you need to operate in a subdirectory, chain commands in one call with `&&` or pass absolute paths (for example, `cd /api-reference && ls` or `ls /api-reference`). Do **not** assume that `cd` in one call affects the next call. **Examples** * `tree / -L 2` — see the top-level directory layout * `rg -il "rate limit" /` — find all files mentioning “rate limit” * `rg -C 3 "apiKey" /api-reference/` — show matches with three lines of context around each hit * `head -80 /quickstart.mdx` — read the top 80 lines of a specific page * `head -80 /quickstart.mdx /installation.mdx /guides/first-deploy.mdx` — read multiple pages in one call * `cat /api-reference/create-customer.mdx` — read a full page when you need everything * `cat /openapi/spec.json | jq '.paths | keys'` — list OpenAPI endpoints Output is truncated to 30KB per call. Prefer targeted `rg -C` or `head -N` over broad `cat` on large files. To read only the relevant sections of a large file, use `rg -C 3 "pattern" /path/file.mdx`. Batch multiple file reads into a single `head` or `cat` call whenever possible. ## Rate limits The following **hourly rate limits** are used so the service stays available: * **Per user (IP address)** — **5,000** requests per hour for **MCP server configuration** queries. * **Search** — **10,000** search tool calls per hour for the public MCP path; **5,000** per hour when using **authenticated** search quotas. * **Query docs filesystem** — **10,000** filesystem tool calls per hour for the public MCP path. These limits are **in addition** to the **\~30 KB per-call** response truncation for `query_docs_filesystem_prove` described above. If you hit throttling, narrow queries, batch reads, and space out tool calls. # Pre-Fill® CX Requirements - MobileAuth℠ Source: https://developer.prove.com/explanation/pre-fill-cx-requirements-mobileauthsm Review the Prove Standard℠ frontend requirements for Pre-Fill when implementing with MobileAuth℠ ## Context **Pre-Fill** only delivers value when customers **finish** the flow and **understand** how their data is used. Friction, vague copy, or weak consent erode completion and trust before verification ever runs. The **Prove Standard℠** CX requirements describe how screens, language, and consent work together so onboarding stays coherent—for the customer, for risk controls, and for downstream **Pre-Fill** use of verified data. ## Terms These labels appear throughout the Pre-Fill CX material: * **Prove Standard℠** — A set of frontend and customer-experience (CX) patterns Prove recommends so onboarding stays high-converting, legible, and aligned with how possession and identity steps should feel end to end. * **Pre-Fill** — Using **verified** attributes to populate your forms so customers repeat less manual entry after a successful identity path. * **MobileAuth℠** — Prove’s possession-oriented authentication path (silent where the product allows it), often upstream of or alongside Pre-Fill depending on your flow. * **Informed consent** — Clear, upfront terms and privacy disclosure so customers know what data is collected, for what purpose, and what happens next **before** they commit to the next step. ## Why these requirements exist Prove Standard℠ CX is not decorative layout guidance: it encodes **why** certain patterns show up again in successful Pre-Fill programs. * **Friction and conversion** — Heavy or confusing steps (especially around phone entry, possession, and handoffs) correlate with abandonment. Requirements keep the path **short and legible** so more customers reach verified state—the precondition for Pre-Fill. * **Simplicity and cognitive load** — Dense legal walls, inconsistent labels, or parallel “extra” paths make it harder for customers to know where they are in the journey. Requirements favor **plain language** and **predictable** sequencing so attention stays on the task, not on decoding the UI. * **Trust and informed consent** — Pre-Fill depends on customers accepting a legitimate data exchange. Visible security posture, honest data-use explanation, and accessible policies reduce perceived risk and support **meaningful** consent—not just checkbox completion. * **Regulatory and program fit** — Jurisdictions and partner programs impose expectations on disclosure, minimization, and evidence of consent. The CX requirements align the **experience** with those constraints so technical success (verify → pre-fill) is not undermined by process gaps upstream. # Pre-Fill for Business Authenticate Source: https://developer.prove.com/explanation/pre-fill-for-business-authenticate Learn how the Prove Platform authenticates users through many integration methods. Prove simplifies and strengthens your authentication security by automatically choosing the right authenticator based on trusted device recognition and authenticator availability—reducing friction while protecting against fraud. Architecturally, Unify is **one coordinated session** across **client SDKs** (Web, iOS, Android) and **Unify platform APIs** such as [`/v3/unify`](https://developer.prove.com/reference/unify-request), with **channel fallbacks**—Instant Link, SMS OTP, Mobile Auth where enabled—so you are not maintaining a separate integration path for every surface and channel. Unify focuses on **phone possession**, **device trust**, and **Prove Key** lifecycle for that session. It does **not** replace your full **identity provider**, **authorization** (scopes, roles, entitlements), or **every** downstream fraud policy—you still map Prove outcomes into your own accounts, sessions, and risk stack. **Possession** is coordinated work: your app runs a **client-side SDK** (Web, iOS, or Android) while Prove’s **Unify platform APIs** advance the session and bind outcomes. Which endpoints apply depends on the integration variant. ## Concepts and definitions * **Prove Key:** Non-extractable cryptographic key used for **authentication** by Prove. * **Bind:** Establishes possession of the phone number to register the Prove Key. * **Customer-Initiated Bind:** Registry of a Prove Key based on customer-supplied possession of the phone number. * **Key Persistence:** Browser fingerprinting technology for the Web SDK that helps recover device registration when the Prove Key is unavailable, eliminating the need for SMS OTP or Instant Link re-authentication. ### What the Prove Key is associated with On Prove’s systems, each Prove Key is **correlated** with: * **`phoneNumber`:** the one used when the key is bound or authenticated * **`proveId`:** Prove’s identifier for the consumer when it is known * **`clientCustomerId`** and **`clientHumanId`:** when passed in the API request, these client-supplied identifiers are correlated with the same Prove Key Pass optional identifiers on your API requests when you want that linkage recorded for the session. ### Prove Key validity and expiration The Prove Key doesn't expire on a fixed calendar date. It stays valid with normal use, subject to the following: * **Inactivity:** Prove **deactivates the key on our servers** if the device remains inactive for an extended period. Each time the customer completes a **new authentication**, that inactivity period resets from that activity. * **App uninstall and reinstall for native apps:** If the user **deletes the app and reinstalls it**, behavior depends on platform: * **iOS:** The Prove Key **remains on the device**, so it is still available after reinstall. **Fingerprint technology is not needed** to reconstitute the key in that scenario. * **Android:** Key Persistence for recovering registration when the Prove Key is unavailable isn't available for the Android SDK. * **Forcing a possession check:** You can require an **active possession check** even when a key may still be valid; the Unify request supports signaling that intent (see the [`/v3/unify`](https://developer.prove.com/reference/unify-request) reference and [implementation guide](https://developer.prove.com/how-to/unify-implementation-guide)). ## Geographic availability **Prove Key** is available in **all countries**. **Mobile Auth** isn't supported in every country. Coverage varies by region—confirm availability for your markets when your flow depends on Mobile Auth. ## Integration variants Prove supports several **integration models**. They differ in **who runs possession first**, **which channels are in play**, and **when the Prove Key is considered bound**, but the **stages** below describe the same mental model for all of them. ### Stages every variant shares * **Open a protected session** — Typically scoped to the phone number so possession and downstream checks stay tied to one line. * **Possession or key reuse** — The SDK and server work to either **reuse a bound Prove Key** or complete a **possession** step through the channels your configuration allows. * **Status** — [`/v3/unify-status`](https://developer.prove.com/reference/unify-status-request) (and your app logic) resolve whether authentication finished, possession is still required, or the customer used an existing key. * **Bind when needed** — [`/v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) finalizes registration of the key when your flow requires an explicit bind after possession. * **Return visits** — A bound key often allows **lighter** re-authentication until policy or **rebind** forces a full possession ritual again. ### Choosing an integration model Use this as a **decision lens**, not a rigid rule: * **[Prove’s possession flow](https://developer.prove.com/how-to/pre-fill-business-prove-possession-web-sdk)** — Prefer when you want **Prove-led channels** (Instant Link, OTP, Mobile Auth) to carry **primary** possession and you do not need your own vendor in the critical path. * **[Customer-supplied possession with force bind](https://developer.prove.com/how-to/pre-fill-business-customer-possession)** — Prefer when you already run **your own possession** (or must) and still want Prove to **issue and register** the Prove Key after your step succeeds. * **[Prove passive authentication with customer-supplied possession fallback](https://developer.prove.com/how-to/pre-fill-business-prove-auth-then-customer)** — Prefer when you want **passive Mobile Auth first** on supported mobile networks, with **your** possession channel only when passive paths cannot complete binding. ### Prove’s possession flow **In scope:** Prove Key; **Mobile Auth** and **Prove OTP** on mobile; **Instant Link** on desktop. **Out of scope:** Your own possession channel as the primary path. Diagram illustrating Prove's unified authentication possession flow In this model, **Prove’s channels** carry possession on each surface. The flow opens a **protected session** scoped to the phone number. The **client SDK** then tries the lightest path first: if a **bound Prove Key** already exists, the customer may authenticate without repeating a full possession ritual. If no usable key is present, **possession** depends on context. On **desktop**, that typically means **Instant Link** so the user can complete proof in the mobile environment. On **mobile**, Prove-led channels apply—most often **SMS OTP**, and when **Mobile Auth** is enabled, silent network authentication may run ahead of OTP with OTP as fallback. The key may not be **fully bound** until **status** confirms the session, depending on how channels stack in your configuration. The **status** stage tells you whether authentication succeeded, whether possession is still outstanding, or whether the customer authenticated with an existing key—so your application knows what to surface next. After a successful bind or authentication, the same customer can usually return through **Prove Key–based** authentication on later visits. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/pre-fill-business-prove-possession-web-sdk). ### Customer-supplied possession with force bind **Scope:** Mobile channels where Prove still issues the **Prove Key**, but **your possession vendor** performs the primary possession step. Diagram illustrating the Unify flow using customer-supplied possession Here the session is shaped so **Prove does not lead possession first**; instead, your integration drives the primary possession experience. The SDK still checks for an existing key and may stage key state, but **binding** completes only after **your** possession succeeds. An early **status** outcome distinguishes **“already authenticated with the key”** from **“possession still required.”** When your customer completes your possession step, a **bind** phase registers the key in Prove’s registry. Later visits behave like other Prove Key flows. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/pre-fill-business-customer-possession). ### Prove passive authentication with customer-supplied possession fallback **In scope:** **Prove Key**; passive **Mobile Auth** on mobile where available; **Prove OTP** where applicable; **your** possession when passive paths do not finish binding; a **bind** step when your possession is required to close the loop. Diagram illustrating the Unify flow using customer-supplied possession This variant **layers** strategies: Prove attempts **passive** mobile authentication first to minimize friction. If the session cannot complete on passive paths alone, **status** indicates that **additional possession** is needed; **your** channel can supply that proof, and **bind** finalizes registration when required. The same customer can then authenticate with the **Prove Key** on return visits. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/pre-fill-business-prove-auth-then-customer). # Pre-Fill for Business Manage Source: https://developer.prove.com/explanation/pre-fill-for-business-manage Learn how the Prove Platform manages identity information and provides continuous monitoring of phone number changes for programs that enroll businesses and their users from Pre-Fill for Business. ## Receive Prove Manage webhooks Use this guide to **register an HTTPS endpoint** in the Prove Portal, **verify** each delivery with the signed JWT in **`X-Prove-Authorization`**, and **process** phone-change notifications from the JSON body. Webhook events are available in the **US** only. ### Prerequisites * **Public HTTPS URL** — Prove must be able to `POST` to your endpoint from the internet. Configure the URL in the [Prove Portal](https://portal.prove.com). * **Portal access** — Sign-in to the [Prove Portal](https://portal.prove.com/en/login) with permission to create or open an **Identity Manager** project. * **JWT verification** — Your service can validate **HS256** JWTs using the **shared secret** from the Portal (before you accept webhook JSON). * **Delivery expectations** — Prove emits events as changes are detected; delivery is **best-effort** and failed deliveries are re-queued for up to **3 delivery attempts**. Design your endpoint to respond quickly and return a **2xx** status when you accept a delivery. * **No retroactive history** — Events that occurred **before** you saved the webhook configuration are not sent later. * **Per-identity stream** — After certain outcomes (**disconnect**, **moved out of coverage**, or some **phone number change** situations), Prove may send **no further** notifications for that identity until you **re-verify** and re-enroll an updated number if the customer supplies one. ### Register the endpoint in the Portal Open the [Prove Portal](https://portal.prove.com/en/login) and authenticate. Go to **Projects**. Create a project (**Create Project**) and choose **Prove Identity Manager**, or open an existing Identity Manager project. In the project, open the **Configure** tab and locate **Sandbox** webhook settings (or the environment you are integrating). For a throwaway test URL, use [Webhook.site](https://webhook.site/) to mint a unique HTTPS endpoint. Enter your **HTTPS** listener URL in the webhook field, then use **Save and Test Webhook** so Prove persists the configuration and issues a **test** `POST` to your endpoint. For each request, treat the body as **untrusted** until the JWT checks pass. 1. Read **`Authorization`** from the **`X-Prove-Authorization`** header (`Bearer `). 2. Verify the JWT with **HS256** and the **shared secret** from the Portal. 3. Validate standard time claims (`iat`, `nbf`, `exp`), allowing about **±60 seconds** clock skew. Reject tokens past **`exp`** (Prove uses about **5 minutes** of validity from issuance). 4. Confirm **`iss`** equals the literal string **`Prove Identity`**. 5. **Replay protection** — Reject any **`jti`** you have already accepted within your replay window (each delivery uses a **new** UUID **`jti`**). 6. **Integrity** — Compute **SHA-256** over the **raw request body bytes** (before JSON parsing). Compare a **constant-time** hex string equality against the **`body_hash`** claim inside the JWT payload. If it fails, reject the request. Prove also sends **`X-Correlation-ID`** on each attempt—log it for support and tracing. Example decoded payload (middle segment of the JWT): ```json Example JWT payload theme={"dark"} { "iss": "Prove Identity", "iat": 1710864000, "nbf": 1710864000, "exp": 1710864300, "jti": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "body_hash": "8af324cfdeb52d549a2504f7ba20ea51950ee4593ce6182d2b1cea927e41944d" } ``` After JWT and **`body_hash`** succeed, parse the JSON. Expect a top-level **`notifications`** array (Prove may send **up to 100** objects per call; batches can be smaller). Handle each element using the fields below. Optional keys such as **`newCarrier`** or **`newLineType`** appear only for some event types. | Field | Use | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `eventId` | Store for idempotency and support references. | | `event` | Human-readable description. | | `eventType` | Branch logic. Values include `PHONE_NUMBER_CHANGE`, `DISCONNECT`, `PORT`, `LINE_TYPE_CHANGE`, `MOVED_OUT_OF_COVERAGE`. | | `eventTimestamp` | ISO 8601 time from Prove. | | `clientCustomerId` | Your customer key, if supplied on enrollment. | | `proveId` | Prove identifier for the enrolled identity. | ```json Example payload theme={"dark"} { "notifications": [ { "eventId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "event": "phone number change detected", "eventType": "PHONE_NUMBER_CHANGE", "eventTimestamp": "2025-01-23T10:11:12Z", "clientCustomerId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "proveId": "81d3829a-7207-4fd7-9a78-2dbf33fd54ad" } ] } ``` * Trigger **Save and Test Webhook** and confirm your handler receives a **test** request, returns **2xx**, and passes JWT + **`body_hash`** checks. * Force an **invalid** signature or body tamper in a dev environment and confirm your service **rejects** the call. * In Sandbox, exercise at least one real notification path you plan to support and confirm your persistence and idempotency logic behave as expected. ### Stop monitoring an identity Use the following endpoints when you need to stop webhook notifications for a specific enrolled identity: * [`POST /v3/identity/{identityId}/deactivate`](https://developer.prove.com/reference/identity-manager-deactivate-identity) stops webhook notifications without disenrolling the identity. * [`DELETE /v3/identity/{identityId}`](https://developer.prove.com/reference/identity-manager-delete-identity) disenrolls the identity from Identity Manager. To monitor that identity again later, you must re-enroll it. # Pre-Fill for Business Source: https://developer.prove.com/explanation/pre-fill-for-business-overview How Prove Pre-Fill for Business links an individual applicant to a business with phone-anchored authentication, then returns verified KYB-oriented attributes. ## Prove Pre-Fill for Business solution Prove Pre-Fill for Business streamlines **Know Your Business (KYB)**-style flows with **minimal customer input** up front: keep the application form to **only what you require before authentication**, then return **bank-grade, verified attributes** in **real time** after the user completes possession on their **phone**. The product is **mobile-centric at the trust layer**: passive and step-up channels connect the **individual** to the **business entity** so you are not relying on thin, self-typed application data alone. Returned data can include signals appropriate to your workflow—for example **legal name and address**, **business entity** attributes, **EIN/TIN** and related tax-identifier context, **document-related verification outcomes** where your agreement and product configuration include them, and other fields your agreement covers—so teams can **pre-fill forms**, drive **risk segmentation**, and keep **fraudsters from impersonating legitimate businesses** while legitimate applicants move faster. You reduce abandonment and improve conversion, while strengthening fraud defenses. Possession checks, **Prove Identity Network** identity and coverage signals, and the [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) combine to deliver consistent evaluation outcomes. Prove Pre-Fill for Business is **United States only**. ## How fits together At a high level, rests on **Authenticate**, **Verify**, and optionally **Manage**. They describe **how the real-time product is structured**, not a rigid checklist: your integration may combine or revisit them depending on channel, risk, and whether the customer already has a bound **Prove Key**. ### Authenticate (possession) **Authenticate** means proving possession of the phone number. Your app integrates **Prove’s Authenticate SDK** (Web, iOS, or Android) so the customer can complete possession on the device. That establishes trust in the **device and number** before or alongside your server-side checks. End-to-end setup—including how your back end coordinates with Prove—is covered in the Authenticate guide. ### Verify **Verify** is your back end calling [`POST /v3/verify`](https://developer.prove.com/reference/verify) with the verification type and the inputs your flow requires to obtain a **verification result** and, when successful, identifiers such as **`proveId`** that you can persist for later calls, support, and linking to other Prove capabilities. End-to-end setup, Sandbox testing, and response handling are in the Verify guide. ### Manage **Manage** applies when a verified identity meets Prove’s [**Global Fraud Policy**](https://developer.prove.com/reference/global-fraud-policy): it can be **enrolled for continuous monitoring** of phone and related identity changes. Number changes, disconnects, and coverage moves are signals you want **after** day one, not only at signup. That path is documented under the Manage guide. **Webhook** notifications for those phone and identity change events are **available in the US**. # Pre-Fill for Consumers Authenticate Source: https://developer.prove.com/explanation/pre-fill-for-consumers-authenticate Learn how the Prove Platform authenticates users through many integration methods. Prove simplifies and strengthens your authentication security by automatically choosing the right authenticator based on trusted device recognition and authenticator availability—reducing friction while protecting against fraud. Architecturally, Unify is **one coordinated session** across **client SDKs** (Web, iOS, Android) and **Unify platform APIs** such as [`/v3/unify`](https://developer.prove.com/reference/unify-request), with **channel fallbacks**—Instant Link, SMS OTP, Mobile Auth where enabled—so you are not maintaining a separate integration path for every surface and channel. Unify focuses on **phone possession**, **device trust**, and **Prove Key** lifecycle for that session. It does **not** replace your full **identity provider**, **authorization** (scopes, roles, entitlements), or **every** downstream fraud policy—you still map Prove outcomes into your own accounts, sessions, and risk stack. **Possession** is coordinated work: your app runs a **client-side SDK** (Web, iOS, or Android) while Prove’s **Unify platform APIs** advance the session and bind outcomes. Which endpoints apply depends on the integration variant. ## Concepts and definitions * **Prove Key:** Non-extractable cryptographic key used for **authentication** by Prove. * **Bind:** Establishes possession of the phone number to register the Prove Key. * **Customer-Initiated Bind:** Registry of a Prove Key based on customer-supplied possession of the phone number. * **Key Persistence:** Browser fingerprinting technology for the Web SDK that helps recover device registration when the Prove Key is unavailable, eliminating the need for SMS OTP or Instant Link re-authentication. ### What the Prove Key is associated with On Prove’s systems, each Prove Key is **correlated** with: * **`phoneNumber`:** the one used when the key is bound or authenticated * **`proveId`:** Prove’s identifier for the consumer when it is known * **`clientCustomerId`** and **`clientHumanId`:** when passed in the API request, these client-supplied identifiers are correlated with the same Prove Key Pass optional identifiers on your API requests when you want that linkage recorded for the session. ### Prove Key validity and expiration The Prove Key doesn't expire on a fixed calendar date. It stays valid with normal use, subject to the following: * **Inactivity:** Prove **deactivates the key on our servers** if the device remains inactive for an extended period. Each time the customer completes a **new authentication**, that inactivity period resets from that activity. * **App uninstall and reinstall for native apps:** If the user **deletes the app and reinstalls it**, behavior depends on platform: * **iOS:** The Prove Key **remains on the device**, so it is still available after reinstall. **Fingerprint technology is not needed** to reconstitute the key in that scenario. * **Android:** Key Persistence for recovering registration when the Prove Key is unavailable isn't available for the Android SDK. * **Forcing a possession check:** You can require an **active possession check** even when a key may still be valid; the Unify request supports signaling that intent (see the [`/v3/unify`](https://developer.prove.com/reference/unify-request) reference and [implementation guide](https://developer.prove.com/how-to/unify-implementation-guide)). ## Geographic availability **Prove Key** is available in **all countries**. **Mobile Auth** isn't supported in every country. Coverage varies by region—confirm availability for your markets when your flow depends on Mobile Auth. ## Integration variants Prove supports several **integration models**. They differ in **who runs possession first**, **which channels are in play**, and **when the Prove Key is considered bound**, but the **stages** below describe the same mental model for all of them. ### Stages every variant shares * **Open a protected session** — Typically scoped to the phone number so possession and downstream checks stay tied to one line. * **Possession or key reuse** — The SDK and server work to either **reuse a bound Prove Key** or complete a **possession** step through the channels your configuration allows. * **Status** — [`/v3/unify-status`](https://developer.prove.com/reference/unify-status-request) (and your app logic) resolve whether authentication finished, possession is still required, or the customer used an existing key. * **Bind when needed** — [`/v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) finalizes registration of the key when your flow requires an explicit bind after possession. * **Return visits** — A bound key often allows **lighter** re-authentication until policy or **rebind** forces a full possession ritual again. ### Choosing an integration model Use this as a **decision lens**, not a rigid rule: * **[Prove’s possession flow](https://developer.prove.com/how-to/pre-fill-prove-possession-web-sdk)** — Prefer when you want **Prove-led channels** (Instant Link, OTP, Mobile Auth) to carry **primary** possession and you do not need your own vendor in the critical path. * **[Customer-supplied possession with force bind](https://developer.prove.com/how-to/pre-fill-customer-possession)** — Prefer when you already run **your own possession** (or must) and still want Prove to **issue and register** the Prove Key after your step succeeds. * **[Prove passive authentication with customer-supplied possession fallback](https://developer.prove.com/how-to/pre-fill-prove-auth-then-customer)** — Prefer when you want **passive Mobile Auth first** on supported mobile networks, with **your** possession channel only when passive paths cannot complete binding. ### Prove’s possession flow **In scope:** Prove Key; **Mobile Auth** and **Prove OTP** on mobile; **Instant Link** on desktop. **Out of scope:** Your own possession channel as the primary path. Diagram illustrating Prove's unified authentication possession flow In this model, **Prove’s channels** carry possession on each surface. The flow opens a **protected session** scoped to the phone number. The **client SDK** then tries the lightest path first: if a **bound Prove Key** already exists, the customer may authenticate without repeating a full possession ritual. If no usable key is present, **possession** depends on context. On **desktop**, that typically means **Instant Link** so the user can complete proof in the mobile environment. On **mobile**, Prove-led channels apply—most often **SMS OTP**, and when **Mobile Auth** is enabled, silent network authentication may run ahead of OTP with OTP as fallback. The key may not be **fully bound** until **status** confirms the session, depending on how channels stack in your configuration. The **status** stage tells you whether authentication succeeded, whether possession is still outstanding, or whether the customer authenticated with an existing key—so your application knows what to surface next. After a successful bind or authentication, the same customer can usually return through **Prove Key–based** authentication on later visits. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/pre-fill-prove-possession-web-sdk). ### Customer-supplied possession with force bind **Scope:** Mobile channels where Prove still issues the **Prove Key**, but **your possession vendor** performs the primary possession step. Diagram illustrating the Unify flow using customer-supplied possession Here the session is shaped so **Prove does not lead possession first**; instead, your integration drives the primary possession experience. The SDK still checks for an existing key and may stage key state, but **binding** completes only after **your** possession succeeds. An early **status** outcome distinguishes **“already authenticated with the key”** from **“possession still required.”** When your customer completes your possession step, a **bind** phase registers the key in Prove’s registry. Later visits behave like other Prove Key flows. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/pre-fill-customer-possession). ### Prove passive authentication with customer-supplied possession fallback **In scope:** **Prove Key**; passive **Mobile Auth** on mobile where available; **Prove OTP** where applicable; **your** possession when passive paths do not finish binding; a **bind** step when your possession is required to close the loop. Diagram illustrating the Unify flow using customer-supplied possession This variant **layers** strategies: Prove attempts **passive** mobile authentication first to minimize friction. If the session cannot complete on passive paths alone, **status** indicates that **additional possession** is needed; **your** channel can supply that proof, and **bind** finalizes registration when required. The same customer can then authenticate with the **Prove Key** on return visits. Implementation options are documented in the [implementation guide](https://developer.prove.com/how-to/pre-fill-prove-auth-then-customer). # Pre-Fill for Consumers Manage Source: https://developer.prove.com/explanation/pre-fill-for-consumers-manage Learn how the Prove Platform manages identity information and provides continuous monitoring of phone number changes for programs that enroll consumers from Pre-Fill for Consumers. ## Receive Prove Manage webhooks Use this guide to **register an HTTPS endpoint** in the Prove Portal, **verify** each delivery with the signed JWT in **`X-Prove-Authorization`**, and **process** phone-change notifications from the JSON body. Webhook events are available in the **US** only. ### Prerequisites * **Public HTTPS URL** — Prove must be able to `POST` to your endpoint from the internet. Configure the URL in the [Prove Portal](https://portal.prove.com). * **Portal access** — Sign-in to the [Prove Portal](https://portal.prove.com/en/login) with permission to create or open an **Identity Manager** project. * **JWT verification** — Your service can validate **HS256** JWTs using the **shared secret** from the Portal (before you accept webhook JSON). * **Delivery expectations** — Prove emits events as changes are detected; delivery is **best-effort** and failed deliveries are re-queued for up to **3 delivery attempts**. Design your endpoint to respond quickly and return a **2xx** status when you accept a delivery. * **No retroactive history** — Events that occurred **before** you saved the webhook configuration are not sent later. * **Per-identity stream** — After certain outcomes (**disconnect**, **moved out of coverage**, or some **phone number change** situations), Prove may send **no further** notifications for that identity until you **re-verify** and re-enroll an updated number if the customer supplies one. ### Register the endpoint in the Portal Open the [Prove Portal](https://portal.prove.com/en/login) and authenticate. Go to **Projects**. Create a project (**Create Project**) and choose **Prove Identity Manager**, or open an existing Identity Manager project. In the project, open the **Configure** tab and locate **Sandbox** webhook settings (or the environment you are integrating). For a throwaway test URL, use [Webhook.site](https://webhook.site/) to mint a unique HTTPS endpoint. Enter your **HTTPS** listener URL in the webhook field, then use **Save and Test Webhook** so Prove persists the configuration and issues a **test** `POST` to your endpoint. For each request, treat the body as **untrusted** until the JWT checks pass. 1. Read **`Authorization`** from the **`X-Prove-Authorization`** header (`Bearer `). 2. Verify the JWT with **HS256** and the **shared secret** from the Portal. 3. Validate standard time claims (`iat`, `nbf`, `exp`), allowing about **±60 seconds** clock skew. Reject tokens past **`exp`** (Prove uses about **5 minutes** of validity from issuance). 4. Confirm **`iss`** equals the literal string **`Prove Identity`**. 5. **Replay protection** — Reject any **`jti`** you have already accepted within your replay window (each delivery uses a **new** UUID **`jti`**). 6. **Integrity** — Compute **SHA-256** over the **raw request body bytes** (before JSON parsing). Compare a **constant-time** hex string equality against the **`body_hash`** claim inside the JWT payload. If it fails, reject the request. Prove also sends **`X-Correlation-ID`** on each attempt—log it for support and tracing. Example decoded payload (middle segment of the JWT): ```json Example JWT payload theme={"dark"} { "iss": "Prove Identity", "iat": 1710864000, "nbf": 1710864000, "exp": 1710864300, "jti": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "body_hash": "8af324cfdeb52d549a2504f7ba20ea51950ee4593ce6182d2b1cea927e41944d" } ``` After JWT and **`body_hash`** succeed, parse the JSON. Expect a top-level **`notifications`** array (Prove may send **up to 100** objects per call; batches can be smaller). Handle each element using the fields below. Optional keys such as **`newCarrier`** or **`newLineType`** appear only for some event types. | Field | Use | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `eventId` | Store for idempotency and support references. | | `event` | Human-readable description. | | `eventType` | Branch logic. Values include `PHONE_NUMBER_CHANGE`, `DISCONNECT`, `PORT`, `LINE_TYPE_CHANGE`, `MOVED_OUT_OF_COVERAGE`. | | `eventTimestamp` | ISO 8601 time from Prove. | | `clientCustomerId` | Your customer key, if supplied on enrollment. | | `proveId` | Prove identifier for the enrolled identity. | ```json Example payload theme={"dark"} { "notifications": [ { "eventId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "event": "phone number change detected", "eventType": "PHONE_NUMBER_CHANGE", "eventTimestamp": "2025-01-23T10:11:12Z", "clientCustomerId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "proveId": "81d3829a-7207-4fd7-9a78-2dbf33fd54ad" } ] } ``` * Trigger **Save and Test Webhook** and confirm your handler receives a **test** request, returns **2xx**, and passes JWT + **`body_hash`** checks. * Force an **invalid** signature or body tamper in a dev environment and confirm your service **rejects** the call. * In Sandbox, exercise at least one real notification path you plan to support and confirm your persistence and idempotency logic behave as expected. ### Stop monitoring an identity Use the following endpoints when you need to stop webhook notifications for a specific enrolled identity: * [`POST /v3/identity/{identityId}/deactivate`](https://developer.prove.com/reference/identity-manager-deactivate-identity) stops webhook notifications without disenrolling the identity. * [`DELETE /v3/identity/{identityId}`](https://developer.prove.com/reference/identity-manager-delete-identity) disenrolls the identity from Identity Manager. To monitor that identity again later, you must re-enroll it. # Pre-Fill for Consumers Source: https://developer.prove.com/explanation/pre-fill-for-consumers-overview How Prove Pre-Fill for Consumers uses phone-first recognition to return real-time verified attributes via Authenticate and POST /v3/verify, and optional Manage. ## Prove Pre-Fill for Consumers solution Prove Pre-Fill for Consumers streamlines digital onboarding by filling forms with **verified consumer data** tied to an **authenticated phone number**—so you can **resolve identity before asking for long manual entry**. Businesses reduce abandonment, improve conversion, and strengthen fraud defenses by combining possession checks, **identity graph** context (devices, credentials, and attributes connected through the **Prove Identity Network**), and consistent evaluation under Prove’s [**Global Fraud Policy**](https://developer.prove.com/reference/global-fraud-policy). The underlying idea is simple: once the consumer has **proved possession** of the number, you can **reuse Prove-backed attributes** instead of asking them to **re-type sensitive data** at the edge of your funnel—so you cut friction without relying on self-reported PII alone. Pre-Fill for Consumers is **not** a full KYC/AML program or the only control you need for regulated onboarding. It gives you **structured `/v3/verify` outcomes** (including fields you map to [**assurance levels**](/reference/assurance-levels) when applicable) and **identity attributes** when verification succeeds, so your application can **pre-fill**, **step up**, or **stop** according to your own policies and any other checks you run in parallel. Pre-Fill for Consumers is available only in the **United States**. Flowchart: Pre-Fill for Consumers from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success ## How fits together At a high level, rests on **Authenticate**, **Verify**, and optionally **Manage**. They describe **how the real-time product is structured**, not a rigid checklist: your integration may combine or revisit them depending on channel, risk, and whether the customer already has a bound **Prove Key**. ### Authenticate (possession) **Authenticate** means proving possession of the phone number. Your app integrates **Prove’s Authenticate SDK** (Web, iOS, or Android) so the customer can complete possession on the device. That establishes trust in the **device and number** before or alongside your server-side checks. End-to-end setup—including how your back end coordinates with Prove—is covered in the Authenticate guide. ### Verify **Verify** is your back end calling [`POST /v3/verify`](https://developer.prove.com/reference/verify) with the verification type and the inputs your flow requires to obtain a **verification result** and, when successful, identifiers such as **`proveId`** that you can persist for later calls, support, and linking to other Prove capabilities. End-to-end setup, Sandbox testing, and response handling are in the Verify guide. ### Manage **Manage** applies when a verified identity meets Prove’s [**Global Fraud Policy**](https://developer.prove.com/reference/global-fraud-policy): it can be **enrolled for continuous monitoring** of phone and related identity changes. Number changes, disconnects, and coverage moves are signals you want **after** day one, not only at signup. That path is documented under the Manage guide. **Webhook** notifications for those phone and identity change events are **available in the US**. # CX Requirements Source: https://developer.prove.com/explanation/prove-pre-fill-cx-requirements Review the Prove Standard℠ frontend requirements for Pre-Fill when implementing without MobileAuth℠ ## Context **Pre-Fill** only delivers value when customers **finish** the flow and **understand** how their data is used. Friction, vague copy, or weak consent erode completion and trust before verification ever runs. The **Prove Standard℠** CX requirements describe how screens, language, and consent work together so onboarding stays coherent—for the customer, for risk controls, and for downstream **Pre-Fill** use of verified data. ## Terms These labels appear throughout the Pre-Fill CX material: * **Prove Standard℠** — A set of frontend and customer-experience (CX) patterns Prove recommends so onboarding stays high-converting, legible, and aligned with how possession and identity steps should feel end to end. * **Pre-Fill** — Using **verified** attributes to populate your forms so customers repeat less manual entry after a successful identity path. * **MobileAuth℠** — Prove’s possession-oriented authentication path (silent where the product allows it), often upstream of or alongside Pre-Fill depending on your flow. * **Informed consent** — Clear, upfront terms and privacy disclosure so customers know what data is collected, for what purpose, and what happens next **before** they commit to the next step. ## Why these requirements exist Prove Standard℠ CX is not decorative layout guidance: it encodes **why** certain patterns show up again in successful Pre-Fill programs. * **Friction and conversion** — Heavy or confusing steps (especially around phone entry, possession, and handoffs) correlate with abandonment. Requirements keep the path **short and legible** so more customers reach verified state—the precondition for Pre-Fill. * **Simplicity and cognitive load** — Dense legal walls, inconsistent labels, or parallel “extra” paths make it harder for customers to know where they are in the journey. Requirements favor **plain language** and **predictable** sequencing so attention stays on the task, not on decoding the UI. * **Trust and informed consent** — Pre-Fill depends on customers accepting a legitimate data exchange. Visible security posture, honest data-use explanation, and accessible policies reduce perceived risk and support **meaningful** consent—not just checkbox completion. * **Regulatory and program fit** — Jurisdictions and partner programs impose expectations on disclosure, minimization, and evidence of consent. The CX requirements align the **experience** with those constraints so technical success (verify → pre-fill) is not undermined by process gaps upstream. # ProveX Source: https://developer.prove.com/explanation/provex-overview What ProveX is—Prove’s digital trust exchange using discover and fetch endpoints for verified partner data without one-off integrations. ## ProveX solution With ProveX, you can keep helping consumers when they still need to take action in your product—such as funding an account or choosing how to pay. Instead of sending them elsewhere or rebuilding separate integrations for every data source, you surface Prove partner capabilities through a **consistent pattern** so the handoff feels like one journey. That means fewer abandoned flows, less custom plumbing on your side, and a clearer path for customers to finish what they started. Architecturally, ProveX is a **Prove-mediated marketplace layer**: once the consumer has a **`proveId`** from Prove authentication or verification, you **list** which partner-linked attributes exist ([**`/v3/discover`**](https://developer.prove.com/reference/discover-request)), then you **retrieve** the values you need ([**`/v3/fetch`**](https://developer.prove.com/reference/fetch-request)). You do not need a bespoke integration to every partner for every flow; you integrate **Prove’s APIs** and let **discover → fetch** reflect what is in scope for that identity. * **Identity at the center** — **Discover** and **fetch** are always scoped to a **`proveId`** from successful **Prove authentication or verification**. There is no anonymous “browse the marketplace”; the trust exchange starts from **who** Prove has already established for that session. * **Consent built in** — **Fetch** returns attribute values only when **identity, program policy, and the consumer’s consent** for that pull align with what Prove and the issuers enforce. **Discover** tells you what *could* be in play; **fetch** is where permitted data is actually released. * **Cryptographically bound** — In product terms, each **discover** and **fetch** is **mediated by Prove** and **scoped to the verified consumer** behind the **`proveId`**, using **platform OAuth** rather than ad hoc sharing of static identifiers or credentials with every issuer. That is how the exchange aims to turn **authorization into a durable, identity-tied request** and limit **misuse, replay, or impersonation** versus one-off pipes. ProveX is **not** a guarantee that every partner or attribute is available for every consumer. What you see in **discover** depends on **partner coverage**, **product configuration**, and the **identity** behind the **`proveId`**; **fetch** only returns data the consumer has agreed to pull, under the same constraints as your upstream verification. ## How ProveX fits together These stages describe **how the product is structured**, not a single rigid checklist—your app may already have **`proveId`** from an earlier session. The sequence diagram shows that **authentication or verification** of the consumer must succeed and yield a **`proveId`** you use in API calls before **`/v3/discover`**. It also shows how **discover** answers whether **partner-linked data** exists for that identity, and how **`/v3/fetch`** follows when that data is available. Flowchart: ProveX from authentication or verification of the consumer through a Success decision to Success=False or Success=True, then v3/discover, a decision on available partner data, Success=False or v3/fetch when partner data is available ### Establish identity (`proveId`) ProveX assumes you have completed **authentication and/or verification** for the end user and persisted a **`proveId`** from a successful response. That identifier is Prove’s anchor for **which consumer** you are asking about in the marketplace layer. ### Discover [**Discover**](https://developer.prove.com/reference/discover-request) takes the consumer’s **`proveId`** and returns which **attribute IDs** and **issuers** exist for that identity—so you know **whether** partner-linked data is in play **before** you promise a UI or call **fetch**. You **must** call **discover** before **fetch** so you do not request attributes that do not exist. ### Fetch [**Fetch**](https://developer.prove.com/reference/fetch-request) returns the **attribute values** for the **`attributeId`** values you selected from **discover**. **Discover** and **fetch** surface partner-linked data Prove is allowed to return; they do **not** complete the partner’s UI or backend on your behalf. You are responsible for wiring **`attributeValue`** into your integration with that issuer or partner. # Prove Unified Authentication Overview Source: https://developer.prove.com/explanation/unify-flow What Prove Unified Authentication is—shared session stages, integration models, Prove Key lifecycle, and how Unify fits alongside your identity provider, authorization, and risk stack. ## Prove Unified Authentication solution Prove simplifies and strengthens your authentication security by automatically choosing the right authenticator based on trusted device recognition and authenticator availability—reducing friction while protecting against fraud. Architecturally, Unify is **one coordinated session** across **client SDKs** (Web, iOS, Android) and **Unify platform APIs** such as [`/v3/unify`](https://developer.prove.com/reference/unify-request), with **channel fallbacks**—Instant Link, SMS OTP, Mobile Auth where enabled—so you are not maintaining a separate integration path for every surface and channel. Unify focuses on **phone possession**, **device trust**, and **Prove Key** lifecycle for that session. It does **not** replace your full **identity provider**, **authorization** (scopes, roles, entitlements), or **every** downstream fraud policy—you still map Prove outcomes into your own accounts, sessions, and risk stack. **Possession** is coordinated work: your app runs a **client-side SDK** (Web, iOS, or Android) while Prove’s **Unify platform APIs** advance the session and bind outcomes. Which endpoints apply depends on the integration variant. ## Concepts and definitions * **Prove Key:** Non-extractable cryptographic key used for **authentication** by Prove. * **Bind:** Establishes possession of the phone number to register the Prove Key. * **Customer-Initiated Bind:** Registry of a Prove Key based on customer-supplied possession of the phone number. * **Key Persistence:** Browser fingerprinting technology for the Web SDK that helps recover device registration when the Prove Key is unavailable, eliminating the need for SMS OTP or Instant Link re-authentication. ### What the Prove Key is associated with On Prove’s systems, each Prove Key is **correlated** with: * **`phoneNumber`:** the one used when the key is bound or authenticated * **`proveId`:** Prove’s identifier for the consumer when it is known * **`clientCustomerId`** and **`clientHumanId`:** when passed in the API request, these client-supplied identifiers are correlated with the same Prove Key Pass optional identifiers on your API requests when you want that linkage recorded for the session. ### Prove Key validity and expiration The Prove Key doesn't expire on a fixed calendar date. It stays valid with normal use, subject to the following: * **Inactivity:** Prove **deactivates the key on our servers** if the device remains inactive for an extended period. Each time the customer completes a **new authentication**, that inactivity period resets from that activity. * **App uninstall and reinstall for native apps:** If the user **deletes the app and reinstalls it**, behavior depends on platform: * **iOS:** The Prove Key **remains on the device**, so it is still available after reinstall. **Fingerprint technology is not needed** to reconstitute the key in that scenario. * **Android:** Key Persistence for recovering registration when the Prove Key is unavailable isn't available for the Android SDK. * **Forcing a possession check:** You can require an **active possession check** even when a key may still be valid; the Unify request supports signaling that intent (see the [`/v3/unify`](https://developer.prove.com/reference/unify-request) reference and [implementation guide](https://developer.prove.com/how-to/unify-implementation-guide)). ## Geographic availability **Prove Key** is available in **all countries**. **Mobile Auth** isn't supported in every country. Coverage varies by region—confirm availability for your markets when your flow depends on Mobile Auth. ## Integration variants Prove supports several **integration models**. They differ in **who runs possession first**, **which channels are in play**, and **when the Prove Key is considered bound**, but the **stages** below describe the same mental model for all of them. ### Stages every variant shares * **Open a protected session** — Typically scoped to the phone number so possession and downstream checks stay tied to one line. * **Possession or key reuse** — The SDK and server work to either **reuse a bound Prove Key** or complete a **possession** step through the channels your configuration allows. * **Status** — [`/v3/unify-status`](https://developer.prove.com/reference/unify-status-request) (and your app logic) resolve whether authentication finished, possession is still required, or the customer used an existing key. * **Bind when needed** — [`/v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) finalizes registration of the key when your flow requires an explicit bind after possession. * **Return visits** — A bound key often allows **lighter** re-authentication until policy or **rebind** forces a full possession ritual again. # Verified User Manage Source: https://developer.prove.com/explanation/verified-users-manage Learn how the Prove Platform manages identity information and provides continuous monitoring of phone number changes. ## Receive Prove Manage webhooks Use this guide to **register an HTTPS endpoint** in the Prove Portal, **verify** each delivery with the signed JWT in **`X-Prove-Authorization`**, and **process** phone-change notifications from the JSON body. Webhook events are available in the **US** only. ### Prerequisites * **Public HTTPS URL** — Prove must be able to `POST` to your endpoint from the internet. Configure the URL in the [Prove Portal](https://portal.prove.com). * **Portal access** — Sign-in to the [Prove Portal](https://portal.prove.com/en/login) with permission to create or open an **Identity Manager** project. * **JWT verification** — Your service can validate **HS256** JWTs using the **shared secret** from the Portal (before you accept webhook JSON). * **Delivery expectations** — Prove emits events as changes are detected; delivery is **best-effort** and failed deliveries are re-queued for up to **3 delivery attempts**. Design your endpoint to respond quickly and return a **2xx** status when you accept a delivery. * **No retroactive history** — Events that occurred **before** you saved the webhook configuration are not sent later. * **Per-identity stream** — After certain outcomes (**disconnect**, **moved out of coverage**, or some **phone number change** situations), Prove may send **no further** notifications for that identity until you **re-verify** and re-enroll an updated number if the customer supplies one. ### Register the endpoint in the Portal Open the [Prove Portal](https://portal.prove.com/en/login) and authenticate. Go to **Projects**. Create a project (**Create Project**) and choose **Prove Identity Manager**, or open an existing Identity Manager project. In the project, open the **Configure** tab and locate **Sandbox** webhook settings (or the environment you are integrating). For a throwaway test URL, use [Webhook.site](https://webhook.site/) to mint a unique HTTPS endpoint. Enter your **HTTPS** listener URL in the webhook field, then use **Save and Test Webhook** so Prove persists the configuration and issues a **test** `POST` to your endpoint. For each request, treat the body as **untrusted** until the JWT checks pass. 1. Read **`Authorization`** from the **`X-Prove-Authorization`** header (`Bearer `). 2. Verify the JWT with **HS256** and the **shared secret** from the Portal. 3. Validate standard time claims (`iat`, `nbf`, `exp`), allowing about **±60 seconds** clock skew. Reject tokens past **`exp`** (Prove uses about **5 minutes** of validity from issuance). 4. Confirm **`iss`** equals the literal string **`Prove Identity`**. 5. **Replay protection** — Reject any **`jti`** you have already accepted within your replay window (each delivery uses a **new** UUID **`jti`**). 6. **Integrity** — Compute **SHA-256** over the **raw request body bytes** (before JSON parsing). Compare a **constant-time** hex string equality against the **`body_hash`** claim inside the JWT payload. If it fails, reject the request. Prove also sends **`X-Correlation-ID`** on each attempt—log it for support and tracing. Example decoded payload (middle segment of the JWT): ```json Example JWT payload theme={"dark"} { "iss": "Prove Identity", "iat": 1710864000, "nbf": 1710864000, "exp": 1710864300, "jti": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "body_hash": "8af324cfdeb52d549a2504f7ba20ea51950ee4593ce6182d2b1cea927e41944d" } ``` After JWT and **`body_hash`** succeed, parse the JSON. Expect a top-level **`notifications`** array (Prove may send **up to 100** objects per call; batches can be smaller). Handle each element using the fields below. Optional keys such as **`newCarrier`** or **`newLineType`** appear only for some event types. | Field | Use | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `eventId` | Store for idempotency and support references. | | `event` | Human-readable description. | | `eventType` | Branch logic. Values include `PHONE_NUMBER_CHANGE`, `DISCONNECT`, `PORT`, `LINE_TYPE_CHANGE`, `MOVED_OUT_OF_COVERAGE`. | | `eventTimestamp` | ISO 8601 time from Prove. | | `clientCustomerId` | Your customer key, if supplied on enrollment. | | `proveId` | Prove identifier for the enrolled identity. | ```json Example payload theme={"dark"} { "notifications": [ { "eventId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "event": "phone number change detected", "eventType": "PHONE_NUMBER_CHANGE", "eventTimestamp": "2025-01-23T10:11:12Z", "clientCustomerId": "c3702333-ddd0-4aad-8f8f-c2813c1dd253", "proveId": "81d3829a-7207-4fd7-9a78-2dbf33fd54ad" } ] } ``` * Trigger **Save and Test Webhook** and confirm your handler receives a **test** request, returns **2xx**, and passes JWT + **`body_hash`** checks. * Force an **invalid** signature or body tamper in a dev environment and confirm your service **rejects** the call. * In Sandbox, exercise at least one real notification path you plan to support and confirm your persistence and idempotency logic behave as expected. ### Stop monitoring an identity Use the following endpoints when you need to stop webhook notifications for a specific enrolled identity: * [`POST /v3/identity/{identityId}/deactivate`](https://developer.prove.com/reference/identity-manager-deactivate-identity) stops webhook notifications without disenrolling the identity. * [`DELETE /v3/identity/{identityId}`](https://developer.prove.com/reference/identity-manager-delete-identity) disenrolls the identity from Identity Manager. To monitor that identity again later, you must re-enroll it. # Verified User Source: https://developer.prove.com/explanation/verified-users-overview What Prove Verified User is—phone-anchored match to Prove Identity Network signals, POST /v3/verify, batch Connect, optional Manage, and trust UX such as badges or gated actions. Prove Verified User verification method diagram ## Prove Verified User solution The Prove Verified User solution helps you **compare what your systems know** about a consumer—phone, name, and optional attributes—to **Prove’s view** of that identity drawn from the **Prove Identity Network** (devices, credentials, and longitudinal phone context), so decisions rest on **structured verification outcomes** instead of CRM data alone. You can use Verified User to verify **existing users** in your CRM, validate users **before they complete an action** on your platform, or at other moments when you need a **fresh Prove read** on identity tied to a phone number. On the implementation side, you translate successful **`/v3/verify`** results and **`proveId`** into the **trust signals your UX needs** (badges, labels, eligibility to list, buy, message, or transact) consistent with your policies. **Typical programs** include **digital marketplaces**, **social and dating** apps, **ticketing** and **high-value peer-to-peer** flows, **gig and staffing** platforms, **live streaming** or **gaming** communities with age or safety rules, and **eCommerce** flows where impersonation and chargeback risk matter—always with **phone-anchored verification** at the core. The underlying idea is to **match what you know** to **Prove-backed identity signals** so you can **approve**, **step up**, or **decline** with structured outcomes—not guesswork from stale CRM fields alone. Depending on your **program configuration and contract**, you can combine Verified User with **optional step-up paths** (for example additional KYC or **sanctions and watchlist screening**) so higher-risk segments get more friction without forcing document-first journeys on everyone. Where policy allows, **phone-first** verification can also **reduce reliance on document-centric** identity steps for every user—lowering UX cost compared to always-on document capture, while your own risk team decides when document or liveness paths are still required. Verified User is **not** a replacement for your **customer master**, **authorization**, or **every** compliance control. It adds **verification results** and **`proveId`** linkage when checks succeed, which you map into your own policies and workflows. Verified User is available only in the **United States**. Flowchart: Verified User from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success ## How fits together At a high level, rests on **Verify** and optionally **Manage**. They describe **how the real-time product is structured**, not a rigid checklist: your integration may combine or revisit them depending on channel, risk, and whether the customer already has a bound **Prove Key**. ### Verify **Verify** is your back end calling [`POST /v3/verify`](https://developer.prove.com/reference/verify) with the verification type and the inputs your flow requires to obtain a **verification result** and, when successful, identifiers such as **`proveId`** that you can persist for later calls, support, and linking to other Prove capabilities. End-to-end setup, Sandbox testing, and response handling are in the Verify guide. ### Manage **Manage** applies when a verified identity meets Prove’s [**Global Fraud Policy**](https://developer.prove.com/reference/global-fraud-policy): it can be **enrolled for continuous monitoring** of phone and related identity changes. Number changes, disconnects, and coverage moves are signals you want **after** day one, not only at signup. That path is documented under the Manage guide. **Webhook** notifications for those phone and identity change events are **available in the US**. # Account Opening Verify Source: https://developer.prove.com/how-to/account-opening-verify How to verify identity for Account Opening using POST /v3/verify, handle the response, and test in Sandbox. Flowchart: Account Opening from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success Identities that pass the [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) are automatically enrolled in [Manage](https://developer.prove.com/explanation/account-opening-manage). ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](https://developer.prove.com/reference/authentication)). ## Implementation steps Collect the required customer information from your CRM or database: * Phone number * First name * Last name Make a request to the [`/v3/verify endpoint`](https://developer.prove.com/reference/verify) including the Authorization header. Generate a bearer token as outlined on the [Authentication page](https://developer.prove.com/reference/authentication). ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "accountOpening", "firstName": "Bettina", "lastName": "Halbert", "phoneNumber": "2001004049", "clientRequestId": "test-001" }' ``` Replace `` with your acquired access token. For **Account Opening**, set `verificationType` to `accountOpening` and include `phoneNumber`, `firstName`, `lastName`, and `clientRequestId`. You can also pass `clientCustomerId`, `clientHumanId`, or `proveId` when you need that linkage. Use the samples below together with the [`/v3/verify`](https://developer.prove.com/reference/verify) response schema. Cross-check field meanings with [Assurance levels](https://developer.prove.com/reference/assurance-levels) and [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) when interpreting codes and evaluations. ```json Success Response theme={"dark"} { "success": "true", "clientRequestId": "test-001", "phoneNumber": "2001004049", "proveId": "41570934-6d6b-476d-9425-3eaf305cf2e5", "identity": { "firstName": "Bettina", "lastName": "Halbert", "assuranceLevel": "AL2" }, "additionalIdentities": [ { "firstName": "Johnny", "lastName": "Halbert", "assuranceLevel": "AL1" } ], "evaluation": { "authentication": { "result": "pass" }, "risk": { "result": "pass" } } } ``` ```json Failure Response theme={"dark"} { "success": "false", "clientRequestId": "test-002", "phoneNumber": "2001004050", "identity": { "assuranceLevel": "AL0" }, "evaluation": { "authentication": { "failureReasons": { "9175": "No active identity can be associated with the phone number.", "9177": "No information can be found for the identity or phone number." }, "result": "fail" }, "risk": { "result": "pass" } } } ``` ### In practice * **`success`** — Branch your UX and backend logic on pass vs fail. * **`identity`** and **`additionalIdentities`** — Read verified attributes and [`assuranceLevel`](https://developer.prove.com/reference/assurance-levels) for policy (step-up, deny, manual review). * **`evaluation`** — Interpret authentication and risk outcomes under [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy); use failure codes when `success` is false. * **`proveId`** — Persist `proveId` when present if you need support, auditing, or linking to other Prove flows. If you passed optional identifiers on the request, the response may echo `clientCustomerId` and `clientHumanId`; see the [`/v3/verify`](https://developer.prove.com/reference/verify) reference for details. ## Sandbox testing ### Test users You must use project credentials when working with sandbox test users. Attempting to use these test users with different project credentials results in an unauthorized access error. The following test users are available for testing using the `/v3/verify` endpoint in the Sandbox environment. Use these test users to simulate different verification scenarios and outcomes. Use these test phone numbers exactly as shown. The sandbox environment doesn't validate real customer information. | Phone Number | First Name | Last Name | Verification Type | Expected Outcome | | ------------ | ---------- | ------------ | ----------------- | ---------------- | | `2001004049` | Bettina | Halbert | `accountOpening` | Success | | `2001004050` | Laurel | Van Der Beek | `accountOpening` | Failed | ### Testing steps Use test user Bettina Halbert to verify a successful verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "accountOpening", "firstName": "Bettina", "lastName": "Halbert", "phoneNumber": "2001004049", "clientRequestId": "test-001" }' ``` Expected response: ```json theme={"dark"} { "success": "true", "clientRequestId": "test-001", "phoneNumber": "2001004049", "proveId": "41570934-6d6b-476d-9425-3eaf305cf2e5", "identity": { "firstName": "Bettina", "lastName": "Halbert", "assuranceLevel": "AL2" }, "additionalIdentities": [ { "firstName": "Johnny", "lastName": "Halbert", "assuranceLevel": "AL1" } ], "evaluation": { "authentication": { "result": "pass" }, "risk": { "result": "pass" } } } ``` Use test user Laurel Van Der Beek to simulate a failed verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "accountOpening", "firstName": "Laurel", "lastName": "Van Der Beek", "phoneNumber": "2001004050", "clientRequestId": "test-002" }' ``` Expected response: ```json theme={"dark"} { "success": "false", "clientRequestId": "test-002", "phoneNumber": "2001004050", "identity": { "assuranceLevel": "AL0" }, "evaluation": { "authentication": { "failureReasons": { "9175": "No active identity can be associated with the phone number.", "9177": "No information can be found for the identity or phone number." }, "result": "fail" }, "risk": { "result": "pass" } } } ``` # Set up Prove SDKs in your project Source: https://developer.prove.com/how-to/dev-environment Install the Prove server SDK for your language, configure Sandbox OAuth credentials in code, and optionally add the Web, Android, or iOS client SDK for possession flows. Use this guide when you already have an application and need **Prove packages** and **Sandbox configuration** in the repo. Complete **server** steps for any backend that calls Prove Platform APIs. Add **one** client tab (Web, Android, or iOS) only if your product runs possession (Prove Key, Mobile Auth, OTP, and so on) in that client. For a first **bearer token** and `curl` to Prove without an SDK, use [Get started with Prove API authentication](/tutorial/access-api-keys) first. ## Prerequisites * **Sandbox credentials** — OAuth **client ID** and **client secret** from the [Developer Portal](https://portal.prove.com) for the project you are integrating. * **Tooling** — Language runtime and package manager for your stack (for example Go modules, npm, Gradle/Maven, .NET CLI, CocoaPods). Add the Prove server SDK for your language using your normal dependency workflow. ```go Go theme={"dark"} # The Go library is hosted on GitHub so you can use this command to import it # to your Go application. go get github.com/prove-identity/prove-sdk-server-go # Ensure you import the SDK in your code like this: import ( provesdkservergo "github.com/prove-identity/prove-sdk-server-go" "github.com/prove-identity/prove-sdk-server-go/models/components" ) ``` ```typescript TypeScript theme={"dark"} # Run this command to install package from GitHub and save as a dependency npm install -S @prove-identity/prove-api # Import the SDK in your code like this: import { Proveapi } from "@prove-identity/prove-api"; import { OAuthClient, WithAuthorization } from "@prove-identity/prove-api/sdk/oauth" ``` ```java Java theme={"dark"} # See the latest version number: https://central.sonatype.com/artifact/com.prove/proveapi Gradle: implementation 'com.prove:proveapi:0.10.0' Maven: com.prove proveapi 0.10.0 ``` ```csharp .NET theme={"dark"} // Run this command to install package from Nuget and save as a dependency dotnet add package Prove.Proveapi --version 1.0.1 // Ensure you import the SDK in your code like this: using Prove.Proveapi; using Prove.Proveapi.Models.Components; ``` Initialize the SDK with your Sandbox **client ID** and **client secret** from environment variables or your secrets store (see [Secure API credentials](/how-to/secure-api-credentials)). ```go Go theme={"dark"} clientID := os.Getenv("PROVE_CLIENT_ID") clientSecret := os.Getenv("PROVE_CLIENT_SECRET") proveEnv := "uat-us" // Use UAT in US region. client := provesdkservergo.New( provesdkservergo.WithServer(proveEnv), provesdkservergo.WithSecurity(components.Security{ ClientID: provesdkservergo.String(clientID), ClientSecret: provesdkservergo.String(clientSecret), }), ) ``` ```typescript TypeScript theme={"dark"} export const DEVICE_API_BASE_URL = process.env.DEVICE_API_BASE_URL || ''; export const PROVE_CLIENT_ID = process.env.PROVE_CLIENT_ID; export const PROVE_CLIENT_SECRET = process.env.PROVE_CLIENT_SECRET; export function getProveSdk(): Proveapi { return new Proveapi({ server: 'uat-us', security: { clientID: PROVE_CLIENT_ID, clientSecret: PROVE_CLIENT_SECRET, }, }); } ``` ```java Java theme={"dark"} String clientId = System.getenv("PROVE_CLIENT_ID"); String clientSecret = System.getenv("PROVE_CLIENT_SECRET"); Proveapi sdk = Proveapi.builder() .security(Security.builder() .clientID(clientId) .clientSecret(clientSecret) .build()) .build(); ``` ```csharp .NET theme={"dark"} var clientId = Environment.GetEnvironmentVariable("PROVE_CLIENT_ID"); var clientSecret = Environment.GetEnvironmentVariable("PROVE_CLIENT_SECRET"); var sdk = new ProveAPI(serverUrl: "https://platform.uat.proveapis.com"); var tokenRequest = new V3TokenRequest { ClientId = clientId, ClientSecret = clientSecret, GrantType = "client_credentials" }; var tokenResponse = await sdk.V3.V3TokenRequestAsync(tokenRequest); var accessToken = tokenResponse.V3TokenResponse?.AccessToken; _sdk = new ProveAPI(auth: accessToken, serverUrl: "https://platform.uat.proveapis.com"); ``` Replace `uat-us` with `uat-eu` if you are testing outside North America. Add a client SDK only when your UX runs Prove possession in the browser or native app. Pick **one** tab below. ```shell NPM theme={"dark"} # Run this command to install the package (ensure you have the latest version). npm install @prove-identity/prove-auth@3.4.2 ``` ```html No Package Manager theme={"dark"} # You can include this file in your web application from jsDelivr (update with the latest version). # You can also download the JavaScript file from https://cdn.jsdelivr.net/npm/@prove-identity/prove-auth@3.4.2/build/bundle/release/prove-auth.js and store it locally. ``` If you're including the file from jsDelivr and using [Content Security Policy headers](https://content-security-policy.com/), ensure you add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. Prove hosts Android binaries in a Maven repository for Gradle. Update the `dependencies` block in `build.gradle`: ```java Java theme={"dark"} dependencies { // Existing dependencies are here. // Add the Prove Link dependencies: implementation 'com.prove.sdk:proveauth:6.10.7' } ``` Point `settings.gradle` at the Prove Maven repository: ```java Java theme={"dark"} dependencyResolutionManagement { // Existing repository settings are here. repositories { // Existing repositories are here. // Add the Prove Link Maven repository: maven { url = "https://prove.jfrog.io/artifactory/libs-public-maven/" } } } ``` Add the following to `build.gradle` so dependency libraries resolve: ```java Java theme={"dark"} dependencies { implementation fileTree('libs') } ``` If you see an error on `application@fullBackupContent`, add this attribute on the opening `` tag in `AndroidManifest.xml`: ```xml XML theme={"dark"} ``` The SDK merges standard network permissions into your manifest. After sync, confirm they meet your policy; for the full list and possession wiring, see [Integrate the Unified Authentication client SDK](/how-to/unify-client-sdk) (Android tab). Prove hosts iOS artifacts for CocoaPods integration. Run the following to register the Prove pod repo and install pods: ```shell shell theme={"dark"} # Run this command to install the cocoapods-art plugin (authored by Artifactory) gem install cocoapods-art # Run this command to add the Prove pod repository pod repo-art add prove.jfrog.io https://prove.jfrog.io/artifactory/api/pods/libs-public-cocoapods # In your Podfile, paste in the Prove pod repository as a source plugin 'cocoapods-art', :sources => [ 'prove.jfrog.io' ] # In your Podfile, paste in the SDK pods pod 'ProveAuth', '6.10.4' # Run this command to install the SDK pods pod install ``` ## Verify your setup * **Server** — Project restores or builds with the new dependency; imports resolve with no version conflicts. * **Credentials** — At runtime, the client ID and secret your code reads match the **Sandbox** credentials from the Developer Portal for the intended region (`uat-us` vs `uat-eu`). * **Client (if installed)** — Package or pods install completes; for native apps, open the project in Xcode or Android Studio and confirm the Prove modules are linked. # Implement Customer Possession With Force Bind Source: https://developer.prove.com/how-to/human-assurance-customer-possession Learn how to implement customer-supplied possession with force bind for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps This authentication type only applies to mobile channels. Send a request to your back end server with the phone number and `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional fields (for example `rebind`, `deviceId`, `proveId`) and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "none", }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'none' } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("none") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "none", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Initialize the client-side SDK to place a Prove Key on the device or to check whether a Prove Key is bound. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` In the `AuthFinishStep` of the client SDK, have the function make a call to an endpoint on your back end server. Your backend server should then call the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (for example `possession_required`) to decide whether to run the possession step and [`POST /v3/unify-bind`](/reference/unify-bind-request). See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). Your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration using. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004026 | Bonnie | Sidon | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger using customer-supplied possession. This user passes the entire Unified Authentication flow using the customer's possession and return `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. They fail out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. Fail the user out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required`, reminding you to perform your own possession check. Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. # Implement Prove Passive Authentication With Customer-Supplied Possession Fallback Source: https://developer.prove.com/how-to/human-assurance-prove-auth-then-customer Learn how to implement Prove Mobile Auth with customer-supplied possession fallback for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. This section only applies to mobile channels. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Send a request to your back end server with `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional identifiers such as `clientCustomerId` and `clientRequestId` and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PossessionType: "none", ClientRequestID: provesdkservergo.String("client-abc-123"), }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { possessionType: 'none', clientRequestId: 'client-abc-123', } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .possessionType("none") .clientRequestId("client-abc-123") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PossessionType = "none", ClientRequestID = "client-abc-123", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (including `possession_required` when a possession check is still required) to drive your UI and next server calls. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If `success` returned `possession_required`, your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you send a different phone number to /unify than the one registered to the Prove key, the customer receives `possession_required` on the /unify-status call. Call /unify-bind to rebind the Prove Key to the new number. Once it's rebound, the first number responds with `possession_required`. The Prove key only supports one phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Jesse Mashro on mobile. This user fails the Unified Authentication flow using Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Jesse Mashro. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth and OTP without prompting. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer OTP and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer possession check and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. # Implement Prove Possession Using The Android SDK Source: https://developer.prove.com/how-to/human-assurance-prove-possession-android-sdk Learn how to implement Prove possession for authentication using the Android SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Android SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on Android**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: OtpStartStep, otpFinish: OtpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the interfaces, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it. Typical signs include a validation or reported error such as code 10001 with a message that the PIN doesn't match. You may also see flow activity related to AuthFinishStep, such as ProveAuth.builder().withAuthFinishStep(...), that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep.execute to pass the error in otpException. The SDK invokes OtpFinishStep.execute when a retry or error UI is needed. Your app should implement the required step interfaces, but the Prove client SDK orchestrates when each step runs. Don't duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with an **`OtpStartInput`** instance that reflects “number already known”—typically an empty string or **`null`** for the phone value, as in the snippet—so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, ProveAuthException otpException, OtpStartStepCallback callback) { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } ``` Call **`OtpStartStepCallback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, call **`OtpFinishStepCallback.onSuccess(OtpFinishInput)`** with the OTP the customer entered, wrapped in **`OtpFinishInput`**. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` Use this path when the **Android app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with the collected number. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can request another SMS, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class MultipleResendFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForResend("Didn't receive the SMS OTP? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onOtpResend(). otpFinishStepCallback.onOtpResend(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can supply a new number, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PhoneChangeFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForPhoneNumberChange("Didn't receive the SMS OTP? Try a different phone number.")) { // If the end user wants to correct the phone number already in use, or changing to a // different phone number to receive the future SMS OTP, call onMobileNumberChange(), and // the otpStartStep will re-prompt for phone number input from the end user. otpFinishStepCallback.onMobileNumberChange(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep.execute`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for Android is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the Android client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Verified App Links** (recommended) so the SMS redirect opens your app with the full URL string. See [App Links](https://developer.android.com/training/app-links/about) in the Android documentation. When building the authenticator, use **`withInstantLinkFallback(InstantLinkStartStep startStep, @Nullable InstantLinkRetryStep retryStep)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When you have a mobile number, pass it in **`InstantLinkStartInput`** (for example the `mobileNumber` field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(InstantLinkStartInput input)`** so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} InstantLinkStartStep noPromptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (!phoneNumberNeeded) { callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Use this path when the client collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(InstantLinkStartInput input)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptMultiResendRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForResend( "Didn't receive the InstantLink SMS? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptPhoneNumChangeRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForChangePhoneNumber( "Didn't receive the InstantLink SMS? Try a different phone number.")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` After the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your App Link (or equivalent) must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(String redirectUrl)`**. Call **`finishInstantLink`** from the code path that handles the incoming deep link (for example **`onCreate()`** in your App Link activity). Register an activity similar to this (replace the host with yours): ```xml theme={"dark"} ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK throws **`ProveAuthException`** and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your handler activity; **`finishInstantLink`** should run with the full URL and the session should resume without **`ProveAuthException`** from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The iOS SDK Source: https://developer.prove.com/how-to/human-assurance-prove-possession-ios-sdk Learn how to implement Prove possession for authentication using the iOS SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the iOS SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on iOS**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the protocols, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it (for example, a reported error such as code 10001 with a message that the PIN does not match). You may also see flow activity related to AuthFinishStep (the handler you pass to ProveAuth.builder(authFinish:)) that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep to pass the validation error in otpError. The SDK invokes OtpFinishStep.execute(otpError:callback:) when a retry or error UI is needed. Your app should implement the required step protocols, but the Prove client SDK orchestrates when each step runs. Do not duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class OtpStartStepNoPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for SMS OTP, // or to obtain user confirmation for initiating an SMS message. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Call **`callback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, return the OTP the customer entered through your **`OtpFinishStep`** implementation, as in the snippet below. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` Use this path when the **iOS app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`callback.onSuccess(input: otpStartInput)`** with the collected number. ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step: ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class OtpFinishStepMultipleResend: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered in the SMS message. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the OTP value to the SDK func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Communicate to the SDK any issues when host app trys to obtain the OTP value // or when users cancel the OTP flow func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } // Call this method to request a new OTP code for the same mobile number. func sendNewOtp() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onOtpResend() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can supply a new number, for example: ```swift Swift theme={"dark"} class OtpFinishStepWithPhoneChange: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OTP finish view. DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid. self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the collected OTP value to the SDK. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // When callback.onMobileNumberChange() is evoked, OtpStartStep will be re-initiated // so that end-users can enter a different phone number via OtpStartStep. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set") return } callback.onMobileNumberChange() } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for iOS is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the iOS client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Universal Links** (recommended) so the SMS redirect opens your app with the full URL string. See [Supporting universal links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) in the Apple documentation. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep, retryStep: InstantLinkRetryStep?)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When the client supplies a number, pass it in **`InstantLinkStartInput`** (for example the **`phoneNumber`** field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class InstantLinkStartStepNoPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for instant link, // or to obtain user confirmation for initiating an instant link. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Use this path when the **iOS app** collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(input: instantLinkStartInput)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepMultipleResend: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend to the same phone number or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish) or request resend to the same phone number. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request a new instant link to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepPhoneChange: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend, change phone number, or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish), request resend, or request phone number change. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request resend to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // When this is invoked, InstantLinkStartStep will be re-initiated so that the user // can enter a different phone number. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onMobileNumberChange() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` After you implement the start step above and the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your Universal Link handling must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(redirectUrl:)`**. For how **`finalTargetUrl`** fits into server-side **Start** with Mobile Auth and Instant Link fallback, see [Prove Pre-Fill implementation guide](/how-to/prove-pre-fill-implementation-guide). Call **`finishInstantLink`** from the entry point that receives the opened URL, for example **`application(_:open:options:)`** or **`application(_:continue:restorationHandler:)`**. ```swift Swift theme={"dark"} /// Finishes the Instant Link authentication flow using the redirect URL from the deep link. /// This is the URL to which the user is redirected after tapping the Instant Link in SMS. finishInstantLink(redirectUrl: redirectUrl) { error in // Handle errors due to invalid format of redirectUrl here } ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK surfaces an error via the **`finishInstantLink`** completion and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your app with the full URL; **`finishInstantLink`** should run and the session should resume without an error from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The Web SDK Source: https://developer.prove.com/how-to/human-assurance-prove-possession-web-sdk Learn how to implement Prove possession for authentication using the Web SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Installation The Prove Platform Web SDK has an unpacked size of 324 KB, and a single dependency: `@prove-identity/mobile-auth`. Install the client-side SDK of your choice by running a command in your terminal, or by using a dependency management tool specific to your project. ```shell NPM theme={"dark"} # Run this command to install the package (ensure you have the latest version). npm install @prove-identity/prove-auth@3.4.2 ``` ```html No Package Manager theme={"dark"} # You can include this file in your web application from jsDelivr (update with the latest version). # You can also download the JavaScript file from https://cdn.jsdelivr.net/npm/@prove-identity/prove-auth@3.4.2/build/bundle/release/prove-auth.js and store it locally. ``` If you're including the file from jsDelivr and using [Content Security Policy headers](https://content-security-policy.com/), ensure you add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. ## Prerequisites * **Backend** — Your server calls [`POST /v3/unify`](/reference/unify-request) and the rest of the Unified Authentication sequence. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web SDK for possession checks and the Prove Key. * **Languages** — TypeScript or JavaScript. For native iOS, see [Unify iOS SDK](/how-to/unify-ios-sdk). ## Flow overview: mobile vs desktop In a **mobile** flow, the mobile phone validates the one-time password (OTP). In a **desktop** flow, Instant Link sends a text message to the mobile phone for verification. In the mobile flow, once OTP validation completes, the `AuthFinishStep` callback finishes. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while the customer opens the link in the text message. When they do, the WebSocket closes and the `AuthFinishStep` callback finishes. ## `Authenticate()` and `authToken` The Web SDK requires an `authToken` for `Authenticate()`. That value comes from your server after it calls [`POST /v3/unify`](/reference/unify-request) (often exposed to the browser through your own start or initialize endpoint). The token is **session-specific** (one flow) and **expires after about 15 minutes**. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. **Example: browser requests `authToken` from your backend** Send possession type and an optional phone number when your flow uses Prove possession to your server. Your server calls Prove and returns the token to the page. ```javascript JavaScript theme={"dark"} async function initialize(phoneNumber, possessionType) { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` ```typescript TypeScript theme={"dark"} async function initialize( phoneNumber: string, possessionType: string ): Promise { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Mobile Auth Implementations Only If your app uses [Content Security Policy headers](https://content-security-policy.com/), you must configure them to allow connections to Prove's authentication services: Sandbox Environment * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` Failure to configure these settings prevents Mobile Auth capability from working correctly in web flows. AT\&T Carrier Support (Pixel Mode) If support for the AT\&T carrier is also required, you must use **Pixel** mode (`withMobileAuthImplementation("pixel")`) with the following Content Security Policy (CSP) configuration: Allowed domain for Sandbox Environment * `https://att-device.uat.proveapis.com:4443` * `https://att-device.uat.proveapis.com` * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Allowed domain for Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` * `https://att-device.proveapis.com:4443` * `https://att-device.proveapis.com` * `https://snap.att.com` * `https://snap.mobile.att.com` * `https://snap.mobile.att.net` Add img-src for Pixel implementation Example img-src for Pixel mode: ```http theme={"dark"} Content-Security-Policy: img-src 'self' https://auth.svcs.verizon.com:22790 https://snap.att.com https://snap.mobile.att.com https://snap.mobile.att.net https://device.uat.proveapis.com:4443 https://device.uat.proveapis.com https://att-device.uat.proveapis.com:4443 https://att-device.uat.proveapis.com http://device.uat.proveapis.com:4443 http://device.uat.proveapis.com https://device.proveapis.com:4443 https://device.proveapis.com https://att-device.proveapis.com:4443 https://att-device.proveapis.com http://device.proveapis.com:4443 http://device.proveapis.com; connect-src self https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com ``` For desktop mode, Prove ignores the Prove Key and runs Instant Link. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ## Configure OTP Use this section to **configure SMS OTP fallback in the Web SDK**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(startStep: OtpStartStep | OtpStartStepFn, finishStep: OtpFinishStep | OtpFinishStepFn)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. Return the phone number from your start step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. The **Prove client SDK orchestrates** when each step runs—do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it and may surface activity related to AuthFinishStep that looks unexpected. This behavior is **expected**. Do **not** add your own extra invocation of the OTP finish step to pass the error in otpError. The SDK runs your OtpFinishStep when retry or error UI is needed. Implement the required step functions, but let the Prove client SDK orchestrate when each step runs. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt for it again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const otpStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Call **`reject('some error message')`** if something prevents sending the SMS or completing the flow (for example the customer cancels or leaves the UI with the back button). In the finish step, return the OTP through **`resolve(result: OtpFinishResult)`** with the **`OnSuccess`** result type and the value wrapped in **`OtpFinishInput`**, as in the snippet below. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Use this path when the **page** collects the phone number in the browser and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpMultipleResendFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can supply a new number, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPhoneChangeFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** stack extra **`OtpFinishStep`** invocations on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Use this section to **configure the Web SDK** so Instant Link SMS can be sent from the browser flow and optional resend or phone-number change behaves as expected. Instant Link for mobile web isn't supported. **Custom or vanity Instant Links** aren't supported. You can't substitute a custom link for the default Instant Link. ### Prerequisites * Integrate on **desktop (or other supported) web**; see the callout above for mobile web. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep | InstantLinkStartStepFn, retryStep?: InstantLinkRetryStep | InstantLinkRetryStepFn)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). Return the phone number from your step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkNoPromptStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Use this path when the **page** collects the number in the browser and you **do not** need resend or phone-number change. For resend or change, use the other tabs. Call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. Call **`reject('some error message')`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(0); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkMultipleResendRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(InstantLinkResultType.OnResend); }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(1); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkPhoneChangeRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(InstantLinkResultType.OnMobileNumberChange); }); }, }; ``` In Sandbox, run through each path you ship (default, prompt, and any retry flows). Confirm the SMS sends, the customer can complete the link within the timeout window, and **`resolve`** / **`reject`** match the UX you expect when the customer cancels or retries. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while waiting for the customer to select the link in the text message. Once clicked, the WebSocket closes and the `AuthFinishStep` function finishes. If your UI already has `flowType` from the server (`mobile` or `desktop`), you can configure the builder from that value instead of `isMobileWeb()`: ```javascript JavaScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` ```typescript TypeScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. If the user cancels the client flow, your backend should still call `UnifyStatus()`; you may receive `success=false`. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). **Prove Key persistence enhancement** Using Prove Key in web applications can be affected by strict browser privacy policies aimed at preventing cross-site tracking. For example, Safari completely disables third-party cookies and limits the lifetime of all script-writable website data. Prove Auth uses script-writable website data (localStorage and IndexedDB) for storing Prove Auth `deviceId`, crypto key material, and other critical metadata for subsequent device re-authentication. If the browser deletes this script-writable data, Prove Auth is no longer able to recognize the device and perform authentication. To mitigate this limitation and avoid repeating the device registration process, follow these steps to enable **Key Persistence**. The Key Persistence feature is not enabled by default. Contact Prove to enable this add-on feature. **Web app setup** Follow these steps to add Prove Key persistence in your web app: Install the Key Persistence integration module and add activation code to your app: ```shell NPM theme={"dark"} npm install @prove-identity/prove-auth-device-context ``` Then activate it in your app: ```javascript JavaScript theme={"dark"} import * as dcMod from "@prove-identity/prove-auth-device-context"; dcMod.activate(); ``` ```html No Package Manager theme={"dark"} ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow access to the following common resources: * `script-src https://fpnpmcdn.net https://*.prove-auth.proveapis.com`, * `connect-src https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com` * `worker-src blob:` Additionally, if you're including the file from jsDelivr, add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. When configuring `authenticatorBuilder` for any flow, add the `withDeviceContext()` method to enable Key Persistence data collection: ```javascript JavaScript theme={"dark"} // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` ```typescript TypeScript theme={"dark"} import { BuildConfig, DeviceContextOptions } from "@prove-identity/prove-auth"; // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey: string = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options: DeviceContextOptions = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` Call `authenticatorBuilder.build()` immediately after the web app loads. This enables the Web SDK to load components for Key Persistence collection and collect signals before authentication begins, which helps decrease latency. Ensure cookies are enabled when you call `AuthStart` and `AuthFinish`: ```javascript Axios theme={"dark"} // Enable cookies for AuthStart axios.post(backendUrl + '/authStart', data, { withCredentials: true }); // Enable cookies for AuthFinish axios.post(backendUrl + '/authFinish', data, { withCredentials: true }); ``` ```javascript Fetch theme={"dark"} // Enable cookies for AuthStart fetch(backendUrl + '/authStart', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); // Enable cookies for AuthFinish fetch(backendUrl + '/authFinish', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` **Handling multiple device registrations** If a user registers their device many times before using Key Persistence, Prove Auth may have many registration candidates matching the same visitor ID. To improve recovery accuracy: 1. Store the `deviceId` returned from successful authentications in your backend (database or web cookie) 2. Pass the stored `deviceId` with the `/unify` request to specify which registration to restore ```go Go theme={"dark"} // Send the Unify request with deviceId for registration recovery rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", DeviceId: provesdkservergo.String("stored-device-id-from-previous-auth"), }) ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', deviceId: 'stored-device-id-from-previous-auth', } const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); ``` ```java Java theme={"dark"} V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .deviceId("stored-device-id-from-previous-auth") .build(); ``` Continue the flow using the response from [`POST /v3/unify`](/reference/unify-request). Return updated session or auth details to the client as needed. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy) when present. If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. #### Mobile vs desktop When a tab includes both flows, **follow the steps below on mobile first.** On **desktop**, keep the same **Prompt customer** and **Verify mobile number** timing; only these parts change: * **Initiate Start Request** — The front end also sends **final target URL** with the phone number and possession type before `/unify`. * **Send Auth Token to the Front End** — The client completes **Instant Link** instead of OTP or Mobile Auth. Example of the Instant Link screen in the Prove Unified Authentication sandbox testing flow * **Verify Mobile Number** — On **success**, expect `proveId` and `phoneNumber` only. On **failure**, expect the same `success=false` and `phoneNumber` behavior as on mobile. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. **Mobile:** Test passive authentication with customer-supplied possession fallback; Mobile Auth passes and [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. On mobile, Mobile Auth fails and OTP succeeds (`1234`); [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Use this flow to test Prove Key revocation. Run a successful flow for [Penny](#penny). Copy the `deviceId` from the /unify-status response. Call the /device/revoke endpoint with the device ID to revoke the Prove Key tied to that user. ```batch cURL Request theme={"dark"} curl --request POST \ --url https://platform.uat.proveapis.com/v3/device/revoke \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "clientRequestId": "71010d88-d0e7-4a24-9297-d1be6fefde81", "deviceId": "" } ' ``` ```json JSON Response theme={"dark"} { "success": true } ``` If you send Penny through Unified Authentication again, attempting to authenticate just using the Prove Key, the endpoint returns ```json Error Message theme={"dark"} { "code": 8019, "message": "device has been revoked" } ``` You must complete a new authentication flow to reestablish the Prove Key. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Human Assurance Verify Source: https://developer.prove.com/how-to/human-assurance-verify How to verify Human Assurance using POST /v3/verify, interpret the response, and test in Sandbox. Flowchart: Human Assurance from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success Identities that pass the [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) are automatically enrolled in [Manage](https://developer.prove.com/explanation/account-opening-manage). ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](https://developer.prove.com/reference/authentication)). ## Implementation steps Collect the phone number from your CRM or database. Make a request to the [`/v3/verify endpoint`](https://developer.prove.com/reference/verify) including the Authorization header. Use the bearer token as outlined on the [Authentication page](https://developer.prove.com/reference/authentication). ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "2001004051", "verificationType": "humanAssurance", "clientRequestId": "test-001" }' ``` Replace `` with your acquired access token. For **Human Assurance**, set `verificationType` to `humanAssurance` and include `phoneNumber`. You can also pass `clientRequestId`, `clientCustomerId`, `clientHumanId`, or `proveId` when you need that linkage. Use the samples below together with the [`/v3/verify`](https://developer.prove.com/reference/verify) response schema. Cross-check field meanings with [Assurance levels](https://developer.prove.com/reference/assurance-levels) and [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) when interpreting codes and evaluations. ```json Success Response theme={"dark"} { "success": "true", "clientRequestId": "test-001", "phoneNumber": "2001004051", "identity": { "assuranceLevel": "AL1" }, "evaluation": { "authentication": { "result": "pass" }, "risk": { "result": "pass" } } } ``` ```json Failure Response theme={"dark"} { "success": "false", "clientRequestId": "test-001", "phoneNumber": "2001004052", "identity": { "assuranceLevel": "AL-1" }, "evaluation": { "authentication": { "failureReasons": { "9176": "The phone number and identity is strongly associated with a fraud vector." }, "result": "fail" }, "risk": { "failureReasons": { "9304": "Suspicious large amount of recent activity across different identity attributes." }, "result": "fail" } } } ``` ### In practice * **`success`** — Branch your UX and backend logic on pass vs fail. * **`identity`** — Read [`assuranceLevel`](https://developer.prove.com/reference/assurance-levels) and `reasons` for human-vs-automation policy (allow, challenge, block). * **`evaluation`** — Interpret authentication and risk outcomes under [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy); use failure codes when `success` is false. For Human Assurance, weigh both **authentication** and **risk** results when you tune thresholds. * **`proveId`** — Persist `proveId` when present if you need support, auditing, or linking to other Prove flows. If you passed optional identifiers on the request, the response may echo `clientCustomerId` and `clientHumanId`; see the [`/v3/verify`](https://developer.prove.com/reference/verify) reference for details. ## Sandbox testing ### Test users You must use project credentials when working with sandbox test users. Attempting to use these test users with different project credentials results in an unauthorized access error. The following test users are available for testing using the `/v3/verify` endpoint in the Sandbox environment. Use these test users to simulate different verification scenarios and outcomes. Use these test phone numbers exactly as shown. The sandbox environment doesn't validate real customer information. | Phone Number | First Name | Last Name | Verification Type | Expected Outcome | | ------------ | ---------- | ---------- | ----------------- | ---------------- | | `2001004051` | Ewen | Brimilcome | `humanAssurance` | Success | | `2001004052` | Hilary | Kumaar | `humanAssurance` | Failed | ### Testing steps Use test user Ewen Brimilcome to verify a successful verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "humanAssurance", "clientRequestId": "test-001", "phoneNumber": "2001004051" }' ``` Expected response: ```json theme={"dark"} { "success": "true", "clientRequestId": "test-001", "phoneNumber": "2001004051", "identity": { "assuranceLevel": "AL1" }, "evaluation": { "authentication": { "result": "pass" }, "risk": { "result": "pass" } } } ``` Use test user Hilary Kumaar to simulate a failed verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "humanAssurance", "phoneNumber": "2001004052", "clientRequestId": "test-001" }' ``` Expected response: ```json theme={"dark"} { "success": "false", "clientRequestId": "test-001", "phoneNumber": "2001004052", "identity": { "assuranceLevel": "AL-1" }, "evaluation": { "authentication": { "failureReasons": { "9176": "The phone number and identity is strongly associated with a fraud vector." }, "result": "fail" }, "risk": { "failureReasons": { "9304": "Suspicious large amount of recent activity across different identity attributes." }, "result": "fail" } } } ``` # Implement Customer Possession With Force Bind Source: https://developer.prove.com/how-to/implement-customer-possession Learn how to implement customer-supplied possession with force bind for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps This authentication type only applies to mobile channels. Send a request to your back end server with the phone number and `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional fields (for example `rebind`, `deviceId`, `proveId`) and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "none", }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'none' } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("none") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "none", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Initialize the client-side SDK to place a Prove Key on the device or to check whether a Prove Key is bound. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` In the `AuthFinishStep` of the client SDK, have the function make a call to an endpoint on your back end server. Your backend server should then call the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (for example `possession_required`) to decide whether to run the possession step and [`POST /v3/unify-bind`](/reference/unify-bind-request). See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). Your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration using. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004026 | Bonnie | Sidon | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger using customer-supplied possession. This user passes the entire Unified Authentication flow using the customer's possession and return `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. They fail out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. Fail the user out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required`, reminding you to perform your own possession check. Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. # Implement Prove Passive Authentication With Customer-Supplied Possession Fallback Source: https://developer.prove.com/how-to/implement-prove-auth-then-customer Learn how to implement Prove Mobile Auth with customer-supplied possession fallback for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. This section only applies to mobile channels. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Send a request to your back end server with `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional identifiers such as `clientCustomerId` and `clientRequestId` and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PossessionType: "none", ClientRequestID: provesdkservergo.String("client-abc-123"), }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { possessionType: 'none', clientRequestId: 'client-abc-123', } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .possessionType("none") .clientRequestId("client-abc-123") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PossessionType = "none", ClientRequestID = "client-abc-123", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (including `possession_required` when a possession check is still required) to drive your UI and next server calls. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If `success` returned `possession_required`, your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you send a different phone number to /unify than the one registered to the Prove key, the customer receives `possession_required` on the /unify-status call. Call /unify-bind to rebind the Prove Key to the new number. Once it's rebound, the first number responds with `possession_required`. The Prove key only supports one phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Jesse Mashro on mobile. This user fails the Unified Authentication flow using Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Jesse Mashro. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth and OTP without prompting. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer OTP and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer possession check and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. # Implement Prove Possession Using The Android SDK Source: https://developer.prove.com/how-to/implement-prove-possession-android-sdk Learn how to implement Prove possession for authentication using the Android SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Android SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on Android**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: OtpStartStep, otpFinish: OtpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the interfaces, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it. Typical signs include a validation or reported error such as code 10001 with a message that the PIN doesn't match. You may also see flow activity related to AuthFinishStep, such as ProveAuth.builder().withAuthFinishStep(...), that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep.execute to pass the error in otpException. The SDK invokes OtpFinishStep.execute when a retry or error UI is needed. Your app should implement the required step interfaces, but the Prove client SDK orchestrates when each step runs. Don't duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with an **`OtpStartInput`** instance that reflects “number already known”—typically an empty string or **`null`** for the phone value, as in the snippet—so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, ProveAuthException otpException, OtpStartStepCallback callback) { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } ``` Call **`OtpStartStepCallback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, call **`OtpFinishStepCallback.onSuccess(OtpFinishInput)`** with the OTP the customer entered, wrapped in **`OtpFinishInput`**. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` Use this path when the **Android app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with the collected number. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can request another SMS, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class MultipleResendFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForResend("Didn't receive the SMS OTP? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onOtpResend(). otpFinishStepCallback.onOtpResend(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can supply a new number, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PhoneChangeFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForPhoneNumberChange("Didn't receive the SMS OTP? Try a different phone number.")) { // If the end user wants to correct the phone number already in use, or changing to a // different phone number to receive the future SMS OTP, call onMobileNumberChange(), and // the otpStartStep will re-prompt for phone number input from the end user. otpFinishStepCallback.onMobileNumberChange(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep.execute`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for Android is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the Android client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Verified App Links** (recommended) so the SMS redirect opens your app with the full URL string. See [App Links](https://developer.android.com/training/app-links/about) in the Android documentation. When building the authenticator, use **`withInstantLinkFallback(InstantLinkStartStep startStep, @Nullable InstantLinkRetryStep retryStep)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When you have a mobile number, pass it in **`InstantLinkStartInput`** (for example the `mobileNumber` field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(InstantLinkStartInput input)`** so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} InstantLinkStartStep noPromptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (!phoneNumberNeeded) { callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Use this path when the client collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(InstantLinkStartInput input)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptMultiResendRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForResend( "Didn't receive the InstantLink SMS? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptPhoneNumChangeRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForChangePhoneNumber( "Didn't receive the InstantLink SMS? Try a different phone number.")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` After the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your App Link (or equivalent) must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(String redirectUrl)`**. Call **`finishInstantLink`** from the code path that handles the incoming deep link (for example **`onCreate()`** in your App Link activity). Register an activity similar to this (replace the host with yours): ```xml theme={"dark"} ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK throws **`ProveAuthException`** and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your handler activity; **`finishInstantLink`** should run with the full URL and the session should resume without **`ProveAuthException`** from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The iOS SDK Source: https://developer.prove.com/how-to/implement-prove-possession-ios-sdk Learn how to implement Prove possession for authentication using the iOS SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the iOS SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on iOS**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the protocols, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it (for example, a reported error such as code 10001 with a message that the PIN does not match). You may also see flow activity related to AuthFinishStep (the handler you pass to ProveAuth.builder(authFinish:)) that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep to pass the validation error in otpError. The SDK invokes OtpFinishStep.execute(otpError:callback:) when a retry or error UI is needed. Your app should implement the required step protocols, but the Prove client SDK orchestrates when each step runs. Do not duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class OtpStartStepNoPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for SMS OTP, // or to obtain user confirmation for initiating an SMS message. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Call **`callback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, return the OTP the customer entered through your **`OtpFinishStep`** implementation, as in the snippet below. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` Use this path when the **iOS app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`callback.onSuccess(input: otpStartInput)`** with the collected number. ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step: ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class OtpFinishStepMultipleResend: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered in the SMS message. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the OTP value to the SDK func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Communicate to the SDK any issues when host app trys to obtain the OTP value // or when users cancel the OTP flow func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } // Call this method to request a new OTP code for the same mobile number. func sendNewOtp() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onOtpResend() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can supply a new number, for example: ```swift Swift theme={"dark"} class OtpFinishStepWithPhoneChange: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OTP finish view. DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid. self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the collected OTP value to the SDK. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // When callback.onMobileNumberChange() is evoked, OtpStartStep will be re-initiated // so that end-users can enter a different phone number via OtpStartStep. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set") return } callback.onMobileNumberChange() } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for iOS is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the iOS client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Universal Links** (recommended) so the SMS redirect opens your app with the full URL string. See [Supporting universal links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) in the Apple documentation. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep, retryStep: InstantLinkRetryStep?)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When the client supplies a number, pass it in **`InstantLinkStartInput`** (for example the **`phoneNumber`** field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class InstantLinkStartStepNoPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for instant link, // or to obtain user confirmation for initiating an instant link. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Use this path when the **iOS app** collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(input: instantLinkStartInput)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepMultipleResend: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend to the same phone number or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish) or request resend to the same phone number. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request a new instant link to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepPhoneChange: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend, change phone number, or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish), request resend, or request phone number change. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request resend to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // When this is invoked, InstantLinkStartStep will be re-initiated so that the user // can enter a different phone number. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onMobileNumberChange() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` After you implement the start step above and the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your Universal Link handling must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(redirectUrl:)`**. For how **`finalTargetUrl`** fits into server-side **Start** with Mobile Auth and Instant Link fallback, see [Prove Pre-Fill implementation guide](/how-to/prove-pre-fill-implementation-guide). Call **`finishInstantLink`** from the entry point that receives the opened URL, for example **`application(_:open:options:)`** or **`application(_:continue:restorationHandler:)`**. ```swift Swift theme={"dark"} /// Finishes the Instant Link authentication flow using the redirect URL from the deep link. /// This is the URL to which the user is redirected after tapping the Instant Link in SMS. finishInstantLink(redirectUrl: redirectUrl) { error in // Handle errors due to invalid format of redirectUrl here } ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK surfaces an error via the **`finishInstantLink`** completion and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your app with the full URL; **`finishInstantLink`** should run and the session should resume without an error from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The Web SDK Source: https://developer.prove.com/how-to/implement-prove-possession-web-sdk Learn how to implement Prove possession for authentication using the Web SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Installation The Prove Platform Web SDK has an unpacked size of 324 KB, and a single dependency: `@prove-identity/mobile-auth`. Install the client-side SDK of your choice by running a command in your terminal, or by using a dependency management tool specific to your project. ```shell NPM theme={"dark"} # Run this command to install the package (ensure you have the latest version). npm install @prove-identity/prove-auth@3.4.2 ``` ```html No Package Manager theme={"dark"} # You can include this file in your web application from jsDelivr (update with the latest version). # You can also download the JavaScript file from https://cdn.jsdelivr.net/npm/@prove-identity/prove-auth@3.4.2/build/bundle/release/prove-auth.js and store it locally. ``` If you're including the file from jsDelivr and using [Content Security Policy headers](https://content-security-policy.com/), ensure you add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. ## Prerequisites * **Backend** — Your server calls [`POST /v3/unify`](/reference/unify-request) and the rest of the Unified Authentication sequence. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web SDK for possession checks and the Prove Key. * **Languages** — TypeScript or JavaScript. For native iOS, see [Unify iOS SDK](/how-to/unify-ios-sdk). ## Flow overview: mobile vs desktop In a **mobile** flow, the mobile phone validates the one-time password (OTP). In a **desktop** flow, Instant Link sends a text message to the mobile phone for verification. In the mobile flow, once OTP validation completes, the `AuthFinishStep` callback finishes. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while the customer opens the link in the text message. When they do, the WebSocket closes and the `AuthFinishStep` callback finishes. ## `Authenticate()` and `authToken` The Web SDK requires an `authToken` for `Authenticate()`. That value comes from your server after it calls [`POST /v3/unify`](/reference/unify-request) (often exposed to the browser through your own start or initialize endpoint). The token is **session-specific** (one flow) and **expires after about 15 minutes**. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. **Example: browser requests `authToken` from your backend** Send possession type and an optional phone number when your flow uses Prove possession to your server. Your server calls Prove and returns the token to the page. ```javascript JavaScript theme={"dark"} async function initialize(phoneNumber, possessionType) { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` ```typescript TypeScript theme={"dark"} async function initialize( phoneNumber: string, possessionType: string ): Promise { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Mobile Auth Implementations Only If your app uses [Content Security Policy headers](https://content-security-policy.com/), you must configure them to allow connections to Prove's authentication services: Sandbox Environment * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` Failure to configure these settings prevents Mobile Auth capability from working correctly in web flows. AT\&T Carrier Support (Pixel Mode) If support for the AT\&T carrier is also required, you must use **Pixel** mode (`withMobileAuthImplementation("pixel")`) with the following Content Security Policy (CSP) configuration: Allowed domain for Sandbox Environment * `https://att-device.uat.proveapis.com:4443` * `https://att-device.uat.proveapis.com` * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Allowed domain for Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` * `https://att-device.proveapis.com:4443` * `https://att-device.proveapis.com` * `https://snap.att.com` * `https://snap.mobile.att.com` * `https://snap.mobile.att.net` Add img-src for Pixel implementation Example img-src for Pixel mode: ```http theme={"dark"} Content-Security-Policy: img-src 'self' https://auth.svcs.verizon.com:22790 https://snap.att.com https://snap.mobile.att.com https://snap.mobile.att.net https://device.uat.proveapis.com:4443 https://device.uat.proveapis.com https://att-device.uat.proveapis.com:4443 https://att-device.uat.proveapis.com http://device.uat.proveapis.com:4443 http://device.uat.proveapis.com https://device.proveapis.com:4443 https://device.proveapis.com https://att-device.proveapis.com:4443 https://att-device.proveapis.com http://device.proveapis.com:4443 http://device.proveapis.com; connect-src self https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com ``` For desktop mode, Prove ignores the Prove Key and runs Instant Link. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ## Configure OTP Use this section to **configure SMS OTP fallback in the Web SDK**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(startStep: OtpStartStep | OtpStartStepFn, finishStep: OtpFinishStep | OtpFinishStepFn)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. Return the phone number from your start step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. The **Prove client SDK orchestrates** when each step runs—do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it and may surface activity related to AuthFinishStep that looks unexpected. This behavior is **expected**. Do **not** add your own extra invocation of the OTP finish step to pass the error in otpError. The SDK runs your OtpFinishStep when retry or error UI is needed. Implement the required step functions, but let the Prove client SDK orchestrate when each step runs. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt for it again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const otpStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Call **`reject('some error message')`** if something prevents sending the SMS or completing the flow (for example the customer cancels or leaves the UI with the back button). In the finish step, return the OTP through **`resolve(result: OtpFinishResult)`** with the **`OnSuccess`** result type and the value wrapped in **`OtpFinishInput`**, as in the snippet below. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Use this path when the **page** collects the phone number in the browser and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpMultipleResendFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can supply a new number, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPhoneChangeFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** stack extra **`OtpFinishStep`** invocations on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Use this section to **configure the Web SDK** so Instant Link SMS can be sent from the browser flow and optional resend or phone-number change behaves as expected. Instant Link for mobile web isn't supported. **Custom or vanity Instant Links** aren't supported. You can't substitute a custom link for the default Instant Link. ### Prerequisites * Integrate on **desktop (or other supported) web**; see the callout above for mobile web. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep | InstantLinkStartStepFn, retryStep?: InstantLinkRetryStep | InstantLinkRetryStepFn)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). Return the phone number from your step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkNoPromptStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Use this path when the **page** collects the number in the browser and you **do not** need resend or phone-number change. For resend or change, use the other tabs. Call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. Call **`reject('some error message')`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(0); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkMultipleResendRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(InstantLinkResultType.OnResend); }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(1); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkPhoneChangeRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(InstantLinkResultType.OnMobileNumberChange); }); }, }; ``` In Sandbox, run through each path you ship (default, prompt, and any retry flows). Confirm the SMS sends, the customer can complete the link within the timeout window, and **`resolve`** / **`reject`** match the UX you expect when the customer cancels or retries. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while waiting for the customer to select the link in the text message. Once clicked, the WebSocket closes and the `AuthFinishStep` function finishes. If your UI already has `flowType` from the server (`mobile` or `desktop`), you can configure the builder from that value instead of `isMobileWeb()`: ```javascript JavaScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` ```typescript TypeScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. If the user cancels the client flow, your backend should still call `UnifyStatus()`; you may receive `success=false`. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). **Prove Key persistence enhancement** Using Prove Key in web applications can be affected by strict browser privacy policies aimed at preventing cross-site tracking. For example, Safari completely disables third-party cookies and limits the lifetime of all script-writable website data. Prove Auth uses script-writable website data (localStorage and IndexedDB) for storing Prove Auth `deviceId`, crypto key material, and other critical metadata for subsequent device re-authentication. If the browser deletes this script-writable data, Prove Auth is no longer able to recognize the device and perform authentication. To mitigate this limitation and avoid repeating the device registration process, follow these steps to enable **Key Persistence**. The Key Persistence feature is not enabled by default. Contact Prove to enable this add-on feature. **Web app setup** Follow these steps to add Prove Key persistence in your web app: Install the Key Persistence integration module and add activation code to your app: ```shell NPM theme={"dark"} npm install @prove-identity/prove-auth-device-context ``` Then activate it in your app: ```javascript JavaScript theme={"dark"} import * as dcMod from "@prove-identity/prove-auth-device-context"; dcMod.activate(); ``` ```html No Package Manager theme={"dark"} ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow access to the following common resources: * `script-src https://fpnpmcdn.net https://*.prove-auth.proveapis.com`, * `connect-src https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com` * `worker-src blob:` Additionally, if you're including the file from jsDelivr, add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. When configuring `authenticatorBuilder` for any flow, add the `withDeviceContext()` method to enable Key Persistence data collection: ```javascript JavaScript theme={"dark"} // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` ```typescript TypeScript theme={"dark"} import { BuildConfig, DeviceContextOptions } from "@prove-identity/prove-auth"; // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey: string = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options: DeviceContextOptions = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` Call `authenticatorBuilder.build()` immediately after the web app loads. This enables the Web SDK to load components for Key Persistence collection and collect signals before authentication begins, which helps decrease latency. Ensure cookies are enabled when you call `AuthStart` and `AuthFinish`: ```javascript Axios theme={"dark"} // Enable cookies for AuthStart axios.post(backendUrl + '/authStart', data, { withCredentials: true }); // Enable cookies for AuthFinish axios.post(backendUrl + '/authFinish', data, { withCredentials: true }); ``` ```javascript Fetch theme={"dark"} // Enable cookies for AuthStart fetch(backendUrl + '/authStart', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); // Enable cookies for AuthFinish fetch(backendUrl + '/authFinish', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` **Handling multiple device registrations** If a user registers their device many times before using Key Persistence, Prove Auth may have many registration candidates matching the same visitor ID. To improve recovery accuracy: 1. Store the `deviceId` returned from successful authentications in your backend (database or web cookie) 2. Pass the stored `deviceId` with the `/unify` request to specify which registration to restore ```go Go theme={"dark"} // Send the Unify request with deviceId for registration recovery rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", DeviceId: provesdkservergo.String("stored-device-id-from-previous-auth"), }) ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', deviceId: 'stored-device-id-from-previous-auth', } const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); ``` ```java Java theme={"dark"} V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .deviceId("stored-device-id-from-previous-auth") .build(); ``` Continue the flow using the response from [`POST /v3/unify`](/reference/unify-request). Return updated session or auth details to the client as needed. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy) when present. If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. #### Mobile vs desktop When a tab includes both flows, **follow the steps below on mobile first.** On **desktop**, keep the same **Prompt customer** and **Verify mobile number** timing; only these parts change: * **Initiate Start Request** — The front end also sends **final target URL** with the phone number and possession type before `/unify`. * **Send Auth Token to the Front End** — The client completes **Instant Link** instead of OTP or Mobile Auth. Example of the Instant Link screen in the Prove Unified Authentication sandbox testing flow * **Verify Mobile Number** — On **success**, expect `proveId` and `phoneNumber` only. On **failure**, expect the same `success=false` and `phoneNumber` behavior as on mobile. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. **Mobile:** Test passive authentication with customer-supplied possession fallback; Mobile Auth passes and [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. On mobile, Mobile Auth fails and OTP succeeds (`1234`); [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Use this flow to test Prove Key revocation. Run a successful flow for [Penny](#penny). Copy the `deviceId` from the /unify-status response. Call the /device/revoke endpoint with the device ID to revoke the Prove Key tied to that user. ```batch cURL Request theme={"dark"} curl --request POST \ --url https://platform.uat.proveapis.com/v3/device/revoke \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "clientRequestId": "71010d88-d0e7-4a24-9297-d1be6fefde81", "deviceId": "" } ' ``` ```json JSON Response theme={"dark"} { "success": true } ``` If you send Penny through Unified Authentication again, attempting to authenticate just using the Prove Key, the endpoint returns ```json Error Message theme={"dark"} { "code": 8019, "message": "device has been revoked" } ``` You must complete a new authentication flow to reestablish the Prove Key. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Customer Possession With Force Bind Source: https://developer.prove.com/how-to/pre-fill-business-customer-possession Learn how to implement customer-supplied possession with force bind for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps This authentication type only applies to mobile channels. Send a request to your back end server with the phone number and `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional fields (for example `rebind`, `deviceId`, `proveId`) and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "none", }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'none' } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("none") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "none", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Initialize the client-side SDK to place a Prove Key on the device or to check whether a Prove Key is bound. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` In the `AuthFinishStep` of the client SDK, have the function make a call to an endpoint on your back end server. Your backend server should then call the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (for example `possession_required`) to decide whether to run the possession step and [`POST /v3/unify-bind`](/reference/unify-bind-request). See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). Your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration using. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004026 | Bonnie | Sidon | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger using customer-supplied possession. This user passes the entire Unified Authentication flow using the customer's possession and return `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. They fail out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. Fail the user out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required`, reminding you to perform your own possession check. Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. # Implement Prove Passive Authentication With Customer-Supplied Possession Fallback Source: https://developer.prove.com/how-to/pre-fill-business-prove-auth-then-customer Learn how to implement Prove Mobile Auth with customer-supplied possession fallback for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. This section only applies to mobile channels. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Send a request to your back end server with `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional identifiers such as `clientCustomerId` and `clientRequestId` and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PossessionType: "none", ClientRequestID: provesdkservergo.String("client-abc-123"), }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { possessionType: 'none', clientRequestId: 'client-abc-123', } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .possessionType("none") .clientRequestId("client-abc-123") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PossessionType = "none", ClientRequestID = "client-abc-123", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (including `possession_required` when a possession check is still required) to drive your UI and next server calls. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If `success` returned `possession_required`, your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you send a different phone number to /unify than the one registered to the Prove key, the customer receives `possession_required` on the /unify-status call. Call /unify-bind to rebind the Prove Key to the new number. Once it's rebound, the first number responds with `possession_required`. The Prove key only supports one phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Jesse Mashro on mobile. This user fails the Unified Authentication flow using Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Jesse Mashro. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth and OTP without prompting. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer OTP and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer possession check and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. # Implement Prove Possession Using The Android SDK Source: https://developer.prove.com/how-to/pre-fill-business-prove-possession-android-sdk Learn how to implement Prove possession for authentication using the Android SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Android SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on Android**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: OtpStartStep, otpFinish: OtpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the interfaces, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it. Typical signs include a validation or reported error such as code 10001 with a message that the PIN doesn't match. You may also see flow activity related to AuthFinishStep, such as ProveAuth.builder().withAuthFinishStep(...), that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep.execute to pass the error in otpException. The SDK invokes OtpFinishStep.execute when a retry or error UI is needed. Your app should implement the required step interfaces, but the Prove client SDK orchestrates when each step runs. Don't duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with an **`OtpStartInput`** instance that reflects “number already known”—typically an empty string or **`null`** for the phone value, as in the snippet—so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, ProveAuthException otpException, OtpStartStepCallback callback) { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } ``` Call **`OtpStartStepCallback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, call **`OtpFinishStepCallback.onSuccess(OtpFinishInput)`** with the OTP the customer entered, wrapped in **`OtpFinishInput`**. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` Use this path when the **Android app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with the collected number. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can request another SMS, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class MultipleResendFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForResend("Didn't receive the SMS OTP? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onOtpResend(). otpFinishStepCallback.onOtpResend(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can supply a new number, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PhoneChangeFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForPhoneNumberChange("Didn't receive the SMS OTP? Try a different phone number.")) { // If the end user wants to correct the phone number already in use, or changing to a // different phone number to receive the future SMS OTP, call onMobileNumberChange(), and // the otpStartStep will re-prompt for phone number input from the end user. otpFinishStepCallback.onMobileNumberChange(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep.execute`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for Android is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the Android client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Verified App Links** (recommended) so the SMS redirect opens your app with the full URL string. See [App Links](https://developer.android.com/training/app-links/about) in the Android documentation. When building the authenticator, use **`withInstantLinkFallback(InstantLinkStartStep startStep, @Nullable InstantLinkRetryStep retryStep)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When you have a mobile number, pass it in **`InstantLinkStartInput`** (for example the `mobileNumber` field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(InstantLinkStartInput input)`** so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} InstantLinkStartStep noPromptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (!phoneNumberNeeded) { callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Use this path when the client collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(InstantLinkStartInput input)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptMultiResendRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForResend( "Didn't receive the InstantLink SMS? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptPhoneNumChangeRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForChangePhoneNumber( "Didn't receive the InstantLink SMS? Try a different phone number.")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` After the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your App Link (or equivalent) must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(String redirectUrl)`**. Call **`finishInstantLink`** from the code path that handles the incoming deep link (for example **`onCreate()`** in your App Link activity). Register an activity similar to this (replace the host with yours): ```xml theme={"dark"} ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK throws **`ProveAuthException`** and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your handler activity; **`finishInstantLink`** should run with the full URL and the session should resume without **`ProveAuthException`** from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The iOS SDK Source: https://developer.prove.com/how-to/pre-fill-business-prove-possession-ios-sdk Learn how to implement Prove possession for authentication using the iOS SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the iOS SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on iOS**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the protocols, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it (for example, a reported error such as code 10001 with a message that the PIN does not match). You may also see flow activity related to AuthFinishStep (the handler you pass to ProveAuth.builder(authFinish:)) that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep to pass the validation error in otpError. The SDK invokes OtpFinishStep.execute(otpError:callback:) when a retry or error UI is needed. Your app should implement the required step protocols, but the Prove client SDK orchestrates when each step runs. Do not duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class OtpStartStepNoPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for SMS OTP, // or to obtain user confirmation for initiating an SMS message. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Call **`callback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, return the OTP the customer entered through your **`OtpFinishStep`** implementation, as in the snippet below. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` Use this path when the **iOS app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`callback.onSuccess(input: otpStartInput)`** with the collected number. ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step: ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class OtpFinishStepMultipleResend: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered in the SMS message. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the OTP value to the SDK func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Communicate to the SDK any issues when host app trys to obtain the OTP value // or when users cancel the OTP flow func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } // Call this method to request a new OTP code for the same mobile number. func sendNewOtp() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onOtpResend() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can supply a new number, for example: ```swift Swift theme={"dark"} class OtpFinishStepWithPhoneChange: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OTP finish view. DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid. self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the collected OTP value to the SDK. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // When callback.onMobileNumberChange() is evoked, OtpStartStep will be re-initiated // so that end-users can enter a different phone number via OtpStartStep. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set") return } callback.onMobileNumberChange() } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for iOS is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the iOS client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Universal Links** (recommended) so the SMS redirect opens your app with the full URL string. See [Supporting universal links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) in the Apple documentation. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep, retryStep: InstantLinkRetryStep?)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When the client supplies a number, pass it in **`InstantLinkStartInput`** (for example the **`phoneNumber`** field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class InstantLinkStartStepNoPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for instant link, // or to obtain user confirmation for initiating an instant link. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Use this path when the **iOS app** collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(input: instantLinkStartInput)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepMultipleResend: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend to the same phone number or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish) or request resend to the same phone number. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request a new instant link to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepPhoneChange: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend, change phone number, or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish), request resend, or request phone number change. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request resend to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // When this is invoked, InstantLinkStartStep will be re-initiated so that the user // can enter a different phone number. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onMobileNumberChange() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` After you implement the start step above and the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your Universal Link handling must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(redirectUrl:)`**. For how **`finalTargetUrl`** fits into server-side **Start** with Mobile Auth and Instant Link fallback, see [Prove Pre-Fill implementation guide](/how-to/prove-pre-fill-implementation-guide). Call **`finishInstantLink`** from the entry point that receives the opened URL, for example **`application(_:open:options:)`** or **`application(_:continue:restorationHandler:)`**. ```swift Swift theme={"dark"} /// Finishes the Instant Link authentication flow using the redirect URL from the deep link. /// This is the URL to which the user is redirected after tapping the Instant Link in SMS. finishInstantLink(redirectUrl: redirectUrl) { error in // Handle errors due to invalid format of redirectUrl here } ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK surfaces an error via the **`finishInstantLink`** completion and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your app with the full URL; **`finishInstantLink`** should run and the session should resume without an error from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The Web SDK Source: https://developer.prove.com/how-to/pre-fill-business-prove-possession-web-sdk Learn how to implement Prove possession for authentication using the Web SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Installation The Prove Platform Web SDK has an unpacked size of 324 KB, and a single dependency: `@prove-identity/mobile-auth`. Install the client-side SDK of your choice by running a command in your terminal, or by using a dependency management tool specific to your project. ```shell NPM theme={"dark"} # Run this command to install the package (ensure you have the latest version). npm install @prove-identity/prove-auth@3.4.2 ``` ```html No Package Manager theme={"dark"} # You can include this file in your web application from jsDelivr (update with the latest version). # You can also download the JavaScript file from https://cdn.jsdelivr.net/npm/@prove-identity/prove-auth@3.4.2/build/bundle/release/prove-auth.js and store it locally. ``` If you're including the file from jsDelivr and using [Content Security Policy headers](https://content-security-policy.com/), ensure you add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. ## Prerequisites * **Backend** — Your server calls [`POST /v3/unify`](/reference/unify-request) and the rest of the Unified Authentication sequence. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web SDK for possession checks and the Prove Key. * **Languages** — TypeScript or JavaScript. For native iOS, see [Unify iOS SDK](/how-to/unify-ios-sdk). ## Flow overview: mobile vs desktop In a **mobile** flow, the mobile phone validates the one-time password (OTP). In a **desktop** flow, Instant Link sends a text message to the mobile phone for verification. In the mobile flow, once OTP validation completes, the `AuthFinishStep` callback finishes. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while the customer opens the link in the text message. When they do, the WebSocket closes and the `AuthFinishStep` callback finishes. ## `Authenticate()` and `authToken` The Web SDK requires an `authToken` for `Authenticate()`. That value comes from your server after it calls [`POST /v3/unify`](/reference/unify-request) (often exposed to the browser through your own start or initialize endpoint). The token is **session-specific** (one flow) and **expires after about 15 minutes**. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. **Example: browser requests `authToken` from your backend** Send possession type and an optional phone number when your flow uses Prove possession to your server. Your server calls Prove and returns the token to the page. ```javascript JavaScript theme={"dark"} async function initialize(phoneNumber, possessionType) { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` ```typescript TypeScript theme={"dark"} async function initialize( phoneNumber: string, possessionType: string ): Promise { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Mobile Auth Implementations Only If your app uses [Content Security Policy headers](https://content-security-policy.com/), you must configure them to allow connections to Prove's authentication services: Sandbox Environment * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` Failure to configure these settings prevents Mobile Auth capability from working correctly in web flows. AT\&T Carrier Support (Pixel Mode) If support for the AT\&T carrier is also required, you must use **Pixel** mode (`withMobileAuthImplementation("pixel")`) with the following Content Security Policy (CSP) configuration: Allowed domain for Sandbox Environment * `https://att-device.uat.proveapis.com:4443` * `https://att-device.uat.proveapis.com` * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Allowed domain for Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` * `https://att-device.proveapis.com:4443` * `https://att-device.proveapis.com` * `https://snap.att.com` * `https://snap.mobile.att.com` * `https://snap.mobile.att.net` Add img-src for Pixel implementation Example img-src for Pixel mode: ```http theme={"dark"} Content-Security-Policy: img-src 'self' https://auth.svcs.verizon.com:22790 https://snap.att.com https://snap.mobile.att.com https://snap.mobile.att.net https://device.uat.proveapis.com:4443 https://device.uat.proveapis.com https://att-device.uat.proveapis.com:4443 https://att-device.uat.proveapis.com http://device.uat.proveapis.com:4443 http://device.uat.proveapis.com https://device.proveapis.com:4443 https://device.proveapis.com https://att-device.proveapis.com:4443 https://att-device.proveapis.com http://device.proveapis.com:4443 http://device.proveapis.com; connect-src self https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com ``` For desktop mode, Prove ignores the Prove Key and runs Instant Link. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ## Configure OTP Use this section to **configure SMS OTP fallback in the Web SDK**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(startStep: OtpStartStep | OtpStartStepFn, finishStep: OtpFinishStep | OtpFinishStepFn)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. Return the phone number from your start step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. The **Prove client SDK orchestrates** when each step runs—do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it and may surface activity related to AuthFinishStep that looks unexpected. This behavior is **expected**. Do **not** add your own extra invocation of the OTP finish step to pass the error in otpError. The SDK runs your OtpFinishStep when retry or error UI is needed. Implement the required step functions, but let the Prove client SDK orchestrate when each step runs. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt for it again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const otpStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Call **`reject('some error message')`** if something prevents sending the SMS or completing the flow (for example the customer cancels or leaves the UI with the back button). In the finish step, return the OTP through **`resolve(result: OtpFinishResult)`** with the **`OnSuccess`** result type and the value wrapped in **`OtpFinishInput`**, as in the snippet below. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Use this path when the **page** collects the phone number in the browser and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpMultipleResendFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can supply a new number, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPhoneChangeFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** stack extra **`OtpFinishStep`** invocations on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Use this section to **configure the Web SDK** so Instant Link SMS can be sent from the browser flow and optional resend or phone-number change behaves as expected. Instant Link for mobile web isn't supported. **Custom or vanity Instant Links** aren't supported. You can't substitute a custom link for the default Instant Link. ### Prerequisites * Integrate on **desktop (or other supported) web**; see the callout above for mobile web. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep | InstantLinkStartStepFn, retryStep?: InstantLinkRetryStep | InstantLinkRetryStepFn)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). Return the phone number from your step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkNoPromptStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Use this path when the **page** collects the number in the browser and you **do not** need resend or phone-number change. For resend or change, use the other tabs. Call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. Call **`reject('some error message')`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(0); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkMultipleResendRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(InstantLinkResultType.OnResend); }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(1); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkPhoneChangeRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(InstantLinkResultType.OnMobileNumberChange); }); }, }; ``` In Sandbox, run through each path you ship (default, prompt, and any retry flows). Confirm the SMS sends, the customer can complete the link within the timeout window, and **`resolve`** / **`reject`** match the UX you expect when the customer cancels or retries. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while waiting for the customer to select the link in the text message. Once clicked, the WebSocket closes and the `AuthFinishStep` function finishes. If your UI already has `flowType` from the server (`mobile` or `desktop`), you can configure the builder from that value instead of `isMobileWeb()`: ```javascript JavaScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` ```typescript TypeScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. If the user cancels the client flow, your backend should still call `UnifyStatus()`; you may receive `success=false`. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). **Prove Key persistence enhancement** Using Prove Key in web applications can be affected by strict browser privacy policies aimed at preventing cross-site tracking. For example, Safari completely disables third-party cookies and limits the lifetime of all script-writable website data. Prove Auth uses script-writable website data (localStorage and IndexedDB) for storing Prove Auth `deviceId`, crypto key material, and other critical metadata for subsequent device re-authentication. If the browser deletes this script-writable data, Prove Auth is no longer able to recognize the device and perform authentication. To mitigate this limitation and avoid repeating the device registration process, follow these steps to enable **Key Persistence**. The Key Persistence feature is not enabled by default. Contact Prove to enable this add-on feature. **Web app setup** Follow these steps to add Prove Key persistence in your web app: Install the Key Persistence integration module and add activation code to your app: ```shell NPM theme={"dark"} npm install @prove-identity/prove-auth-device-context ``` Then activate it in your app: ```javascript JavaScript theme={"dark"} import * as dcMod from "@prove-identity/prove-auth-device-context"; dcMod.activate(); ``` ```html No Package Manager theme={"dark"} ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow access to the following common resources: * `script-src https://fpnpmcdn.net https://*.prove-auth.proveapis.com`, * `connect-src https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com` * `worker-src blob:` Additionally, if you're including the file from jsDelivr, add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. When configuring `authenticatorBuilder` for any flow, add the `withDeviceContext()` method to enable Key Persistence data collection: ```javascript JavaScript theme={"dark"} // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` ```typescript TypeScript theme={"dark"} import { BuildConfig, DeviceContextOptions } from "@prove-identity/prove-auth"; // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey: string = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options: DeviceContextOptions = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` Call `authenticatorBuilder.build()` immediately after the web app loads. This enables the Web SDK to load components for Key Persistence collection and collect signals before authentication begins, which helps decrease latency. Ensure cookies are enabled when you call `AuthStart` and `AuthFinish`: ```javascript Axios theme={"dark"} // Enable cookies for AuthStart axios.post(backendUrl + '/authStart', data, { withCredentials: true }); // Enable cookies for AuthFinish axios.post(backendUrl + '/authFinish', data, { withCredentials: true }); ``` ```javascript Fetch theme={"dark"} // Enable cookies for AuthStart fetch(backendUrl + '/authStart', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); // Enable cookies for AuthFinish fetch(backendUrl + '/authFinish', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` **Handling multiple device registrations** If a user registers their device many times before using Key Persistence, Prove Auth may have many registration candidates matching the same visitor ID. To improve recovery accuracy: 1. Store the `deviceId` returned from successful authentications in your backend (database or web cookie) 2. Pass the stored `deviceId` with the `/unify` request to specify which registration to restore ```go Go theme={"dark"} // Send the Unify request with deviceId for registration recovery rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", DeviceId: provesdkservergo.String("stored-device-id-from-previous-auth"), }) ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', deviceId: 'stored-device-id-from-previous-auth', } const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); ``` ```java Java theme={"dark"} V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .deviceId("stored-device-id-from-previous-auth") .build(); ``` Continue the flow using the response from [`POST /v3/unify`](/reference/unify-request). Return updated session or auth details to the client as needed. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy) when present. If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. #### Mobile vs desktop When a tab includes both flows, **follow the steps below on mobile first.** On **desktop**, keep the same **Prompt customer** and **Verify mobile number** timing; only these parts change: * **Initiate Start Request** — The front end also sends **final target URL** with the phone number and possession type before `/unify`. * **Send Auth Token to the Front End** — The client completes **Instant Link** instead of OTP or Mobile Auth. Example of the Instant Link screen in the Prove Unified Authentication sandbox testing flow * **Verify Mobile Number** — On **success**, expect `proveId` and `phoneNumber` only. On **failure**, expect the same `success=false` and `phoneNumber` behavior as on mobile. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. **Mobile:** Test passive authentication with customer-supplied possession fallback; Mobile Auth passes and [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. On mobile, Mobile Auth fails and OTP succeeds (`1234`); [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Use this flow to test Prove Key revocation. Run a successful flow for [Penny](#penny). Copy the `deviceId` from the /unify-status response. Call the /device/revoke endpoint with the device ID to revoke the Prove Key tied to that user. ```batch cURL Request theme={"dark"} curl --request POST \ --url https://platform.uat.proveapis.com/v3/device/revoke \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "clientRequestId": "71010d88-d0e7-4a24-9297-d1be6fefde81", "deviceId": "" } ' ``` ```json JSON Response theme={"dark"} { "success": true } ``` If you send Penny through Unified Authentication again, attempting to authenticate just using the Prove Key, the endpoint returns ```json Error Message theme={"dark"} { "code": 8019, "message": "device has been revoked" } ``` You must complete a new authentication flow to reestablish the Prove Key. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Customer Possession With Force Bind Source: https://developer.prove.com/how-to/pre-fill-customer-possession Learn how to implement customer-supplied possession with force bind for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps This authentication type only applies to mobile channels. Send a request to your back end server with the phone number and `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional fields (for example `rebind`, `deviceId`, `proveId`) and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "none", }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'none' } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("none") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "none", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Initialize the client-side SDK to place a Prove Key on the device or to check whether a Prove Key is bound. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for the mobile flow. let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)); const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` In the `AuthFinishStep` of the client SDK, have the function make a call to an endpoint on your back end server. Your backend server should then call the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (for example `possession_required`) to decide whether to run the possession step and [`POST /v3/unify-bind`](/reference/unify-bind-request). See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). Your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration using. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004026 | Bonnie | Sidon | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger using customer-supplied possession. This user passes the entire Unified Authentication flow using the customer's possession and return `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. They fail out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=possession_required` (Prove is not performing the possession check). See [`POST /v3/unify-status`](/reference/unify-status-request). Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon using customer-supplied possession. Treat this user as a fail case and assume they fail your possession check. This user receives `success=possession_required` on `/unify-status`. You then proceed to run your possession. Fail the user out of the flow. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required`, reminding you to perform your own possession check. Perform your own possession check outside of Prove's system, simulating a failure and ending the flow. # Pre-Fill for Business Verify Source: https://developer.prove.com/how-to/pre-fill-for-business-verify Learn how to implement Prove Pre-Fill for Business using the Verify API to verify the phone session and return trusted identity and business-oriented attributes. Identities that pass the [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) are automatically enrolled in [Manage](https://developer.prove.com/explanation/account-opening-manage). ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](https://developer.prove.com/reference/authentication)). ## Implementation steps Collect the phone number from your CRM or database. Make a request to the [`/v3/verify endpoint`](https://developer.prove.com/reference/verify) including the Authorization header. Generate a bearer token as outlined on the [Authentication page](https://developer.prove.com/reference/authentication). ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "2005550123", "verificationType": "prefillForBiz", "clientRequestId": "request-123" }' ``` Replace `` with your acquired access token. For **Pre-Fill for Business**, set `verificationType` to `prefillForBiz` and include `phoneNumber` and `clientRequestId`. You can also pass `clientCustomerId`, `clientHumanId`, or `proveId` when you need that linkage. Use the samples below together with the [`/v3/verify`](https://developer.prove.com/reference/verify) response schema. Cross-check field meanings with [Assurance levels](https://developer.prove.com/reference/assurance-levels) and [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) when interpreting codes and evaluations. ```json Success Response theme={"dark"} { "success": "true", "correlationId": "713189b8-5555-4b08-83ba-75d08780aebd", "clientRequestId": "request-123", "phoneNumber": "+12005550123", "proveId": "a07b94ce-218c-461f-beda-d92480e40f61", "provePhoneAlias": "4B2C41FC4VKDEO100F960011D0AD4A8050MEK19P4SF9PD23EFE27CD2C76A6FAA8375E60AC0550604F6G32D9ED60E06262CCC570F3C15F2D16900184E", "isEnrolled": true, "identity": { "firstName": "Jordan", "lastName": "Lee", "dateOfBirth": "1985-07-19", "nationalId": "111-22-6789", "emails": [ "jordan@example.com" ], "addresses": [ { "address": "400 Market St", "extendedAddress": "Suite 12", "city": "San Francisco", "region": "CA", "zipCode": "94105" } ], "assuranceLevel": "AL3", "reasons": ["AL3a"] }, "businesses": [ { "businessName": "Acme Widgets LLC", "businessAddress": { "address": "100 Commerce Way", "extendedAddress": "Suite 200", "city": "Austin", "region": "TX", "zipCode": "78701" }, "tradeName": "Acme Widgets", "taxId": "12-3456789", "relatedPersons": [ { "firstName": "Jane", "lastName": "Doe" } ], "registrationFiling": { "date": "2015-03-01", "registrationNumber": "8021234567", "registrationType": "", "region": "TX" } } ], "evaluation": { "authentication": { "result": "pass" }, "risk": { "result": "pass" } } } ``` ```json Failure Response theme={"dark"} { "success": "false", "correlationId": "", "clientRequestId": "request-123", "phoneNumber": "+12005550123", "identity": { "firstName": "", "lastName": "", "assuranceLevel": "AL-1" }, "evaluation": { "authentication": { "failureReasons": { "9175": "No active identity can be associated with the phone number.", "9177": "No information can be found for the identity or phone number." }, "result": "fail" }, "identification": { "result": "pass" }, "risk": { "result": "pass" } } } ``` ### In practice * **`success`** — Branch your UX and backend logic on pass vs fail. * **`identity`** and **`businesses`** — Read verified attributes and [`assuranceLevel`](https://developer.prove.com/reference/assurance-levels) for policy (step-up, deny, manual review). * **`evaluation`** — Interpret authentication and risk outcomes under [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy); use failure codes when `success` is false. * **IDs** — Persist `correlationId` (and `proveId` when present) if you need support, auditing, or linking to other Prove flows. If you passed optional identifiers on the request, the response may echo `clientCustomerId` and `clientHumanId`; see the [`/v3/verify`](https://developer.prove.com/reference/verify) reference for details. ## Sandbox testing ### Test users You must use project credentials when working with sandbox test users. Attempting to use these test users with different project credentials results in an unauthorized access error. The following test users are available for testing using the `/v3/verify` endpoint in the Sandbox environment. Use these test users to simulate different verification scenarios and outcomes. Use these test phone numbers exactly as shown. The sandbox environment doesn't validate real customer information. | Phone Number | First Name | Last Name | Verification Type | Expected Outcome | | ------------ | ---------- | --------- | ----------------- | ---------------- | | `2001004069` | Brita | Thomassen | `prefillForBiz` | Success | | `2001004070` | Skippie | O'Kerin | `prefillForBiz` | Success | ### Testing steps Use test user Brita Thomassen to verify a successful verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "prefillForBiz", "phoneNumber": "2001004069", "clientRequestId": "test-request-001" }' ``` Expected response: ```json theme={"dark"} { "success": "true", "correlationId": "484965d1-aed3-4f7c-89fb-3a8c1dffea50", "clientRequestId": "test-request-001", "phoneNumber": "2001004069", "provePhoneAlias": "RCM0B10CFCFGGG7CJMD291ND3C7Z666C0DR8V141FUQQY0EBPPUSPD6FBCP5S2KTY0TN4BUWEY54E9GKSZZ8J2H2QVNERY68QHVD202TPBXCRHUF8AMUDRDBE1W5Y184", "identity": { "firstName": "Brita", "lastName": "Thomassen", "dateOfBirth": "1976-12-03", "nationalId": "465825623", "emails": [ "bthomassen1x@taobao.com" ], "addresses": [ { "address": "04568 Kropf Drive", "extendedAddress": "", "city": "Charleston", "region": "WV", "zipCode": "25389" } ], "assuranceLevel": "AL2", "reasons": null }, "businesses": [ { "businessName": "Acme Widgets LLC", "businessAddress": { "address": "100 Commerce Way", "extendedAddress": "Suite 200", "city": "Austin", "region": "TX", "zipCode": "78701" }, "tradeName": "Acme Widgets", "taxId": "12-3456789", "relatedPersons": [ { "firstName": "Jane", "lastName": "Doe" } ], "registrationFiling": { "date": "2015-03-01", "registrationNumber": "8021234567", "region": "TX" } } ], "evaluation": { "authentication": { "result": "pass" }, "identification": { "result": "pass" }, "risk": { "result": "pass" } }, "isEnrolled": false } ``` Use test user Skippie O'Kerin to verify a successful verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "prefillForBiz", "phoneNumber": "2001004070", "clientRequestId": "test-request-001" }' ``` Expected response: ```json theme={"dark"} { "success": "true", "correlationId": "25ebb2df-9151-4e43-ac97-244dff81f322", "clientRequestId": "test-request-001", "phoneNumber": "2001004070", "provePhoneAlias": "F35996M7H0ZKJNTQHV4UGG5D4BUJDC9UTNZHSLYVLDXZ66X1A041Y3N4LZVT98B3PHM0XBB5XRS9HG78Y9XKP6ET9ZBBZ53CF9P5XTTUCYTM1AHJ8P8NDD12K7BMSMCE", "identity": { "firstName": "Skippie", "lastName": "O'Kerin", "dateOfBirth": "1963-08-22", "nationalId": "861785023", "emails": [ "sokerin1y@wikipedia.org" ], "addresses": [ { "address": "9 Petterle Trail", "extendedAddress": "", "city": "Minneapolis", "region": "MN", "zipCode": "55448" } ], "assuranceLevel": "AL3", "reasons": null }, "businesses": [ { "businessName": "Acme Widgets LLC", "businessAddress": { "address": "100 Commerce Way", "extendedAddress": "Suite 200", "city": "Austin", "region": "TX", "zipCode": "78701" }, "tradeName": "Acme Widgets", "taxId": "12-3456789", "relatedPersons": [ { "firstName": "Jane", "lastName": "Doe" } ], "registrationFiling": { "date": "2015-03-01", "registrationNumber": "8021234567", "region": "TX" } } ], "evaluation": { "authentication": { "result": "pass" }, "identification": { "result": "pass" }, "risk": { "result": "pass" } }, "isEnrolled": false } ``` # Pre-Fill for Consumers Verify Source: https://developer.prove.com/how-to/pre-fill-for-consumers-verify How to verify Pre-Fill for Consumers using POST /v3/verify, handle the response, and test in Sandbox. Flowchart: Pre-Fill for Consumers from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success Identities that pass the [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) are automatically enrolled in [Manage](https://developer.prove.com/explanation/account-opening-manage). ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](https://developer.prove.com/reference/authentication)). ## Implementation steps Collect the phone number from your CRM or database. Make a request to the [`/v3/verify endpoint`](https://developer.prove.com/reference/verify) including the Authorization header. Generate a bearer token as outlined on the [Authentication page](https://developer.prove.com/reference/authentication). ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "phoneNumber": "2005550123", "verificationType": "prefill", "clientRequestId": "request-123" }' ``` Replace `` with your acquired access token. For **Pre-Fill for Consumers**, set `verificationType` to `prefill` and include `phoneNumber`. You can also pass `clientRequestId`, `clientCustomerId`, `clientHumanId`, or `proveId` when you need that linkage. Use the samples below together with the [`/v3/verify`](https://developer.prove.com/reference/verify) response schema. Cross-check field meanings with [Assurance levels](https://developer.prove.com/reference/assurance-levels) and [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) when interpreting codes and evaluations. ```json Success Response theme={"dark"} { "success": "true", "correlationId": "713189b8-5555-4b08-83ba-75d08780aebd", "phoneNumber": "12005550123", "proveId": "a07b94ce-218c-461f-beda-d92480e40f61", "provePhoneAlias": "4B2C41FC4VKDEO100F960011D0AD4A8050MEK19P4SF9PD23EFE27CD2C76A6FAA8375E60AC0550604F6G32D9ED60E06262CCC570F3C15F2D16900184E", "isEnrolled": true, "clientRequestId": "request-123", "identity": { "firstName": "Alice", "lastName": "Smith", "dateOfBirth": "1992-03-02", "nationalId": "111-22-6789", "emails": [ "alice@example.com", "a.smith@example.com", "ajohnson@globocorp.com" ], "addresses": [ { "address": "123 Main St", "extendedAddress": "Apt A", "city": "New York", "region": "NY", "zipCode": "10001" }, { "address": "Farm Road 223", "extendedAddress": "", "city": "Fargo", "region": "ND", "zipCode": "58801" } ], "assuranceLevel": "AL3", "reasons": ["AL3a"] }, "evaluation": { "authentication": { "result": "pass" }, "identification": { "result": "pass" }, "risk": { "result": "pass" } } } ``` ```json Failure Response theme={"dark"} { "success": "false", "correlationId": "713189b8-5555-4b08-83ba-75d08780aebd", "phoneNumber": "+12005550123", "clientRequestId": "request-123", "identity": { "assuranceLevel": "AL0", "reasons": ["AL0b"] }, "evaluation": { "authentication": { "failureReasons": { "9175": "No active identity can be associated with the phone number.", "9177": "No information can be found for the identity or phone number." }, "result": "fail" }, "identification": { "result": "pass" }, "risk": { "result": "pass" } } } ``` ### In practice * **`success`** — Branch your UX and backend logic on pass vs fail. * **`identity`** — Use returned attributes for pre-fill where appropriate; read [`assuranceLevel`](/reference/assurance-levels) and `reasons` for policy (allow, step-up, deny). * **`evaluation`** — Interpret **authentication**, **identification**, and **risk** under [Global Fraud Policy](/reference/global-fraud-policy); use failure codes when `success` is false. * **IDs** — Persist `correlationId` (and `proveId` when present) if you need support, auditing, or linking to other Prove flows. If you passed optional identifiers on the request, the response may echo `clientCustomerId` and `clientHumanId`; see [`/v3/verify`](/reference/verify) for details. ## Sandbox testing ### Test users You must use project credentials when working with sandbox test users. Attempting to use these test users with different project credentials results in an unauthorized access error. The following test users are available for testing using the `/v3/verify` endpoint in the Sandbox environment. Use these test users to simulate different verification scenarios and outcomes. Use these test phone numbers exactly as shown. The sandbox environment doesn't validate real customer information. | Phone Number | First Name | Last Name | Verification Type | Expected Outcome | | ------------ | ---------- | --------- | ----------------- | ---------------- | | `2001004065` | Mead | Willment | `prefill` | Success | | `2001004067` | Roxi | Mannering | `prefill` | Success | ### Testing steps Use test user Mead Willment to verify a successful verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "prefill", "phoneNumber": "2001004065", "clientRequestId": "test-request-001" }' ``` Expected response: ```json theme={"dark"} { "success": "true", "correlationId": "7c7f84bb-6b1d-4a7a-8570-8b14e3aed3d6", "clientRequestId": "test-request-001", "phoneNumber": "2001004065", "proveId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "provePhoneAlias": "PSDMF50KQKCE882PARQH4RFPA1SAF31JNJDQM9J1Y61STZ4CQPB4EENRW2QN346BSXTSESXNDZ7S7GZ50NJSQRAEE7394RR1G4Q7EDB9SREK3T9KM4LZHX0PUB30H9FE", "identity": { "firstName": "Mead", "lastName": "Willment", "dateOfBirth": "1988-11-28", "emails": [ "mwillment1t@digg.com" ], "assuranceLevel": "AL3", "reasons": null }, "evaluation": { "authentication": { "result": "pass" }, "identification": { "result": "pass" }, "risk": { "result": "pass" } }, "isEnrolled": true } ``` Use test user Roxi Mannering to verify a successful verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "prefill", "phoneNumber": "2001004067", "clientRequestId": "test-request-001" }' ``` Expected response: ```json theme={"dark"} { "success": "true", "correlationId": "00a18485-b7d6-48be-98ad-17f085afadf3", "clientRequestId": "test-request-001", "phoneNumber": "2001004067", "proveId": "8e2f91a4-c3b6-4d51-9e72-10f84a39b2e5", "provePhoneAlias": "6L990LDCJEU2DHM3ZRE524TQWX3JRZEB3X5LQY1MDET5JVSZ2X19X6KLBUV2FX31QQW8Q0E4YQV8ZT79YVBBDFHZ7G6SSGEN5K2FQDEZK5AKL0MP05ZJSKBUG1PM2Y4F", "identity": { "firstName": "Roxi", "lastName": "Mannering", "dateOfBirth": "1995-03-12", "emails": [ "rmannering1v@stanford.edu" ], "assuranceLevel": "AL2", "reasons": null }, "evaluation": { "authentication": { "result": "pass" }, "identification": { "result": "pass" }, "risk": { "result": "pass" } }, "isEnrolled": true } ``` # Implement Prove Passive Authentication With Customer-Supplied Possession Fallback Source: https://developer.prove.com/how-to/pre-fill-prove-auth-then-customer Learn how to implement Prove Mobile Auth with customer-supplied possession fallback for authentication **Possession** requires **both** a **client-side SDK** (Web, iOS, or Android) and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) * [`POST /v3/unify-bind`](https://developer.prove.com/reference/unify-bind-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web, Android, or iOS SDK for possession checks and the Prove Key. For the browser flow, see [Identity Web SDK](/how-to/identity-web-sdk). ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. This section only applies to mobile channels. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Send a request to your back end server with `possessionType=none` to start the flow. See [`POST /v3/unify`](/reference/unify-request) for optional identifiers such as `clientCustomerId` and `clientRequestId` and the full response. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PossessionType: "none", ClientRequestID: provesdkservergo.String("client-abc-123"), }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { possessionType: 'none', clientRequestId: 'client-abc-123', } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .possessionType("none") .clientRequestId("client-abc-123") .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PossessionType = "none", ClientRequestID = "client-abc-123", }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Use `success` (including `possession_required` when a possession check is still required) to drive your UI and next server calls. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If `success` returned `possession_required`, your app must perform a customer-supplied possession check such as SMS OTP. Only proceed to the next step if the possession check passes. Call `UnifyBind()` after `UnifyStatus()` returns `success=possession_required`. Call this endpoint if and only if the possession check has passed. This binds the phone number to the Prove Key for future authentications. This function requires `correlationId` and `phoneNumber`. See the [`/v3/unify-bind` reference](https://developer.prove.com/reference/unify-bind-request) for the full schema. You can also send optional fields such as `clientRequestId` for session tracking. ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` Return the binding outcome to the client. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for all response fields. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you send a different phone number to /unify than the one registered to the Prove key, the customer receives `possession_required` on the /unify-status call. Call /unify-bind to rebind the Prove Key to the new number. Once it's rebound, the first number responds with `possession_required`. The Prove key only supports one phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Jesse Mashro on mobile. This user fails the Unified Authentication flow using Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Jesse Mashro. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth and OTP without prompting. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer OTP and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Follow this flow when testing Prove passive authentication with customer-supplied possession fallback. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile with customer-supplied possession fallback. This user fails Mobile Auth but passes after a successful customer possession check and returns `success=true` in the /unify-bind response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The back end then calls the /unify-status endpoint with the correlation ID to validate the phone number. The response provides `success=possession_required` reminding you to perform your own possession check. Perform your own possession check outside of Prove's system. If the consumer fails, end the flow. If the consumer passes, then proceed to Bind Prove Key. The back end calls [`POST /v3/unify-bind`](/reference/unify-bind-request) with the correlation ID. Expect `success=true`, `proveId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-bind`](/reference/unify-bind-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. # Implement Prove Possession Using The Android SDK Source: https://developer.prove.com/how-to/pre-fill-prove-possession-android-sdk Learn how to implement Prove possession for authentication using the Android SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Android SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on Android**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: OtpStartStep, otpFinish: OtpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the interfaces, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it. Typical signs include a validation or reported error such as code 10001 with a message that the PIN doesn't match. You may also see flow activity related to AuthFinishStep, such as ProveAuth.builder().withAuthFinishStep(...), that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep.execute to pass the error in otpException. The SDK invokes OtpFinishStep.execute when a retry or error UI is needed. Your app should implement the required step interfaces, but the Prove client SDK orchestrates when each step runs. Don't duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with an **`OtpStartInput`** instance that reflects “number already known”—typically an empty string or **`null`** for the phone value, as in the snippet—so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, ProveAuthException otpException, OtpStartStepCallback callback) { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } ``` Call **`OtpStartStepCallback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, call **`OtpFinishStepCallback.onSuccess(OtpFinishInput)`** with the OTP the customer entered, wrapped in **`OtpFinishInput`**. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` Use this path when the **Android app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with the collected number. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can request another SMS, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class MultipleResendFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForResend("Didn't receive the SMS OTP? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onOtpResend(). otpFinishStepCallback.onOtpResend(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can supply a new number, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PhoneChangeFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForPhoneNumberChange("Didn't receive the SMS OTP? Try a different phone number.")) { // If the end user wants to correct the phone number already in use, or changing to a // different phone number to receive the future SMS OTP, call onMobileNumberChange(), and // the otpStartStep will re-prompt for phone number input from the end user. otpFinishStepCallback.onMobileNumberChange(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep.execute`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for Android is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the Android client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Verified App Links** (recommended) so the SMS redirect opens your app with the full URL string. See [App Links](https://developer.android.com/training/app-links/about) in the Android documentation. When building the authenticator, use **`withInstantLinkFallback(InstantLinkStartStep startStep, @Nullable InstantLinkRetryStep retryStep)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When you have a mobile number, pass it in **`InstantLinkStartInput`** (for example the `mobileNumber` field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(InstantLinkStartInput input)`** so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} InstantLinkStartStep noPromptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (!phoneNumberNeeded) { callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Use this path when the client collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(InstantLinkStartInput input)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptMultiResendRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForResend( "Didn't receive the InstantLink SMS? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptPhoneNumChangeRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForChangePhoneNumber( "Didn't receive the InstantLink SMS? Try a different phone number.")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` After the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your App Link (or equivalent) must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(String redirectUrl)`**. Call **`finishInstantLink`** from the code path that handles the incoming deep link (for example **`onCreate()`** in your App Link activity). Register an activity similar to this (replace the host with yours): ```xml theme={"dark"} ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK throws **`ProveAuthException`** and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your handler activity; **`finishInstantLink`** should run with the full URL and the session should resume without **`ProveAuthException`** from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The iOS SDK Source: https://developer.prove.com/how-to/pre-fill-prove-possession-ios-sdk Learn how to implement Prove possession for authentication using the iOS SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the iOS SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on iOS**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the protocols, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it (for example, a reported error such as code 10001 with a message that the PIN does not match). You may also see flow activity related to AuthFinishStep (the handler you pass to ProveAuth.builder(authFinish:)) that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep to pass the validation error in otpError. The SDK invokes OtpFinishStep.execute(otpError:callback:) when a retry or error UI is needed. Your app should implement the required step protocols, but the Prove client SDK orchestrates when each step runs. Do not duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class OtpStartStepNoPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for SMS OTP, // or to obtain user confirmation for initiating an SMS message. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Call **`callback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, return the OTP the customer entered through your **`OtpFinishStep`** implementation, as in the snippet below. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` Use this path when the **iOS app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`callback.onSuccess(input: otpStartInput)`** with the collected number. ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step: ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class OtpFinishStepMultipleResend: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered in the SMS message. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the OTP value to the SDK func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Communicate to the SDK any issues when host app trys to obtain the OTP value // or when users cancel the OTP flow func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } // Call this method to request a new OTP code for the same mobile number. func sendNewOtp() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onOtpResend() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can supply a new number, for example: ```swift Swift theme={"dark"} class OtpFinishStepWithPhoneChange: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OTP finish view. DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid. self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the collected OTP value to the SDK. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // When callback.onMobileNumberChange() is evoked, OtpStartStep will be re-initiated // so that end-users can enter a different phone number via OtpStartStep. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set") return } callback.onMobileNumberChange() } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for iOS is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the iOS client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Universal Links** (recommended) so the SMS redirect opens your app with the full URL string. See [Supporting universal links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) in the Apple documentation. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep, retryStep: InstantLinkRetryStep?)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When the client supplies a number, pass it in **`InstantLinkStartInput`** (for example the **`phoneNumber`** field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class InstantLinkStartStepNoPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for instant link, // or to obtain user confirmation for initiating an instant link. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Use this path when the **iOS app** collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(input: instantLinkStartInput)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepMultipleResend: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend to the same phone number or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish) or request resend to the same phone number. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request a new instant link to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepPhoneChange: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend, change phone number, or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish), request resend, or request phone number change. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request resend to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // When this is invoked, InstantLinkStartStep will be re-initiated so that the user // can enter a different phone number. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onMobileNumberChange() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` After you implement the start step above and the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your Universal Link handling must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(redirectUrl:)`**. For how **`finalTargetUrl`** fits into server-side **Start** with Mobile Auth and Instant Link fallback, see [Prove Pre-Fill implementation guide](/how-to/prove-pre-fill-implementation-guide). Call **`finishInstantLink`** from the entry point that receives the opened URL, for example **`application(_:open:options:)`** or **`application(_:continue:restorationHandler:)`**. ```swift Swift theme={"dark"} /// Finishes the Instant Link authentication flow using the redirect URL from the deep link. /// This is the URL to which the user is redirected after tapping the Instant Link in SMS. finishInstantLink(redirectUrl: redirectUrl) { error in // Handle errors due to invalid format of redirectUrl here } ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK surfaces an error via the **`finishInstantLink`** completion and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your app with the full URL; **`finishInstantLink`** should run and the session should resume without an error from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement Prove Possession Using The Web SDK Source: https://developer.prove.com/how-to/pre-fill-prove-possession-web-sdk Learn how to implement Prove possession for authentication using the Web SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Installation The Prove Platform Web SDK has an unpacked size of 324 KB, and a single dependency: `@prove-identity/mobile-auth`. Install the client-side SDK of your choice by running a command in your terminal, or by using a dependency management tool specific to your project. ```shell NPM theme={"dark"} # Run this command to install the package (ensure you have the latest version). npm install @prove-identity/prove-auth@3.4.2 ``` ```html No Package Manager theme={"dark"} # You can include this file in your web application from jsDelivr (update with the latest version). # You can also download the JavaScript file from https://cdn.jsdelivr.net/npm/@prove-identity/prove-auth@3.4.2/build/bundle/release/prove-auth.js and store it locally. ``` If you're including the file from jsDelivr and using [Content Security Policy headers](https://content-security-policy.com/), ensure you add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. ## Prerequisites * **Backend** — Your server calls [`POST /v3/unify`](/reference/unify-request) and the rest of the Unified Authentication sequence. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web SDK for possession checks and the Prove Key. * **Languages** — TypeScript or JavaScript. For native iOS, see [Unify iOS SDK](/how-to/unify-ios-sdk). ## Flow overview: mobile vs desktop In a **mobile** flow, the mobile phone validates the one-time password (OTP). In a **desktop** flow, Instant Link sends a text message to the mobile phone for verification. In the mobile flow, once OTP validation completes, the `AuthFinishStep` callback finishes. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while the customer opens the link in the text message. When they do, the WebSocket closes and the `AuthFinishStep` callback finishes. ## `Authenticate()` and `authToken` The Web SDK requires an `authToken` for `Authenticate()`. That value comes from your server after it calls [`POST /v3/unify`](/reference/unify-request) (often exposed to the browser through your own start or initialize endpoint). The token is **session-specific** (one flow) and **expires after about 15 minutes**. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. **Example: browser requests `authToken` from your backend** Send possession type and an optional phone number when your flow uses Prove possession to your server. Your server calls Prove and returns the token to the page. ```javascript JavaScript theme={"dark"} async function initialize(phoneNumber, possessionType) { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` ```typescript TypeScript theme={"dark"} async function initialize( phoneNumber: string, possessionType: string ): Promise { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Mobile Auth Implementations Only If your app uses [Content Security Policy headers](https://content-security-policy.com/), you must configure them to allow connections to Prove's authentication services: Sandbox Environment * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` Failure to configure these settings prevents Mobile Auth capability from working correctly in web flows. AT\&T Carrier Support (Pixel Mode) If support for the AT\&T carrier is also required, you must use **Pixel** mode (`withMobileAuthImplementation("pixel")`) with the following Content Security Policy (CSP) configuration: Allowed domain for Sandbox Environment * `https://att-device.uat.proveapis.com:4443` * `https://att-device.uat.proveapis.com` * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Allowed domain for Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` * `https://att-device.proveapis.com:4443` * `https://att-device.proveapis.com` * `https://snap.att.com` * `https://snap.mobile.att.com` * `https://snap.mobile.att.net` Add img-src for Pixel implementation Example img-src for Pixel mode: ```http theme={"dark"} Content-Security-Policy: img-src 'self' https://auth.svcs.verizon.com:22790 https://snap.att.com https://snap.mobile.att.com https://snap.mobile.att.net https://device.uat.proveapis.com:4443 https://device.uat.proveapis.com https://att-device.uat.proveapis.com:4443 https://att-device.uat.proveapis.com http://device.uat.proveapis.com:4443 http://device.uat.proveapis.com https://device.proveapis.com:4443 https://device.proveapis.com https://att-device.proveapis.com:4443 https://att-device.proveapis.com http://device.proveapis.com:4443 http://device.proveapis.com; connect-src self https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com ``` For desktop mode, Prove ignores the Prove Key and runs Instant Link. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ## Configure OTP Use this section to **configure SMS OTP fallback in the Web SDK**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(startStep: OtpStartStep | OtpStartStepFn, finishStep: OtpFinishStep | OtpFinishStepFn)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. Return the phone number from your start step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. The **Prove client SDK orchestrates** when each step runs—do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it and may surface activity related to AuthFinishStep that looks unexpected. This behavior is **expected**. Do **not** add your own extra invocation of the OTP finish step to pass the error in otpError. The SDK runs your OtpFinishStep when retry or error UI is needed. Implement the required step functions, but let the Prove client SDK orchestrate when each step runs. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt for it again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const otpStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Call **`reject('some error message')`** if something prevents sending the SMS or completing the flow (for example the customer cancels or leaves the UI with the back button). In the finish step, return the OTP through **`resolve(result: OtpFinishResult)`** with the **`OnSuccess`** result type and the value wrapped in **`OtpFinishInput`**, as in the snippet below. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Use this path when the **page** collects the phone number in the browser and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpMultipleResendFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can supply a new number, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPhoneChangeFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** stack extra **`OtpFinishStep`** invocations on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Use this section to **configure the Web SDK** so Instant Link SMS can be sent from the browser flow and optional resend or phone-number change behaves as expected. Instant Link for mobile web isn't supported. **Custom or vanity Instant Links** aren't supported. You can't substitute a custom link for the default Instant Link. ### Prerequisites * Integrate on **desktop (or other supported) web**; see the callout above for mobile web. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep | InstantLinkStartStepFn, retryStep?: InstantLinkRetryStep | InstantLinkRetryStepFn)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). Return the phone number from your step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkNoPromptStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Use this path when the **page** collects the number in the browser and you **do not** need resend or phone-number change. For resend or change, use the other tabs. Call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. Call **`reject('some error message')`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(0); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkMultipleResendRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(InstantLinkResultType.OnResend); }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(1); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkPhoneChangeRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(InstantLinkResultType.OnMobileNumberChange); }); }, }; ``` In Sandbox, run through each path you ship (default, prompt, and any retry flows). Confirm the SMS sends, the customer can complete the link within the timeout window, and **`resolve`** / **`reject`** match the UX you expect when the customer cancels or retries. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while waiting for the customer to select the link in the text message. Once clicked, the WebSocket closes and the `AuthFinishStep` function finishes. If your UI already has `flowType` from the server (`mobile` or `desktop`), you can configure the builder from that value instead of `isMobileWeb()`: ```javascript JavaScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` ```typescript TypeScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. If the user cancels the client flow, your backend should still call `UnifyStatus()`; you may receive `success=false`. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). **Prove Key persistence enhancement** Using Prove Key in web applications can be affected by strict browser privacy policies aimed at preventing cross-site tracking. For example, Safari completely disables third-party cookies and limits the lifetime of all script-writable website data. Prove Auth uses script-writable website data (localStorage and IndexedDB) for storing Prove Auth `deviceId`, crypto key material, and other critical metadata for subsequent device re-authentication. If the browser deletes this script-writable data, Prove Auth is no longer able to recognize the device and perform authentication. To mitigate this limitation and avoid repeating the device registration process, follow these steps to enable **Key Persistence**. The Key Persistence feature is not enabled by default. Contact Prove to enable this add-on feature. **Web app setup** Follow these steps to add Prove Key persistence in your web app: Install the Key Persistence integration module and add activation code to your app: ```shell NPM theme={"dark"} npm install @prove-identity/prove-auth-device-context ``` Then activate it in your app: ```javascript JavaScript theme={"dark"} import * as dcMod from "@prove-identity/prove-auth-device-context"; dcMod.activate(); ``` ```html No Package Manager theme={"dark"} ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow access to the following common resources: * `script-src https://fpnpmcdn.net https://*.prove-auth.proveapis.com`, * `connect-src https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com` * `worker-src blob:` Additionally, if you're including the file from jsDelivr, add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. When configuring `authenticatorBuilder` for any flow, add the `withDeviceContext()` method to enable Key Persistence data collection: ```javascript JavaScript theme={"dark"} // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` ```typescript TypeScript theme={"dark"} import { BuildConfig, DeviceContextOptions } from "@prove-identity/prove-auth"; // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey: string = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options: DeviceContextOptions = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` Call `authenticatorBuilder.build()` immediately after the web app loads. This enables the Web SDK to load components for Key Persistence collection and collect signals before authentication begins, which helps decrease latency. Ensure cookies are enabled when you call `AuthStart` and `AuthFinish`: ```javascript Axios theme={"dark"} // Enable cookies for AuthStart axios.post(backendUrl + '/authStart', data, { withCredentials: true }); // Enable cookies for AuthFinish axios.post(backendUrl + '/authFinish', data, { withCredentials: true }); ``` ```javascript Fetch theme={"dark"} // Enable cookies for AuthStart fetch(backendUrl + '/authStart', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); // Enable cookies for AuthFinish fetch(backendUrl + '/authFinish', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` **Handling multiple device registrations** If a user registers their device many times before using Key Persistence, Prove Auth may have many registration candidates matching the same visitor ID. To improve recovery accuracy: 1. Store the `deviceId` returned from successful authentications in your backend (database or web cookie) 2. Pass the stored `deviceId` with the `/unify` request to specify which registration to restore ```go Go theme={"dark"} // Send the Unify request with deviceId for registration recovery rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", DeviceId: provesdkservergo.String("stored-device-id-from-previous-auth"), }) ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', deviceId: 'stored-device-id-from-previous-auth', } const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); ``` ```java Java theme={"dark"} V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .deviceId("stored-device-id-from-previous-auth") .build(); ``` Continue the flow using the response from [`POST /v3/unify`](/reference/unify-request). Return updated session or auth details to the client as needed. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy) when present. If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. #### Mobile vs desktop When a tab includes both flows, **follow the steps below on mobile first.** On **desktop**, keep the same **Prompt customer** and **Verify mobile number** timing; only these parts change: * **Initiate Start Request** — The front end also sends **final target URL** with the phone number and possession type before `/unify`. * **Send Auth Token to the Front End** — The client completes **Instant Link** instead of OTP or Mobile Auth. Example of the Instant Link screen in the Prove Unified Authentication sandbox testing flow * **Verify Mobile Number** — On **success**, expect `proveId` and `phoneNumber` only. On **failure**, expect the same `success=false` and `phoneNumber` behavior as on mobile. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. **Mobile:** Test passive authentication with customer-supplied possession fallback; Mobile Auth passes and [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. On mobile, Mobile Auth fails and OTP succeeds (`1234`); [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Use this flow to test Prove Key revocation. Run a successful flow for [Penny](#penny). Copy the `deviceId` from the /unify-status response. Call the /device/revoke endpoint with the device ID to revoke the Prove Key tied to that user. ```batch cURL Request theme={"dark"} curl --request POST \ --url https://platform.uat.proveapis.com/v3/device/revoke \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "clientRequestId": "71010d88-d0e7-4a24-9297-d1be6fefde81", "deviceId": "" } ' ``` ```json JSON Response theme={"dark"} { "success": true } ``` If you send Penny through Unified Authentication again, attempting to authenticate just using the Prove Key, the endpoint returns ```json Error Message theme={"dark"} { "code": 8019, "message": "device has been revoked" } ``` You must complete a new authentication flow to reestablish the Prove Key. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Implement ProveX Source: https://developer.prove.com/how-to/provex-implementation-guide Learn how to implement ProveX by authenticating and verifying the consumer, then using `/discover` and `/fetch` to list and retrieve marketplace options such as wallet ID. ## Prerequisites * **Consumer consent** — Obtain consent from the consumer as required for your use case and applicable law. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for `/verify` (if applicable), [`GET /v3/discover`](/reference/discover-request), and [`GET /v3/fetch`](/reference/fetch-request). * **`proveId`** — Complete a verification (or other flow) that returns `proveId`. For Sandbox ProveX testing with Verified User, follow [Verified User Verify](/how-to/verified-users-verify) using `verificationType: verifiedUser` and the [test users](/how-to/verified-users-verify#test-users) on that page. ## Implementation steps Run authentication and/or verification for the end user based on your use case and persist the `proveId` from a successful response. Call [`GET /v3/discover`](/reference/discover-request) with the consumer’s `proveId` to list marketplace attribute UUIDs and issuer names, and to confirm whether partner-linked data exists. Call this before `/fetch` so you know which vendor options are in scope. Use the same bearer token in the `Authorization` header. ```bash Request theme={"dark"} curl -X GET "https://platform.uat.proveapis.com/v3/discover?proveId=41570934-6d6b-476d-9425-3eaf305cf2e5" \ -H "Authorization: Bearer " ``` ```json Response theme={"dark"} { "results": [ { "attributeId": "550e8400-e29b-41d4-a716-446655440000", "issuerId": "AcmeWallet" } ] } ``` See [`GET /v3/discover`](/reference/discover-request) for query parameters and the full response schema. After `/discover`, call [`GET /v3/fetch`](/reference/fetch-request) with their `proveId` and the `attributeId` values they chose from the `/discover` results. Each item in `results` includes `attributeValue` (for example a wallet ID for that issuer). Use the same bearer token as for `/verify` and `/discover`. ```bash Request theme={"dark"} curl -X GET "https://platform.uat.proveapis.com/v3/fetch?proveId=41570934-6d6b-476d-9425-3eaf305cf2e5&attributeId=550e8400-e29b-41d4-a716-446655440000" \ -H "Authorization: Bearer " ``` ```json Response theme={"dark"} { "results": [ { "attributeId": "550e8400-e29b-41d4-a716-446655440000", "issuerId": "AcmeWallet", "attributeValue": "ext-wallet-992834" } ] } ``` After a successful `/fetch` response, take **`attributeValue`** from the result and **input it into your partner integration**—for example by passing it to the partner SDK, API, or account flow so the consumer can continue in that marketplace. See [`GET /v3/fetch`](/reference/fetch-request) for query parameters and the full response schema. Responses will vary depending on the partner data is retrieved from. ## Sandbox testing ### Test users The following test users are available for ProveX flows that start with `/v3/verify` in the Sandbox environment. Use them to exercise successful and failed verification for `/discover` and `/fetch`. | Phone Number | First Name | Last Name | Expected Outcome | | ------------ | ---------- | --------- | ---------------- | | `2001004053` | Elena | Coldman | Success | | `2001004054` | Alf | Novotni | Failed | ### Testing steps Prove's Sandbox environment requires the following testing sequence: `/verify` with `verificationType: verifiedUser` -> `/discover` -> `/fetch`. Use test user Elena Coldman to verify a successful ProveX flow. **Prerequisites** * **`proveId`:** From a successful [**`/v3/verify`** for Elena](https://developer.prove.com/how-to/verified-users-verify#test-users). * **Bearer token:** Keep the same access token you used for **`/v3/verify`**. Use the `proveId` and the bearer token to call `/discover`. ```bash cURL Request theme={"dark"} curl -X GET "https://platform.uat.proveapis.com/v3/discover?proveId=550e8400-e29b-41d4-a716-446655440001" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```json JSON Response theme={"dark"} { "results": [ { "attributeId": "550e8400-e29b-41d4-a716-446655440100", "issuerId": "AcmePay" } ] } ``` Use the `proveId` and the bearer token to call `/fetch`. ```bash cURL Request theme={"dark"} curl -X GET "https://platform.uat.proveapis.com/v3/fetch?proveId=550e8400-e29b-41d4-a716-446655440001&attributeId=550e8400-e29b-41d4-a716-446655440100" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```json JSON Response theme={"dark"} { "results": [ { "attributeId": "550e8400-e29b-41d4-a716-446655440100", "issuerId": "AcmePay", "attributeValue": "sandbox-wallet-elena-coldman" } ] } ``` [Use test user Alf Novotni to simulate a failed verification](https://developer.prove.com/how-to/verified-users-verify#test-users). **`/v3/verify` doesn't return a `proveId` for this outcome**, so you can't call `/discover` or `/fetch`. # Secure API credentials Source: https://developer.prove.com/how-to/secure-api-credentials How to store, rotate, and govern Prove client IDs, client secrets, and bearer tokens in development and production. Secret API keys and client credentials behave like account passwords. If they are exposed, they can be abused against your integration and data. You are responsible for protecting Prove credentials in your environments. Use this guide as a baseline; align it with your security program and any obligations in your agreement with Prove. ## Protect against compromised credentials * **Use a key management system (KMS) or secrets manager for production secrets.** When you create or rotate a production client secret, store it only in a system designed for secrets (encryption at rest, access control, audit). Avoid leaving copies in local files or shared drives. * **Limit who can create, read, or rotate keys.** Define ownership, use least privilege, and review access periodically. * **Do not share secrets in email, chat, or support tickets.** Use approved secret-handling channels only. * **Do not commit secrets to source control.** Public repositories are actively scanned; private repos still leak through clones and tooling. Use environment variables or your CI/CD secret store, never literals in code. * **Do not embed client secrets or long-lived tokens in client-side apps.** Mobile binaries and front-end JavaScript can be reverse-engineered; anything shipped to the device should be treated as public. * **Monitor API usage.** Review logs for unusual patterns and ensure Sandbox credentials are not used against Production endpoints (and vice versa). * **Train your team and document your process.** Keep internal runbooks current for onboarding, incident response, and rotation. * **Rotate credentials on a schedule and after suspected exposure.** Rotating client secrets and invalidating old values limits blast radius if a secret was leaked without your knowledge. For HTTP errors when calling Platform APIs, see [Errors and status codes](/reference/status-and-error-codes). # Implement Prove Possession Using The Android SDK Source: https://developer.prove.com/how-to/unify-android-sdk Learn how to implement Prove possession for authentication using the Android SDK **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Android SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the Android SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```java Java theme={"dark"} // Object implementing AuthFinishStep interface AuthFinishStep authFinishStep = new AuthFinishStep() { ... }; // Objects implementing OtpStartStep/OtpFinishStep interfaces OtpStartStep otpStartStep = new OtpStartStep() { ... }; OtpFinishStep otpFinishStep = new OtpFinishStep() { ... }; ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) // verify(authId) call defined in #Validate the Mobile Phone section .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .build(); ``` The mobile data connection can sometimes be unavailable during testing. The `Builder` class offers a `withTestMode(boolean testMode)` method, which permits simulated successful session results while connected to a Wi-Fi network only. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```java Java theme={"dark"} ProveAuth proveAuth = ProveAuth.builder() .withAuthFinishStep(authId -> verify(authId)) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(this) .withTestMode(true) // Test mode flag .build(); ``` The `ProveAuth` object is thread safe. You can use it as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the ability to process concurrent blocking requests. ```java Java theme={"dark"} public class MyAuthenticator { private final MyBackendClient backend = new MyBackendClient(); // Backend API client private final AuthFinishStep authFinishStep = new AuthFinishStep() { @Override void execute(String authId) { try { AuthFinishResponse response = backend.authFinish("My App", authId); ... // Check the authentication status returned in the response } catch (IOException e) { String failureCause = e.getCause() != null ? e.getCause().getMessage() : "Failed to request authentication results"; // Authentication failed due to request failure } } }; private ProveAuth proveAuth; public MyAuthenticator(Context context) { proveAuth = ProveAuth.builder() .withAuthFinishStep(authFinishStep) .withOtpFallback(otpStartStep, otpFinishStep) .withContext(context) .build(); } public void authenticate() throws IOException, ProveAuthException { AuthStartResponse response = backend.authStart("My Prove Auth App"); proveAuth.authenticate(response.getAuthToken()); } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on Android**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: OtpStartStep, otpFinish: OtpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the interfaces, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it. Typical signs include a validation or reported error such as code 10001 with a message that the PIN doesn't match. You may also see flow activity related to AuthFinishStep, such as ProveAuth.builder().withAuthFinishStep(...), that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep.execute to pass the error in otpException. The SDK invokes OtpFinishStep.execute when a retry or error UI is needed. Your app should implement the required step interfaces, but the Prove client SDK orchestrates when each step runs. Don't duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with an **`OtpStartInput`** instance that reflects “number already known”—typically an empty string or **`null`** for the phone value, as in the snippet—so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, ProveAuthException otpException, OtpStartStepCallback callback) { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } ``` Call **`OtpStartStepCallback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, call **`OtpFinishStepCallback.onSuccess(OtpFinishInput)`** with the OTP the customer entered, wrapped in **`OtpFinishInput`**. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` Use this path when the **Android app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`OtpStartStepCallback.onSuccess(OtpStartInput)`** with the collected number. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can request another SMS, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class MultipleResendFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForResend("Didn't receive the SMS OTP? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onOtpResend(). otpFinishStepCallback.onOtpResend(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class NoPromptFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` To use the Resend/Retry/Phone Change features, install the Android SDK version 6.5.0 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpStartInput; import com.prove.sdk.proveauth.OtpStartStep; import com.prove.sdk.proveauth.OtpStartStepCallback; import com.prove.sdk.proveauth.PhoneNumberValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PromptStart implements OtpStartStep { @Override public void execute(boolean phoneNumberNeeded, @Nullable ProveAuthException otpException, OtpStartStepCallback callback) { // If phone number is needed, need to ask the end user for phone number input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (otpException instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } try { // Prompt the user for phone number to receive OTP SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForPhoneNumber which gives us the collected // phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new OtpStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new OtpStartInput("")); } } } ``` Then implement the finish step so the customer can supply a new number, for example: ```java Java theme={"dark"} import com.prove.sdk.proveauth.OtpFinishInput; import com.prove.sdk.proveauth.OtpFinishStep; import com.prove.sdk.proveauth.OtpFinishStepCallback; import com.prove.sdk.proveauth.OtpValidationException; import com.prove.sdk.proveauth.ProveAuthException; public class PhoneChangeFinish implements OtpFinishStep { @Override public void execute(@Nullable ProveAuthException otpException, OtpFinishStepCallback otpFinishStepCallback) { // If error message is found, handle it. if (otpException instanceof OtpValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = otpException.getMessage(); } // Prompt the user for whether they received the SMS. if (promptForPhoneNumberChange("Didn't receive the SMS OTP? Try a different phone number.")) { // If the end user wants to correct the phone number already in use, or changing to a // different phone number to receive the future SMS OTP, call onMobileNumberChange(), and // the otpStartStep will re-prompt for phone number input from the end user. otpFinishStepCallback.onMobileNumberChange(); return; } try { // Prompt the user for OTP delivered by SMS. You can build UI to provide // best UX based on your application and business logic, here we simplify to a // generic function named promptForOtpCode which gives us the OTP code. String otpCode = promptForOtpCode(); otpFinishStepCallback.onSuccess(new OtpFinishInput(otpCode)); } catch (Exception e) { // if any issue with the OTP collection from the end user or the user wants to cancel // then call onError to exit the flow. In this example we simplify it as catching // an exception. otpFinishStepCallback.onError(); } } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep.execute`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for Android is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the Android client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Verified App Links** (recommended) so the SMS redirect opens your app with the full URL string. See [App Links](https://developer.android.com/training/app-links/about) in the Android documentation. When building the authenticator, use **`withInstantLinkFallback(InstantLinkStartStep startStep, @Nullable InstantLinkRetryStep retryStep)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When you have a mobile number, pass it in **`InstantLinkStartInput`** (for example the `mobileNumber` field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(InstantLinkStartInput input)`** so the SDK knows the customer agreed to receive the SMS. ```java Java theme={"dark"} InstantLinkStartStep noPromptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (!phoneNumberNeeded) { callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Use this path when the client collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(InstantLinkStartInput input)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptMultiResendRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForResend( "Didn't receive the InstantLink SMS? Click resend button for a new one!")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` To use the Resend/Phone Number Change features, install the Android SDK version 6.10.3 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```java Java theme={"dark"} InstantLinkStartStep promptStartStep = (phoneNumberNeeded, instantLinkError, callback) -> { // No phone number needed, no need to ask end user for input. if (phoneNumberNeeded) { // If error message is found around phone number, handle it. // The `PhoneNumberValidationException` is ONLY available when `phoneNumberNeeded` // has a value. if (instantLinkError instanceof PhoneNumberValidationException) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. String errorMsg = instantLinkError.getMessage(); } try { // Prompt the user for phone number to receive InstantLink SMS. // You can build UI to provide best UX based on your application and business logic, // here we simplify to a generic function named promptForPhoneNumber which gives us // the collected phone number. String phoneNumber = promptForPhoneNumber(); callback.onSuccess(new InstantLinkStartInput(phoneNumber)); } catch (Exception e) { // if any issue with the phone number collection from the end user or the user // wants to cancel then call onError to exit the flow. // In this example we simplify it as catching an exception. callback.onError(); } } else { // No phone number needed, no need to ask end user for input. callback.onSuccess(new InstantLinkStartInput("")); } }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```java Java theme={"dark"} InstantLinkRetryStep promptPhoneNumChangeRetryStep = callback -> { // Prompt the user for whether they received the SMS. if (promptForChangePhoneNumber( "Didn't receive the InstantLink SMS? Try a different phone number.")) { // If the end user wants to send again to the same phone number call onResend(). callback.onResend(); } }; ``` After the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your App Link (or equivalent) must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(String redirectUrl)`**. Call **`finishInstantLink`** from the code path that handles the incoming deep link (for example **`onCreate()`** in your App Link activity). Register an activity similar to this (replace the host with yours): ```xml theme={"dark"} ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK throws **`ProveAuthException`** and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your handler activity; **`finishInstantLink`** should run with the full URL and the session should resume without **`ProveAuthException`** from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Client-Side iOS SDK Source: https://developer.prove.com/how-to/unify-ios-sdk Learn how to integrate the Prove client-side iOS SDK for Unified Authentication in a native iOS app. **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the iOS SDK for possession checks and the Prove Key. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. When using the iOS SDK, set `mobile` as the `flowType` for the `Start()` function on the server. Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Implement `OTP` or `Instant Link` - **not both**. ```swift Swift theme={"dark"} // Object implementing ProveAuthFinishStep protocols let finishStep = FinishAuthStep() // Objects implementing OtpStartStep/OtpFinishStep protocols let otpStartStep = MobileOtpStartStep() let otpFinishStep = MobileOtpFinishStep() let proveAuthSdk: ProveAuth proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep) .build() ``` If a mobile data connection is unavailable during testing, use the Builder class. It permits simulated successful session results while connected to a Wi-Fi network. Testing using a Wi-Fi connection is useful in the Sandbox environment. ```swift Swift theme={"dark"} proveAuthSdk = ProveAuth.builder(authFinish: finishStep) .withMobileAuthTestMode() // Test mode flag .build() ``` The Prove Auth object is thread safe and used as a singleton. Most Prove Auth methods are blocking and therefore can't execute in the main app thread. The app employs an executor service with a minimum of two threads to manage threads due to the SDK's ability to process concurrent blocking requests. ```swift Swift theme={"dark"} // authToken retrieved from your server via StartAuthRequest proveAuthSdk.authenticate(authToken) { error in DispatchQueue.main.async { self.messages.finalResultMessage = "ProveAuth.authenticate returned error: \(error.localizedDescription)" print(self.messages.finalResultMessage) } } ``` ## Configure OTP Use this section to **configure SMS OTP fallback on iOS**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(otpStart: otpStartStep, otpFinish: otpFinishStep)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. The **Prove client SDK orchestrates** when each step runs—implement the protocols, but do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it (for example, a reported error such as code 10001 with a message that the PIN does not match). You may also see flow activity related to AuthFinishStep (the handler you pass to ProveAuth.builder(authFinish:)) that looks like an unexpected redirect. This behavior is **expected**. Do **not** add your own follow-up call to OtpFinishStep to pass the validation error in otpError. The SDK invokes OtpFinishStep.execute(otpError:callback:) when a retry or error UI is needed. Your app should implement the required step protocols, but the Prove client SDK orchestrates when each step runs. Do not duplicate that orchestration in application code. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the client must **not** prompt for it again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class OtpStartStepNoPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for SMS OTP, // or to obtain user confirmation for initiating an SMS message. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Call **`callback.onError()`** if something prevents sending the SMS (for example the customer cancels or leaves the flow with the back button). In the finish step, return the OTP the customer entered through your **`OtpFinishStep`** implementation, as in the snippet below. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` Use this path when the **iOS app** collects the phone number in the client and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`callback.onSuccess(input: otpStartInput)`** with the collected number. ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step: ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class OtpFinishStepMultipleResend: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered in the SMS message. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the OTP value to the SDK func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Communicate to the SDK any issues when host app trys to obtain the OTP value // or when users cancel the OTP flow func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } // Call this method to request a new OTP code for the same mobile number. func sendNewOtp() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onOtpResend() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```swift Swift theme={"dark"} class OtpFinishStepNoPrompt: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Signal to UI components to display OtpFinishView DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Signal to your UI components that the last provided OTP is invalid self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Provide the collected OTP value to the SDK for validation. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // Notify the SDK of any issues encountered while obtaining the OTP value or if the user cancels the OTP flow. func handleOtpFinishError() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Retry/Phone Change features, install the iOS SDK version 6.5.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class OtpStartStepWithPrompt: OtpStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: OtpStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display OtpStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display OtpStartView if a phone number is needed. self.sheetObservable.isOtpStartActive = true } } } // Return collected phone number to the SDK func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } let otpStartInput = OtpStartInput(phoneNumber: phoneNumber) // This is how you pass collected phone number to SDK callback.onSuccess(input: otpStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the customer explicitly cancels the SMS OTP transaction // or presses the back button to exit out the SMS OTP start step screen. func handleOtpStartError() { guard let callback = self.callback else { print("Error: OtpStartStepCallback is not set ") return } callback.onError() } } ``` Then implement the finish step so the customer can supply a new number, for example: ```swift Swift theme={"dark"} class OtpFinishStepWithPhoneChange: OtpFinishStep { @ObservedObject var sheetObservable: SheetObservable var callback: OtpFinishStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to collect the OTP value delivered via SMS. func execute(otpError: ProveAuthError?, callback: OtpFinishStepCallback) { self.callback = callback // Handle the OTP validation error if present. // Update your UI to display the OTP finish view. DispatchQueue.main.async { if case .otpValidationError = otpError { print("found otpError: \(String(describing: otpError?.localizedDescription))") // Update your UI to indicate that the provided OTP is invalid. self.sheetObservable.isOtpValidationError = true } else { self.sheetObservable.isOtpValidationError = false } self.sheetObservable.isOtpFinishActive = true } } // Return the collected OTP value to the SDK. func handleOtp(_ otp: String) { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set ") return } let otpFinishInput = OtpFinishInput(otp: otp) callback.onSuccess(input: otpFinishInput) } // When callback.onMobileNumberChange() is evoked, OtpStartStep will be re-initiated // so that end-users can enter a different phone number via OtpStartStep. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: OtpFinishStepCallback is not set") return } callback.onMobileNumberChange() } } ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** manually chain **`OtpFinishStep`** on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Instant Link for iOS is an add-on feature. To enable, contact your Prove representative. Use this section to **configure the iOS client** so an Instant Link SMS opens your app and you pass the returned redirect URL to the SDK to resume the session. ### Prerequisites * **Instant Link** is enabled for your project (contact your Prove representative if needed). * **Universal Links** (recommended) so the SMS redirect opens your app with the full URL string. See [Supporting universal links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) in the Apple documentation. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep, retryStep: InstantLinkRetryStep?)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). When the client supplies a number, pass it in **`InstantLinkStartInput`** (for example the **`phoneNumber`** field) to **`callback.onSuccess(...)`**. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from your initial **Start** call) and the client must **not** prompt again. Call **`callback.onSuccess(input: nil)`** so the SDK knows the customer agreed to receive the SMS. ```swift Swift theme={"dark"} class InstantLinkStartStepNoPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to handle phone number collection for instant link, // or to obtain user confirmation for initiating an instant link. func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback // Since no phone number is needed, don't prompt the user. callback.onSuccess(input: nil) } } ``` Use this path when the **iOS app** collects the number and you **do not** need resend or phone-number change. Call **`callback.onSuccess(input: instantLinkStartInput)`** with the collected number. Call **`callback.onError()`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepMultipleResend: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend to the same phone number or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish) or request resend to the same phone number. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request a new instant link to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` To use the Resend/Phone Number Change features, install the iOS SDK version 6.10.2 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```swift Swift theme={"dark"} class InstantLinkStartStepWithPrompt: InstantLinkStartStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkStartStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } func execute( phoneNumberNeeded: Bool, phoneValidationError: ProveAuthError?, callback: InstantLinkStartStepCallback ) { self.callback = callback if !phoneNumberNeeded { // If no phone number is needed, then don't prompt the user. callback.onSuccess(input: nil) } else { DispatchQueue.main.async { // If a phone number validation error is detected, ensure it is handled to provide feedback to the user. if case .phoneNumberValidationError = phoneValidationError { print( "found phoneValidationError: \(String(describing: phoneValidationError?.localizedDescription))" ) // Update UI components to display InstantLinkStartView with the phone number validation error. self.sheetObservable.isPhoneValidationError = true } else { self.sheetObservable.isPhoneValidationError = false } // Update UI components to display InstantLinkStartView if a phone number is needed. self.sheetObservable.isInstantLinkStartActive = true } } } // Return collected phone number to the SDK. func handlePhoneNumber(phoneNumber: String) { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } let instantLinkStartInput = InstantLinkStartInput(phoneNumber: phoneNumber) callback.onSuccess(input: instantLinkStartInput) } // Communicate any issues encountered while trying to obtain the phone number to the SDK. // Error should be reported if the user cancels the instant link flow or exits the start step screen. func handleInstantLinkStartError() { guard let callback = self.callback else { print("Error: InstantLinkStartStepCallback is not set ") return } callback.onError() } } ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```swift Swift theme={"dark"} class InstantLinkRetryStepPhoneChange: InstantLinkRetryStep { @ObservedObject var sheetObservable: SheetObservable var callback: InstantLinkRetryStepCallback? init(sheetObservable: SheetObservable) { self.sheetObservable = sheetObservable } // Implement this method to present retry options: resend, change phone number, or cancel. func execute(callback: InstantLinkRetryStepCallback) { self.callback = callback // Update your UI to display the InstantLinkRetryView (e.g. "Did you receive a text message?"). // User can confirm (close modal / finish), request resend, or request phone number change. DispatchQueue.main.async { self.sheetObservable.isInstantLinkRetryActive = true } } // Call this method to request resend to the same phone number. func handleResend() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onResend() } // When this is invoked, InstantLinkStartStep will be re-initiated so that the user // can enter a different phone number. func handleMobileNumberChange() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onMobileNumberChange() } // Notify the SDK if the user cancels or if the app fails to handle the retry step. func handleInstantLinkRetryError() { guard let callback = self.callback else { print("Error: InstantLinkRetryStepCallback is not set ") return } callback.onError() } } ``` After you implement the start step above and the user finishes the web step outside your app, Prove redirects to the **`finalTargetUrl`** from your server **Start** call. Your Universal Link handling must deliver that URL into your app so you can pass the **full** string—including query parameters—into **`finishInstantLink(redirectUrl:)`**. For how **`finalTargetUrl`** fits into server-side **Start** with Mobile Auth and Instant Link fallback, see [Prove Pre-Fill implementation guide](/how-to/prove-pre-fill-implementation-guide). Call **`finishInstantLink`** from the entry point that receives the opened URL, for example **`application(_:open:options:)`** or **`application(_:continue:restorationHandler:)`**. ```swift Swift theme={"dark"} /// Finishes the Instant Link authentication flow using the redirect URL from the deep link. /// This is the URL to which the user is redirected after tapping the Instant Link in SMS. finishInstantLink(redirectUrl: redirectUrl) { error in // Handle errors due to invalid format of redirectUrl here } ``` The redirect URL is your original `finalTargetUrl` plus parameters the SDK needs. Example: if Start used `https://yourDeepLinkUrl.com`, the link might look like `https://yourDeepLinkUrl.com?asc=true&authId=some-uuid-string`. | Parameter | Meaning | | --------- | ---------------------------------------------------------------------------------------------- | | `asc` | `"true"` or `"false"`: whether the server considers the auth session complete. | | `authId` | UUID for the session; the SDK uses it to match the redirect to the in-progress client session. | If required parameters are missing or invalid, the SDK surfaces an error via the **`finishInstantLink`** completion and does not continue the flow. **Verify:** In Sandbox, complete a flow where the SMS opens your app with the full URL; **`finishInstantLink`** should run and the session should resume without an error from a well-formed redirect. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox Testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004017 | Jesse | Mashro | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. Follow these steps to test the Prove Unified Authentication flow with Lorant Nerger on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Laney Dyball on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Use this procedure to test the Prove passive authentication with customer-supplied possession fallback flow with Inge Galier on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Penny Jowers on mobile. This user fails Mobile Auth but passes OTP and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bertie Fremont on mobile. This user passes Prove's possession and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Bonnie Sidon on mobile. This user fails Prove's possession and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Server-Side SDK Guide Source: https://developer.prove.com/how-to/unify-server Configure OAuth, call Unify and UnifyStatus from your backend, and optionally UnifyBind—using the official server-side SDKs. Use this guide when you already know you want **Prove Unified Authentication** with a **server-side SDK** (Go, Java, .NET, TypeScript, or JavaScript). When you finish, your backend can start a Unify session, poll for the final possession result, and—if you use customer-supplied possession—bind the phone to the Prove Key. For conceptual background (flows, possession variants, Prove Key), see [Unified Authentication overview](/explanation/unify-flow). For end-to-end wiring with the client SDKs, see [Unified Authentication implementation guide](/how-to/unify-implementation-guide). **Prerequisites** * **Prove OAuth** — Client ID and client secret ([Authentication](/reference/authentication)). * **Client integration** — Your front end uses a Prove Unify client SDK; this guide covers the **backend** calls those clients depend on. **HTTP endpoints for this how-to** * [`POST /v3/unify`](/reference/unify-request) * [`POST /v3/unify-status`](/reference/unify-status-request) * [`POST /v3/unify-bind`](/reference/unify-bind-request) (customer-supplied possession only) ## `Unify()` Add an endpoint to your server such as `POST /unify` so the front end can submit the possession type and phone number. On the back end, start a Prove Unified Authentication flow with a call to the `Unify()` function. This function takes these required parameters: * **Possession Type**: specify `mobile`, `desktop`, or `none` for customer-supplied possession to describe which type of device the end user is starting their flow on. * **Phone Number**: phone number of the end user. In sandbox, the phone number field determines which scenario to test. If you forget to pass in the phone number of a valid test user, then it returns a "no test user found matching the phone number" error. **Timeouts (operational)** `desktop` uses Instant Link (**three minutes** from SMS send to link use). `mobile` tries the Prove Key first, then can fall back to OTP (**two minutes** from SMS send to code entry). For how possession variants fit together, see [Unified Authentication overview](/explanation/unify-flow). **Everything else** — Optional fields (for example `finalTargetUrl` when `possessionType=desktop`, `checkReputation`, identifiers, `deviceId`, `rebind`, `allowOTPRetry`) are documented on [`POST /v3/unify`](/reference/unify-request). Set them according to your product rules. For **OTP retries** (`allowOTPRetry`), implement the matching client behavior described in [Unified Authentication implementation guide](/how-to/unify-implementation-guide). ```go Go theme={"dark"} rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PossessionType: "desktop", FinalTargetURL: provesdkservergo.String("https://google.com"), PhoneNumber: provesdkservergo.String("2001004014"), }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} const rspUnify = await sdk.v3.v3UnifyRequest({ possessionType: 'desktop', finalTargetUrl: 'https://google.com', phoneNumber: '2001004014', }); ``` ```java Java theme={"dark"} V3UnifyRequest unifyReq = V3UnifyRequest.builder() .possessionType("desktop") .finalTargetUrl("https://google.com") .phoneNumber("2001004014") .build(); V3UnifyRequestResponse unifyReqResp = sdk.v3().v3UnifyRequest() .request(unifyReq) .call(); V3UnifyResponse unifyResp = unifyReqResp.v3UnifyResponse().get(); ``` ```csharp .NET theme={"dark"} var unifyReq = new V3UnifyRequest { PossessionType = "desktop", FinalTargetUrl = "https://google.com", PhoneNumber = "2001004014" }; var rspUnify = await _sdk.V3.V3UnifyRequestAsync(unifyReq); ``` **Use these fields from the `Unify()` response** * **Auth token** — When Prove-led possession applies, return this to the client for `Authenticate()`. Short-lived JWT scoped to this session. * **Correlation ID** — Persist on the server and pass into `UnifyStatus()` for the same flow. Expires **15 minutes** after this response. * **Success** — On this first call, expect **`pending`** while possession runs. For every response field, JWT behavior, and errors, see [`POST /v3/unify`](/reference/unify-request). ## `UnifyStatus()` After the client finishes possession, call **`UnifyStatus()`** with the correlation ID from `Unify()` to read the final outcome (including `success`). This maps to [`POST /v3/unify-status`](/reference/unify-status-request). **Required** * **Correlation ID** — UUID returned by `Unify()`. Format: `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(ctx, &components.V3UnifyStatusRequest{ CorrelationID: &rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }); ``` ```java Java theme={"dark"} V3UnifyStatusRequest statusReq = V3UnifyStatusRequest.builder() .correlationId(correlationId) .build(); V3UnifyStatusRequestResponse unifyStatusReqResp = sdk.v3().v3UnifyStatusRequest() .request(statusReq) .call(); V3UnifyStatusResponse unifyStatusResp = unifyStatusReqResp.v3UnifyStatusResponse().get(); ``` ```csharp .NET theme={"dark"} var reqBody = new V3UnifyStatusRequest { CorrelationId = rspUnify.V3UnifyResponse?.CorrelationId }; var rspUnifyStatus = await _sdk.V3.V3UnifyStatusRequestAsync(reqBody); ``` The function returns the following fields: For the full response schema and status semantics, see [`POST /v3/unify-status`](/reference/unify-status-request). You can then respond to the front end with the results of the authentication. ## `UnifyBind()` **Customer-supplied possession only:** after **your** possession check succeeds, call **`UnifyBind()`** to bind the phone to the Prove Key. This maps to [`POST /v3/unify-bind`](/reference/unify-bind-request). **Required** * **Correlation ID** — From the `Unify()` response. * **Phone number** — To bind to the Prove Key. Optional fields (for example `clientRequestId`) are listed on [`POST /v3/unify-bind`](/reference/unify-bind-request). ```go Go theme={"dark"} rspUnifyBind, err := client.V3.V3UnifyBindRequest(context.TODO(), &components.V3UnifyBindRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, PhoneNumber: "2001004018", }) if err != nil { return fmt.Errorf("error on UnifyBind(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyBind = await sdk.v3.v3UnifyBindRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', phoneNumber: '2001004018', }, }); if (!rspUnifyBind) { console.error("Unify Bind error.") return } ``` ```java Java theme={"dark"} V3UnifyBindRequest req = V3UnifyBindRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .phoneNumber("2001004018") .build(); V3UnifyBindResponse res = sdk.v3().v3UnifyBindRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3UnifyBindRequest req = new V3UnifyBindRequest() { CorrelationId = "713189b8-5555-4b08-83ba-75d08780aebd", PhoneNumber = "2001004018", }; var res = await sdk.V3.V3UnifyBindRequestAsync(req); ``` The function returns the following fields: * **Success**: `true` if the binding succeeded, `false` if it failed. * **Phone Number**: the phone number bound to the Prove Key. * **clientHumanId**: a client-generated unique ID to identify a specific customer across business lines. * **clientRequestId**: a client-generated unique ID for a specific session. * **deviceId**: the unique identifier for the Prove Key on the device. For all response fields and errors, see [`POST /v3/unify-bind`](/reference/unify-bind-request). Interpret **evaluation** using the [Global Fraud Policy](/reference/global-fraud-policy) when present. # Client-Side Web SDK Source: https://developer.prove.com/how-to/unify-web-sdk Learn how to integrate the client-side web SDK into your web app **Possession** requires **both** a **client-side SDK** and these **platform APIs**: * [`POST /v3/unify`](https://developer.prove.com/reference/unify-request) * [`POST /v3/unify-status`](https://developer.prove.com/reference/unify-status-request) ## Installation The Prove Platform Web SDK has an unpacked size of 324 KB, and a single dependency: `@prove-identity/mobile-auth`. Install the client-side SDK of your choice by running a command in your terminal, or by using a dependency management tool specific to your project. ```shell NPM theme={"dark"} # Run this command to install the package (ensure you have the latest version). npm install @prove-identity/prove-auth@3.4.2 ``` ```html No Package Manager theme={"dark"} # You can include this file in your web application from jsDelivr (update with the latest version). # You can also download the JavaScript file from https://cdn.jsdelivr.net/npm/@prove-identity/prove-auth@3.4.2/build/bundle/release/prove-auth.js and store it locally. ``` If you're including the file from jsDelivr and using [Content Security Policy headers](https://content-security-policy.com/), ensure you add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. ## Prerequisites * **Backend** — Your server calls [`POST /v3/unify`](/reference/unify-request) and the rest of the Unified Authentication sequence. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)). Use the same token for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Server** — Use a Prove server SDK or call those endpoints directly. Use the reference pages for full request bodies, optional fields, and responses. * **Client SDK** — Install the Web SDK for possession checks and the Prove Key. * **Languages** — TypeScript or JavaScript. For native iOS, see [Unify iOS SDK](/how-to/unify-ios-sdk). ## Flow overview: mobile vs desktop In a **mobile** flow, the mobile phone validates the one-time password (OTP). In a **desktop** flow, Instant Link sends a text message to the mobile phone for verification. In the mobile flow, once OTP validation completes, the `AuthFinishStep` callback finishes. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while the customer opens the link in the text message. When they do, the WebSocket closes and the `AuthFinishStep` callback finishes. ## `Authenticate()` and `authToken` The Web SDK requires an `authToken` for `Authenticate()`. That value comes from your server after it calls [`POST /v3/unify`](/reference/unify-request) (often exposed to the browser through your own start or initialize endpoint). The token is **session-specific** (one flow) and **expires after about 15 minutes**. ## Implementation steps You must integrate the **client-side SDK** for possession checks and the Prove Key. Decide if the customer is on a mobile or desktop browser using this example. If the `isMobile` is true, set `mobile` as the `flowType` for the `Start()` function on the server, otherwise you can set `desktop`: ```javascript JavaScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` ```typescript TypeScript theme={"dark"} // Check if the customer is on a mobile or desktop browser. const authCheck = new proveAuth.AuthenticatorBuilder().build(); let isMobile = authCheck.isMobileWeb() ``` Mobile Auth in the United States doesn't require a phone number, but all other countries do. Send a request to your back end server with the phone number and possession type to start the flow. See [`POST /v3/unify`](/reference/unify-request) for all request fields, optional parameters (for example `finalTargetURL`, `deviceId`, `allowOTPRetry`), and the full response. For OTP retries (`allowOTPRetry`), implement the client SDK behavior in the Authenticate step. ```go Go theme={"dark"} // Send the Unify request. rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", ClientRequestID: provesdkservergo.String("client-abc-123"), AllowOTPRetry: true, }) if err != nil { t.Fatal(err) } ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', clientRequestId: 'client-abc-123', allowOTPRetry: true, } // Send the Unify request. const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); if (!rspUnify) { console.error("Unify error.") return } ``` ```java Java theme={"dark"} // Send the Unify request. V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .clientRequestId("client-abc-123") .allowOTPRetry(true) .build(); // You may want to use the .get() method when working with the response object. ``` ```csharp .NET theme={"dark"} using Prove.Proveapi; using Prove.Proveapi.Models.Components; var sdk = new ProveAPI(auth: ""); V3StartRequest req = new V3UnifyRequest() { PhoneNumber = "2001004014", PossessionType = "mobile", ClientRequestID = "client-abc-123", AllowOTPRetry = true, }; var res = await sdk.V3.V3StartRequestAsync(req); ``` Return the `authToken` to the client for `Authenticate()`. Persist `correlationId` for `UnifyStatus()`. See [`POST /v3/unify`](/reference/unify-request) for all response fields, JWT behavior, and session timing. **Example: browser requests `authToken` from your backend** Send possession type and an optional phone number when your flow uses Prove possession to your server. Your server calls Prove and returns the token to the page. ```javascript JavaScript theme={"dark"} async function initialize(phoneNumber, possessionType) { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` ```typescript TypeScript theme={"dark"} async function initialize( phoneNumber: string, possessionType: string ): Promise { const response = await fetch(backendUrl + "/initialize", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ phoneNumber: phoneNumber, possessionType: possessionType, }), }); const rsp = await response.json(); const authToken = rsp.authToken; return authToken; } ``` Once you have the `authToken`, build the authenticator for both the mobile and desktop flows. Mobile Auth Implementations Only If your app uses [Content Security Policy headers](https://content-security-policy.com/), you must configure them to allow connections to Prove's authentication services: Sandbox Environment * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` Failure to configure these settings prevents Mobile Auth capability from working correctly in web flows. AT\&T Carrier Support (Pixel Mode) If support for the AT\&T carrier is also required, you must use **Pixel** mode (`withMobileAuthImplementation("pixel")`) with the following Content Security Policy (CSP) configuration: Allowed domain for Sandbox Environment * `https://att-device.uat.proveapis.com:4443` * `https://att-device.uat.proveapis.com` * `https://device.uat.proveapis.com:4443` * `https://device.uat.proveapis.com` * `http://device.uat.proveapis.com:4443` * `http://device.uat.proveapis.com` Allowed domain for Production Environment * `https://device.proveapis.com:4443` * `https://device.proveapis.com` * `http://device.proveapis.com:4443` * `http://device.proveapis.com` * `https://auth.svcs.verizon.com:22790` * `https://att-device.proveapis.com:4443` * `https://att-device.proveapis.com` * `https://snap.att.com` * `https://snap.mobile.att.com` * `https://snap.mobile.att.net` Add img-src for Pixel implementation Example img-src for Pixel mode: ```http theme={"dark"} Content-Security-Policy: img-src 'self' https://auth.svcs.verizon.com:22790 https://snap.att.com https://snap.mobile.att.com https://snap.mobile.att.net https://device.uat.proveapis.com:4443 https://device.uat.proveapis.com https://att-device.uat.proveapis.com:4443 https://att-device.uat.proveapis.com http://device.uat.proveapis.com:4443 http://device.uat.proveapis.com https://device.proveapis.com:4443 https://device.proveapis.com https://att-device.proveapis.com:4443 https://att-device.proveapis.com http://device.proveapis.com:4443 http://device.proveapis.com; connect-src self https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com ``` For desktop mode, Prove ignores the Prove Key and runs Instant Link. ```javascript JavaScript theme={"dark"} async function authenticate(isMobileWeb, authToken) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ```typescript TypeScript theme={"dark"} async function authenticate(isMobileWeb: boolean, authToken: string) { // Set up the authenticator for either mobile or desktop flow. let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { // Set up Mobile Auth and OTP. builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else { // Set up Instant Link. builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } const authenticator = builder.build(); // Authenticate with the authToken. return authenticator.authenticate(authToken); } ``` ## Configure OTP Use this section to **configure SMS OTP fallback in the Web SDK**: wire **`OtpStartStep`** and **`OtpFinishStep`** so the SDK can send the code, collect the PIN, and optionally support resend, OTP retry, or phone-number change. ### Prerequisites * After Prove sends the SMS, the customer has **about two minutes** to enter the OTP before the session times out. * When building the authenticator, call **`withOtpFallback(startStep: OtpStartStep | OtpStartStepFn, finishStep: OtpFinishStep | OtpFinishStepFn)`** and implement both **`OtpStartStep`** and **`OtpFinishStep`**. Return the phone number from your start step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. The **Prove client SDK orchestrates** when each step runs—do not duplicate that orchestration in app code (see **Invalid OTP and step orchestration** below). Invalid OTP and step orchestration When the customer enters an invalid OTP, the Prove client SDK detects it and may surface activity related to AuthFinishStep that looks unexpected. This behavior is **expected**. Do **not** add your own extra invocation of the OTP finish step to pass the error in otpError. The SDK runs your OtpFinishStep when retry or error UI is needed. Implement the required step functions, but let the Prove client SDK orchestrate when each step runs. ### Configure the client Open the tab that matches how the phone number is collected and which OTP options you need. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt for it again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const otpStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Call **`reject('some error message')`** if something prevents sending the SMS or completing the flow (for example the customer cancels or leaves the UI with the back button). In the finish step, return the OTP through **`resolve(result: OtpFinishResult)`** with the **`OnSuccess`** result type and the value wrapped in **`OtpFinishInput`**, as in the snippet below. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Use this path when the **page** collects the phone number in the browser and you **do not** need SMS resend, dedicated OTP retry handling beyond defaults, or phone-number change. For those capabilities, use **Resend**, **Retry OTP**, or **Phone Number Change**. In the start step, call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow a new OTP SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpMultipleResendFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then resend to the same phone number. resolve({ resultType: 1, // OnResendOtp enum type = 1 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter the OTP** after a wrong PIN (up to **three** attempts). On the server, pass **`allowOTPRetry=true`** on **`POST /v3/start`** (or your equivalent **Start** request). Implement the start step—no extra client changes beyond your normal prompt path: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Implement the finish step—no extra client changes. If the OTP is invalid, the finish step drives retry UI until attempts are exhausted, then **`AuthFinish`** runs. ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // Close the modal if a text message was not received. return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function otpStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPromptStartStep: OtpStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement the finish step so the customer can supply a new number, for example: ```javascript JavaScript theme={"dark"} function otpFinishStep(otpError) { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: 0, // OnSuccess enum type = 0 }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const otpPhoneChangeFinishStep: OtpFinishStep = { execute: async (otpError?: OtpError): Promise => { return new Promise((resolve, reject) => { // If error message is found, handle it. if (otpError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = otpError.message; } // Prompt the user for whether they received the SMS. // Typically, this is a page that shows the OTP already. We are simplifying // it by requiring an input. var input = confirm('Did you receive a text message?'); if (!input) { // If `Cancel`, then trigger the otpStartStep to re-prompt for // phone number. resolve({ resultType: 2, // OnMobileNumberChange enum type = 2 }); return; } // Prompt the user for the OTP. var otp = prompt('Enter OTP code:'); if (otp) { // If the input is valid and the user clicked `OK`, return the OTP. resolve({ input: { otp }, // OTP value resultType: OtpFinishResultType.OnSuccess, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` In Sandbox, walk each shipped path (default, prompt, resend, retry if enabled, phone change if enabled). Confirm SMS delivery, OTP entry within the timeout, and that you **never** stack extra **`OtpFinishStep`** invocations on top of the SDK’s invalid-OTP handling—the SDK should remain the single orchestrator for retries and errors. ## Configure Instant Link Use this section to **configure the Web SDK** so Instant Link SMS can be sent from the browser flow and optional resend or phone-number change behaves as expected. Instant Link for mobile web isn't supported. **Custom or vanity Instant Links** aren't supported. You can't substitute a custom link for the default Instant Link. ### Prerequisites * Integrate on **desktop (or other supported) web**; see the callout above for mobile web. When building the authenticator, use **`withInstantLinkFallback(startStep: InstantLinkStartStep | InstantLinkStartStepFn, retryStep?: InstantLinkRetryStep | InstantLinkRetryStepFn)`**. Implement **`InstantLinkStartStep`** in every flow. Add **`InstantLinkRetryStep`** only if you support **Resend** or **Phone Number Change** (see those tabs). Return the phone number from your step as an object with a **`phoneNumber`** field passed to **`resolve(...)`**; use **`reject('message')`** when collection fails. ### Configure the client Open the tab that matches how the phone number is collected and sent to Prove. Use this path when the **server** already has the phone number (for example from **`POST /v3/start`** or your server **Start** wrapper) and the browser must **not** prompt again. Call **`resolve(null)`** (or the equivalent in your snippet) so the SDK knows the customer agreed to receive the SMS. Follow the sample for the exact **`resolve`** shape your SDK version expects. ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkNoPromptStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // Since no phone number is needed, don't prompt the user. resolve(null); }); }, }; ``` Use this path when the **page** collects the number in the browser and you **do not** need resend or phone-number change. For resend or change, use the other tabs. Call **`resolve(input)`** with an object that includes the collected **`phoneNumber`**. Call **`reject('some error message')`** if collection fails, the customer cancels, or they leave the Instant Link start UI (for example with the back button). ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow a new SMS to the **same** number (up to **three** send attempts including the first). Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** so the customer can request another SMS, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(0); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkMultipleResendRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then resend to the same phone number. resolve(InstantLinkResultType.OnResend); }); }, }; ``` To use the Resend/Retry/Phone Number Change features, install the Web SDK version 2.15.1 or later. Allow the customer to **re-enter** the phone number (up to **three** entries/send attempts). Manual Request Required To enable phone number change capabilities on your credentials, contact your Prove representative. Implement the start step: ```javascript JavaScript theme={"dark"} function instantLinkStartStep(phoneNumberNeeded, phoneValidationError) { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, }); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkStartStep: InstantLinkStartStep = { execute: async ( phoneNumberNeeded: boolean, phoneValidationError?: PhoneValidationError ): Promise => { return new Promise((resolve, reject) => { // If no phone number is needed, then don't prompt the user. if (!phoneNumberNeeded) { resolve(null); return; } // If error message is found around phone number, handle it. // The `phoneValidationError` is ONLY available when `phoneNumberNeeded` // has a value. if (phoneValidationError) { // Set to a variable and display it in a field. // In this example, we don't do anything with the error. var someErrorMessage = phoneValidationError.message; } // Prompt the user for the phone number. var input = prompt('Enter phone number:'); if (input) { // If the input is valid and the user clicked `OK`, return the phone // number. resolve({ phoneNumber: input, } as InstantLinkStartInput); } else { // Else, exit the flow. reject('phone invalid or user cancelled'); } }); }, }; ``` Then implement **`InstantLinkRetryStep`** to collect a new number, for example: ```javascript JavaScript theme={"dark"} function instantLinkRetryStep() { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(1); }); } ``` ```typescript TypeScript theme={"dark"} const instantLinkPhoneChangeRetryStep: InstantLinkRetryStep = { execute: async (): Promise => { return new Promise((resolve, reject) => { // There are multiple return options: // - resolve(0): request resend to the same phone number // - resolve(1): request phone number change/re-prompt // - reject('user clicked cancel'): error out of the possession flow // Prompt the user for the phone number. // Typically, this is a page that is automatically closed or redirected // once the `AuthFinish` function is called. We are simplifying it by // requiring an input. var input = confirm('Did you receive a text message?'); if (input) { // If `OK`, close the modal. return; } // Else `Cancel`, then trigger the instantLinkStartStep to re-prompt for // phone number. resolve(InstantLinkResultType.OnMobileNumberChange); }); }, }; ``` In Sandbox, run through each path you ship (default, prompt, and any retry flows). Confirm the SMS sends, the customer can complete the link within the timeout window, and **`resolve`** / **`reject`** match the UX you expect when the customer cancels or retries. In the desktop flow, a WebSocket opens for up to 5 minutes on the desktop browser while waiting for the customer to select the link in the text message. Once clicked, the WebSocket closes and the `AuthFinishStep` function finishes. If your UI already has `flowType` from the server (`mobile` or `desktop`), you can configure the builder from that value instead of `isMobileWeb()`: ```javascript JavaScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` ```typescript TypeScript theme={"dark"} const isMobileWeb = flowType === "mobile"; const isDesktop = flowType === "desktop"; let builder = new proveAuth.AuthenticatorBuilder(); if (isMobileWeb) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish); } else if (isDesktop) { builder = builder .withAuthFinishStep((input) => verify(input.authId)) .withInstantLinkFallback(instantLink) .withRole("secondary"); } else { builder = builder.withAuthFinishStep((input) => verify(input.authId)); } const authenticator = builder.build(); await authenticator.authenticate(authToken); ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`. In the `AuthFinishStep`, specify a function to call once the possession checks complete on the mobile phone. This endpoint on your back end server calls the `UnifyStatus()` function to validate the phone number. The `AuthFinishStep` then completes. If the user cancels the client flow, your backend should still call `UnifyStatus()`; you may receive `success=false`. ```go Go theme={"dark"} rspUnifyStatus, err := client.V3.V3UnifyStatusRequest(context.TODO(), &components.V3UnifyStatusRequest{ CorrelationID: rspUnify.V3UnifyResponse.CorrelationID, }) if err != nil { return fmt.Errorf("error on UnifyStatus(): %w", err) } ``` ```typescript TypeScript theme={"dark"} const rspUnifyStatus = await sdk.v3.v3UnifyStatusRequest({ correlationId: rspUnify.v3UnifyResponse?.correlationId || '', }, }); if (!rspUnifyStatus) { console.error("Unify Status error.") return } ``` ```java Java theme={"dark"} V3UnifyStatusRequest req = V3UnifyStatusRequest.builder() .correlationId("713189b8-5555-4b08-83ba-75d08780aebd") .build(); V3UnifyStatusResponse res = sdk.v3().v3UnifyStatusRequest() .request(req) .call(); // You may want to use the .get() method when working with the response object. ``` See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response schema. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy). If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow `connect-src` for `wss://*.prove-auth.proveapis.com`, `wss://device.uat.proveapis.com`, and `wss://device.proveapis.com` as required for your environment. **Prove Key validity:** Prove deactivates the key on our servers after an extended period of inactivity (each successful authentication resets that period). On **iOS**, the Prove Key **remains on the device** after uninstall and reinstall. **Key Persistence** is available for the **Web SDK** only—not the native **Android** SDK. Pass **`rebind`** on `/v3/unify` to force a full possession check when needed. See [Prove Key validity and expiration](/explanation/unify-flow#prove-key-validity-and-expiration). **Prove Key persistence enhancement** Using Prove Key in web applications can be affected by strict browser privacy policies aimed at preventing cross-site tracking. For example, Safari completely disables third-party cookies and limits the lifetime of all script-writable website data. Prove Auth uses script-writable website data (localStorage and IndexedDB) for storing Prove Auth `deviceId`, crypto key material, and other critical metadata for subsequent device re-authentication. If the browser deletes this script-writable data, Prove Auth is no longer able to recognize the device and perform authentication. To mitigate this limitation and avoid repeating the device registration process, follow these steps to enable **Key Persistence**. The Key Persistence feature is not enabled by default. Contact Prove to enable this add-on feature. **Web app setup** Follow these steps to add Prove Key persistence in your web app: Install the Key Persistence integration module and add activation code to your app: ```shell NPM theme={"dark"} npm install @prove-identity/prove-auth-device-context ``` Then activate it in your app: ```javascript JavaScript theme={"dark"} import * as dcMod from "@prove-identity/prove-auth-device-context"; dcMod.activate(); ``` ```html No Package Manager theme={"dark"} ``` If you're using [Content Security Policy headers](https://content-security-policy.com/), ensure you allow access to the following common resources: * `script-src https://fpnpmcdn.net https://*.prove-auth.proveapis.com`, * `connect-src https://api.fpjs.io https://*.api.fpjs.io https://*.prove-auth.proveapis.com` * `worker-src blob:` Additionally, if you're including the file from jsDelivr, add `https://cdn.jsdelivr.net/npm/@prove-identity/` to `script-src`, plus corresponding `sha256-...` or `nonce-...`. When configuring `authenticatorBuilder` for any flow, add the `withDeviceContext()` method to enable Key Persistence data collection: ```javascript JavaScript theme={"dark"} // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` ```typescript TypeScript theme={"dark"} import { BuildConfig, DeviceContextOptions } from "@prove-identity/prove-auth"; // Prove supports integration within the US and EU regions for Key Persistence data collection // The public API Key for each build config will be provided by Prove const publicApiKey: string = "apiKeyAssignedForThisBuildConfig"; const buildConfig = BuildConfig.US_UAT; const options: DeviceContextOptions = { publicApiKey: publicApiKey, buildConfig: buildConfig, }; // Enable Key Persistence data with builder method let builder = new proveAuth.AuthenticatorBuilder(); builder = builder .withAuthFinishStep((input) => verify(input.authId)) // If support for AT&T carrier is required, pixel mode should be selected via // .withMobileAuthImplementation("pixel") // otherwise, "fetch" mode should be preferred by default .withMobileAuthImplementation("fetch") .withOtpFallback(otpStart, otpFinish) .withDeviceContext(options); const authenticator = builder.build(); ``` Call `authenticatorBuilder.build()` immediately after the web app loads. This enables the Web SDK to load components for Key Persistence collection and collect signals before authentication begins, which helps decrease latency. Ensure cookies are enabled when you call `AuthStart` and `AuthFinish`: ```javascript Axios theme={"dark"} // Enable cookies for AuthStart axios.post(backendUrl + '/authStart', data, { withCredentials: true }); // Enable cookies for AuthFinish axios.post(backendUrl + '/authFinish', data, { withCredentials: true }); ``` ```javascript Fetch theme={"dark"} // Enable cookies for AuthStart fetch(backendUrl + '/authStart', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); // Enable cookies for AuthFinish fetch(backendUrl + '/authFinish', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` **Handling multiple device registrations** If a user registers their device many times before using Key Persistence, Prove Auth may have many registration candidates matching the same visitor ID. To improve recovery accuracy: 1. Store the `deviceId` returned from successful authentications in your backend (database or web cookie) 2. Pass the stored `deviceId` with the `/unify` request to specify which registration to restore ```go Go theme={"dark"} // Send the Unify request with deviceId for registration recovery rspUnify, err := client.V3.V3UnifyRequest(ctx, &components.V3UnifyRequest{ PhoneNumber: "2001004014", PossessionType: "mobile", DeviceId: provesdkservergo.String("stored-device-id-from-previous-auth"), }) ``` ```typescript TypeScript theme={"dark"} let unifyReq = { phoneNumber: '2001004014', possessionType: 'mobile', deviceId: 'stored-device-id-from-previous-auth', } const rspUnify = await sdk.v3.v3UnifyRequest(unifyReq); ``` ```java Java theme={"dark"} V3UnifyRequest req = V3UnifyRequest.builder() .phoneNumber("2001004014") .possessionType("mobile") .deviceId("stored-device-id-from-previous-auth") .build(); ``` Continue the flow using the response from [`POST /v3/unify`](/reference/unify-request). Return updated session or auth details to the client as needed. Interpret `evaluation` using the [Global Fraud Policy](/reference/global-fraud-policy) when present. If you send a different phone number to /unify than the one registered to the Prove key, `success=false` returns when calling /unify-status. This is because the Prove Key is bound to a different number. Run the flow again with `rebind=true` in the /unify endpoint. This forces a full possession check and then, if valid, rebinds the Prove Key to the new phone number. ## Sandbox testing ### Prerequisites * **Sandbox access** — Prove Sandbox credentials and environment configuration from the Prove Portal. * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](/reference/authentication)) for [`POST /v3/unify`](/reference/unify-request), [`POST /v3/unify-status`](/reference/unify-status-request), and [`POST /v3/unify-bind`](/reference/unify-bind-request). * **Implementation** — Complete client and server integration. ### Test users list #### Short-term test users Use this test user when performing initial testing with cURL or Postman. This test user skips the client-side SDK authentication to walk you through the sequence of API calls. | Phone Number | First Name | Last Name | | :----------- | :---------- | :-------- | | 2001004018 | Barbaraanne | Canet | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004029 | Janos | Martina | After initial short-term testing, implement the client-side SDK and use the remaining test users to test your implementation. #### Unified Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Customer-Supplied Possession with Force Bind" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004014 | Lorant | Nerger | | 2001004015 | Laney | Dyball | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004025 | Bertie | Fremont | | +2001004026 | Bonnie | Sidon | #### Mobile Auth test users Follow the [Testing Steps](#testing-steps) for expected behavior per step. These users allow you to test "Prove Possession" and "Prove Passive Authentication with Customer-Supplied Possession Fallback" flows. | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | 2001004016 | Inge | Galier | | 2001004041 | Penny | Jowers | | Phone Number | First Name | Last Name | | :----------- | :--------- | :-------- | | +2001004027 | Allissa | Zoren | | +2001004028 | Wendy | Strover | | +2001004043 | Amii | Porritt | ### Testing steps Now that you’ve done client-side, server-side, and CX implementation, test using the test users. #### Mobile vs desktop When a tab includes both flows, **follow the steps below on mobile first.** On **desktop**, keep the same **Prompt customer** and **Verify mobile number** timing; only these parts change: * **Initiate Start Request** — The front end also sends **final target URL** with the phone number and possession type before `/unify`. * **Send Auth Token to the Front End** — The client completes **Instant Link** instead of OTP or Mobile Auth. Example of the Instant Link screen in the Prove Unified Authentication sandbox testing flow * **Verify Mobile Number** — On **success**, expect `proveId` and `phoneNumber` only. On **failure**, expect the same `success=false` and `phoneNumber` behavior as on mobile. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Lorant Nerger. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key for this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Laney Dyball. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. **Mobile:** Test passive authentication with customer-supplied possession fallback; Mobile Auth passes and [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Inge Galier. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Penny fails the reputation check and returns `success=false` in the final response. On mobile, Mobile Auth fails and OTP succeeds (`1234`); [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. Start the onboarding flow on the initial screen and enter the phone number for Penny Jowers. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK then falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. **Mobile and desktop:** This user passes Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=true`. On desktop there is no Prove Key. Start the onboarding flow on the initial screen and enter the phone number for Bertie Fremont. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Send the user on through your authenticated flow. **Mobile and desktop:** This user fails Prove possession; [`POST /v3/unify-status`](/reference/unify-status-request) returns `success=false`. Start the onboarding flow on the initial screen and enter the phone number for Bonnie Sidon. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs OTP handling. Enter 1111 to simulate an unsuccessful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). The test user failed. Send the user through your exception process. Follow these steps to test the Prove Unified Authentication flow with Allissa Zoren on mobile. This user passes Mobile Auth and returns `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Allissa Zoren. Your front end sends the possession type to the back end. Your back end calls the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end runs Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` from Mobile Auth in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Follow these steps to test the Prove Unified Authentication flow with Wendy Strover on mobile. This user fails Mobile Auth and return `success=false` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Wendy Strover. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end fails Mobile Auth. Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=false` and `phoneNumber`. See [`POST /v3/unify-status`](/reference/unify-status-request). If you are testing the reputation check flow by sending `checkReputation=true` in the /unify request, Amii fails the reputation check and returns `success=false` in the final response. Follow these steps to test the Prove Unified Authentication flow with Amii Porritt on mobile. This user fails Mobile Auth but pass OTP and return `success=true` in the /unify-status response. Start the onboarding flow on the initial screen and enter the phone number for Amii Porritt. Your front end sends the phone number and possession type to the back end. Your back end sends the phone number to the /unify endpoint. The response provides an auth token, correlation ID, and `success=pending`. Your back end sends the `authToken` to the front end. The front end attempts Mobile Auth, which fails. The SDK falls back to OTP handling. Enter 1234 to simulate a successful OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls [`POST /v3/unify-status`](/reference/unify-status-request) with the correlation ID to validate the phone number. Expect `success=true`, `proveId`, `deviceId`, and `phoneNumber` in Sandbox. See [`POST /v3/unify-status`](/reference/unify-status-request) for the full response. You have a successful flow and a Prove key tied to this phone number. Sending this user through again bypasses the possession check due to the Prove key. Send the user on through your authenticated flow. Use this flow to test Prove Key revocation. Run a successful flow for [Penny](#penny). Copy the `deviceId` from the /unify-status response. Call the /device/revoke endpoint with the device ID to revoke the Prove Key tied to that user. ```batch cURL Request theme={"dark"} curl --request POST \ --url https://platform.uat.proveapis.com/v3/device/revoke \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "clientRequestId": "71010d88-d0e7-4a24-9297-d1be6fefde81", "deviceId": "" } ' ``` ```json JSON Response theme={"dark"} { "success": true } ``` If you send Penny through Unified Authentication again, attempting to authenticate just using the Prove Key, the endpoint returns ```json Error Message theme={"dark"} { "code": 8019, "message": "device has been revoked" } ``` You must complete a new authentication flow to reestablish the Prove Key. Follow these steps to test the Prove Unified Authentication flow with Lorant, Jesse, Bertie, or Wendy. This introduces failures into the flow and return `success=false` at various points. During the mobile flow, use 1111 to simulate a failed OTP. Example of the OTP screen in the Prove Unified Authentication sandbox testing flow Once the front end finishes the possession check, the back end calls the /unify-status endpoint with the correlation ID to validate the phone number. The user then fails /unify-status. ```json Response theme={"dark"} { "phoneNumber": "2001004017", "success": "false" } ``` The Prove flow terminates and the front end proceeds to another authentication method. # Verified User Verify Source: https://developer.prove.com/how-to/verified-users-verify How to verify Verified User using POST /v3/verify, handle the response, and test in Sandbox. Flowchart: Verified User from v3/verify through a Success decision to Success=False or Success=True, then Continuous Monitoring and a phone number change event after success Identities that pass the [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) are automatically enrolled in [Manage](https://developer.prove.com/explanation/account-opening-manage). ## Prerequisites * **Access token** — Obtain a bearer token using Prove OAuth ([Authentication](https://developer.prove.com/reference/authentication)). ## Implementation steps Collect the required customer information from your CRM or database: * Phone number * First name * Last name Make a request to the [`/v3/verify endpoint`](https://developer.prove.com/reference/verify) including the Authorization header. Generate a bearer token as outlined on the [Authentication page](https://developer.prove.com/reference/authentication). ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "verificationType": "verifiedUser", "firstName": "Elena", "lastName": "Coldman", "phoneNumber": "2001004053", "clientRequestId": "test-001" }' ``` Replace `` with your acquired access token. For **Verified User**, set `verificationType` to `verifiedUser` and include `phoneNumber`, `firstName`, and `lastName`. You can pass `clientRequestId`, `clientCustomerId`, `clientHumanId`, `proveId`, `identityAttributes`, and other optional fields as your flow requires. See [`POST /v3/verify`](/reference/verify) for the full request schema, optional fields, and validation rules. Use the samples below together with the [`/v3/verify`](https://developer.prove.com/reference/verify) response schema. Cross-check field meanings with [Assurance levels](https://developer.prove.com/reference/assurance-levels) and [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy) when interpreting codes and evaluations. ```json Success Response theme={"dark"} { "success": "true", "correlationId": "b8ec33cd-37cb-4b83-9f08-330d4b984bc2", "clientRequestId": "test-001", "phoneNumber": "2001004053", "proveId": "550e8400-e29b-41d4-a716-446655440001", "provePhoneAlias": "67D6F2VYAUGG8Y0YS937PGCHXKULMDN9AYELU1RNLF2J02N43DJQ14ZDC7Q8HHTJYZ20H54UFTGSA090VXVGAUALCXTE4VSWX3P4CJHTZ887VGYBT0WK8YES9B50XTSP", "identity": { "firstName": "Elena", "lastName": "Coldman", "assuranceLevel": "AL2" }, "additionalIdentities": [ { "firstName": "Eric", "lastName": "Coldman", "assuranceLevel": "AL1" } ], "evaluation": { "authentication": { "result": "pass" }, "risk": { "result": "pass" } }, "isEnrolled": true } ``` ```json Failure Response theme={"dark"} { "success": "false", "correlationId": "06a3d745-ef09-434e-8210-1481bb3ceafc", "clientRequestId": "test-002", "phoneNumber": "2001004054", "identity": { "assuranceLevel": "AL0" }, "evaluation": { "authentication": { "failureReasons": { "9175": "No active identity can be associated with the phone number.", "9177": "No information can be found for the identity or phone number." }, "result": "fail" }, "risk": { "result": "pass" } }, "isEnrolled": false } ``` ### In practice * **`success`** — Branch your UX and backend logic on pass vs fail. * **`identity`** — Read verified attributes and [`assuranceLevel`](https://developer.prove.com/reference/assurance-levels) for policy (step-up, deny, manual review). * **`evaluation`** — Interpret authentication and risk outcomes under [Global Fraud Policy](https://developer.prove.com/reference/global-fraud-policy); use failure codes when `success` is false. * **IDs** — Persist `proveId` when present if you need support, auditing, or linking to other Prove flows. If you passed optional identifiers on the request, the response may echo `clientCustomerId` and `clientHumanId`; see the [`/v3/verify`](https://developer.prove.com/reference/verify) reference for details. ## Sandbox testing ### Test users You must use project credentials when working with sandbox test users. Attempting to use these test users with different project credentials results in an unauthorized access error. The following test users are available for testing using the `/v3/verify` endpoint in the Sandbox environment. Use these test users to simulate different verification scenarios and outcomes. Use these test phone numbers exactly as shown. The sandbox environment doesn't validate real customer information. | Phone Number | First Name | Last Name | Verification Type | Expected Outcome | | ------------ | ---------- | --------- | ----------------- | ---------------- | | `2001004053` | Elena | Coldman | `verifiedUser` | Success | | `2001004054` | Alf | Novotni | `verifiedUser` | Failed | ### Testing steps Use test user Elena Coldman to verify a successful verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "verificationType": "verifiedUser", "firstName": "Elena", "lastName": "Coldman", "phoneNumber": "2001004053", "clientRequestId": "test-001" }' ``` Expected response: ```json theme={"dark"} { "success": "true", "correlationId": "d09ed935-99f1-4ef9-b430-418c1544c4e6", "clientRequestId": "test-001", "phoneNumber": "2001004053", "proveId": "550e8400-e29b-41d4-a716-446655440001", "provePhoneAlias": "JFJUNCA4WAJ3FQK3JRV0RRPLE7DCPWYBVZUBTA1JJA51KVWFBSQ26JGNM3H5WCSX0X458A8GEH1PB2YPAX2NPFKXNCBUFLZHQLK8MFTZL0XCBE0VTD11HDMZ3A28Y5A5", "identity": { "firstName": "Elena", "lastName": "Coldman", "assuranceLevel": "AL2" }, "additionalIdentities": [ { "firstName": "Eric", "lastName": "Coldman", "assuranceLevel": "AL1" } ], "evaluation": { "authentication": { "result": "pass" }, "risk": { "result": "pass" } }, "isEnrolled": true } ``` Use test user Alf Novotni to simulate a failed verification: ```bash cURL theme={"dark"} curl -X POST "https://platform.uat.proveapis.com/v3/verify" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "verificationType": "verifiedUser", "firstName": "Alf", "lastName": "Novotni", "phoneNumber": "2001004054", "clientRequestId": "test-002" }' ``` Expected response: ```json theme={"dark"} { "success": "false", "correlationId": "99b2e9f3-08f2-4674-bd71-a116ede7642d", "clientRequestId": "test-002", "phoneNumber": "2001004054", "identity": { "assuranceLevel": "AL0" }, "evaluation": { "authentication": { "failureReasons": { "9175": "No active identity can be associated with the phone number.", "9177": "No information can be found for the identity or phone number." }, "result": "fail" }, "risk": { "result": "pass" } }, "isEnrolled": false } ``` # Home Source: https://developer.prove.com/index-3-1
Documentation

Explore our guides and examples to integrate Prove.

Documentation illustration

Explore Our Solutions

A suite of verification solutions that combine Prove’s authentication and verification engines to streamline digital interactions while combating fraud. A suite of solutions designed to help you understand and manage your users' identities, ensuring compliance and reducing risk. A curated ecosystem that connects trusted data and service partners through the power of the Prove Identity Graph.
# Assurance Levels Source: https://developer.prove.com/reference/assurance-levels Understand Prove's Assurance Levels (AL) and how they indicate the confidence in a verified identity. ## Definition In [`POST /v3/verify`](/reference/verify) responses, the **identity** object includes: | Field | Role | | :--------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `assuranceLevel` | Tier such as `AL-1`, `AL0`, `AL1`, `AL2`, or `AL3`. Higher tiers indicate stronger confidence in the association between the phone number and the identity. | | `reasons` | One or more reason codes from the tables below (for example `-1A`, `1B`). | Tiers are ordered from lowest (AL-1) to highest (AL3). ## Reason codes ### AL-1 | Reason code | Description | | :---------- | :----------------------------------------------------------------------------------------------------- | | -1A | Too many identities bound to a single phone number. | | -1F | Premium rate number globally or listed on the US Override Services Registry (OSR) registry. | | -1G | Online rentable temporary eSIM or VoIP number. | | -1H | Online rentable temporary eSIM or VoIP number, with high activity. This indicates fraudulent activity. | ### AL0 | Reason code | Description | | :---------- | :------------------------------------------------------------------------------------------------------------ | | 0B | Country doesn't give behavioral data. | | 0C | Assigned number with no behavioral activity. | | 0D | Short tenure, no activity. Reclassifies to AL0E if activity appears. | | 0E | Short tenure with behavioral activity present. | | 0F | Activity volume exceeds normal human thresholds. | | 0G | Pagers or other non-standard line types. Behavioral signals incompatible with standard identity verification. | ### AL1 | Reason code | Description | | :---------- | :-------------------------------------------------------------------------------------------------------- | | 1A | Enough ownership tenure and longitudinal human behavior. | | 1B | Tenure and longitudinal human behavior with Prove binding phone to a specific identity, superseding AL1A. | | 1D | Low longitudinal behavior but human signals such as ports, calls, or logins indicate not an eSIM bot. | ### AL2 | Reason code | Description | | :---------- | :---------------------------------------------------------------------------- | | 2A | Prove has seen this phone and identity before at moderate-to-high confidence. | | 2B | Prove corroborated data across more than one identity data source. | ### AL3 | Reason code | Description | | :---------- | :------------------------------------------------------------------------------------------------------ | | 3A | Prove has seen this phone and identity before at high confidence, this is this person's primary number. | # Challenge page requirements - Mobile Auth℠ Source: https://developer.prove.com/reference/challenge-page-requirements-mobile-authsm Required page summary copy and challenge-field options for the Challenge step after Mobile Auth℠ authentication in Pre-Fill for Consumers. ## Scope Required UI copy and challenge-field rules for the **Challenge** screen after **Mobile Auth℠** authentication in Pre-Fill for Consumers. Screenshot of Mobile Auth Challenge Page example interface ## Page summary | Element | Required copy | | :----------------------- | :------------------------------------------------------------------------------------- | | Page summary language | Let's Begin by Finding Your Information | | Page summary description | We can prefill some of this request like your name, address, and contact info for you. | ## Challenge data by contracted solution | Contracted solution | Customer prompt language requirements | Required challenge data | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Prove Pre-Fill | Customer can choose one from the following prompts:
"Last 4 SSN/ITIN"
"SSN/ITIN"
"MM/DD/YYYY"
"MM/YYYY"
"MM/DD" | Last 4 SSN/ITIN
Full SSN\*
Full DOB
DOB - Month and Year
DOB - Month and Day

**If the customer is applying to open a demand deposit account (DDA), the customer must enter their full social security number (SSN) for the challenge.** | | Prove Pre-Fill with KYC | "Full SSN/ITIN" | Full SSN | # Challenge page requirements - when Mobile Auth℠ fails Source: https://developer.prove.com/reference/challenge-page-requirements-when-mobile-authsm-fails Required page summary copy and challenge-field options for the Challenge step when Mobile Auth℠ does not succeed or when using the Desktop authentication flow (Pre-Fill for Consumers). ## Scope Required UI copy and challenge-field rules for the **Challenge** screen when the customer does not complete **Mobile Auth℠** successfully or uses the **Desktop** authentication flow in Pre-Fill for Consumers. Screenshot of Mobile Auth Challenge Page example interface ## Page summary | Element | Required copy | | :----------------------- | :------------------------------------------------------------------------------------- | | Page summary language | Let's Begin by Finding Your Information | | Page summary description | We can prefill some of this request like your name, address, and contact info for you. | ## Challenge data by contracted solution | Contracted solution | Customer prompt language requirements | Required challenge data | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Prove Pre-Fill | Customer can choose one from the following prompts:
"Last 4 SSN/ITIN"
"SSN/ITIN"
"MM/DD/YYYY"
"MM/YYYY"
"MM/DD" | Last 4 SSN/ITIN
Full SSN\*
Full DOB
DOB - Month and Year
DOB - Month and Day

**If the customer is applying to open a demand deposit account (DDA), the customer must enter their full social security number (SSN) for the challenge.** | | Prove Pre-Fill with KYC | "Full SSN/ITIN" | Full SSN | # Discover Identity Attributes Source: https://developer.prove.com/reference/discover-request openapi-discover-fetch.yaml get /v3/discover Discover which identity attributes (e.g., walletID, email) are available for a given ProveID. This endpoint returns a list of attribute IDs and their corresponding issuer IDs, which can then be used to fetch actual attribute values in the /v3/fetch endpoint. # Fetch Identity Attributes Source: https://developer.prove.com/reference/fetch-request openapi-discover-fetch.yaml get /v3/fetch Fetch actual identity attribute values (e.g., walletID) based on the customer ProveID and attribute UUID. # Global fraud policy Source: https://developer.prove.com/reference/global-fraud-policy Evaluation categories and failure reason codes in the evaluation object on Prove Platform API responses. ## Definition The **Global Fraud Policy (GFP)** drives pass/fail outcomes and machine-readable failure reasons in the **`evaluation`** object returned by Prove Platform APIs (for example [`POST /v3/complete`](/reference/complete-request)). GFP classifies each transaction into one or more of the following categories: | Category | Evaluates | | :------------- | :----------------------------------------------------------------------------------------------------- | | Identification | Consistency of personal details (for example name, address, date of birth, national identifier). | | Authentication | Whether the claimant matches the identity and phone possession signals. | | Compliance | Regulatory and program requirements for the industry and use case. | | Risk | Factors outside identification and authentication (for example behavioral or carrier-related signals). | ## Response examples Representative **`evaluation`** payloads from **`POST /v3/complete`**. **Pass** ```json theme={"dark"} { "success": true, "next": { "done": "done" }, "evaluation": { "authentication": { "result": "pass" }, "identification": { "result": "pass" } } } ``` **Fail** (with `failureReasons`) ```json theme={"dark"} { "success": false, "next": { "done": "done" }, "evaluation": { "authentication": { "result": "fail", "failureReasons": { "9161": "Very short tenure association between phone number and identity.", "9171": "Potential injection attack due to 2 identities both with short tenure association to the phone number.", "9180": "No phone ownership confirmed using 4 elements." } }, "identification": { "result": "fail", "failureReasons": { "9001": "Identification isn't confirmed using 4 elements." } } } } ``` ## Failure reason codes | Code | Category | Description | | :--- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 9001 | Identification | Identification isn't confirmed using 4 elements. | | 9003 | Identification | Identity isn't found | | 9004 | Identification | Identification isn't confirmed using 4 elements with secondary source. | | 9005 | Identification | Identification isn't confirmed using 4 elements with more than 1 source. | | 9011 | Identification | Identification isn't confirmed using less than 4 elements. | | 9012 | Identification | SSN doesn't match. | | 9013 | Identification | Birth Date doesn't match. | | 9014 | Identification | Name doesn't match. | | 9015 | Identification | Address doesn't match. | | 9081 | Identification | SSN is issued before the birth date. | | 9082 | Identification | Address is a correctional facility. | | 9083 | Identification | Deceased indicator is present for individual. | | 9100 | Authentication | Phone possession incomplete. Occurs when `/validate` is called before the Mobile Auth or OTP possession flow completes. | | 9101 | Authentication | Phone number doesn't match Prove Key. Rebind required. | | 9131 | Authentication | Indicates the phone number is at a higher risk for fraud due to being listed as a non-mobile line on the Override Services Registry (OSR). | | 9132 | Authentication | This phone number is listed on a public website with the contents of its text messages posted publicly to allow a user to bypass phone possession check controls. | | 9133 | Authentication | The phone isn't trusted based on the SIM Key Trust Score. | | 9134 | Authentication | The phone isn't trusted because it's a non-fixed VoIP number. | | 9135 | Authentication | Potential account takeover (ATO) due to recent SIM Swap under 24 hours. | | 9136 | Authentication | Potential ATO due to recent Port under 24 hours. | | 9137 | Authentication | Potential ATO due to call forwarding enabled for phone number. | | 9161 | Authentication | Short tenure association between phone number and identity. | | 9162 | Authentication | Short tenure association between phone number and identity. | | 9163 | Authentication | Potential recycled phone fraud due to a high number of identities associated to phone number with a newer owner present. | | 9164 | Authentication | Potential recycled phone fraud due to identity having no recent association to phone number and a phone disconnect event is observed after the phone association. | | 9165 | Authentication | Potential recycled phone fraud due to a newer owner observed and a phone disconnect event is observed after the phone association. | | 9166 | Authentication | Address is a correctional facility. | | 9167 | Authentication | Deceased indicator is present for individual. | | 9168 | Authentication | Person verification was performed indicating phone ownership isn't verified | | 9171 | Authentication | Potential injection attack due to 2 identities both with short tenure association to the phone number. | | 9172 | Authentication | Potential injection attack due to 3 identities all with short tenure association to the phone number. | | 9173 | Authentication | Potential injection attack due to 4 identities all with short tenure association to the phone number. | | 9174 | Authentication | Potential injection attack due to 5 or more identities all with short tenure association to the phone number. | | 9175 | Authentication | No active identity can be associated with the phone number. | | 9176 | Authentication | The phone number and identity is strongly associated negative bot activity. | | 9177 | Authentication | No information can be found for the identity or phone number. | | 9180 | Authentication | No phone ownership confirmed using 4 elements. | | 9181 | Authentication | No phone ownership confirmed using less than 4 elements | | 9201 | Compliance | Anti-Money Laundering (AML) alerts are present for this identity. | | 9301 | Risk | No historical behavioral activity. | | 9302 | Risk | Suspicious large amount recent of activity. | | 9303 | Risk | Unusual amount of recent carrier phone number change events. | | 9304 | Risk | Suspicious large amount recent of activity across different identity attributes. | # Human Assurance Connect Source: https://developer.prove.com/reference/human-assurance-connect Learn how to use Prove Connect for secure batch identity verification via SFTP Connect is the Prove Platform **batch verification** interface. Customers upload CSV files over **SFTP**; each valid row is verified with **[`POST /v3/verify`](/reference/verify)**. Rows that pass the **Global Fraud Policy** are enrolled in **Identity Manager**. Responses match the same fields as synchronous [`POST /v3/verify`](/reference/verify) calls. Connect supports the `verifiedUser` and `humanAssurance` verification type. ## Scope | Item | Specification | | ------------ | ----------------------------------------------------------------------------- | | Transport | SFTP to dedicated directories (`ToProve` / `FromProve`) | | Verification | [`POST /v3/verify`](/reference/verify) per valid detail row | | Enrollment | Successful verifications enrolled in Identity Manager | | Result shape | Same parameters as real-time [`POST /v3/verify`](/reference/verify) responses | ## Provisioning The following items are provided during onboarding, and some correspond to fields in the input file. See [Input file format](#input-file-format). | Credential | Description | | ----------------- | --------------------------------------------- | | Enroll Key | Validates file authenticity | | Verification Type | `verifiedUser` or `humanAssurance` | | Directory Paths | `ToProve` (upload) and `FromProve` (download) | | SFTP Credentials | Authentication for file transfer | ## Processing Batch processing follows this sequence (see also the diagram). ```mermaid theme={"dark"} sequenceDiagram participant Customer participant SFTP as Prove participant API as v3/verify API participant IDM as Identity Manager Customer->>SFTP: Upload CSV to `ToProve` folder loop Each valid record SFTP->>API: POST /v3/verify API->>SFTP: Verification result alt Passes Global Fraud Policy SFTP->>IDM: Enroll identity end end SFTP->>Customer: Receive email notification SFTP->>Customer: Download output file in `FromProve` folder ``` | Stage | Behavior | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Upload | CSV is placed in the provisioned **`ToProve`** directory via SFTP. | | Validation | Each row is checked for required fields, phone format and country code, and region consistency with the file. If **authorization** checks fail, the **entire file** is rejected and an error response is written under **`FromProve`**. | | Verification | Valid rows are sent to **[`POST /v3/verify`](/reference/verify)**; response fields align with the live API. | | Enrollment | Rows that pass the Global Fraud Policy are enrolled in Identity Manager. | | Delivery | When processing completes, an **email** notification is sent; the output file is available under **`FromProve`**. | ## File requirements Input and output CSVs use the structures below. File names must follow the naming pattern. ### Naming convention `{client_name}_{region}_{verification_type}_YYYYMMDDHHMMSS.csv` | Component | Values | Description | | ------------------- | ---------------------------------- | -------------------------------------------- | | `client_name` | Agreed during setup | Organization identifier | | `region` | `US` or `INTL` | `US` — US and Canada; `INTL` — international | | `verification_type` | `verifiedUser` or `humanAssurance` | Verification type | ### Input file format Each batch file uses a **Header → Detail(s) → Trailer** layout so Prove can validate the service type and record count. #### Header record (input) | Column | Sample value | Required | | ----------------- | ---------------------------------- | ---------------- | | Record Type | `H` | Yes — always `H` | | Enroll Key | `SecretKey` | Yes | | Verification Type | `verifiedUser` or `humanAssurance` | Yes | #### Detail records (input) | Column | Sample value | Required | Description | | ------------------ | -------------- | -------------------------- | ------------------------------------------------------------------- | | Record Type | `D` | Yes — always `D` | | | Phone Number | `+12008040444` | Yes — include country code | | | First Name | `John` | Yes for `verifiedUser` | | | Last Name | `Doe` | Yes for `verifiedUser` | | | Client Customer ID | `zaq12wsx` | No | | | Attribute Type | `userId` | Yes for partners | Metadata type for the attribute value. | | Issuer ID | `AcmeWallet` | Yes for partners | Partner company name. | | Attribute Value | `A4B1F13BCCFC` | Yes for partners | Non-PII unique value for the consumer for the given attribute type. | #### Trailer record (input) | Column | Sample value | Required | | ----------------- | ------------ | ---------------- | | Record Type | `T` | Yes — always `T` | | Number of Records | `132789` | Yes | There is **no formal cap** on input file size or detail record count per file. When splitting work across uploads, Prove recommends **800,000 detail records or fewer per file**. Verification throughput is subject to **Data Partner transactions-per-second (TPS)** limits; **completion time** varies with detail record count and effective throughput. #### Example file (input) ```csv theme={"dark"} H,ENROLL_KEY_US_123,verifiedUser D,+12008040444,Sara,Hu,xsw23edc,userId,AcmeWallet,A4B1F13BCCFC D,+12005551234,John,Doe,abc12345,,, T,2 ``` ### Output file format After processing, Prove writes an output file to **`FromProve`**. It mirrors the input layout and appends verification fields (for example assurance level, identity attributes, status). #### Header record (output) | Column | Sample value | Notes | | ----------- | ------------ | ---------- | | Record Type | `H` | Always `H` | #### Detail records (output) | Column | Sample value | Notes | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------- | | Record Type | `D` | Always `D` | | Phone Number | `+12008040444` | | | First Name | `John` | | | Last Name | `Doe` | | | Client Customer ID | `zaq12wsx` | If present on input | | Attribute Type | `userId` | | | Issuer ID | `AcmeWallet` | | | Attribute Value | `A4B1F13BCCFC` | | | Assurance Level | `AL2` | If successful | | Assurance Level Reason Codes | `AL2a`, `AL2b` | If successful | | Prove Phone Alias | `474628DC4VK83842DB25DA4B1F13BCCFC0MEK4P664Z9PCC6E6C3CB92DC40191E5E868E5E38B1AC85F6G359B74880097B63C51E7E8636B33FA344CB8E` | If successful | | ProveID | `6b942541-abab-40ed-9970-5a28217836c0` | If successful | | Error | `200` | | | Error Message | `NA` | | | GFP Authentication Result | `pass` | If successful | | GFP Risk Result | `pass` | If successful | Only the top-level identity is included in the output; additional identities are omitted. #### Trailer record (output) | Column | Sample value | Notes | | ----------------- | ------------ | ---------- | | Record Type | `T` | Always `T` | | Number of Records | `132789` | | ### Error file format Rows that fail **validation** are written to a separate error file under **`FromProve`**, with machine-readable codes. | Error Code | Description | | ----------------------------------------- | ----------------------------------- | | `ERR_MISSING_FNAME` | First name is missing when required | | `ERR_MISSING_LNAME` | Last name is missing when required | | `ERR_MISSING_PHONE` | Phone number is missing | | `ERR_FORMAT_PHONE` | Invalid phone number format | | `ERR_PHONE_COUNTRY_NOTSUPPORTED` | Phone country not supported | | `ERR_DUPLICATE_RECORD_ORIGINAL_PRESERVED` | Duplicate record detected | US phone numbers must not appear in international-region files, and international numbers must not appear in US-region files; mismatches produce row errors. #### Header record (error file) | Column | Sample value | Notes | | ----------------- | ---------------------------------- | ---------- | | Record Type | `H` | Always `H` | | Verification Type | `verifiedUser` or `humanAssurance` | | #### Detail records (error file) | Column | Sample value | Notes | | ------------------ | ------------------- | ------------------- | | Record Type | `D` | Always `D` | | Phone Number | `+12008040444` | | | First Name | `John` | | | Last Name | `Doe` | | | Client Customer ID | `zaq12wsx` | If present on input | | Error Attribution | `ERR_MISSING_FNAME` | | #### Trailer record (error file) | Column | Sample value | Notes | | ----------------- | ------------ | ---------- | | Record Type | `T` | Always `T` | | Number of Records | `132789` | | ## Notifications ### Email alerts | Event | Email sent | Body | | --------------------------- | -------------------- | ----------------------------------------------- | | File rejected | Yes, when configured | Rejection details per onboarding configuration. | | File processed successfully | Yes, when configured | Includes **data quality statistics**.. | # Deactivate Identity Source: https://developer.prove.com/reference/identity-manager-deactivate-identity post /v3/identity/{identityId}/deactivate Stops webhook notifications without disenrolling the identity. # Disenroll Identity Source: https://developer.prove.com/reference/identity-manager-delete-identity delete /v3/identity/{identityId} Disenrolls an identity from Identity Manager. If you wish to monitor in future, re-enrollment of that identity is required. # Pre-filled consumer review page requirements - Mobile Auth℠ Source: https://developer.prove.com/reference/pre-filled-consumer-review-page-requirements-mobile-authsm Required field display, masking, edit behavior, and KYC confirmation copy for the customer review step with Mobile Auth℠ (Pre-Fill for Consumers). ## Scope UI rules for the **pre-filled consumer review** screen when the flow uses **Mobile Auth℠** in Pre-Fill for Consumers. Example display of required fields and masking for personal information ## Display fields and requirements | Required display field | Display requirements | Editable field | | :--------------------- | :--------------------------------- | :------------- | | First Name | Yes — unmasked | Yes | | Last Name | Yes — unmasked | Yes | | Address | Yes — unmasked | Yes | | Extended Address | Yes — unmasked | Yes | | City | Yes — unmasked | Yes | | State | Yes — unmasked | Yes | | Postal Code | Yes — unmasked | Yes | | Phone Number | Yes — unmasked | No | | Social Security Number | Mask first five, display last four | Yes\* | | Date of Birth | Yes — MM/DD/YYYY | Yes\*\* | If cusomter edits SSN, clear data and require the customer to enter full SSN. If customer edits date of birth, clear the data and require the customer to enter the full date of birth. | Required display field | Display requirements | Editable field | | :--------------------- | :--------------------------------- | :----------------------------------------- | | First Name | Yes — unmasked | Yes | | Last Name | Yes — unmasked | Yes | | Address | Yes — unmasked | Yes | | Extended Address | Yes — unmasked | Yes | | City | Yes — unmasked | Yes | | State | Yes — unmasked | Yes | | Postal Code | Yes — unmasked | Yes | | Phone Number | Yes — unmasked | No | | Social Security Number | Mask first five, display last four | No — full SSN captured earlier in the flow | | Date of Birth | Yes — MM/DD/YYYY | Yes\* | If customer edits date of birth, clear the data and require the customer to enter the full date of birth. ### KYC confirmation copy (Pre-Fill with KYC) | Placement | Copy | Optional UI | | :---------------------------------------- | :-------------------------------------------------------------------- | :------------------------ | | Immediately before the **Submit** control | "I have reviewed the information provided and confirm it's accurate." | Confirmation **checkbox** | # Challenge page requirements (without Mobile Auth℠) Source: https://developer.prove.com/reference/prove-pre-fill-challenge-page-requirements Required page summary copy, phone number capture, challenge-field options, and no-mobile path for the Challenge step when not using Mobile Auth℠ (Pre-Fill for Consumers). ## Scope Required UI copy and challenge-field rules for the **Challenge** screen in Pre-Fill for Consumers when **Mobile Auth℠ is not** used. Challenge Page user interface example ## Page summary | Element | Required copy | | :----------------------- | :------------------------------------------------------------------------------------- | | Page summary language | Let's Begin by Finding Your Information | | Page summary description | We can prefill some of this request like your name, address, and contact info for you. | ## Challenge data by contracted solution | Contracted solution | Customer prompt language requirements | Required challenge data | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Prove Pre-Fill | Customer can choose one from the following prompts:
"Last 4 SSN/ITIN"
"SSN/ITIN"
"MM/DD/YYYY"
"MM/YYYY"
"MM/DD" | Last 4 SSN/ITIN
Full SSN\*
Full DOB
DOB - Month and Year
DOB - Month and Day

**If the customer is applying to open a demand deposit account (DDA), the customer must enter their full social security number (SSN) for the challenge.** | | Prove Pre-Fill with KYC | "Full SSN/ITIN" | Full SSN | ### Phone number field Challenge Page phone number entry example Phone number entry must match the layout and behavior shown above. ## Customers without a mobile number End the Prove flow and present a **manual** form. The customer may still continue the broader application outside Prove. # Pre-filled consumer review page requirements (without Mobile Auth℠) Source: https://developer.prove.com/reference/prove-pre-fill-pre-filled-consumer-review-page-requirements Required field display, masking, edit behavior, and KYC confirmation copy for the customer review step when not using Mobile Auth℠ (Pre-Fill for Consumers). ## Scope UI rules for the **pre-filled consumer review** screen in Pre-Fill for Consumers when **Mobile Auth℠ is not** used. Example display of required fields and masking for personal information ## Display fields and requirements | Required display field | Display requirements | Editable field | | :--------------------- | :--------------------------------- | :------------- | | First Name | Yes — unmasked | Yes | | Last Name | Yes — unmasked | Yes | | Address | Yes — unmasked | Yes | | Extended Address | Yes — unmasked | Yes | | City | Yes — unmasked | Yes | | State | Yes — unmasked | Yes | | Postal Code | Yes — unmasked | Yes | | Phone Number | Yes — unmasked | No | | Social Security Number | Mask first five, display last four | Yes\* | | Date of Birth | Yes — MM/DD/YYYY | Yes\*\* | If cusomter edits SSN, clear data and require the customer to enter full SSN. If customer edits date of birth, clear the data and require the customer to enter the full date of birth. | Required display field | Display requirements | Editable field | | :--------------------- | :--------------------------------- | :----------------------------------------- | | First Name | Yes — unmasked | Yes | | Last Name | Yes — unmasked | Yes | | Address | Yes — unmasked | Yes | | Extended Address | Yes — unmasked | Yes | | City | Yes — unmasked | Yes | | State | Yes — unmasked | Yes | | Postal Code | Yes — unmasked | Yes | | Phone Number | Yes — unmasked | No | | Social Security Number | Mask first five, display last four | No — full SSN captured earlier in the flow | | Date of Birth | Yes — MM/DD/YYYY | Yes\* | If customer edits date of birth, clear the data and require the customer to enter the full date of birth. ### KYC confirmation copy (Pre-Fill with KYC) | Placement | Copy | Optional UI | | :---------------------------------------- | :-------------------------------------------------------------------- | :------------------------ | | Immediately before the **Submit** control | "I have reviewed the information provided and confirm it's accurate." | Confirmation **checkbox** | # Verify your mobile page requirements (Pre-Fill, without Mobile Auth℠) Source: https://developer.prove.com/reference/prove-pre-fill-verify-your-mobile-page-requirements Required OTP and Instant Link screen copy for the verify-mobile step in Pre-Fill for Consumers when Mobile Auth℠ is not used. ## Scope UI rules for the **verify mobile** step (OTP and Instant Link) in Pre-Fill for Consumers when **Mobile Auth℠ is not** part of the flow. ## OTP OTP entry screen example ### Page summary | Flow | Required copy | | :----- | :----------------------------------------------------------------------------- | | Mobile | Enter verification code. Please enter the code we just sent to (XXX) XXX-XXXX. | ## Instant Link Instant Link screen example ### Page summary | Flow | Required copy | | | :------ | :------------------------------------------------------------------------------------------------------------------------------- | - | | Desktop | Check your Phone. A text message with a link was just sent to the phone ending in XXXX (the last 4 digits of the mobile number). | | ## Alternate path If the customer does not complete **Instant Link** or **SMS OTP**, they **exit the Prove flow**. Provide a **manual** verification path outside Prove. # Revoke Device Source: https://developer.prove.com/reference/revoke-request post /v3/device/revoke This endpoint allows you to revoke a Prove Key device, marking it as inactive so it can no longer be used in an auth flow. # Start page requirements - when Mobile Auth℠ fails Source: https://developer.prove.com/reference/start-page-requirements-when-mobile-authsm-fails Required page summary copy, phone number capture, and opt-out behavior for the Start step when Mobile Auth℠ does not succeed or when using the Desktop flow (Pre-Fill for Consumers). ## Scope Required UI copy and field behavior for the **Start** screen when the customer does not complete **Mobile Auth℠** successfully or uses the **Desktop** authentication flow in Pre-Fill for Consumers. Mobile Number Collection Page screenshot ## Page summary | Element | Required copy | | :----------------------- | :-------------------------------------------------------------------- | | Page summary language | Let's Get Started | | Page summary description | Please enter your phone number to begin the account creation process. | ## Phone number field Prompt for the mobile number **inside** the phone number input (placeholder or equivalent). ## Opt out | Control | Behavior | | :------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | **I don't have a mobile number** | Ends the Prove flow and routes the customer to a **manual** form so they can continue the application without a mobile number. | # Errors and status codes Source: https://developer.prove.com/reference/status-and-error-codes HTTP status values, JSON application error codes, rate-limit headers, and replay-related responses for Prove Platform APIs. ## Scope This page lists **HTTP status codes**, **`code` / `message` pairs** on request errors, **rate-limit response headers**, and **replay** behavior. It complements operation-specific pages in **Reference**. ## API conventions JSON responses omit: * Optional fields with no value. * Empty JSON objects. * Empty JSON arrays. ## HTTP status codes | Code | Definition | Description | | :--- | :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 200 | OK | The request completed successfully. | | 400 | Bad Request | There was a problem with the submitted request. | | 401 | Unauthorized | The request lacks valid authentication credentials for the target resource. Check for missing characters or whitespace. Confirm region (US or EU) and tier (Production or Sandbox) match the endpoint. | | 403 | Forbidden | The client doesn't have permission to access the target resource. | | 404 | Not Found | The server didn't find anything matching the Request-URI. | | 429 | Too Many Requests | The rate limit of 25 requests per second has been exceeded. Wait for the time specified in the `Retry-After` header before making more requests. | | 500 | Internal Server Error | The server encountered an unexpected condition preventing it from fulfilling the request. Retry the request, and if the problem persists, contact Prove support. | ### HTTP 403 A **403 Forbidden** response means the client isn't allowed to access the resource as requested. **Typical causes** * **Incorrect URL:** Typo in the path, wrong host or region, or invalid path segment. A common mistake is treating the [token](/reference/token-request) URL like a versioned API path (for example appending **`/v3`** to the token endpoint). * **Incorrect HTTP method:** The endpoint expects a specific verb (**POST**, **GET**, and so on). The wrong method can produce **403 Forbidden**. **Resolution** * **URL:** Match the domain, full path, and any version prefix to the reference for the operation. * **Method:** Use the HTTP verb documented for that endpoint. * **Request shape:** Align headers, body, and `Content-Type` with that operation's reference. ## API error codes On request errors, the JSON body includes a **`code`** field and a **`message`** field. | Error code | Description | Resolution path | | :--------- | :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 8000 | Internal Error | The server encountered an unexpected condition preventing it from fulfilling the request. Retry the request, and if the problem persists, contact Prove support. | | 8001 | Malformed Request | Request must be valid JSON, at most 4 KB, with valid parameters. Invalid parameters and reasons appear in `message`. | | 8002 | Unauthorized Request | The request lacks valid authentication credentials for the target resource. Use the correct credentials. | | 8003 | Step Called Out of Order | The request called an endpoint out of order. Use the `next` field for the correct endpoint. | | 8007 | Sandbox User Not Found | Use test user inputs only unless the test case specifies otherwise. | | 8008 | Invalid Correlation ID | Correlation ID must be valid and unique per session. | | 8009 | Sandbox Test User Access Denied | Product credentials must match the test user’s Prove solution. | | 8010 | Unauthorized for Country | **(1)** Tenant not authorized to verify numbers for that country — contact your Prove representative. **(2)** Region mismatch — EU number sent to the US endpoint or the reverse; use the endpoint that matches the phone number. | | 8011 | Identity Not Found | No identity exists for the provided Identity ID. | | 8019 | Device has been revoked | Returned when the customer tries to authenticate with a **phone number** tied to a **revoked** Prove Key. **Resolution:** Confirm with the customer, then start a **new** [`POST /v3/unify`](/reference/unify-request) session and complete possession. | ### Unify possession error codes If Unified Authentication possession is not successful, review `evaluation` from [`POST /v3/unify-status`](/reference/unify-status-request) and apply the [Global Fraud Policy](/reference/global-fraud-policy). | Error code | Description | Resolution path | | :--------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 9100 | Phone possession incomplete | Possession did not complete in mobile or desktop flow. Run the flow again and confirm the client completed possession before calling `/v3/unify-status`. | | 9101 | Phone number doesn't match Prove Key | The provided phone number doesn't match the existing key binding. Restart with `rebind=true` on [`POST /v3/unify`](/reference/unify-request) to force full possession and rebind on success. | When you use `rebind=true`: 1. Prove starts a new possession check. 2. On successful possession, Prove rebinds the key to the phone number in that request. 3. Future authentications for that number can proceed without rebind. ## Rate limiting Prove APIs enforce **25 requests per second**. Above that limit, responses use **HTTP 429** and include: | Header | Description | | :---------------------- | :--------------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed per second | | `X-RateLimit-Remaining` | Requests remaining in the current window | | `X-RateLimit-Reset` | Unix epoch seconds when the limit resets | | `Retry-After` | Seconds to wait before another request | ### Example rate limit response ```json theme={"dark"} { "code": 8018, "message": "Rate limit exceeded. Please wait before making more requests." } ``` **Rate limit handling** * `X-RateLimit-Remaining` reflects remaining quota in the window. * After **429**, wait at least **`Retry-After`** seconds before retrying; exponential backoff is appropriate for repeated throttling. * Spread traffic over time instead of bursts. ## Replay after errors After a **non-200** response, a corrected request may be retried. After a **200** response for a step, **repeating the same request** returns **HTTP 403** with a message such as **step called out of order** or **correlation ID is expired or invalid**, limiting replay and replay-style abuse. # Request OAuth Token Source: https://developer.prove.com/reference/token-request openapi-token.yaml post /token This endpoint allows you to request an OAuth token. # Bind Prove Key Source: https://developer.prove.com/reference/unify-bind-request openapi-unify.yaml post /v3/unify-bind This endpoint allows you to bind a Prove Key to a phone number of a Unify session and get the possession result. # Initiate Possession Check Source: https://developer.prove.com/reference/unify-request openapi-unify.yaml post /v3/unify This endpoint allows you to initiate the possession check. # Check Status Source: https://developer.prove.com/reference/unify-status-request openapi-unify.yaml post /v3/unify-status This endpoint allows you to check the status of a Unify session and get the possession result. # US Carrier (MNO) Consent Requirements Source: https://developer.prove.com/reference/us-carrier-mno-consent-requirements US mobile network operator consent text, UI placement, and approval inputs required for Prove integrations that use carrier data under MNO programs. ## Applicability The following US mobile network operators (MNOs) impose consent and disclosure requirements on use of subscriber or device data in relevant Prove services (for example **Mobile Auth** in [Pre-Fill for Consumers](/explanation/pre-fill-for-consumers-overview) and similar flows). Contractual obligations in your **MSA** and carrier addenda take precedence where they differ from or extend this page. | MNO | Requirement set | | -------- | ------------------------------------------------------------------- | | Verizon | In scope | | T-Mobile | In scope | | AT\&T | In scope; additional carrier- or agreement-specific terms may apply | ## Information required for MNO approval For T-Mobile and related approvals, Prove typically requires the following **artifacts and metadata** (exact package is defined in your MSA and onboarding materials): | Item | Requirement | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Production URLs | Final URLs where the MNO consent language is published in Production. | | Go-live timeline | Estimated date when consent language is live in Production. **T-Mobile** approval is contingent on language being live. | | UI mock-up | Screen capture or design showing where the customer accepts carrier terms. Acceptance may use **either** inline copy such as **by clicking *Continue* you agree to our T\&C** **or** a dedicated consent control (for example a checkbox). | Example MNO carrier consent placement in a customer flow ## Required consent language "You authorize your wireless carrier to use or share information about your account and your wireless device, if available, to `` or its service provider during your business relationship, to help identify you or your wireless device and to prevent fraud. See our Privacy Policy for how we treat your data." Replace `` with your legal entity or product name as agreed with Prove. The MNOs require customers to accept carrier consent language in the customer flow before calling the endpoint in scope. ## Required language placement | Rule | Specification | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Timing | The customer must accept the MNO consent language **before** any flow step that invokes Prove **carrier-assisted** checks covered by these MNO programs (for example **Mobile Auth** prior to downstream Pre-Fill or verification API calls). | | Page type | MNO terms and conditions may appear on the **Landing Page** or the **Challenge Page**, per your approved UX and MSA. | | Mobile Auth | When **Mobile Auth** is enabled, the MNO consent language must appear on the **Landing Page** and be referenced from your **Terms and Conditions** (or equivalent legal surface). | # Verified User Connect Source: https://developer.prove.com/reference/verified-users-connect Learn how to use Prove Connect for secure batch identity verification via SFTP Connect is the Prove Platform **batch verification** interface. Customers upload CSV files over **SFTP**; each valid row is verified with **[`POST /v3/verify`](/reference/verify)**. Rows that pass the **Global Fraud Policy** are enrolled in **Identity Manager**. Responses match the same fields as synchronous [`POST /v3/verify`](/reference/verify) calls. Connect supports the `verifiedUser` and `humanAssurance` verification type. ## Scope | Item | Specification | | ------------ | ----------------------------------------------------------------------------- | | Transport | SFTP to dedicated directories (`ToProve` / `FromProve`) | | Verification | [`POST /v3/verify`](/reference/verify) per valid detail row | | Enrollment | Successful verifications enrolled in Identity Manager | | Result shape | Same parameters as real-time [`POST /v3/verify`](/reference/verify) responses | ## Provisioning The following items are provided during onboarding, and some correspond to fields in the input file. See [Input file format](#input-file-format). | Credential | Description | | ----------------- | --------------------------------------------- | | Enroll Key | Validates file authenticity | | Verification Type | `verifiedUser` or `humanAssurance` | | Directory Paths | `ToProve` (upload) and `FromProve` (download) | | SFTP Credentials | Authentication for file transfer | ## Processing Batch processing follows this sequence (see also the diagram). ```mermaid theme={"dark"} sequenceDiagram participant Customer participant SFTP as Prove participant API as v3/verify API participant IDM as Identity Manager Customer->>SFTP: Upload CSV to `ToProve` folder loop Each valid record SFTP->>API: POST /v3/verify API->>SFTP: Verification result alt Passes Global Fraud Policy SFTP->>IDM: Enroll identity end end SFTP->>Customer: Receive email notification SFTP->>Customer: Download output file in `FromProve` folder ``` | Stage | Behavior | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Upload | CSV is placed in the provisioned **`ToProve`** directory via SFTP. | | Validation | Each row is checked for required fields, phone format and country code, and region consistency with the file. If **authorization** checks fail, the **entire file** is rejected and an error response is written under **`FromProve`**. | | Verification | Valid rows are sent to **[`POST /v3/verify`](/reference/verify)**; response fields align with the live API. | | Enrollment | Rows that pass the Global Fraud Policy are enrolled in Identity Manager. | | Delivery | When processing completes, an **email** notification is sent; the output file is available under **`FromProve`**. | ## File requirements Input and output CSVs use the structures below. File names must follow the naming pattern. ### Naming convention `{client_name}_{region}_{verification_type}_YYYYMMDDHHMMSS.csv` | Component | Values | Description | | ------------------- | ---------------------------------- | -------------------------------------------- | | `client_name` | Agreed during setup | Organization identifier | | `region` | `US` or `INTL` | `US` — US and Canada; `INTL` — international | | `verification_type` | `verifiedUser` or `humanAssurance` | Verification type | ### Input file format Each batch file uses a **Header → Detail(s) → Trailer** layout so Prove can validate the service type and record count. #### Header record (input) | Column | Sample value | Required | | ----------------- | ---------------------------------- | ---------------- | | Record Type | `H` | Yes — always `H` | | Enroll Key | `SecretKey` | Yes | | Verification Type | `verifiedUser` or `humanAssurance` | Yes | #### Detail records (input) | Column | Sample value | Required | Description | | ------------------ | -------------- | -------------------------- | ------------------------------------------------------------------- | | Record Type | `D` | Yes — always `D` | | | Phone Number | `+12008040444` | Yes — include country code | | | First Name | `John` | Yes for `verifiedUser` | | | Last Name | `Doe` | Yes for `verifiedUser` | | | Client Customer ID | `zaq12wsx` | No | | | Attribute Type | `userId` | Yes for partners | Metadata type for the attribute value. | | Issuer ID | `AcmeWallet` | Yes for partners | Partner company name. | | Attribute Value | `A4B1F13BCCFC` | Yes for partners | Non-PII unique value for the consumer for the given attribute type. | #### Trailer record (input) | Column | Sample value | Required | | ----------------- | ------------ | ---------------- | | Record Type | `T` | Yes — always `T` | | Number of Records | `132789` | Yes | There is **no formal cap** on input file size or detail record count per file. When splitting work across uploads, Prove recommends **800,000 detail records or fewer per file**. Verification throughput is subject to **Data Partner transactions-per-second (TPS)** limits; **completion time** varies with detail record count and effective throughput. #### Example file (input) ```csv theme={"dark"} H,ENROLL_KEY_US_123,verifiedUser D,+12008040444,Sara,Hu,xsw23edc,userId,AcmeWallet,A4B1F13BCCFC D,+12005551234,John,Doe,abc12345,,, T,2 ``` ### Output file format After processing, Prove writes an output file to **`FromProve`**. It mirrors the input layout and appends verification fields (for example assurance level, identity attributes, status). #### Header record (output) | Column | Sample value | Notes | | ----------- | ------------ | ---------- | | Record Type | `H` | Always `H` | #### Detail records (output) | Column | Sample value | Notes | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------- | | Record Type | `D` | Always `D` | | Phone Number | `+12008040444` | | | First Name | `John` | | | Last Name | `Doe` | | | Client Customer ID | `zaq12wsx` | If present on input | | Attribute Type | `userId` | | | Issuer ID | `AcmeWallet` | | | Attribute Value | `A4B1F13BCCFC` | | | Assurance Level | `AL2` | If successful | | Assurance Level Reason Codes | `AL2a`, `AL2b` | If successful | | Prove Phone Alias | `474628DC4VK83842DB25DA4B1F13BCCFC0MEK4P664Z9PCC6E6C3CB92DC40191E5E868E5E38B1AC85F6G359B74880097B63C51E7E8636B33FA344CB8E` | If successful | | ProveID | `6b942541-abab-40ed-9970-5a28217836c0` | If successful | | Error | `200` | | | Error Message | `NA` | | | GFP Authentication Result | `pass` | If successful | | GFP Risk Result | `pass` | If successful | Only the top-level identity is included in the output; additional identities are omitted. #### Trailer record (output) | Column | Sample value | Notes | | ----------------- | ------------ | ---------- | | Record Type | `T` | Always `T` | | Number of Records | `132789` | | ### Error file format Rows that fail **validation** are written to a separate error file under **`FromProve`**, with machine-readable codes. | Error Code | Description | | ----------------------------------------- | ----------------------------------- | | `ERR_MISSING_FNAME` | First name is missing when required | | `ERR_MISSING_LNAME` | Last name is missing when required | | `ERR_MISSING_PHONE` | Phone number is missing | | `ERR_FORMAT_PHONE` | Invalid phone number format | | `ERR_PHONE_COUNTRY_NOTSUPPORTED` | Phone country not supported | | `ERR_DUPLICATE_RECORD_ORIGINAL_PRESERVED` | Duplicate record detected | US phone numbers must not appear in international-region files, and international numbers must not appear in US-region files; mismatches produce row errors. #### Header record (error file) | Column | Sample value | Notes | | ----------------- | ---------------------------------- | ---------- | | Record Type | `H` | Always `H` | | Verification Type | `verifiedUser` or `humanAssurance` | | #### Detail records (error file) | Column | Sample value | Notes | | ------------------ | ------------------- | ------------------- | | Record Type | `D` | Always `D` | | Phone Number | `+12008040444` | | | First Name | `John` | | | Last Name | `Doe` | | | Client Customer ID | `zaq12wsx` | If present on input | | Error Attribution | `ERR_MISSING_FNAME` | | #### Trailer record (error file) | Column | Sample value | Notes | | ----------------- | ------------ | ---------- | | Record Type | `T` | Always `T` | | Number of Records | `132789` | | ## Notifications ### Email alerts | Event | Email sent | Body | | --------------------------- | -------------------- | ----------------------------------------------- | | File rejected | Yes, when configured | Rejection details per onboarding configuration. | | File processed successfully | Yes, when configured | Includes **data quality statistics**.. | # Verify Source: https://developer.prove.com/reference/verify openapi.yaml POST /v3/verify Runs Prove verification flows in one endpoint. Set `verificationType` in the request body to select the flow. For **`/v3/verify`**, request and response fields depend on the **`verificationType`** and product flow. See the verify guides for flow-specific request and response details: * [Human Assurance](https://developer.prove.com/how-to/human-assurance-verify) * [Verified User](https://developer.prove.com/how-to/verified-users-verify) * [Account Opening](https://developer.prove.com/how-to/account-opening-verify) * [Pre-Fill for Consumers](https://developer.prove.com/how-to/pre-fill-for-consumers-verify) * [Pre-Fill for Business](https://developer.prove.com/how-to/pre-fill-for-business-verify) # Verify Your Mobile Page Requirements - Mobile Auth℠ Source: https://developer.prove.com/reference/verify-your-mobile-page-requirements-mobile-authsm Required page summary copy for the Verify your mobile step when using Mobile Auth with SMS OTP (mobile) or Instant Link (desktop) in Pre-Fill for Consumers. ## Scope UI rules for the **verify mobile** step (OTP and Instant Link) in Pre-Fill for Consumers when using \*\*Mobile Auth℠. ## OTP OTP entry screen example ### Page summary | Flow | Required copy | | :----- | :----------------------------------------------------------------------------- | | Mobile | Enter verification code. Please enter the code we just sent to (XXX) XXX-XXXX. | ## Instant Link Instant Link screen example ### Page summary | Flow | Required copy | | | :------ | :------------------------------------------------------------------------------------------------------------------------------- | - | | Desktop | Check your Phone. A text message with a link was just sent to the phone ending in XXXX (the last 4 digits of the mobile number). | | ## Alternate path If the customer does not complete **Instant Link** or **SMS OTP**, they **exit the Prove flow**. Provide a **manual** verification path outside Prove. # Get started with Prove API authentication Source: https://developer.prove.com/tutorial/access-api-keys Obtain Sandbox credentials from the Developer Portal, request an OAuth 2.0 bearer token, and make your first authenticated Platform API call. ## Get started **Prerequisites** * **Portal account** — A registered account on the [Portal](https://portal.prove.com/auth/login). * **Local tools** — A terminal with **cURL**, or an HTTP client such as Postman, to run the requests in this tutorial. **What you'll learn** By the end of this tutorial, you will have: * Opened a **project** in the Developer Portal and copied your Sandbox **client ID** and **client secret** * Exchanged them for an **OAuth 2.0 bearer token** using `POST /token` Sign in to the Developer Portal. Select **Create Project**. Select an existing project or create one. Create a project, choose the solution that matches your use case, name the project, then select **Create Project**. Open the **Credentials** tab for your project. Copy the **client ID** and **client secret** you will use for Sandbox (`uat`). You will paste them into the token request in the next step. Prove Platform APIs use **OAuth 2.0 client credentials**. Send a **POST** to the **token** endpoint with `Content-Type: application/x-www-form-urlencoded` and your values in place of the placeholders. If you are testing outside North America, use the **EU** token host and the **International** credentials from your project: `https://platform.uat.eu.proveapis.com/token`. ```bash Request bearer token (US Sandbox) theme={"dark"} curl -X POST https://platform.uat.proveapis.com/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET" ``` ```bash Request bearer token (EU Sandbox) theme={"dark"} curl -X POST https://platform.uat.eu.proveapis.com/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET" ``` ```json Example token response theme={"dark"} { "access_token": "eyJ...", "refresh_token": "eyJ...", "refresh_expires_in": 3600, "token_type": "Bearer", "expires_in": 3600 } ``` Copy the **`access_token`** value. You will send it as a **Bearer** token on API calls. If a Platform request returns **HTTP 403 Forbidden**, double-check the **URL**, **HTTP method**, and **Content-Type** for that operation. A frequent mistake is treating the [token](/reference/token-request) URL like a versioned API path (for example adding **`/v3`**). See [HTTP 403](/reference/status-and-error-codes#http-403) on the errors reference page. # Build Prove with LLMs Source: https://developer.prove.com/explanation/build-with-llm How Prove’s documentation is structured for machine readability and LLM-assisted integration work. ## Concepts and definitions To better understand the technical infrastructure of Prove’s documentation, here are the key concepts and terms used: * **Machine Readability:** The design of content in a format that can be processed and "understood" by computer programs or AI, rather than just being optimized for human visual consumption. * **LLM (Large Language Model):** AI systems that process and generate human-like text, commonly reached through chat interfaces or APIs. ## Plain text docs Prove designs its documentation for AI consumption. Every page maintains a parallel Markdown representation accessible via the `.md` extension—for example, [this page as Markdown](https://developer.prove.com/explanation/build-with-llm.md). That helps AI tools and agents consume Prove content. | Feature | Markdown (.md) | HTML/JS Rendered Pages | | :----------------------- | :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | | **Token Efficiency** | **High.** Minimal syntax means more actual content fits in the context window. | **Low.** Dense with tags (`
`, ``) and scripts that waste tokens. | | **Data Extraction** | **Direct.** Content is ready to be parsed as-is without extra processing. | **Complex.** Requires a browser engine to execute JS before content is visible. | | **Content Visibility** | **Complete.** Includes all text, including content hidden in UI tabs or toggles. | **Partial.** Hidden or "lazy-loaded" content is often missing from the initial scrape. | | **Contextual Hierarchy** | **Explicit.** Headers (`#`, `##`) signal the importance and relationship of data. | **Inferred.** AI must guess hierarchy based on nested tags or CSS classes. | | **Formatting Noise** | **Minimal.** Focuses on the data, reducing the risk of the AI getting distracted. | **Significant.** Inline styles and attributes add "noise" to the signal. | Prove hosts /llms.txt and /llms-full.txt files which instruct AI tools and agents how to retrieve the plain text versions of Prove pages. The /llms.txt file is an [emerging standard for making websites and content more accessible to LLMs](https://llmstxt.org/). ## Contextual menu Plain Markdown and `/llms.txt` help tools *find* content. The harder part is getting the **page you are reading** into an assistant or agent **without fragile copy-paste**. The **contextual menu** at the top of each page is where you turn the current doc into **grounding context**: copy text, open Markdown, or connect to assistants and editor tooling. ## Model Context Protocol (MCP) The Prove [Model Context Protocol (MCP)](https://developer.prove.com/explanation/model-context-protocol) exposes tools that AI agents can use to search Prove’s documentation and read full pages from a virtual documentation filesystem. # Model Context Protocol (MCP) Source: https://developer.prove.com/explanation/model-context-protocol What Prove’s hosted Model Context Protocol server is—how assistants search documentation and read pages—and how to connect using your own MCP-capable client. ## Prove MCP overview The Prove **Model Context Protocol (MCP)** server exposes tools to connected AI clients so assistants can **search** Prove documentation and **read** full pages from a virtual documentation filesystem. It complements other doc access patterns such as [plain Markdown and `llms.txt`](https://developer.prove.com/explanation/build-with-llm). If your team uses AI-powered editors, you can point your client at Prove’s **hosted** MCP endpoint—no separate Prove package to run on your infrastructure for this service. ## Hosted endpoint Prove hosts an [MCP server](https://developer.prove.com/mcp). Register this URL in your MCP client using that product’s workflow for **HTTP** MCP servers. Exact steps depend on the vendor; follow their documentation for adding or enabling MCP servers, ensure your network allows HTTPS to `developer.prove.com`, then confirm the **prove** (or equivalent) server appears connected in the client’s MCP or tools UI. ## MCP resources (`skill.md`) The MCP server exposes **[`skill.md` files](https://mintlify.com/docs/ai/skillmd) as MCP resources** so agents discover capability descriptions without installing those files separately. Resources appear in the MCP resource list alongside the tools below. ## Tools available to assistants Tools are exposed to connected AI clients as follows. ### `search_prove` Search across the Prove knowledge base to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about Prove, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages. If you need the full content of a specific page, use the `query_docs_filesystem_prove` tool to `head` or `cat` the page path (append `.mdx` to the path returned from search — for example, `head -200 /api-reference/create-customer.mdx`). Optional parameters for `search_prove` (when the client passes them through) include: * **`pageSize`** — Number of results to return, between **1** and **50**; defaults to **10**. * **`scoreThreshold`** — Minimum relevance score between **0** and **1**; filters out lower-confidence matches. * **`version`** — Restrict results to a documentation version tag. ### `query_docs_filesystem_prove` Run a read-only shell-like query against a virtualized, in-memory filesystem rooted at `/` that contains **only** the Prove documentation pages and OpenAPI specs. This is **not** a shell on any real machine — nothing runs on the user's computer, the server host, or any network. The filesystem is a sandbox backed by documentation chunks. This is how you read documentation pages: there is no separate “get page” tool. To read a page, pass its `.mdx` path (for example, `/quickstart.mdx`, `/api-reference/create-customer.mdx`) to `head` or `cat`. To search the docs with exact keyword or regex matches, use `rg`. To understand the docs structure, use `tree` or `ls`. **Workflow:** Start with the search tool for broad or conceptual queries like “how to authenticate” or “rate limiting”. Use `query_docs_filesystem_prove` when you need exact keyword or regex matching, structural exploration, or to read the full content of a specific page by path. **Supported commands:** `rg` (ripgrep), `grep`, `find`, `tree`, `ls`, `cat`, `head`, `tail`, `stat`, `wc`, `sort`, `uniq`, `cut`, `sed`, `awk`, `jq`, plus basic text utilities. No writes, no network, no process control. Run `--help` on any command for usage. **Stateless calls:** Each call is stateless: the working directory always resets to `/` and no shell variables, aliases, or history carry over between calls. If you need to operate in a subdirectory, chain commands in one call with `&&` or pass absolute paths (for example, `cd /api-reference && ls` or `ls /api-reference`). Do **not** assume that `cd` in one call affects the next call. **Examples** * `tree / -L 2` — see the top-level directory layout * `rg -il "rate limit" /` — find all files mentioning “rate limit” * `rg -C 3 "apiKey" /api-reference/` — show matches with three lines of context around each hit * `head -80 /quickstart.mdx` — read the top 80 lines of a specific page * `head -80 /quickstart.mdx /installation.mdx /guides/first-deploy.mdx` — read multiple pages in one call * `cat /api-reference/create-customer.mdx` — read a full page when you need everything * `cat /openapi/spec.json | jq '.paths | keys'` — list OpenAPI endpoints Output is truncated to 30KB per call. Prefer targeted `rg -C` or `head -N` over broad `cat` on large files. To read only the relevant sections of a large file, use `rg -C 3 "pattern" /path/file.mdx`. Batch multiple file reads into a single `head` or `cat` call whenever possible. ## Rate limits The following **hourly rate limits** are used so the service stays available: * **Per user (IP address)** — **5,000** requests per hour for **MCP server configuration** queries. * **Search** — **10,000** search tool calls per hour for the public MCP path; **5,000** per hour when using **authenticated** search quotas. * **Query docs filesystem** — **10,000** filesystem tool calls per hour for the public MCP path. These limits are **in addition** to the **\~30 KB per-call** response truncation for `query_docs_filesystem_prove` described above. If you hit throttling, narrow queries, batch reads, and space out tool calls. # Secure API credentials Source: https://developer.prove.com/how-to/secure-api-credentials How to store, rotate, and govern Prove client IDs, client secrets, and bearer tokens in development and production. Secret API keys and client credentials behave like account passwords. If they are exposed, they can be abused against your integration and data. You are responsible for protecting Prove credentials in your environments. Use this guide as a baseline; align it with your security program and any obligations in your agreement with Prove. ## Protect against compromised credentials * **Use a key management system (KMS) or secrets manager for production secrets.** When you create or rotate a production client secret, store it only in a system designed for secrets (encryption at rest, access control, audit). Avoid leaving copies in local files or shared drives. * **Limit who can create, read, or rotate keys.** Define ownership, use least privilege, and review access periodically. * **Do not share secrets in email, chat, or support tickets.** Use approved secret-handling channels only. * **Do not commit secrets to source control.** Public repositories are actively scanned; private repos still leak through clones and tooling. Use environment variables or your CI/CD secret store, never literals in code. * **Do not embed client secrets or long-lived tokens in client-side apps.** Mobile binaries and front-end JavaScript can be reverse-engineered; anything shipped to the device should be treated as public. * **Monitor API usage.** Review logs for unusual patterns and ensure Sandbox credentials are not used against Production endpoints (and vice versa). * **Train your team and document your process.** Keep internal runbooks current for onboarding, incident response, and rotation. * **Rotate credentials on a schedule and after suspected exposure.** Rotating client secrets and invalidating old values limits blast radius if a secret was leaked without your knowledge. For HTTP errors when calling Platform APIs, see [Errors and status codes](/reference/status-and-error-codes). # Assurance Levels Source: https://developer.prove.com/reference/assurance-levels Understand Prove's Assurance Levels (AL) and how they indicate the confidence in a verified identity. ## Definition In [`POST /v3/verify`](/reference/verify) responses, the **identity** object includes: | Field | Role | | :--------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `assuranceLevel` | Tier such as `AL-1`, `AL0`, `AL1`, `AL2`, or `AL3`. Higher tiers indicate stronger confidence in the association between the phone number and the identity. | | `reasons` | One or more reason codes from the tables below (for example `-1A`, `1B`). | Tiers are ordered from lowest (AL-1) to highest (AL3). ## Reason codes ### AL-1 | Reason code | Description | | :---------- | :----------------------------------------------------------------------------------------------------- | | -1A | Too many identities bound to a single phone number. | | -1F | Premium rate number globally or listed on the US Override Services Registry (OSR) registry. | | -1G | Online rentable temporary eSIM or VoIP number. | | -1H | Online rentable temporary eSIM or VoIP number, with high activity. This indicates fraudulent activity. | ### AL0 | Reason code | Description | | :---------- | :------------------------------------------------------------------------------------------------------------ | | 0B | Country doesn't give behavioral data. | | 0C | Assigned number with no behavioral activity. | | 0D | Short tenure, no activity. Reclassifies to AL0E if activity appears. | | 0E | Short tenure with behavioral activity present. | | 0F | Activity volume exceeds normal human thresholds. | | 0G | Pagers or other non-standard line types. Behavioral signals incompatible with standard identity verification. | ### AL1 | Reason code | Description | | :---------- | :-------------------------------------------------------------------------------------------------------- | | 1A | Enough ownership tenure and longitudinal human behavior. | | 1B | Tenure and longitudinal human behavior with Prove binding phone to a specific identity, superseding AL1A. | | 1D | Low longitudinal behavior but human signals such as ports, calls, or logins indicate not an eSIM bot. | ### AL2 | Reason code | Description | | :---------- | :---------------------------------------------------------------------------- | | 2A | Prove has seen this phone and identity before at moderate-to-high confidence. | | 2B | Prove corroborated data across more than one identity data source. | ### AL3 | Reason code | Description | | :---------- | :------------------------------------------------------------------------------------------------------ | | 3A | Prove has seen this phone and identity before at high confidence, this is this person's primary number. | # Global fraud policy Source: https://developer.prove.com/reference/global-fraud-policy Evaluation categories and failure reason codes in the evaluation object on Prove Platform API responses. ## Definition The **Global Fraud Policy (GFP)** drives pass/fail outcomes and machine-readable failure reasons in the **`evaluation`** object returned by Prove Platform APIs (for example [`POST /v3/complete`](/reference/complete-request)). GFP classifies each transaction into one or more of the following categories: | Category | Evaluates | | :------------- | :----------------------------------------------------------------------------------------------------- | | Identification | Consistency of personal details (for example name, address, date of birth, national identifier). | | Authentication | Whether the claimant matches the identity and phone possession signals. | | Compliance | Regulatory and program requirements for the industry and use case. | | Risk | Factors outside identification and authentication (for example behavioral or carrier-related signals). | ## Response examples Representative **`evaluation`** payloads from **`POST /v3/complete`**. **Pass** ```json theme={"dark"} { "success": true, "next": { "done": "done" }, "evaluation": { "authentication": { "result": "pass" }, "identification": { "result": "pass" } } } ``` **Fail** (with `failureReasons`) ```json theme={"dark"} { "success": false, "next": { "done": "done" }, "evaluation": { "authentication": { "result": "fail", "failureReasons": { "9161": "Very short tenure association between phone number and identity.", "9171": "Potential injection attack due to 2 identities both with short tenure association to the phone number.", "9180": "No phone ownership confirmed using 4 elements." } }, "identification": { "result": "fail", "failureReasons": { "9001": "Identification isn't confirmed using 4 elements." } } } } ``` ## Failure reason codes | Code | Category | Description | | :--- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 9001 | Identification | Identification isn't confirmed using 4 elements. | | 9003 | Identification | Identity isn't found | | 9004 | Identification | Identification isn't confirmed using 4 elements with secondary source. | | 9005 | Identification | Identification isn't confirmed using 4 elements with more than 1 source. | | 9011 | Identification | Identification isn't confirmed using less than 4 elements. | | 9012 | Identification | SSN doesn't match. | | 9013 | Identification | Birth Date doesn't match. | | 9014 | Identification | Name doesn't match. | | 9015 | Identification | Address doesn't match. | | 9081 | Identification | SSN is issued before the birth date. | | 9082 | Identification | Address is a correctional facility. | | 9083 | Identification | Deceased indicator is present for individual. | | 9100 | Authentication | Phone possession incomplete. Occurs when `/validate` is called before the Mobile Auth or OTP possession flow completes. | | 9101 | Authentication | Phone number doesn't match Prove Key. Rebind required. | | 9131 | Authentication | Indicates the phone number is at a higher risk for fraud due to being listed as a non-mobile line on the Override Services Registry (OSR). | | 9132 | Authentication | This phone number is listed on a public website with the contents of its text messages posted publicly to allow a user to bypass phone possession check controls. | | 9133 | Authentication | The phone isn't trusted based on the SIM Key Trust Score. | | 9134 | Authentication | The phone isn't trusted because it's a non-fixed VoIP number. | | 9135 | Authentication | Potential account takeover (ATO) due to recent SIM Swap under 24 hours. | | 9136 | Authentication | Potential ATO due to recent Port under 24 hours. | | 9137 | Authentication | Potential ATO due to call forwarding enabled for phone number. | | 9161 | Authentication | Short tenure association between phone number and identity. | | 9162 | Authentication | Short tenure association between phone number and identity. | | 9163 | Authentication | Potential recycled phone fraud due to a high number of identities associated to phone number with a newer owner present. | | 9164 | Authentication | Potential recycled phone fraud due to identity having no recent association to phone number and a phone disconnect event is observed after the phone association. | | 9165 | Authentication | Potential recycled phone fraud due to a newer owner observed and a phone disconnect event is observed after the phone association. | | 9166 | Authentication | Address is a correctional facility. | | 9167 | Authentication | Deceased indicator is present for individual. | | 9168 | Authentication | Person verification was performed indicating phone ownership isn't verified | | 9171 | Authentication | Potential injection attack due to 2 identities both with short tenure association to the phone number. | | 9172 | Authentication | Potential injection attack due to 3 identities all with short tenure association to the phone number. | | 9173 | Authentication | Potential injection attack due to 4 identities all with short tenure association to the phone number. | | 9174 | Authentication | Potential injection attack due to 5 or more identities all with short tenure association to the phone number. | | 9175 | Authentication | No active identity can be associated with the phone number. | | 9176 | Authentication | The phone number and identity is strongly associated negative bot activity. | | 9177 | Authentication | No information can be found for the identity or phone number. | | 9180 | Authentication | No phone ownership confirmed using 4 elements. | | 9181 | Authentication | No phone ownership confirmed using less than 4 elements | | 9201 | Compliance | Anti-Money Laundering (AML) alerts are present for this identity. | | 9301 | Risk | No historical behavioral activity. | | 9302 | Risk | Suspicious large amount recent of activity. | | 9303 | Risk | Unusual amount of recent carrier phone number change events. | | 9304 | Risk | Suspicious large amount recent of activity across different identity attributes. | # Revoke Device Source: https://developer.prove.com/reference/revoke-request post /v3/device/revoke This endpoint allows you to revoke a Prove Key device, marking it as inactive so it can no longer be used in an auth flow. # Request OAuth Token Source: https://developer.prove.com/reference/token-request openapi-token.yaml post /token This endpoint allows you to request an OAuth token. # Bind Prove Key Source: https://developer.prove.com/reference/unify-bind-request openapi-unify.yaml post /v3/unify-bind This endpoint allows you to bind a Prove Key to a phone number of a Unify session and get the possession result. # Initiate Possession Check Source: https://developer.prove.com/reference/unify-request openapi-unify.yaml post /v3/unify This endpoint allows you to initiate the possession check. # Check Status Source: https://developer.prove.com/reference/unify-status-request openapi-unify.yaml post /v3/unify-status This endpoint allows you to check the status of a Unify session and get the possession result. # Verify Source: https://developer.prove.com/reference/verify openapi.yaml POST /v3/verify Runs Prove verification flows in one endpoint. Set `verificationType` in the request body to select the flow. For **`/v3/verify`**, request and response fields depend on the **`verificationType`** and product flow. See the verify guides for flow-specific request and response details: * [Human Assurance](https://developer.prove.com/how-to/human-assurance-verify) * [Verified User](https://developer.prove.com/how-to/verified-users-verify) * [Account Opening](https://developer.prove.com/how-to/account-opening-verify) * [Pre-Fill for Consumers](https://developer.prove.com/how-to/pre-fill-for-consumers-verify) * [Pre-Fill for Business](https://developer.prove.com/how-to/pre-fill-for-business-verify)