SDK integration contracts

The SDK ships four integration entry points. Each is purposefully shaped for a different host environment, but they MUST hold the same observable contracts for the same input. This page is the canonical reference — pick a path knowing exactly what you trade off, and verify the invariants below if you're writing the fifth.

The four paths

PathImport / mountBest for
Non-managed Reactimport { VerifyHuman } from '@verifyhuman/sdk/react'Next.js / Remix / Vite apps that want the React component directly in their tree. Full control of the JSX surrounding the widget.
Managed React (VH.mount)await VH.mount(el, { siteKey })Vanilla / non-React apps that still want the polished React-rendered widget. The SDK manages the Preact render lifecycle internally.
CDN HTML drop-in<script src="https://vhuman.riwi.com/sdk/v1/verifyhuman.umd.js"> + <div data-sitekey="...">Static sites, marketing pages, anywhere a build step is overkill. Same React-rendered widget as the managed path; bundled as UMD with Preact.
Core programmaticimport { VerifyHuman as VHSDK } from '@verifyhuman/sdk'Server-to-server / Node.js / advanced integrations that bring their own UI. Direct access to verify(), requestChallenge(), etc. No widget.

Invariants — true on every path

The seven contracts below hold across all four entry points. They're enforced by packages/sdk/tests/contract-invariants.test.ts which fails CI when any path drifts. If you find a divergence in production that this page doesn't explain, that's a bug — file it with the same urgency as a security issue.

1. Server verdict is the source of truth

A server-rejected verification reaches the customer as a failure on every path. Channel differs (promise rejection vs. onError callback vs. verify() return), but the customer never sees a silent pass-with-token when the server said no.

2. Local fail still submits

When the widget concludes “fail” locally (low passive score, missed challenge), the SDK still submits to /public/verify. The server records the row, the dashboard shows it, the failure webhook fires. Verification attempts that never reach the server are a bug — see F-CRIT-16.

3. Typed error codes on rejection

Every server rejection raises a typed VerifyError with a category-specific code — never a bare SERVER_ERROR when the failure category is known. See typed errors for the integration pattern.

4. Result fields come from the server's echo

VHMountResult.demographics / .glassesDetected are populated from the server's echo on the /public/verify response, NOT from widget-local data. An integrator reading these fields sees a platform-confirmed value — a server-side parse failure leaves the field undefined so data loss is detectable.

5. Idempotent cleanup

endSession() hits DELETE /api/v1/public/sessions/{id} on every terminal mount state. The server returns 204 whether the session was active, completed, or already cleaned up — no 404s in customer consoles.

6. Circuit breaker on transport failure

When /public/verify is unreachable (network / timeout / 5xx after retries), the SDK applies the project's degradedMode policy on every path. Default FAIL_OPEN_FLAGGED resolves with degraded: true; opt into FAIL_CLOSED for fraud-sensitive surveys. See degraded mode.

7. Dist freshness

The SDK's build artifacts under packages/sdk/dist/ are kept in lockstep with source via prepublishOnly / prepack/ postinstall lifecycle hooks. Consumers using a file: workspace dep get a fresh build on every npm install. See packages/marketing/docs/integration-gaps.md#g11 for the historical incident that locked this contract.

Path-specific behavior differences

These ARE legitimate differences between paths — they reflect the host environment, not contract divergence. Read carefully before switching paths:

BehaviorNon-managed ReactManaged React / CDNCore programmatic
How you receive the resultonVerify / onError callbacksawait VH.mount(...) resolves withVHMountResult OR throws VerifyErrorawait sdk.verify(envelope) returns aResult wrapper
Token shape on successJWT stringJWT string on result.tokenJWT string on result.verificationToken
Widget UI renderedYes — you control the surrounding JSXYes — SDK manages Preact lifecycleNo — bring your own UI
Network submission ownershipReact component owns submission (submitVerification)Outer SDK layer owns submission (VHSession.mountWidget → vh.verify)Caller calls verify() directly
SCORE-mode dispatchPre-Phase-3 path (legacy)Supported via VHWidgetMountOptions.onResultCaller composes verify() + VHSession.getResult() manually

If you're adding a fifth path

Update three places in one PR:

  1. This page. Add a row to the four paths table describing what host environment the new entry point serves.
  2. packages/sdk/tests/contract-invariants.test.ts ENTRY_POINTS constant. Add the file path of the new entry point so the test asserts it exists, AND extend every individual invariant check to cover the new path's implementation.
  3. CHANGELOG. Document the new path under a new F-CRIT-N (or feature) entry, including which invariants required new test coverage.

The contract-invariants suite is the gate. New paths without matching test coverage will fail CI on first PR — the divergence surfaces before customer reports, not after.