Overview
The identiPay checkout integration allows customers to pay using the identiPay mobile wallet by scanning a QR code. This guide demonstrates a complete checkout implementation using real code from the identiPay demo store.
Architecture
The checkout flow involves three components:
Your Frontend : Displays cart, generates proposals, shows QR codes
Your Backend : Creates proposals via identiPay API with your API key
identiPay API : Generates payment proposals and tracks settlement
Backend: Proposal Creation
Create an API route that forwards proposal creation to identiPay:
Next.js API Route
Express.js
// app/api/proposal/route.ts
import { NextResponse } from "next/server" ;
const BACKEND_URL = process . env . BACKEND_URL || "http://localhost:8000" ;
const API_KEY = process . env . IDENTIPAY_API_KEY || "" ;
export async function POST ( request : Request ) {
const body = await request . json ();
const res = await fetch (
` ${ BACKEND_URL } /api/identipay/v1/proposals` ,
{
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
Authorization: `Bearer ${ API_KEY } ` ,
},
body: JSON . stringify ( body ),
}
);
const data = await res . json ();
if ( ! res . ok ) {
return NextResponse . json ( data , { status: res . status });
}
return NextResponse . json ( data , { status: 201 });
}
Never expose your API key in frontend code! Always create proposals from your backend to keep your API key secure.
Frontend: Create Proposal
When the customer clicks “Pay with identiPay”, create a proposal:
const createProposal = async () => {
setStep ( "creating" );
setError ( null );
try {
const res = await fetch ( "/api/proposal" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
items: items . map (( i ) => ({
name: i . product . name ,
quantity: i . quantity ,
unitPrice: i . product . price . toFixed ( 2 ),
currency: i . product . currency ,
})),
amount: {
value: total . toFixed ( 2 ),
currency: "USDC" ,
},
deliverables: {
receipt: true ,
},
// Add age gate if any item requires it
... ( maxAgeGate > 0 && {
constraints: {
ageGate: maxAgeGate ,
},
}),
expiresInSeconds: 900 , // 15 minutes
}),
});
if ( ! res . ok ) {
const errData = await res . json (). catch (() => ({}));
throw new Error ( errData . message || "Failed to create proposal" );
}
const data = await res . json ();
setProposal ( data );
setCountdown ( 900 );
setStep ( "pay" );
} catch ( err ) {
setError ( err instanceof Error ? err . message : "Failed to create proposal" );
setStep ( "review" );
}
};
Array of line items in the cart Quantity (must be positive)
Price per unit as a string (e.g., “9.99”)
Currency code (defaults to “USDC”)
Total payment amount Total amount as a string (e.g., “29.99”)
Currency code (e.g., “USDC”)
What the buyer will receive Show Deliverables properties
Whether to deliver an encrypted receipt
Optional warranty terms Warranty duration in days
Whether warranty can be transferred
Payment constraints (age verification, region locks, etc.) Show Constraint properties
Minimum age required (e.g., 18, 21)
Array of allowed country codes
Proposal expiration time in seconds (max 86400 = 24 hours) Default: 900 (15 minutes)
{
"transactionId" : "f47ac10b-58cc-4372-a567-0e02b2c3d479" ,
"intentHash" : "a3d5f8e9c2b1a4d6f8e9c2b1a4d6f8e9c2b1a4d6f8e9c2b1a4d6f8e9c2b1a4d6" ,
"qrDataUrl" : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." ,
"uri" : "did:identipay:techvault.store:f47ac10b-58cc-4372-a567-0e02b2c3d479" ,
"expiresAt" : "2026-03-09T15:30:00.000Z"
}
Unique transaction identifier (UUID)
Cryptographic hash of the payment intent
QR code as a data URL (PNG image)
DID-based URI for the proposal (encoded in QR code)
ISO 8601 timestamp when proposal expires
Display QR Code
Render the QR code for customers to scan:
import { QRCodeSVG } from "qrcode.react" ;
function PaymentQRCode ({ proposal } : { proposal : ProposalData }) {
return (
< div className = "qr-container" >
< QRCodeSVG
value = {proposal. uri }
size = { 200 }
level = "M"
bgColor = "#ffffff"
fgColor = "#0a0a0a"
/>
< div className = "expiry-timer" >
< span > Expires in { formatTime ( countdown )} </ span >
</ div >
</ div >
);
}
The QR code encodes a DID-based URI:
did:identipay:<hostname>:<transaction-id>
Example:
did:identipay:techvault.store:f47ac10b-58cc-4372-a567-0e02b2c3d479
When scanned, the wallet:
Parses the DID to extract hostname and transaction ID
Fetches proposal details: GET https://<hostname>/api/identipay/v1/intents/<transaction-id>
Displays payment details to the user
Generates required zero-knowledge proofs (age verification, etc.)
Signs and submits the settlement transaction
The wallet verifies your merchant identity against the on-chain trust registry before allowing payment.
Monitor Settlement Status
Use WebSocket to receive real-time payment updates:
useEffect (() => {
if ( step !== "pay" || ! proposal ?. transactionId ) return ;
// Connect to WebSocket
const protocol = window . location . protocol === "https:" ? "wss:" : "ws:" ;
const backendHost = process . env . NEXT_PUBLIC_BACKEND_URL
? new URL ( process . env . NEXT_PUBLIC_BACKEND_URL ). host
: "localhost:8000" ;
const wsUrl = ` ${ protocol } // ${ backendHost } /ws/transactions/ ${ proposal . transactionId } ` ;
const ws = new WebSocket ( wsUrl );
ws . onmessage = ( event ) => {
try {
const data = JSON . parse ( event . data );
if ( data . type === "settlement" ||
( data . type === "status" && data . status === "settled" )) {
setStep ( "confirming" );
setTxHash ( data . suiTxDigest || "" );
// Show success after brief confirming animation
setTimeout (() => setStep ( "success" ), 2000 );
}
} catch ( error ) {
console . error ( "Failed to parse WebSocket message" , error );
}
};
ws . onerror = ( err ) => {
console . error ( "WebSocket error:" , err );
};
return () => {
ws . close ();
};
}, [ step , proposal ?. transactionId ]);
See WebSocket API for complete documentation.
Age-Gated Products
For products requiring age verification, add constraints to your proposal:
const items = [
{
name: "Premium Vaporizer Kit" ,
quantity: 1 ,
unitPrice: "15.00" ,
currency: "USDC" ,
}
];
// Create proposal with age gate
const proposalData = {
items ,
amount: { value: "15.00" , currency: "USDC" },
deliverables: { receipt: true },
constraints: {
ageGate: 18 , // Require 18+
},
expiresInSeconds: 900 ,
};
The wallet will automatically:
Generate a zero-knowledge proof that the user meets the age requirement
Submit the proof with the payment
Never reveal the user’s actual birthdate or age
Age verification uses zk-SNARKs to prove current_year - birth_year >= required_age without revealing the birth year.
Complete Example
Here’s a minimal complete checkout implementation:
import { useState , useEffect } from "react" ;
import { QRCodeSVG } from "qrcode.react" ;
interface ProposalData {
transactionId : string ;
intentHash : string ;
qrDataUrl : string ;
uri : string ;
expiresAt : string ;
}
export default function Checkout ({ items , total } : CheckoutProps ) {
const [ step , setStep ] = useState < "review" | "pay" | "success" >( "review" );
const [ proposal , setProposal ] = useState < ProposalData | null >( null );
const [ countdown , setCountdown ] = useState ( 900 );
const createProposal = async () => {
const res = await fetch ( "/api/proposal" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({
items ,
amount: { value: total . toFixed ( 2 ), currency: "USDC" },
deliverables: { receipt: true },
expiresInSeconds: 900 ,
}),
});
const data = await res . json ();
setProposal ( data );
setStep ( "pay" );
};
// Countdown timer
useEffect (() => {
if ( step !== "pay" ) return ;
const interval = setInterval (() => {
setCountdown (( prev ) => Math . max ( 0 , prev - 1 ));
}, 1000 );
return () => clearInterval ( interval );
}, [ step ]);
// WebSocket listener
useEffect (() => {
if ( step !== "pay" || ! proposal ) return ;
const ws = new WebSocket (
`wss://api.identipay.net/ws/transactions/ ${ proposal . transactionId } `
);
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
if ( data . type === "settlement" && data . status === "settled" ) {
setStep ( "success" );
}
};
return () => ws . close ();
}, [ step , proposal ]);
if ( step === "review" ) {
return (
< button onClick = { createProposal } >
Pay { total . toFixed (2)} USDC with identiPay
</ button >
);
}
if ( step === "pay" && proposal ) {
return (
< div >
< h2 > Scan to pay </ h2 >
< QRCodeSVG value = {proposal. uri } size = { 200 } />
< p > Expires in { Math . floor ( countdown / 60)} : {( countdown % 60). toString (). padStart (2, '0' )}</ p >
</ div >
);
}
if ( step === "success" ) {
return < h2 > Payment successful !</ h2 > ;
}
return null ;
}
Testing
Test your integration:
Use Sandbox Mode : Set BACKEND_URL to the sandbox API endpoint
Install identiPay Wallet : Download the test wallet app
Fund Test Wallet : Get test USDC from the faucet
Complete Test Purchase : Scan QR code and verify settlement
Test your integration locally using the Sui testnet or devnet.
Next Steps
Payment Flow Understand the complete payment lifecycle
WebSocket API Real-time settlement notifications