SCROLL
PassID Connect  ·  API Keys  ·  Code Verification  ·  Connection Requests  ·  Identity & Income APIs  ·  Webhooks PassID Connect  ·  API Keys  ·  Code Verification  ·  Connection Requests  ·  Identity & Income APIs  ·  Webhooks
PassID Connect — Integration Guide

Integrate in minutes.
No SDK needed.

PassID Connect is a hosted authorization flow. Your backend creates a scoped session, your customer clicks Connect with PassID, PassID handles authentication and consent, and your backend receives a connection_id for approved scopes.

Step 1

Get your API keys

Sign up at passid.io/onboard. Once approved, log in to the institution dashboard, go to PassID Connect → API Keys, and create your first sandbox key. Use sandbox while building, then switch to your live institution API key after KYB and pilot approval.

🔑
Copy Connect keys immediately — secret keys are shown once and never stored in plaintext. Put them in server-side environment variables only. Never expose them in frontend code, mobile apps, logs, or version control.
Store your key as an environment variable YOUR SERVER
# .env (never commit this file)
PASSID_CONNECT_KEY=sk_test_your_key_here
PASSID_CONNECT_BASE=https://api.passid.io/api/sandbox/connect
Where to find your keys
1
Go to passid.io/institution → log in with your institution account
2
Open PassID Connect and use the API Keys / Live Access tabs
3
Create a sandbox key for testing. After production approval, use your live institution API key with the production base URL.
Step 2

Authentication & base URL

All server-side PassID Connect requests use Bearer token authentication. Pass your sandbox or live institution API key in the Authorization header. The institution is derived from the key, never from a caller-supplied institution ID.

Every API request — sandbox YOUR BACKEND
# Sandbox base URL
https://api.passid.io/api/sandbox/connect

# Production base URL for approved institutions
https://api.passid.io/v1/connect

# Required header on every server-side request
Authorization: Bearer $PASSID_CONNECT_KEY

# Optional — prevents double-execution on retry
Idempotency-Key: unique-id-per-request

Quick smoke test

Run this to confirm your key is working before writing any code:

terminal TEST YOUR KEY
curl https://api.passid.io/api/sandbox/connect/keys \
  -H "Authorization: Bearer sk_test_your_key_here"

# 200 → your key is active. 401 → check the key value.
i
For production, replace the base URL with https://api.passid.io/v1/connect and use your live institution API key. Requested scopes are checked against your institution package before PassID creates a session.
Step 3

The PassID Connect flow

The scalable integration starts inside your application and moves the customer into PassID-hosted authorization. Institution staff never ask for, copy, or enter the customer's PassID Code:

1
Customer clicks Connect with PassID
The customer starts an application on your website or app and clicks Connect with PassID.
2
Your backend creates a scoped session
Your backend calls POST /sessions with the approved package scopes, purpose, access duration, return URL, and your internal application reference.
3
PassID-hosted UI opens
Your frontend opens the hosted URL or SDK modal. The customer signs in or, if needed, enters a temporary PassID Code inside PassID.
4
Customer reviews and approves
PassID shows your verified institution name, requested scopes, purpose, and duration. The customer approves or declines.
5
PassID redirects back
PassID sends the customer to your return_url. Your backend verifies GET /sessions/:session_id before trusting the callback.
6
You read approved customer data
Call data endpoints with the connection_id and your secret key. Each endpoint enforces approved scopes independently.
Step 4

Create a hosted session

This is the main call. Your backend creates a hosted Connect session with requested scopes, purpose, access duration, return URL, and your internal application reference. PassID returns a client_secret for server-side retrieval and hosted_url for the customer-facing authorization step.

POST
/v1/connect/sessions
Creates a hosted Connect session. Returns session_id, server-side client_secret, hosted_url, and backing request_id. Supports idempotency via the Idempotency-Key header.
POST /sessions YOUR BACKEND
curl -X POST https://api.passid.io/v1/connect/sessions \
  -H "Authorization: Bearer live_institution_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: req-$(uuidgen)" \
  -d '{' \
    '"return_url": "https://institution.example/app/passid/callback",' \
    '"application_reference": "app_123",' \
    '"scopes": ["identity.read", "accounts.read", "income.read"],' \
    '"purpose": "Personal loan application",' \
    '"access_duration": "90days"' \
  '}'

Request body fields

FieldTypeRequiredDescription
return_urlstringYesAbsolute URL PassID redirects to after approval or decline
application_referencestringNoYour internal application, applicant, or workflow reference
scopesstring[]YesData scopes to request. Scopes must be enabled by your institution package. See scope reference below.
purposestringNoShown to the customer on the approval screen. Be specific: "Rental application", "Personal loan", etc.
access_durationstringNo"90days", "1year", or "permanent". Defaults to 90 days.

