A comprehensive demo application showcasing two integration methods for PrivateID's verification service:
- Redirect Flow: Traditional full-page redirect to PrivateID's hosted verification
- iFrame Flow: Embedded verification with real-time updates via Server-Sent Events (SSE)
- ✅ Dual Integration Patterns: Compare redirect vs iframe approaches
- ✅ Configurable Requirements: Choose between face-only or face + identity document verification
- ✅ Real-time Updates: SSE-powered instant result display for iframe flow
- ✅ Webhook Handling: Server-side webhook processing with automatic fallback polling
- ✅ TypeScript: Full type safety throughout
- ✅ Modern UI: Responsive design with Tailwind CSS v4
- ✅ Session Management: In-memory session store with pub/sub pattern
- ✅ Both Verification Types: Support for ENROLL and VERIFY flows
- ✅ Auto-Detection: Webhook URLs automatically use current hostname (ngrok-friendly)
This demo application shows two ways to integrate PrivateID verification into your project:
- Best for: Simple integration, mobile-friendly
- User Experience: Full-page redirect to PrivateID → returns to your app
- Complexity: Low (3 API calls)
- Use case: Public-facing apps, mobile websites
- Best for: Seamless embedded experience, desktop apps
- User Experience: Verification happens inline, no page redirect
- Complexity: Medium (4 API calls + SSE)
- Use case: Admin panels, progressive web apps, desktop applications
| Factor | Redirect Flow | iFrame Flow |
|---|---|---|
| Mobile compatibility | ✅ Excellent | |
| User experience | Good (leaves your app briefly) | ✅ Excellent (stays in your app) |
| Implementation | ✅ Simple | Medium |
| Real-time updates | Polling (2s intervals) | ✅ Instant (SSE) |
| Network efficiency | Good | ✅ Excellent |
Recommendation: Start with Redirect Flow for simplicity, migrate to iFrame Flow if you need seamless UX.
┌─────────────┐
│ Browser │
└──────┬──────┘
│
├─ Redirect Flow: Full page redirect → PrivateID → Return to result page
│ Result page polls for webhook data (with 10s fallback)
│
└─ iFrame Flow: Embedded iframe → Real-time SSE updates
Results appear instantly when webhook arrives
- Node.js 18+ (currently running on Node 23.2.0, but 22+ recommended)
- npm or yarn
- ngrok or similar tunneling service (for local webhook testing)
- PrivateID API key
npm installCopy the example environment file:
cp .env.example .env.localEdit .env.local with your API key (API key will be provided via email):
PRIVATEID_API_KEY=your-api-key-here
PRIVATEID_API_BASE=https://api-orchestration.uat.privateid.com/v2Note: You don't need to set BASE_URL! The app automatically detects your current URL (works with both localhost and ngrok).
npm run devFor webhook testing, use ngrok to create a public tunnel:
# In a separate terminal
ngrok http 3000Then simply navigate to the ngrok URL (e.g., https://abc123.ngrok.io) instead of localhost:3000.
That's it! No need to update environment variables or restart the server. The app automatically uses the correct webhook URL based on your current hostname.
Open your browser to http://localhost:3000 (or your ngrok URL) and try both integration methods:
- Redirect Flow:
/redirect-flow - iFrame Flow:
/iframe-flow
Open ngrok inspector to see webhook requests in real-time:
http://127.0.0.1:4040
next-iframe-demo/
├── src/
│ ├── app/
│ │ ├── page.tsx # Home page
│ │ ├── layout.tsx # Root layout
│ │ ├── globals.css # Global styles
│ │ ├── redirect-flow/
│ │ │ ├── page.tsx # Redirect flow demo
│ │ │ └── result/
│ │ │ └── page.tsx # Result page (polling)
│ │ ├── iframe-flow/
│ │ │ └── page.tsx # iFrame flow demo (SSE)
│ │ └── api/
│ │ ├── sessions/
│ │ │ ├── route.ts # POST: Create session, GET: List sessions
│ │ │ └── [sessionId]/
│ │ │ ├── route.ts # GET: Session status
│ │ │ └── stream/
│ │ │ └── route.ts # GET: SSE stream
│ │ └── webhook/
│ │ └── route.ts # POST: Webhook handler
│ ├── lib/
│ │ ├── privateId/
│ │ │ ├── client.ts # PrivateID API client
│ │ │ ├── types.ts # TypeScript types
│ │ │ └── config.ts # API configuration
│ │ └── session/
│ │ ├── store.ts # In-memory session store
│ │ └── types.ts # Session types
│ └── components/
│ ├── PrivateIdIframe.tsx # iFrame wrapper
│ └── ResultDisplay.tsx # Results UI component
├── .env.local # Environment variables (gitignored)
├── .env.example # Example env file
├── package.json
├── tsconfig.json
├── tailwind.config.ts
└── README.md
Create a new verification session.
Request:
{
"sessionType": "ENROLL" | "VERIFY",
"flowType": "redirect" | "iframe",
"baseUrl": "https://your-domain.com", // (optional - defaults to window.location.origin)
"requirements": ["face", "identity_document"] // (optional - defaults based on sessionType)
}Requirements Options:
["face"]- Face verification only (suitable for VERIFY or quick ENROLL)["identity_document"]- Identity document only (less common)["face", "identity_document"]- Both face and document (recommended for ENROLL)- If not provided, defaults to: ENROLL = both, VERIFY = face only
Response:
{
"sessionId": "uuid",
"verificationUrl": "https://...",
"expiresAt": "2024-01-01T00:00:00Z"
}Get session status (used by redirect flow for polling).
Response:
{
"sessionId": "uuid",
"sessionType": "ENROLL",
"status": "PENDING" | "IN_PROGRESS" | "SUCCESS" | "FAILED",
"flowType": "redirect" | "iframe",
"webhookData": {...}
}Server-Sent Events stream for real-time updates (used by iframe flow).
Returns SSE stream with session updates when webhook arrives.
Webhook endpoint for PrivateID callbacks.
Request (from PrivateID):
{
"sessionId": "uuid",
"status": "SUCCESS" | "FAILED",
"verificationResult": {...},
"timestamp": "..."
}- User selects verification requirements (Face, Identity Document, or both)
- User clicks "Start Enrollment" or "Start Verify"
- Client calls
POST /api/sessionswithflowType: "redirect"and selected requirements - Server creates session and gets verification URL from PrivateID
- Browser redirects to PrivateID verification URL
- User completes verification on PrivateID
- PrivateID sends webhook to
/api/webhook - Server stores webhook data in session
- PrivateID redirects user back to
/redirect-flow/result?sessionId=xxx - Result page polls
GET /api/sessions/:sessionIdevery 2 seconds - Automatic fallback polling kicks in after 10 seconds if webhook delayed
- Results display when webhook data is available
- User selects verification requirements (Face, Identity Document, or both)
- User clicks "Start Enrollment" or "Start Verify"
- Client calls
POST /api/sessionswithflowType: "iframe"and selected requirements - Server creates session and gets verification URL from PrivateID
- Client renders iframe with verification URL
- Client establishes SSE connection to
/api/sessions/:sessionId/stream - User completes verification in iframe
- PrivateID sends webhook to
/api/webhook - Server updates session and notifies SSE listeners
- Client receives SSE update and displays results instantly
- No page refresh needed!
Both flows support customizable verification requirements:
-
Face Verification (
face)- Captures and verifies user's face biometrics
- Required for both ENROLL and VERIFY flows
- Can be used standalone for quick verification
-
Identity Document (
identity_document)- Captures driver's license, passport, or government ID
- Primarily used for ENROLL flows
- Provides additional identity validation
In the UI:
- Both redirect and iframe flows have checkboxes before starting verification
- Select "Face Verification" and/or "Identity Document"
- Current selection is displayed before starting
Via API:
// Face only (quick verification)
fetch('/api/sessions', {
method: 'POST',
body: JSON.stringify({
sessionType: 'VERIFY',
flowType: 'iframe',
requirements: ['face']
})
});
// Face + Document (comprehensive enrollment)
fetch('/api/sessions', {
method: 'POST',
body: JSON.stringify({
sessionType: 'ENROLL',
flowType: 'redirect',
requirements: ['face', 'identity_document']
})
});If requirements is not specified:
- ENROLL: Defaults to
['face', 'identity_document'](comprehensive) - VERIFY: Defaults to
['face'](quick verification)
- New User Registration (ENROLL): Use both face + identity document for maximum security
- Returning User Login (VERIFY): Use face only for quick, frictionless authentication
- Custom Workflows: Mix and match based on your security requirements
npm run build # Test production build
npm run lint # Run ESLint- Check Next.js console output for server-side logs
- Use ngrok inspector (
http://127.0.0.1:4040) to inspect webhooks - Check browser console for client-side logs
- Use
GET /api/sessionsto view all active sessions
Webhooks not arriving:
- Make sure you're accessing the app via the ngrok URL (not localhost) when testing webhooks
- Check ngrok inspector (
http://127.0.0.1:4040) to see if webhooks are being sent - Verify your PrivateID API key is correct in
.env.local - Check server console logs for webhook processing messages
SSE not updating:
- Check browser console for SSE connection errors
- Ensure session exists before opening SSE stream
- Check server logs for listener subscription messages
Session not found:
- Sessions are stored in memory - restarting the server clears all sessions
- Use
GET /api/sessionsto verify session exists
| Variable | Description | Required | Default |
|---|---|---|---|
PRIVATEID_API_KEY |
PrivateID API key | ✅ Yes | - |
PRIVATEID_API_BASE |
PrivateID API base URL | No | https://api-orchestration.uat.privateid.com/v2 |
BASE_URL |
Your app's base URL | No | Auto-detected via window.location.origin |
NODE_ENV |
Environment | No | development |
Note: The app automatically sends window.location.origin as the webhook URL, so you don't need to manually configure BASE_URL for local development with ngrok!
Security Note: Never commit API keys or secrets to version control. Use environment variables or secret management services (AWS Secrets Manager, Vercel Env Vars, etc.).
- Framework: Next.js 14+ (App Router)
- Language: TypeScript 5+
- Styling: Tailwind CSS 4+
- State: React hooks + Server-Sent Events
- API: Next.js API Routes
- Session Store: In-memory Map (demo) → Redis (production)
This is a demo application. Refer to your PrivateID agreement for production usage terms.
For PrivateID API questions:
- Documentation: https://docs.privateid.com
- Support: Contact your PrivateID representative
For demo application issues:
- Check the troubleshooting section above
- Review server and browser console logs
- Verify ngrok tunnel is active (if testing webhooks)
- Access the app via the ngrok URL (not localhost) for webhook testing
- ✅ Test both flows (redirect and iframe)
- ✅ Monitor webhooks via ngrok inspector
- ✅ Understand the architecture differences
- 🚀 Choose the best integration method for your use case
- 🚀 Plan production deployment with persistent storage
- 🚀 Add authentication and security measures
- 🚀 Deploy to your production environment
✅ Two integration patterns - Choose between redirect or iframe based on your needs ✅ Configurable verification - Select face-only or face + document ✅ Production-ready architecture - Clear path from demo to production ✅ Real-time updates - SSE for instant results (iframe) or polling (redirect) ✅ Webhook handling - Secure server-side result processing
- Choose your integration: Redirect (simple) or iFrame (seamless UX)
- Copy relevant code: Use this demo as a reference implementation
- Add persistence: Replace in-memory store with Redis + Database
- Secure it: Add authentication, webhook verification, rate limiting
- Test thoroughly: Test both ENROLL and VERIFY flows
- Deploy: Follow the production checklist above
# 1. Clone/copy relevant files to your project
# 2. Install dependencies
npm install
# 3. Set environment variables
PRIVATEID_API_KEY=your-key
PRIVATEID_API_BASE=https://api-orchestration.privateid.com/v2
# 4. Implement these files in your project:
- src/lib/privateId/client.ts # API client
- src/app/api/sessions/route.ts # Session creation
- src/app/api/webhook/route.ts # Webhook handler
- Choose: redirect-flow/* OR iframe-flow/*
# 5. Replace session store with Redis/PostgreSQL
# 6. Add authentication and security
# 7. Test and deploy- PrivateID Docs: https://docs.privateid.com
- This Demo: Full working implementation with TypeScript
- Production Checklist: See "Pre-Production Checklist" above
- Questions?: Contact your PrivateID representative
Happy integrating with PrivateID! 🎉