SCROLL
Bank Integration Guide  ·  API Keys  ·  Embedded Code Field  ·  Webhooks  ·  Outcome Reporting Bank Integration Guide  ·  API Keys  ·  Embedded Code Field  ·  Webhooks  ·  Outcome Reporting
Integration Guide

Token in. Signed claims out.

The core verifier flow is simple: receive a PASSID token, call the verify endpoint, get signed claim summaries, freshness, revocation status, and audit metadata. The verifier response does not include raw financial statements or transaction feeds.

Step 1

Workspace & API Keys

Create a workspace at passid.io/onboard. You receive two API keys immediately - one for sandbox testing, one for live production. All API calls require the X-Institution-Key header.

key
Keys are shown once at creation. Store them in your secrets manager (AWS Secrets Manager, Railway variables, etc.) - never in source code or version control.

Authentication header

Every API request YOUR BACKEND
// Sandbox
X-Institution-Key: pk_sandbox_xxxxxxxxxxxxxxxxxxxx

// Live (production)
X-Institution-Key: pk_live_xxxxxxxxxxxxxxxxxxxx

// Base URL
https://api.passid.io

Configure your webhook endpoint

In your institution dashboard -> API & Webhooks, paste the URL on your server that will receive PASSID events. This is required before going live.

PATCH /api/institution/webhook YOUR BACKEND
await fetch("https://api.passid.io/api/institution/webhook", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <institution-jwt>"
  },
  body: JSON.stringify({
    webhookUrl: "https://yourbank.com/passid/webhook",
    webhookSecret: "whsec_your_32char_secret" // optional - omit to keep the current secret
  })
});
Step 2

Install the SDKs

PASSID ships two packages. Install both - @passidio/react for your frontend and @passidio/node for your backend. The React package gives you <PassidCodeVerification /> - a compact inline field that embeds directly in your application forms (loan, onboarding, KYC) and calls onVerified(result) after the customer's code is redeemed and verified. The Node package keeps the API key server-side, verifies tokens, and returns signed claim summaries plus audit context.

terminal - install both packages INSTALL
# Frontend - embedded code field component
npm install @passidio/react

# Backend - Express / Next.js middleware + PASSIDClient
npm install @passidio/node
i
Keep your API key server-side. The apiKey prop on <PassidCodeVerification /> is fine for sandboxing, but in production use a backend proxy or route the key through @passidio/node so it is never exposed in your frontend bundle.

Drop it into your application form

Add <PassidCodeVerification /> directly inside an existing form - loan application, onboarding, KYC, rental. It renders as a compact inline widget. The customer opens their PASSID app, taps Share, reads out the one-time code, and onVerified(result) fires after token verification. No redirect, no new tab, no session dashboard.

LoanApplicationForm.jsx YOUR APP
import { PassidCodeVerification } from "@passidio/react";

export default function LoanApplicationForm() {
  return (
    <form>
      {/* your existing fields */}
      <input name="full_name" />
      <input name="email" />

      {/* PASSID replaces self-reported income / identity */}
      <PassidCodeVerification
        apiKey="pk_live_..."
        environment="live"
        requestedClaims={[
          "income_verified",
          "identity_verified",
          "sanctions_clear"
        ]}
        onVerified={(result) => {
          // result.verified_claims - verified credential object
          handleVerified(result.verified_claims);
        }}
        onError={(err) => console.error(err)}
        theme={{ primary: "#0075eb" }} // optional brand colour
      />

      <button type="submit">Submit application</button>
    </form>
  );
}

Component props - PassidCodeVerification

PropTypeRequiredDescription
apiKeystringYesYour institution live or sandbox API key
environment"sandbox" | "live"NoDefaults to "sandbox". In sandbox mode demo codes are shown inline for testing.
baseUrlstringNoPASSID backend base URL. Defaults to https://api.passid.io
requestedClaimsstring[]NoClaims to request. Defaults to standard claim set. See Claims Reference below.
onVerified(result) => voidNoCalled when code is redeemed. Receives full CodeVerificationResult - claims are in result.verified_claims.
onError(Error) => voidNoCalled on network errors or unexpected API failures.
theme.primarystringNoHex colour for buttons and accent. Defaults to #00c27a.

Backend - protect routes with @passidio/node