Response

201 Created PASSID RESPONSE
{
  "data": {
    "session_id": "pcs_live_a1b2c3d4",
    "client_secret": "pcs_live_secret_...",
    "hosted_url": "https://app.passid.io/connect/authorize?env=live&session=pcs_live_...",
    "status": "pending_customer",
    "requested_scopes": "identity.read accounts.read income.read",
    "expires_at": "2026-06-26T12:20:00Z"
  }
}

Scope bundles by use case

Use caseRecommended scopes
Personal loan / creditidentity.read accounts.read balances.read income.read liabilities.read risk_flags.read
Rental applicationidentity.read accounts.read balances.read income.read verification_status.read
Marketplace onboardingidentity.read accounts.read verification_status.read
Insurance underwritingidentity.read accounts.read income.read liabilities.read risk_flags.read
Employment verificationidentity.read income.read verification_status.read
Full accessidentity.read accounts.read balances.read transactions.read income.read liabilities.read risk_flags.read verification_status.read when enabled by the institution package
Step 5

Handle hosted callback

After approval or decline, PassID redirects the customer to your return_url. Your backend verifies the hosted session status and stores the approved connection_id.

GET
/v1/connect/sessions/:session_id
Returns the current status of a hosted session. Verify this after the return_url callback. When status is "approved", the connection_id is included.
GET /sessions/:id - status check in Node.js YOUR BACKEND
async function waitForApproval(sessionId, timeoutMs = 300_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const res = await fetch(`${BASE}/sessions/${sessionId}`, {
      headers: { "Authorization": `Bearer ${SK}` },
    });
    const { data } = await res.json();
    if (data.status === "approved") return data.connection_id;
    if (data.status === "declined") throw new Error("Customer declined");
    if (data.status === "expired") throw new Error("Session expired");
    await new Promise(r => setTimeout(r, 2500)); // poll every 2.5s
  }
  throw new Error("Timeout waiting for approval");
}

Status values

StatusMeaningAction
pending_customerWaiting for customer to approve or declineKeep polling
approvedCustomer approved — connection_id is in the responseUse connection_id to fetch data
declinedCustomer declined the requestTell the customer and allow them to retry with a new code
expiredCustomer did not respond within the TTLAsk the customer to relaunch PassID authorization

Approved response

200 OK — status: "approved" PASSID RESPONSE
{
  "data": {
    "session_id": "pcs_live_a1b2c3d4",
    "client_secret": "pcs_live_secret_...",
    "hosted_url": "https://app.passid.io/connect/authorize?env=live&session=pcs_live_...",
    "status": "approved",
    "connection_id": "conn_live_x7y8z9",
    "granted_scopes": ["identity.read", "accounts.read", "income.read"],
    "approved_at": "2026-06-26T12:05:42Z"
  }
}
!
Production rule: never trust only the query string on your callback. Always verify the session server-side with GET /v1/connect/sessions/:session_id. In sandbox, use the hosted test flow to approve or decline as a customer.
Step 7

Fetch verified customer data

With a connection_id in hand, call any data endpoint. Each call returns data for the scope that was granted. A 403 with code INSUFFICIENT_SCOPE means the customer did not grant that scope.

GET
/v1/connect/connections/:id/identity
Returns the customer's verified identity: full name, masked date of birth, country, city, verification status, and verified_at timestamp. Requires identity.read scope.
GET
/v1/connect/connections/:id/accounts
Returns the customer's linked financial accounts: account IDs, type (savings/current), institution name, masked last 4 digits, currency, status. Requires accounts.read.
GET
/v1/connect/connections/:id/balances
Returns current balances per account. Requires balances.read.
GET
/v1/connect/connections/:id/income
Returns verified income data: monthly income, income source, currency, frequency, and employer (if employment-based). Requires income.read.
GET
/v1/connect/connections/:id/liabilities
Returns outstanding liabilities: loans, credit facilities, monthly obligations. Requires liabilities.read.
GET
/v1/connect/connections/:id/risk-flags
Returns risk indicators and flags. Requires risk_flags.read.
GET
/v1/connect/connections/:id/verification-status
Returns identity verification status and date. Requires verification_status.read.

Example: fetch identity

GET /connections/:id/identity YOUR BACKEND
curl https://api.passid.io/v1/connect/connections/conn_live_x7y8z9/identity \
  -H "Authorization: Bearer live_institution_api_key"
