Credential-less Discovery

Credential-less discovery

Let a user find their bills from their identity — no per-biller usernames or passwords. They enter a few identity fields, a provider returns the accounts it discovered across billers, and they one-tap to link them into the same Connect pipeline you already use.

Sandbox-only, default-OFF, preview surface

Credential-less discovery is a sandbox/mock feature and is disabled by default. The mock aggregator returns synthetic liabilities for a synthetic identity — it never touches a real credit bureau, real accounts, or real PII. The backend endpoint is gated by the CREDENTIALLESS_DISCOVERY_ENABLED flag on sandbox-service and returns 404 while off. A real Method / Spinwheel / bureau adapter can slot in later behind the CredentiallessDiscoveryProvider seam — but only after the FCRA/GLBA permissible-purpose + legal sign-off called out in Before production.

What it is & when to use it

The default Connect flow asks the user for the credentials of each biller they want to link. Credential-less discovery is a different entry point: the user supplies their identity instead, an aggregator returns the accounts it can attribute to that identity, and the user picks which ones to link. "Credential-less" means no per-biller username/password — it does not mean no link token.

The link token still rides over the SDK INIT message exactly like the embedded Connect flow. Once the user selects accounts, the existing account-selection → link pipeline completes and fires the link.completed webhook — identical to a credentialed link. Nothing downstream changes.

It reuses the Connect pipeline you already have

Discovery injects into the standard sandbox flow at the account-selection step. You mint the same sandbox link token, and the same link.completed webhook is the authoritative signal that a link succeeded. See the Elements SDK and Webhooks guides for the pieces that carry over unchanged.

Quick start

Construct a BillerApiElements client, then call discover() with a sandbox link token and a host element. Discovery renders inline into the element you provide — there is no modal overlay.

import { BillerApiElements } from 'billerapi-js';

// 1. One client for every flow
const elements = new BillerApiElements({
  clientId: 'your_client_id',
  environment: 'sandbox', // credential-less discovery is sandbox-only
});

// 2. Launch inline discovery with a sandbox link_token from your server
const handler = elements.discover({
  linkToken: 'lt_xxx',                                  // from your server
  hostElement: document.getElementById('billerapi-discover'), // inline mount target
  theme: 'light',                                       // 'light' | 'dark' | ElementsTheme
  onSuccess: (publicToken, metadata) => {
    // Send publicToken to your server to exchange for an access token.
    // The link.completed webhook is the source of truth.
    console.log('Linked!', metadata);
  },
  onExit: (error) => {
    if (error) console.error('Discovery error:', error.code, error.message);
  },
  onEvent: (eventName, metadata) => {
    // Optional lifecycle telemetry
  },
});

// 3. Render the secure iframe inline (no modal). Returns a Promise.
await handler.mount();

// handler.isMounted() -> boolean;  handler.unmount() tears it down.

Packages

The vanilla SDK is billerapi-js (exposes elements.discover()); the React bindings are billerapi-react (exposes the useDiscover hook). Both are the same packages that power the rest of the Elements SDK.

discover() & useDiscover()

Both take the same options object. The vanilla factory returns a handler; the hook returns the same handler methods plus reactive ready / error state.

OptionTypeRequiredDescription
linkTokenstringYesSandbox link token minted on your server; carried over the iframe INIT message.
hostElementHTMLElementYesThe element to render the inline iframe into (no modal overlay).
onSuccess(publicToken, metadata) => voidYesFires once the user links their discovered accounts. Exchange the public token server-side.
theme'light' | 'dark' | ElementsThemeNoLight/dark keyword or a full theme object (same shape as the other flows).
onExit(error?) => voidNoFires on user-cancel (no argument) or on error (an object with code + message).
onEvent(eventName, metadata) => voidNoOptional lifecycle telemetry stream.

Handler (returned by discover(), and surfaced by useDiscover()):

  • mount(): Promise<void> — render the inline iframe into hostElement.
  • unmount(): void — tear it down (idempotent).
  • isMounted(): boolean — current mount state.

The hook additionally returns ready (SDK loaded & safe to mount()) and error.

The identity model

The user submits four identity fields — all required, all snake_case on the wire. This mock deliberately collects no SSN and no date of birth: it is a PII-light surface by design.

FieldTypeNotes
full_namestringRequired.
phonestringRequired.
emailstringRequired; validated as an email address.
postal_codestringRequired.

No SSN, no DOB