After the frontend collects the credential, your backend verifies it. Use passidMiddleware (Express) or withPassid (Next.js) to verify the token and enforce a policy in one line. Claims attach to req.passid (Express) or are passed as the second argument (Next.js).

Express - passidMiddleware YOUR BACKEND
import { passidMiddleware } from "@passidio/node";

app.post("/api/apply",
  passidMiddleware({
    apiKey: process.env.PASSID_API_KEY,
    policy: {
      requireClaims: ["income_verified", "sanctions_screening_clear"],
      maxCredentialAgeHours: 72,
    },
  }),
  (req, res) => {
    // req.passid is typed VerificationClaims
    // institution applies its own policy to the verified claims
    res.json({ verified: req.passid.identity_verified, income_band: req.passid.income_band });
  }
);

// Token missing -> 401 TOKEN_MISSING
// Token invalid -> 401 TOKEN_INVALID
// Policy breach -> 403 POLICY_VIOLATION
app/api/apply/route.ts - Next.js App Router YOUR BACKEND
import { withPassid } from "@passidio/node";

export const POST = withPassid(
  { apiKey: process.env.PASSID_API_KEY, policy: { requireClaims: ["income_verified", "sanctions_screening_clear"] } },
  async (req, claims) => Response.json({ verified: claims.identity_verified, income_band: claims.income_band })
);
pages/api/apply.ts - Next.js Pages Router YOUR BACKEND
import { withPassidPages } from "@passidio/node";

export default withPassidPages(
  { apiKey: process.env.PASSID_API_KEY },
  (req, res) => res.status(200).json({ verified: req.passid.identity_verified, income_band: req.passid.income_band })
);

Policy options

OptionTypeEffect on request
requireClaimsstring[]Rejects if any listed claim is missing or false - e.g. ["income_verified","sanctions_screening_clear"]
maxCredentialAgeHoursnumberRejects credentials older than this many hours - enforces freshness requirement -> 403 POLICY_VIOLATION
requireFraudChecksPassedbooleanRejects if identity_and_fraud_checks_passed is false -> 403 POLICY_VIOLATION
requireSanctionsClearbooleanRejects if sanctions_screening_clear is false - always recommended for financial applications -> 403 POLICY_VIOLATION
Step 3

Alternative: QR Flow (in-branch / kiosk)

For in-branch or kiosk scenarios where the customer is physically present and can scan a QR code. Use <PASSIDVerify /> from the same package - it renders a QR overlay and fires onSuccess(claims) when the customer scans and approves in the mobile app. For embedded form use cases, prefer <PassidCodeVerification /> from Step 2.

1
SDK mounts -> creates a verification session
On component mount, the SDK calls POST /v1/sessions with your API key and requested permissions. The backend creates a pending session and returns requestId, code (e.g. AB3X-7YQM), deepLink (passid://verify-request?r=...&i=...), and expiresAt. TTL defaults to 10 minutes.
2
QR code rendered on your page
The SDK renders the deep link as a QR code onto a <canvas> element using the qrcode library - no third-party image service. An 8-char fallback code (AB3X-7YQM) is shown alongside. A countdown timer starts. The SDK polls GET /v1/sessions/:requestId every 2.5 seconds.
3
Customer opens PASSID app and scans
The customer opens the PASSID mobile wallet, taps Verify for Institution, and scans the QR. The app parses the deep link and opens a consent screen showing your institution name and each requested claim individually. Alternatively, they can point their phone camera at the QR - the OS deep-links directly into the PASSID app.
4
Customer reviews and approves
The consent screen fetches GET /v1/sessions/:id/public (no auth required) to confirm the request is still pending and show the expiry countdown. The customer taps Share & Verify. The app generates a one-time share token from their verified claims and calls POST /v1/sessions/:id/fulfill with their user JWT.
5
Claims arrive in your onSuccess callback
The backend validates the token, writes status = "completed", and the SDK poll detects this on its next tick. onSuccess(claims) fires with the verified claims object. A verification.completed webhook is also sent to your registered endpoint.
!
If the customer does not scan within the TTL window (default 10 min, max 60 min via ttlSeconds), the poll returns status: "expired" and onDecline("Request expired") fires. Show a "Generate new code" button.

What the SDK renders - states

StateTriggerWhat the user sees
idleInitial mountGenerate QR button + manual entry input
requestingQR button clickedSpinner - "Creating verification session..."
awaitingSession createdQR code + 8-char code + countdown + pulse dot
verifyingManual token submittedSpinner - "Verifying credential..."
successPoll resolves / token verifiedVerified claims summary + freshness + provenance
declinedCustomer declined / expired / invalidDecline message + friendly reason + try again
errorNetwork / API failureError message + try again
Step 4

PASSID Code Redemption

The primary embedded flow. The customer opens their PASSID mobile app, taps Share, and reads out a short one-time code (e.g. KE8V-3TXY-Z72L). The institution types or pastes it into the <PassidCodeVerification /> field - or calls the redemption API directly from the backend.

1
Customer opens PASSID app -> Share -> reads out code
The code looks like KE8V-3TXY-Z72L. It is a one-time-use token bound to the credential claims the customer chose to share. Expires after first use.
2
Code entered into the embedded field - or backend API
<PassidCodeVerification /> accepts the code, shows verification status, and fires onVerified(result) with verified claims. Works over the phone, in person, or fully remote - no camera needed.
3
Backend calls POST /api/bridge/code/redeem
The code is redeemed in one call. PASSID verifies token status, signature, freshness, and revocation, then returns signed claim summaries and audit metadata. One-time use - subsequent calls with the same code return an error.

Redeeming a code directly from your backend

POST /api/bridge/code/redeem YOUR BACKEND
const res = await fetch("https://api.passid.io/api/bridge/code/redeem", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Institution-Key": "pk_live_..."
  },
  body: JSON.stringify({
    code: "KE8V-3TXY-Z72L" // customer reads this from their PASSID app
  })
});

