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
| Path | Import / mount | Best for |
|---|---|---|
| Non-managed React | import { 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 programmatic | import { 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:
| Behavior | Non-managed React | Managed React / CDN | Core programmatic |
|---|---|---|---|
| How you receive the result | onVerify / onError callbacks | await VH.mount(...) resolves withVHMountResult OR throws VerifyError | await sdk.verify(envelope) returns aResult wrapper |
| Token shape on success | JWT string | JWT string on result.token | JWT string on result.verificationToken |
| Widget UI rendered | Yes — you control the surrounding JSX | Yes — SDK manages Preact lifecycle | No — bring your own UI |
| Network submission ownership | React component owns submission (submitVerification) | Outer SDK layer owns submission (VHSession.mountWidget → vh.verify) | Caller calls verify() directly |
| SCORE-mode dispatch | Pre-Phase-3 path (legacy) | Supported via VHWidgetMountOptions.onResult | Caller composes verify() + VHSession.getResult() manually |
If you're adding a fifth path
Update three places in one PR:
- This page. Add a row to the four paths table describing what host environment the new entry point serves.
packages/sdk/tests/contract-invariants.test.ts→ENTRY_POINTSconstant. 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.- 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.