200 OK — identity response PASSID RESPONSE
{
  "data": {
    "connection_id": "conn_live_x7y8z9",
    "environment": "live",
    "identity": {
      "full_name": "Amara Osei",
      "date_of_birth": "****-**-14", // masked
      "country": "KE",
      "city": "Nairobi",
      "verification_status": "verified",
      "verified_at": "2026-01-15T09:20:00Z"
    }
  }
}

Complete integration — 25 lines

passid-connect.js — full flow NODE.JS
const SK = process.env.PASSID_CONNECT_KEY;
const BASE = process.env.PASSID_CONNECT_BASE ?? "https://api.passid.io/v1/connect";
const h = { "Authorization": `Bearer ${SK}`, "Content-Type": "application/json" };

async function connectCustomer(scopes, purpose, applicationReference) {
  // 1. Create a hosted session
  const req = await fetch(`${BASE}/sessions`, {
    method: "POST", headers: h,
    body: JSON.stringify({ scopes, purpose, access_duration: "90days", return_url: "https://institution.example/app/passid/callback", application_reference: applicationReference }),
  }).then(r => r.json());
  const sessionId = req.data.session_id;

  // 2. Poll until customer approves (or times out)
  let connectionId;
  for (let i = 0; i < 120; i++) {
    await new Promise(r => setTimeout(r, 2500));
    const s = await fetch(`${BASE}/sessions/${sessionId}`, { headers: h }).then(r => r.json());
    if (s.data.status === "approved") { connectionId = s.data.connection_id; break; }
    if (["declined", "expired"].includes(s.data.status)) throw new Error(s.data.status);
  }
  if (!connectionId) throw new Error("timeout");

  // 3. Fetch verified data
  const [identity, income] = await Promise.all([
    fetch(`${BASE}/connections/${connectionId}/identity`, { headers: h }).then(r => r.json()),
    fetch(`${BASE}/connections/${connectionId}/income`, { headers: h }).then(r => r.json()),
  ]);
  return { connectionId, identity: identity.data, income: income.data };
}
Step 8

Webhooks

PassID sends signed HTTP POST events to your registered webhook URL when connection state changes. Configure your URL in the institution dashboard. Treat webhooks as notifications and verify final state through the Connect API before making decisions.

Event types

connection.created
Fires when a customer approves a hosted Connect session. Payload includes connection_id, request_id, and granted_scopes. Use this to trigger your underwriting or onboarding flow.
connect.session.created
Fires when your backend creates a hosted Connect session. Payload includes session_id, request_id, and requested scopes.
connect.session.approved
Fires when the customer approves. Payload includes connection_id, consent_id, and granted_scopes.
connect.session.declined
Fires when the customer declines. No customer data is shared.
connection.created
Fires when an approved connection is created and ready for approved data retrieval.
connection.revoked
Fires when access is revoked. Stop using the connection_id immediately.
connection.expired
Fires when the access duration expires. Create a new hosted session if the customer needs to reauthorize.

Webhook payload

POST https://yourapp.com/webhooks/passid PASSID → YOUR SERVER
// Headers sent by PassID
X-PassID-Event: connection.created
X-PassID-Signature: sha256=abc123...
Content-Type: application/json

// Body
{
  "event_id": "evt_live_9a8b7c6d",
  "event_type": "connection.created",
  "created_at": "2026-06-26T12:05:42Z",
  "data": {
    "connection_id": "conn_live_x7y8z9",
    "session_id": "pcs_live_a1b2c3d4",
    "granted_scopes": ["identity.read", "income.read"]
  }
}

Verify the signature (Node.js)

webhook-handler.js YOUR BACKEND
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(rawBody, sigHeader, sk) {
  const expected = createHmac("sha256", sk)
    .update(rawBody)
    .digest("hex");
  const received = (sigHeader ?? "").replace(/^sha256=/, "");
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  if (a.length !== b.length || !timingSafeEqual(a, b)) throw new Error("Invalid signature");
}

// Express handler
app.post("/webhooks/passid", express.raw({ type: "application/json" }), (req, res) => {
  verifyWebhook(req.body, req.headers["x-passid-signature"], process.env.PASSID_WEBHOOK_SECRET);
  const event = JSON.parse(req.body);
  // handle event.event_type
  res.sendStatus(200); // respond fast — process async
});
i
Use express.raw() (not express.json()) so the body is the exact bytes PassID sent. Parsing before HMAC validation will break the signature check.
Step 9

Error reference

All error responses follow the same envelope. The error.code field is machine-readable; error.message is human-readable.