const { data } = await res.json();
// data.status -> "verified"
// data.verified_claims -> signed claim summaries
// data.audit_id -> verifier audit reference
// data.raw_transactions_included -> false
// data.freshness_status -> "fresh" | "stale"
// data.decisioning_note -> advisory - your institution makes the decision

Or via @passidio/node PASSIDClient

PASSIDClient.redeemCode() YOUR BACKEND
import { PASSIDClient } from "@passidio/node";

const passid = new PASSIDClient({ apiKey: process.env.PASSID_API_KEY });

const result = await passid.redeemCode({ code: "KE8V-3TXY-Z72L" });
// result.verified_claims -> VerificationClaims
// result.freshness_status -> "fresh" | "stale"
Step 5

Claims Reference

The verified_claims object returned in onVerified(result) and in the data.verified_claims field on API responses. Treat it as a verified summary for your policy workflow, not as raw account data.

VerificationClaims - full example PASSID RESPONSE
{
  // -- Identity & Verification -------------------------------
  "identity_verified": true,
  "income_verified": true,
  "income_band": "$3,000-4,000/mo",

  // -- Compliance ----------------------------------------------
  "sanctions_screening_clear": true, // OFAC · EU · UN
  "identity_and_fraud_checks_passed": true,

  // -- Behavioural claims -------------------------------------
  "payment_behavior_verified": true,
  "savings_behavior_verified": true,

  // -- Data context -------------------------------------------
  "data_window_days": 180, // days of verified financial data
  "freshness_hours": 2, // hours since credential was issued
  "linked_accounts": 3, // number of bank accounts in scope
  "corridor": "KE->GB" // optional, origin -> destination
}

Permission values for the permissions[] array

ValueTypeClaims returned
verified_credential_summaryobjectFull standard claim set - all verified claims in one request
income_verifiedboolean + stringincome_verified, income_band
identity_verifiedbooleanidentity_verified
sanctions_clearbooleansanctions_screening_clear - OFAC · EU · UN
payment_reliabilitybooleanpayment_behavior_verified
savings_consistencybooleansavings_behavior_verified
fraud_riskbooleanidentity_and_fraud_checks_passed
Step 6

Direct Verify API

If you are not using the React SDK (e.g. server-side integration, mobile backend, non-React frontend), call POST /v1/verify directly from your backend after receiving the applicant's token.

