DEVELOPERS
See the integration.
Before the meeting.
Create a verification session on your server, send the user through the check, evaluate the result and retain the decision receipt.
SDK access is supplied during onboarding
The Nexion TypeScript SDK is a private npm package: @nexionplatform/sdk. During onboarding, we grant your npm account package access and supply an API key, your API origin, and a deployed workflow ID and version ID. Accept your npm invitation and sign in with that account before installing. npm access and your Nexion API key are separate credentials.
Use Node.js 24.12 or later in the 24.x line in a macOS or Linux terminal. This example targets SDK 0.1.0. This walkthrough runs in demo mode and uses a desktop with a Lognium wallet, or an onboarded hosted provider. The workflow must cover the restricted-area resource, age 18+ and unknown country, without requiring an edge location observation.
For a real wallet journey, onboarding supplies a key for your demo account’s live API environment and a workflow set to demo mode. A sandbox key runs simulated sessions; it does not perform this wallet-and-receipt walkthrough. API environment and workflow execution mode are separate.
A hosted-provider workflow also needs registered HTTPS return and cancel URLs. Obtain these and the provider configuration during onboarding. Do not add a hosted fallback to a wallet-only setup without them.
01 / INSTALL AND CONFIGURE
Start with a clean directory
Create an empty directory and open a terminal there. After onboarding grants package access, these commands sign you in to npm, install the private SDK and download the public reference core, driver and environment template. Signing in alone does not grant package access.
npm init -y
npm pkg set type=module
# Use the npm account granted package access during onboarding.
npm login --scope=@nexionplatform --registry=https://registry.npmjs.org
npm install --save-exact @nexionplatform/sdk qrcode@1.5.4
curl --fail --show-error --location https://nexionlabs.com/developers/verification.ts -o verification.ts
curl --fail --show-error --location https://nexionlabs.com/developers/run.mjs -o run.mjs
curl --fail --show-error --location https://nexionlabs.com/developers/env.example -o .env
chmod 600 .env
# Fill in .env with your onboarding values before running the next command.Edit .env with the values supplied for your integration. Use the API origin without a path. Keep this file, the state directory and npm credentials out of source control and browser bundles. Keep package-lock.json in source control to reproduce your dependency versions.
# Supplied during onboarding. This is not a public/self-service SDK endpoint.
NEXION_API_KEY=REPLACE_WITH_ONBOARDING_KEY
NEXION_API_URL=https://REPLACE_WITH_ASSIGNED_API_HOST
NEXION_WORKFLOW_ID=REPLACE_WITH_DEPLOYED_DEMO_WORKFLOW_ID
NEXION_WORKFLOW_VERSION_ID=REPLACE_WITH_DEPLOYED_VERSION_ID
# Hosted-provider workflows also require both registered HTTPS URLs:
# NEXION_RETURN_URL=https://your-approved-site.example/verification/return
# NEXION_CANCEL_URL=https://your-approved-site.example/verification/cancel
The driver reads that configuration on your server. The SDK sends the API key in X-API-Key; visitors never receive it.
const client = new NexionClient({
apiKey: required('NEXION_API_KEY'),
baseUrl: endpoint.origin,
timeout: 10_000,
});
const api = client.verificationApi;02 / CREATE ONCE, RETRY SAFELY
Persist the attempt before the request
Save the creation intent first. If a response is lost, resend that same request and idempotency key. Save the returned binding before presenting the wallet QR code or hosted journey. The local driver does this in state/<attempt-name>.json.
// Persist this BEFORE calling startCheck. Retry the same request and key.
export function newCheck(
workflowId: string,
workflowVersionId: string,
redirects: { returnUrl?: string; cancelUrl?: string } = {}
): CreationIntent {
return {
workflowVersionId,
request: {
workflowId,
executionMode: 'demo',
requiredAge: 18,
idempotencyKey: randomUUID(),
context: {
country: null,
resource: 'restricted-area',
device: 'desktop',
},
...(redirects.returnUrl ? { returnUrl: redirects.returnUrl } : {}),
...(redirects.cancelUrl ? { cancelUrl: redirects.cancelUrl } : {}),
},
};
}
export async function startCheck(api: Gateway, intent: CreationIntent) {
const session = await api.create(intent.request);
validateAction(session);
const binding: Binding = {
sessionId: session.sessionId,
workflowId: intent.request.workflowId,
workflowVersionId: intent.workflowVersionId,
resource: intent.request.context.resource,
executionMode: 'demo',
};
return { binding, session };
}03 / COMPLETE THE JOURNEY
Run the check and inspect the result
The first command writes a local QR image for Lognium, or prints the hosted provider URL. Complete that journey, then run the second command. Pending results do not grant access. A fallback returns a fresh journey while retaining the same public session ID.
node --env-file=.env run.mjs start first-check
# Open state/first-check-qr.png and scan it with Lognium.
# For a hosted provider, open the URL printed by the command instead.
node --env-file=.env run.mjs check first-check
# Repeat check at the indicated interval while pending or retrying.
# Use a new name only for a genuinely new verification attempt.| Result | What happens next |
|---|---|
| pending | Keep access closed. Check again after at least two seconds. |
| continue | Present the new journey. Keep the original public session binding and poll it again. |
| demo_decision_recorded · allow | The demo check passed and its decision receipt is saved. This is not production authorization. |
| demo_decision_recorded · deny | The check was refused, expired or did not match the pinned policy. Its denial receipt is saved. |
| retry | Keep the same attempt file and payload. Wait for the printed retry time; do not start a replacement session. |
| blocked | Resolve authentication, workflow or contract errors. Keep access closed and preserve the local journal. |
Check a valid credential, a refusal and an expired session during onboarding. The journal retains the creation intent, session binding, decision and receipt. Find the same session in your dashboard’s verification records, in the API key’s environment, and compare its allow or deny effect with the receipt ID and witness references.
04 / RETAIN THE DECISION
Witness both allowed and denied attempts
The complete core checks the session ID, expiry, active policy, workflow, version and execution mode. The driver saves the decision before sending it and retries the exact payload. A denial takes effect locally even if witnessing is unavailable; its unsent evidence stays in the journal. Missing receipts never authorize access.
// Persist the entire Decision before sending it. A retry ONLY resends that
// immutable payload; it must not re-evaluate and reuse its key for another result.
export async function witnessDecision(api: Gateway, decision: Decision) {
const receipt = await api.recordResourceDecision(
decision.sessionId,
decision.effect
);
if (
!receipt ||
typeof receipt.id !== 'string' ||
!receipt.id ||
receipt.evidenceClass !== 'ATTESTED' ||
!Number.isFinite(Date.parse(receipt.recordedAt)) ||
typeof receipt.witness?.recordId !== 'string' ||
!receipt.witness.recordId ||
typeof receipt.witness.chainEntryId !== 'string' ||
!receipt.witness.chainEntryId ||
!/^[a-f0-9]{64}$/.test(receipt.witness.projectionHash)
)
throw new Error('A valid witness receipt is required. Keep access closed.');
return receipt;
}| REST request | Purpose |
|---|---|
| POST /api/v1/verifications | Create a session. The X-API-Key credential selects your account and API environment. |
| GET /api/v1/verifications/:sessionId | Read the authoritative result, expiry, current policy and any next action. |
| POST /api/v1/verifications/:sessionId/effects | Attest an allow or deny decision. Retain the returned witness receipt. |
FROM QUICKSTART TO YOUR APPLICATION
Bind verification
to real access control.
Bind each visitor on your server
This driver is a local, single-operator demo. In your application, store each binding against the authenticated visitor or secure server session, validate browser requests and enforce every page, API and resource endpoint. A client-supplied session ID or callback is not authority to grant access.
Use a durable decision outbox
Replace local files with a transactional database and retry worker. Preserve immutable decisions and timestamps across crashes. New effects must arrive within 24 hours; escalate older unsent records instead of changing their occurrence time. A receipt attests your decision, not content delivery or complete enforcement coverage.
Qualify the production workflow
This demo never serves a protected resource. Production requires onboarded methods, reviewed policy and production mode checked immediately before access. Country-specific routing needs the agreed location integration; this example deliberately sends no country assertion. Reconcile the pinned version after workflow changes; do not remove that check to make an old session pass.
Re-running a finished attempt prints its historical receipt, not a fresh authorization. A new verification uses a new attempt name. After a crash, confirm the old process has stopped before removing its lock file. Resolve blocked errors with your onboarding contact; do not repeatedly create new attempts to work around them.
LET’S MAKE IT CONCRETE
Discuss the integration
with your engineers.
Bring your current stack, provider and questions about the server contract.
A conversation around your product.