From workspace setup to your first verified claim — API keys, the drop-in SDK, QR code flow, webhook verification, signature checking, outcome reporting, and sandbox testing. Complete.
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.
In your institution dashboard → API & Webhooks, paste the URL on your server that will receive PASSID events. This is required before going live.
The @passid/react package is a self-contained React component. It handles QR rendering, deep-link generation, polling, countdown timer, success/decline/error states, and manual token entry — all in one drop-in component. Zero backend changes required.
| Prop | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Yes | Your institution live or sandbox API key |
| baseUrl | string | No | PASSID backend base URL. Defaults to https://api.passid.io |
| institutionName | string | No | Displayed on the consent screen inside the PASSID mobile app and on the component header |
| permissions | string[] | No | Claims to request. Defaults to all. See Claims Reference below. |
| onSuccess | (claims) => void | Yes | Called automatically when the customer approves in the app. Receives the full claims object. |
| onDecline | (reason) => void | No | Called when the customer declines, or the request expires. |
| onError | (Error) => void | No | Called on network errors or unexpected API failures. |
| theme.primary | string | No | Hex colour for buttons and accent. Defaults to #00c27a. |
This is the primary flow. The customer scans a QR code on your page with the PASSID mobile app. No token copying, no manual input — claims arrive in onSuccess automatically. The SDK handles everything internally.
POST /api/bridge/request 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.<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 /api/bridge/request/:requestId every 2.5 seconds.GET /api/bridge/request/: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 /api/bridge/request/:id/fulfill with their user JWT.status = "completed", and the SDK poll detects this on its next tick. onSuccess(claims) fires with the full proven claims object. Simultaneously, a verification.completed webhook is sent to your registered endpoint. Your page never navigated away.ttlSeconds), the poll returns status: "expired" and onDecline("Request expired") fires. Show a "Generate new code" button.| State | Trigger | What the user sees |
|---|---|---|
| idle | Initial mount | Generate QR button + manual entry input |
| requesting | QR button clicked | Spinner — "Creating verification session…" |
| awaiting | Session created | QR code + 8-char code + countdown + pulse dot |
| verifying | Manual token submitted | Spinner — "Verifying credential…" |
| success | Poll resolves / token verified | Score badge + tier + claim-by-claim summary |
| declined | Customer declined / expired / invalid | Decline message + friendly reason + try again |
| error | Network / API failure | Error message + try again |
For in-branch use, accessibility, or when a customer's phone camera isn't available. The customer reads their PASSID token from the mobile app and gives it to a staff member who types it into the input field on the SDK component (or your own UI calling the verify API directly).
PASSID-AB3X-7YQM-K9PQ-3MNX. It is a one-time-use credential tied to the specific permissions the customer selected.POST /api/bridge/verify directlyonSuccess(claims) callback fires. Pass Idempotency-Key to make retries safe.| denyReason | Meaning |
|---|---|
| TOKEN_NOT_FOUND | Token does not exist — ask customer to generate a new share |
| TOKEN_EXPIRED | Token TTL elapsed — ask customer to generate a fresh share |
| TOKEN_REVOKED | Customer revoked this token from their wallet |
| TOKEN_ALREADY_USED | One-time token was already consumed |
| INSTITUTION_MISMATCH | Token was issued for a different institution |
| HARD_DENY_SANCTIONS_MATCH | Customer is on OFAC/EU/UN watchlist — do not proceed |
The claims object returned in onSuccess and on the verify API response.
permissions[] array| Value | Type | Claims returned |
|---|---|---|
| full_score | number | fsi_score, fsi_low, fsi_high, tier, confidence, p_stable |
| income_verified | boolean + string | income_verified, income_band |
| identity_verified | boolean | identity_verified |
| sanctions_clear | boolean | sanctions_clear |
| payment_reliability | number 0–1 | payment_reliability |
| savings_consistency | number 0–100 | savings_consistency |
| fraud_risk | enum | fraud_risk (low / medium / high) |
If you are not using the React SDK (e.g. server-side integration, mobile backend, non-React frontend), call POST /api/bridge/verify directly from your backend after receiving the token.
Idempotency-Key: <unique-id> so retries on network failure never double-bill or double-verify. Response header: X-PASSID-API-Version: 1.
requestId, code, deepLink, expiresAt. Body: { institutionName?, permissions?, ttlSeconds? } — TTL default 600s, max 3600s.
status: "pending" | "completed" | "declined" | "expired". When completed, includes full claims and verifiedAt. Requires institution API key.
410 if expired, fulfilled, or declined.
AB3X-7YQM or AB3X7YQM) to the same response as the public endpoint above. Used by the mobile app manual entry screen.
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.
POST /api/bridge/verify call. Payload includes all proven claims, proof type, and the token. Use this on your backend to update your application state without polling.deny_reason (e.g. TOKEN_EXPIRED, HARD_DENY_SANCTIONS_MATCH). Only fired when the request carries a valid institution API key.POST /api/bridge/outcome. Payload includes the token, outcome type, product type, amount, and FSI score at decision time. Use for audit trails and downstream integrations.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.
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.raw() (not express.json()) when reading the body for HMAC verification. JSON parsing modifies whitespace and may alter the bytes used for the signature.Weeks or months after a PASSID-verified decision, report what actually happened. This closes the model calibration loop — PASSID uses your outcomes to verify that FSI scores predict default rates correctly across tiers. It also gives you cohort analytics in your institution dashboard.
X-Institution-Key. One outcome per token per institution — duplicate reports return 409. The FSI score at time of verification is captured automatically from your verification history — you never send it.
| Value | Meaning |
|---|---|
| repaid | Loan repaid in full, on schedule |
| no_default | Still active, no missed payments to date |
| delinquent | 30+ days past due, not yet defaulted |
| defaulted | 90+ days past due or written off |
| fraud_confirmed | Identity or application fraud confirmed |
| account_closed | Account closed by institution (not default) |
| other | Outcome not covered by above — use notes |
After reporting outcomes, view calibration data in your institution dashboard → Performance tab. The cohort API is also available directly:
Use your sandbox API key (sk_sandbox_...) with the same endpoints. All sandbox responses are deterministic — same token always returns the same claims. Isolated from live data.
Paste these into the token field of POST /api/bridge/verify or into the SDK manual entry input.
| Token | Tier / Score | Corridor | Result |
|---|---|---|---|
| KE8V-3TXY-Z72L | A · 812 | KE→GB | income ✓ · identity ✓ · sanctions clear · fraud low · approved |
| NG4M-XQRS-T88W | B · 743 | NG→US | income ✓ · identity ✓ · sanctions clear · fraud low · approved |
| GH2P-ABCD-56KL | C · 671 | GH→EU | income ✓ · identity ⚠ · fraud medium · review |
<PASSIDVerify apiKey="sk_sandbox_..." />onSuccess on your page.verification.completed payload to your endpoint. Use to validate your signature verification before going live.Complete all items before switching to your live API key.
.env committed to version controlX-PASSID-Signature: sha256=... + HMAC-SHA256 + timingSafeEqual200 within 10 seconds — async processing done in backgroundIdempotency-Key header sent on every POST /api/bridge/verify call to prevent double-verification on retryHARD_DENY_SANCTIONS_MATCH deny reason handled — do not approve applications with this reason under any circumstancesonDecline("Request expired") shows a "Generate new code" button, not an erroronSuccess received, webhook received and signature verified