POST
/v1/verify
Verifies a PASSID token and returns signed claim summaries, signature status, freshness, revocation state, and audit metadata. The response does not include raw transaction feeds. Pass Idempotency-Key: <unique-id> so retries on network failure do not double-bill or double-verify. Response header: X-PASSID-API-Version: 1.
POST
/v1/sessions
Creates a pending verification session (used by SDK automatically). Returns requestId, code, deepLink, expiresAt. Body: { institutionName?, permissions?, ttlSeconds? } - TTL default 600s, max 3600s.
GET
/v1/sessions/:requestId
Poll for session completion. Returns status: "pending" | "completed" | "declined" | "expired". When completed, includes full claims and verifiedAt. Requires institution API key.
GET
/v1/sessions/:id/public NO AUTH
Called by the PASSID mobile app after scanning the QR. Returns institution name, permissions, and expiry. No API key required. Returns 410 if expired, fulfilled, or declined.
GET
/v1/sessions/by-code/:code/public NO AUTH
Resolves the 8-char human-readable fallback code (e.g. AB3X-7YQM or AB3X7YQM) to the same response as the public endpoint above. Used by the mobile app manual entry screen.
Step 7

Webhooks

PASSID sends signed HTTP POST events to your registered endpoint. Register your URL in the institution dashboard -> API & Webhooks. All events use the same envelope format.

Event types

verification.completed
Fires after a token is successfully verified - either via the SDK QR flow or a direct POST /v1/verify call. Payload includes verified claims, proof type, and the token. Use this on your backend to update your application state.
verification.failed
Fires when a token is rejected. Includes deny_reason (e.g. TOKEN_EXPIRED, HARD_DENY_SANCTIONS_MATCH). Only fired when the request carries a valid institution API key.
outcome.reported
Fires when your institution submits a verification outcome context via POST /v1/outcome. Payload includes the token, outcome type, product type, amount, and verification context at time of outcome reporting. Use for audit trails and downstream integrations.
webhook.test
Manual test ping from the dashboard. Use to validate your signature verification logic before going live.

Event envelope

POST https://yourbank.com/passid/webhook PASSID -> YOUR SERVER
// Headers
Content-Type: application/json
X-PASSID-Signature: sha256=a3f8c2d1e9b4... // HMAC-SHA256

// Body - verification.completed
{
  "event": "verification.completed",
  "institution_id": "42",
  "timestamp": "2026-03-29T10:22:14Z",
  "data": {
    "token": "PASSID-AB3X-7YQM-...",
    "verified_at": "2026-03-29T10:22:14Z",
    "proof_type": "groth16/bn254",
    "verified_claims": {
      "identity_verified": true,
      "income_verified": true,
      "sanctions_screening_clear": true,
      "payment_behavior_verified": true
    }
  }
}

Retry policy

PASSID retries non-2xx responses automatically: immediately -> 30 s -> 5 min -> 30 min -> 2 h (5 attempts total). Pending deliveries survive server restarts. Your endpoint must return 200 within 10 seconds - do your async processing in the background.

Step 8

Signature Verification

Every webhook is signed with HMAC-SHA256 using the webhook secret you configured. Always verify the signature before processing - never trust the payload without it.

Express webhook handler - using @passidio/node YOUR BACKEND
import { verifyWebhookSignature } from "@passidio/node";

const WEBHOOK_SECRET = process.env.PASSID_WEBHOOK_SECRET;

app.post("/passid/webhook", express.raw({ type: "application/json" }), (req, res) => {
  // 1. Verify signature - rejects before touching the body
  const ok = verifyWebhookSignature(
    req.body, // raw Buffer - must use express.raw()
    req.headers["x-passid-signature"] as string,
    WEBHOOK_SECRET,
  );
  if (!ok) return res.status(401).send("Invalid signature");

  // 2. Parse and handle
  const event = JSON.parse(req.body.toString());

  if (event.event === "verification.completed") {
    const { token, claims } = event.data;
    updateApplicationStatus(token, claims);
  }

  // 3. Respond 200 quickly - do any heavy work in background
  res.sendStatus(200);
});
!
Always use express.raw() (not express.json()) when reading the body for HMAC verification. JSON parsing modifies whitespace and may alter the bytes used for the signature.
Step 9

Outcome Reporting

Weeks or months after a PASSID-verified application, report what actually happened. This closes the verification coverage loop - PASSID uses your outcomes to improve claim coverage and signal normalization across corridors. It also gives you cohort analytics in your institution dashboard.