A production bureau adapter would collect FCRA-gated identifiers behind its own consent layer — those fields are deliberately out of this mock. Do not extend the sandbox surface to accept them without the sign-off in Before production.

The sandbox endpoint

The SDK drives this for you, but it maps to a single sandbox endpoint. The link token in the path is the credential — there is no API key header on this call, mirroring the other link-token routes on the sandbox host.

discover-by-identity
# Sandbox host serves sb_ / bak_test_ tokens.
# The {link_token} in the path is the credential (no secret header).
curl -X POST https://sandbox.api.billerapi.com/link/token/lt_xxx/discover-by-identity \
  -H "Content-Type: application/json" \
  -d '{
    "full_name": "Ada Lovelace",
    "phone": "5551234242",
    "email": "ada@example.com",
    "postal_code": "94105"
  }'

# => 202 Accepted
# { "success": true, "discovered_count": 2 }
#
# When the CREDENTIALLESS_DISCOVERY_ENABLED flag is OFF (the default):
# => 404 Not Found

On success the sandbox pre-loads the link session with the discovered accounts and emits the standard account-selection step. The user selects accounts, the existing exchange completes, and the link.completed webhook fires — the same terminal signal as any credentialed link.

Confirm outcomes server-side

The onSuccess callback is a UX hint that runs in the user's browser. Treat the link.completed webhook as the authoritative record before fulfilling anything. See the Webhooks guide.

The vendor-swappable provider seam

Discovery is implemented behind a provider interface so the aggregator is a swappable dependency. Today the only binding is a deterministic mock aggregator that returns synthetic liabilities. A real Method / Spinwheel / bureau adapter can be dropped in later by implementing the same interface — no change to the SDK, the endpoint contract, or the downstream link pipeline.

CredentiallessDiscoveryProvider (server-side seam)
// The identity the caller submits (PII-light: no SSN / no DOB).
interface DiscoveryIdentity {
  full_name: string;
  phone: string;
  email: string;
  postal_code: string;
}

// A single discovered account/liability the user can one-tap to link.
interface DiscoveredLiability {
  id: string;          // 'sb_disc_acct_<hex>'
  biller_id: string;   // synthetic in the mock, e.g. 'sb_utility'
  biller_name: string; // 'Sandbox Utility'
  account_name: string;
  mask: string;        // '****4242'
  type: string;        // 'utility' | 'telecom' | 'insurance' | ...
}

// The swap point. The mock implements this today; a real vendor adapter
// (Method / Spinwheel / a bureau) implements the same method later.
interface CredentiallessDiscoveryProvider {
  discover(identity: DiscoveryIdentity): Promise<DiscoveredLiability[]>;
}

Mock behavior

The mock aggregator is deterministic — it keys off the submitted identity and returns a small set of liabilities across billers. It is stateless and touches no real provider. Swapping in a real adapter is an additive binding change on the server, gated by the requirements below.

Before production

Everything above is sandbox/mock. Pointing the CredentiallessDiscoveryProvider seam at a real aggregator or credit bureau moves you into regulated territory. A real adapter must not ship until all of the following are in place, with legal sign-off:

This gates any real-bureau adapter

Do not enable a live provider — and do not flip CREDENTIALLESS_DISCOVERY_ENABLED in a non-sandbox environment — until the checklist below is satisfied and signed off.
  • FCRA permissible purpose. Pulling consumer liabilities from a credit bureau (or a bureau-derived aggregator) is a consumer-report activity under the Fair Credit Reporting Act. You need a documented permissible purpose, the user's informed consent/authorization captured before the pull, and the required disclosures.
  • GLBA safeguards. The Gramm-Leach-Bliley Act privacy + safeguards rules apply to the nonpublic personal information involved. You need privacy notices, access controls, encryption in transit and at rest, retention/deletion policies, and a documented data-handling program.
  • Expanded PII handling. A real bureau match typically needs FCRA-gated identifiers (e.g. SSN / DOB) that the mock intentionally omits. Any field beyond full_name, phone, email, postal_code must go through its own consent layer, minimization review, and secure storage design.
  • Vendor due diligence & contracts. Selecting a provider (Method, Spinwheel, or a bureau) requires vendor security/compliance review, a data processing agreement, and confirmation the vendor's own permissible-purpose and consumer-consent posture matches your use case.
  • Legal sign-off. All of the above must be reviewed and approved by legal/compliance before the seam is bound to a live provider in any non-sandbox environment.

Related

Was this page helpful?