Overview
The identiPay WebSocket API provides real-time notifications when payment settlements occur. This allows your frontend to immediately update the UI when a customer completes payment.
WebSocket connections do not require API key authentication. The transaction ID itself serves as authorization since it’s randomly generated and unguessable.
Connection URL
ws://api.identipay.net/ws/transactions/{transactionId}
wss://api.identipay.net/ws/transactions/{transactionId}
Use ws:// for development (HTTP)
Use wss:// for production (HTTPS)
Connecting to WebSocket
Browser JavaScript
const transactionId = "f47ac10b-58cc-4372-a567-0e02b2c3d479" ;
const protocol = window . location . protocol === "https:" ? "wss:" : "ws:" ;
const wsUrl = ` ${ protocol } //api.identipay.net/ws/transactions/ ${ transactionId } ` ;
const ws = new WebSocket ( wsUrl );
ws . onopen = () => {
console . log ( "Connected to payment status updates" );
};
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
console . log ( "Received:" , data );
if ( data . type === "settlement" ) {
// Payment completed!
showSuccessMessage ( data . suiTxDigest );
}
};
ws . onerror = ( error ) => {
console . error ( "WebSocket error:" , error );
};
ws . onclose = () => {
console . log ( "WebSocket connection closed" );
};
// Clean up when done
function cleanup () {
ws . close ();
}
React Hook
From the checkout demo (checkout-demo/src/components/CheckoutPage.tsx:97-129):
import { useState , useEffect } from "react" ;
interface ProposalData {
transactionId : string ;
intentHash : string ;
uri : string ;
expiresAt : string ;
}
export default function CheckoutPage () {
const [ step , setStep ] = useState < "pay" | "confirming" | "success" >( "pay" );
const [ proposal , setProposal ] = useState < ProposalData | null >( null );
const [ txHash , setTxHash ] = useState ( "" );
// WebSocket listener for real-time settlement updates
useEffect (() => {
if ( step !== "pay" || ! proposal ?. transactionId ) return ;
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 || "" );
// Brief confirming animation, then success
setTimeout (() => setStep ( "success" ), 2000 );
}
} catch {
// ignore parse errors
}
};
ws . onerror = ( err ) => {
console . error ( "WebSocket error:" , err );
};
return () => {
ws . close ();
};
}, [ step , proposal ?. transactionId ]);
return (
< div >
{ step === " pay " && < PaymentQRCode proposal = { proposal } /> }
{ step === " confirming " && < ConfirmingAnimation />}
{ step === " success " && < SuccessMessage txHash = { txHash } /> }
</ div >
);
}
Node.js
const WebSocket = require ( 'ws' );
const transactionId = 'f47ac10b-58cc-4372-a567-0e02b2c3d479' ;
const ws = new WebSocket (
`wss://api.identipay.net/ws/transactions/ ${ transactionId } `
);
ws . on ( 'open' , () => {
console . log ( 'Connected' );
});
ws . on ( 'message' , ( data ) => {
const message = JSON . parse ( data . toString ());
console . log ( 'Received:' , message );
if ( message . type === 'settlement' ) {
console . log ( `Payment settled! Tx: ${ message . suiTxDigest } ` );
ws . close ();
}
});
ws . on ( 'error' , ( error ) => {
console . error ( 'Error:' , error );
});
Message Types
Status Message
Sent immediately upon connection with current proposal status.
{
"type" : "status" ,
"transactionId" : "f47ac10b-58cc-4372-a567-0e02b2c3d479" ,
"status" : "pending" ,
"suiTxDigest" : null
}
Current proposal status: pending, settled, expired, or cancelled
Sui blockchain transaction digest (only present if settled)
Settlement Message
Sent when payment is successfully settled on-chain.
{
"type" : "settlement" ,
"transactionId" : "f47ac10b-58cc-4372-a567-0e02b2c3d479" ,
"status" : "settled" ,
"suiTxDigest" : "8x7HKvUKRhXZ9Q2bNnF4kP3mT6wJ1sY5vL2cD9gE4aB6"
}
Sui blockchain transaction digest for verification You can view the transaction at:
https://suiscan.xyz/mainnet/tx/{suiTxDigest}
Privacy Note : The settlement message does NOT include the buyer’s stealth address or any identifying information. You only receive confirmation that payment was received.
Error Message
Sent if transaction is not found or has been deleted.
{
"type" : "error" ,
"message" : "Transaction not found"
}
Human-readable error description
Backend Implementation
Here’s how the WebSocket handler works on the backend:
Connection Handler
From backend/src/ws/status.ts:14-42:
interface WsConnection {
send ( data : string ) : void ;
close () : void ;
}
// Map of transactionId -> set of WebSocket connections
const connections = new Map < string , Set < WsConnection >>();
export function handleWsConnection (
txId : string ,
ws : WsConnection ,
db : Db ,
onCleanup ?: () => void ,
) {
if ( ! connections . has ( txId )) {
connections . set ( txId , new Set ());
}
connections . get ( txId ) ! . add ( ws );
// Send current status on connect
sendCurrentStatus ( txId , ws , db );
// Return cleanup function
return () => {
const set = connections . get ( txId );
if ( set ) {
set . delete ( ws );
if ( set . size === 0 ) {
connections . delete ( txId );
}
}
onCleanup ?.();
};
}
Current Status Retrieval
From backend/src/ws/status.ts:44-75:
async function sendCurrentStatus ( txId : string , ws : WsConnection , db : Db ) {
try {
const [ proposal ] = await db
. select ()
. from ( proposals )
. where ( eq ( proposals . transactionId , txId ))
. limit ( 1 );
if ( proposal ) {
ws . send (
JSON . stringify ({
type: "status" ,
transactionId: proposal . transactionId ,
status: proposal . status ,
suiTxDigest: proposal . suiTxDigest ,
}),
);
} else {
ws . send (
JSON . stringify ({
type: "error" ,
message: "Transaction not found" ,
}),
);
}
} catch ( error ) {
console . error ( "Failed to send status:" , error );
}
}
Broadcasting Settlement
From backend/src/ws/status.ts:77-103:
export function pushSettlementUpdate (
txId : string ,
status : string ,
suiTxDigest : string ,
) {
const set = connections . get ( txId );
if ( ! set ) return ;
const message = JSON . stringify ({
type: "settlement" ,
transactionId: txId ,
status ,
suiTxDigest ,
});
for ( const ws of set ) {
try {
ws . send ( message );
} catch {
set . delete ( ws );
}
}
}
This function is called when the backend detects a StealthAnnouncement event on the Sui blockchain.
Connection Lifecycle
Client Connects
Frontend establishes WebSocket connection with transaction ID
Initial Status
Backend sends current proposal status (usually pending)
Wait for Settlement
Connection remains open, waiting for blockchain confirmation
Settlement Event
Backend detects on-chain settlement and broadcasts to all connected clients
Client Disconnects
Frontend closes connection after showing success message
Connection Limits
Maximum concurrent connections : 50 per merchant
Connection timeout : 30 minutes of inactivity
Message rate limit : Not applicable (server-sent only)
If you exceed concurrent connection limits, new connections will receive an error and be closed.
Error Handling
Connection Errors
const ws = new WebSocket ( wsUrl );
ws . onerror = ( error ) => {
console . error ( "WebSocket error:" , error );
// Show fallback UI
showPollingFallback ();
};
ws . onclose = ( event ) => {
if ( ! event . wasClean ) {
console . error ( `Connection closed unexpectedly: ${ event . reason } ` );
// Attempt reconnect
setTimeout (() => reconnect (), 5000 );
}
};
Reconnection Strategy
function connectWithRetry ( transactionId , maxRetries = 3 ) {
let retries = 0 ;
function connect () {
const ws = new WebSocket (
`wss://api.identipay.net/ws/transactions/ ${ transactionId } `
);
ws . onerror = () => {
if ( retries < maxRetries ) {
retries ++ ;
const delay = Math . min ( 1000 * Math . pow ( 2 , retries ), 10000 );
console . log ( `Retrying in ${ delay } ms...` );
setTimeout ( connect , delay );
} else {
console . error ( "Max retries reached, falling back to polling" );
startPolling ( transactionId );
}
};
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
handleMessage ( data );
};
return ws ;
}
return connect ();
}
Fallback to Polling
If WebSocket is unavailable, poll the status endpoint:
function startPolling ( transactionId ) {
const interval = setInterval ( async () => {
const response = await fetch (
`https://api.identipay.net/api/identipay/v1/transactions/ ${ transactionId } /status` ,
{
headers: {
Authorization: `Bearer ${ apiKey } ` ,
},
}
);
const data = await response . json ();
if ( data . status === "settled" ) {
clearInterval ( interval );
showSuccessMessage ( data . suiTxDigest );
} else if ( data . status === "expired" || data . status === "cancelled" ) {
clearInterval ( interval );
showErrorMessage ( data . status );
}
}, 3000 ); // Poll every 3 seconds
return interval ;
}
Security Considerations
Transaction ID as Authorization
The transaction ID (UUID v4) provides sufficient entropy:
122 bits of randomness
2^122 possible values (~5.3 × 10^36)
Impossible to guess or brute force
This is why WebSocket connections don’t require additional authentication.
Do not log or expose transaction IDs in public locations (analytics, error tracking, etc.). Treat them as sensitive identifiers.
What Merchants Can See
Merchants receive:
Transaction ID
Settlement status
Sui transaction digest
Merchants cannot see:
Buyer’s identity or address
Buyer’s stealth address
Receipt contents
Any personally identifiable information
What Buyers Receive
Buyers receive encrypted receipts via stealth address announcements on-chain. The merchant’s WebSocket does not receive or relay any buyer information.
Testing
Test WebSocket connections:
Open browser DevTools (F12)
Go to Console tab
Run:
const ws = new WebSocket (
'wss://api.identipay.net/ws/transactions/f47ac10b-58cc-4372-a567-0e02b2c3d479'
);
ws . onmessage = ( e ) => console . log ( 'Received:' , JSON . parse ( e . data ));
You should see the initial status message
Using wscat
npm install -g wscat
wscat -c wss://api.identipay.net/ws/transactions/f47ac10b-58cc-4372-a567-0e02b2c3d479
Simulating Settlement (Sandbox)
In sandbox mode, you can trigger a test settlement:
curl -X POST https://sandbox.identipay.net/api/test/settle \
-H "Content-Type: application/json" \
-d '{
"transactionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}'
This will immediately send a settlement message to all connected WebSocket clients.
Monitoring
Track WebSocket connection health:
const activeConnections = new Map ();
function monitorConnection ( transactionId , ws ) {
const startTime = Date . now ();
activeConnections . set ( transactionId , {
startTime ,
status: 'connected' ,
});
ws . onclose = () => {
const duration = Date . now () - startTime ;
console . log ( `Connection closed after ${ duration } ms` );
activeConnections . delete ( transactionId );
};
// Heartbeat
const heartbeat = setInterval (() => {
if ( ws . readyState === WebSocket . OPEN ) {
ws . send ( JSON . stringify ({ type: 'ping' }));
} else {
clearInterval ( heartbeat );
}
}, 30000 );
}
Alternative: Server-Sent Events (SSE)
If you prefer HTTP-based streaming:
const eventSource = new EventSource (
`https://api.identipay.net/sse/transactions/ ${ transactionId } `
);
eventSource . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
console . log ( 'Received:' , data );
};
eventSource . onerror = () => {
console . error ( 'SSE connection error' );
eventSource . close ();
};
SSE is currently not implemented but may be added in future versions. Use WebSocket for now.
Next Steps
Payment Flow Understand complete payment lifecycle