📊
Outcome reporting is optional but strongly encouraged. The more institutions report outcomes, the better the coverage signal normalization becomes for your specific population and corridor.
POST
/v1/outcome
Report verification outcome context. Requires X-Institution-Key. One outcome per token per institution - duplicate reports return 409. The verification context at time of outcome reporting is captured automatically - you never send it.
POST /v1/outcome YOUR BACKEND
await fetch("https://api.passid.io/v1/outcome", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Institution-Key": "pk_live_..."
  },
  body: JSON.stringify({
    token: "PASSID-AB3X-7YQM-...", // token from the original verify
    outcome: "repaid", // see valid outcomes below
    productType: "personal_loan", // optional
    amountUsd: 5000, // optional, loan amount
    observationDays: 90, // optional, days since disbursement
    notes: "Early repayment" // optional, internal notes
  })
});

Valid outcome values

ValueMeaning
repaidLoan repaid in full, on schedule
no_defaultStill active, no missed payments to date
delinquent30+ days past due, not yet defaulted
defaulted90+ days past due or written off
fraud_confirmedIdentity or application fraud confirmed
account_closedAccount closed by institution (not default)
otherOutcome not covered by above - use notes

Viewing your cohort data

After reporting outcomes, view calibration data in your institution dashboard -> Performance tab. The cohort API is also available directly:

GET
/v1/outcome/cohort
Returns outcome rates broken down by verification coverage level for all outcomes your institution has reported. Requires institution JWT (not API key). Used by the Outcome Analytics page in your dashboard.
Step 10

Sandbox & Testing

Use your sandbox API key (pk_sandbox_...) with the same endpoints. All sandbox responses are deterministic - same token always returns the same claims. Isolated from live data.

Sandbox demo codes

Use these codes with POST /api/bridge/code/redeem or type them into <PassidCodeVerification environment="sandbox" /> - they appear as clickable chips in sandbox mode automatically.

CodeCoverageCorridorResult
KE8V-3TXY-Z72LHigh coverageKE->GBincome ✓ · identity ✓ · sanctions clear · fraud checks passed · verified
NG4M-XQRS-T88WHigh coverageNG->USincome ✓ · identity ✓ · sanctions clear · fraud checks passed · verified
GH2P-ABCD-56KLPartial coverageGH->EUincome ✓ · identity ! unverified · fraud checks incomplete · manual review required

Test the embedded code field end-to-end in sandbox

1
Switch your institution dashboard to Sandbox mode
Top-left environment toggle in the dashboard. Sandbox API key is used automatically.
2
Render your form with <PassidCodeVerification apiKey="pk_sandbox_..." environment="sandbox" />
The component shows sandbox demo code chips (KE8V, NG4M, GH2P). Click any chip to auto-fill and run the sandbox verification path - no real device needed.
3
Open the PASSID mobile app in sandbox mode and scan the QR
Approve the consent screen. Claims resolve automatically in onSuccess on your page.
4
Test webhooks using the dashboard simulator
Dashboard -> API & Webhooks -> Send test event. Sends a real signed webhook.test payload to your endpoint so you can validate signature verification before going live.
Step 11

Go-Live Checklist

Complete all items before switching to your live API key.

  • Live API key stored in secrets manager - never in code or .env committed to version control
  • Webhook URL registered in institution dashboard and tested with simulator
  • Webhook signature verification implemented using X-PASSID-Signature: sha256=... + HMAC-SHA256 + timingSafeEqual
  • Webhook endpoint responds 200 within 10 seconds - async processing done in background
  • Idempotency-Key header sent on every POST /v1/verify call to prevent double-verification on retry
  • HARD_DENY_SANCTIONS_MATCH deny reason handled - do not approve applications with this reason under any circumstances
  • QR session expiry handled - onDecline("Request expired") shows a "Generate new code" button, not an error
  • Sandbox end-to-end test completed - QR scanned from mobile, onSuccess received, webhook received and signature verified
  • Outcome reporting endpoint integrated - plan to POST outcomes after loan completion or default observation period
  • Institution dashboard configured - team members invited, corridors enabled, webhook secret stored securely
Ready to go live?
Our integration team reviews every institution before the live key is activated. Typical review takes 1 business day. We check webhook reachability, signature verification, and sanctions handling.
Request live key review ›
PASSID
Thank you - we'll be in touch.