Error envelope PASSID RESPONSE
{
  "success": false,
  "error": {
    "code": "INVALID_PASSID_CODE",
    "message": "This PassID Code is not recognised."
  },
  "requestId": "req_connect_abc123"
}
Error codeHTTPWhat happened & what to do
INVALID_API_KEY401Your Connect API key is wrong or revoked. Check it in PassID Connect → API Keys.
UNAUTHENTICATED401Authorization header missing or malformed. Format: Bearer <connect_api_key>
INVALID_PASSID_CODE404The fallback code was invalid or cancelled inside PassID-hosted auth. Ask the customer to relaunch PassID.
PASSID_CODE_ALREADY_USED409The fallback code was already consumed inside PassID-hosted auth. Ask the customer to relaunch PassID.
PASSID_CODE_EXPIRED410The fallback code expired inside PassID-hosted auth. Ask the customer to relaunch PassID.
INSUFFICIENT_SCOPE403The connection doesn't grant the requested scope. Check granted_scopes on the connection.
RATE_LIMITED429Too many requests. Back off and retry after the Retry-After header value.
INVALID_REQUEST400The request is malformed, the redirect URL is not registered, or a requested scope is not enabled for the institution package.
LIVE_MODE_NOT_ENABLED403The institution is not approved for live Connect access. Complete KYB and live-access review.
Step 10

Scope reference

Request only the scopes your use case needs. Customers see each scope on the approval screen — fewer scopes means higher approval rates.

ScopeEndpoint suffixData returned
identity.read/identityFull name, masked date of birth, country, city, verification status, verified_at
accounts.read/accountsAccount IDs, type (savings/current), institution, masked last4, currency, status
balances.read/balancesCurrent balance per account
transactions.read/transactions90-day transaction history
income.read/incomeMonthly income, source, currency, frequency, employer
liabilities.read/liabilitiesOutstanding loans, credit facilities, monthly obligations
risk_flags.read/risk-flagsRisk indicators and flags
verification_status.read/verification-statusVerification status and date
Step 11

Sandbox test users

The sandbox has five pre-seeded users covering different scenarios. Use the sandbox dashboard or hosted sandbox authorization flow to run the full session, callback, and data retrieval lifecycle.

UserSandbox IDScenarioWhat to test
Amara Osei (KE)sbuser_success_001Full successHappy path — all scopes, clean data, approved quickly
Tobenna Eze (NG)sbuser_partial_002Partial consentCustomer grants only some requested scopes — handle INSUFFICIENT_SCOPE on denied ones
Priya Mehta (IN)sbuser_highliai_003High liabilityIncome and liabilities data present — test your risk assessment logic
Kwame Asante (GH)sbuser_discon_004DisconnectedConnection exists but account inactive — test stale connection handling
Fatima Al-Hassan (NG)sbuser_blocked_005Blocked userUser is flagged — test your blocked-user flow and user.blocked webhook

Run a hosted Connect sandbox flow

1
Open the sandbox dashboard → PassID Connect tab
Go to passid.io/institution and click PassID Connect in the left sidebar.
2
Select a sandbox user in hosted PassID
Choose any of the 5 test users from the PassID-hosted screen. If a code fallback is shown, it stays inside PassID customer authentication.
3
Create the hosted session from your backend
Your server creates the scoped session and passes the hosted URL or client secret back to the frontend.
4
Approve in hosted PassID
The customer approves in PassID-hosted UI. Your callback handler verifies the session and stores the connection_id.
Step 12

Go-live checklist

Complete all items before switching to your live API key.

  • Sandbox and live Connect keys are stored in a secrets manager (AWS Secrets Manager, Railway variables, Vault) — never in source code or .env committed to version control
  • End-to-end sandbox flow complete: created a hosted session, launched PassID, approved consent, handled callback, stored connection_id, and fetched identity and income data
  • Webhook URL configured in sandbox dashboard → Settings → tested with the Send test event button in the Webhooks tab
  • Webhook signature verification implemented using HMAC-SHA256 with timingSafeEqual — and tested against the test event
  • Webhook endpoint responds 200 within 5 seconds — async processing happens in background (queue, worker, etc.)
  • Idempotency-Key header sent on every POST /sessions to prevent duplicate hosted sessions on network retry
  • Hosted authorization errors handled, including customer cancellation, expired session, and PassID Code fallback failures inside PassID UI
  • Partial consent handled — check granted_scopes before calling data endpoints; don't assume all requested scopes were granted
  • All five sandbox users tested — especially partial_consent and blocked scenarios
  • Institution package is assigned correctly so Connect only allows services approved for the institution's use case
Ready to go live?
Our team activates live keys after a short review. We check that your webhook is reachable, your signature verification works, and your error handling is correct. Typical review takes 1 business day.
Request live key review ›
PASSID
Thank you - we'll be in touch.