Client-Side: Transaction Hash
When a customer completes the payment modal, executePayment resolves with the funding transaction details. At this point the session status is FUNDED — funds are sitting in the one-time wallet (OTW), ready for you to capture.
const result = await cubePayService . executePayment ({
amount: 50.0 ,
currency: "USD" ,
});
console . log ( result . transactionHash ); // "0xabc123..."
console . log ( result . session ); // { paymentSessionId, status: "FUNDED", ... }
Result Object
Field Type Description transactionHashstringOn-chain funding transaction hash sessionobjectFull session details (ID, status, network, token, addresses) detailsobjectAdditional session metadata
FUNDED does not mean the payment is complete. You must capture the funds to transfer them from the OTW to your treasury wallet.
Capture the Payment
Once a session is FUNDED, your server calls the capture endpoint to move funds from the one-time wallet to your treasury wallet. Capture supports both full and partial amounts.
Capture Request
// Server-side: capture the full payment
const captureResponse = await fetch (
` ${ process . env . CUBEPAY_API_HOST } /api/v1/payment-sessions/ ${ paymentSessionId } /captures` ,
{
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
Authorization: `Bearer ${ process . env . CUBEPAY_API_KEY } ` ,
},
body: JSON . stringify ({
requestId: "capture_ord_12345" , // idempotency key
captureAmount: "50.00" , // amount to capture
isFinalCapture: true , // true = capture + refund remaining balance
}),
}
);
const capture = await captureResponse . json ();
// { captureId: "cap_xyz", captureAmount: "50.00", refundAmount: "0.00", status: "CAPTURING" }
Capture Parameters
Parameter Type Required Description requestIdstringYes Idempotency key to prevent duplicate captures captureAmountstringYes Amount to capture (e.g., "50.00") isFinalCapturebooleanNo Default false. When true, any remaining balance is refunded to the customer and the session moves to SUCCEEDED
Capture Response
{
"captureId" : "cap_xyz789" ,
"captureAmount" : "50.00" ,
"refundAmount" : "0.00" ,
"status" : "CAPTURING"
}
Capture is an async operation . The response returns immediately with status CAPTURING. You receive a webhook when the capture completes.
Partial vs Final Capture
Partial Capture Capture a portion of the funded amount. The session returns to FUNDED and remains open for additional captures. {
"captureAmount" : "25.00" ,
"isFinalCapture" : false
}
Final Capture Capture the remaining amount (or a specific amount) and close the session. Any uncaptured balance is refunded to the customer. {
"captureAmount" : "25.00" ,
"isFinalCapture" : true
}
Check Capture Status
const status = await fetch (
` ${ process . env . CUBEPAY_API_HOST } /api/v1/payment-sessions/ ${ paymentSessionId } /captures/ ${ captureId } ` ,
{
headers: { Authorization: `Bearer ${ process . env . CUBEPAY_API_KEY } ` },
}
);
Response:
{
"captureId" : "cap_xyz789" ,
"status" : "CAPTURED" ,
"captureAmount" : "50.00" ,
"refundAmount" : "0.00" ,
"captureTransferTransactionHash" : "0xcapture123..." ,
"refundTransferTransactionHash" : null
}
Field Type Description captureIdstringUnique capture identifier statusstringCAPTURING or CAPTUREDcaptureAmountstringAmount captured refundAmountstringAmount refunded to customer (on final capture) captureTransferTransactionHashstring | nullOn-chain hash of the capture transfer refundTransferTransactionHashstring | nullOn-chain hash of the refund transfer
Fetch Session Details
After funding or capture, retrieve the full session state:
const response = await fetch (
` ${ process . env . CUBEPAY_API_HOST } /payment-sessions/ ${ sessionId } ` ,
{
headers: { Authorization: `Bearer ${ paymentSessionToken } ` },
}
);
The response includes capturableAmount, capturedAmount, and a captures array tracking all capture operations.
Server-Side: Webhook Confirmation
Never rely solely on client-side results to fulfill orders. Always confirm payments server-side via webhooks.
Register Your Webhook Endpoint
Configure your webhook URL in the Grain Dashboard under Settings > Webhooks .
Handle the Webhook
Grain sends payment status updates as JSON-RPC 2.0 requests:
// app/api/transactions/webhook/route.ts
import { NextResponse } from "next/server" ;
export async function POST ( request : Request ) {
const body = await request . json ();
const { method , params } = body ;
if ( method === "cubepay_paymentStatusUpdate" ) {
const update = params [ 0 ];
switch ( update . status ) {
case "FUNDED" :
// Customer funded the OTW — ready to capture
await initiateCapture ( update . paymentSessionId );
break ;
case "CAPTURING" :
// Capture in progress
console . log ( "Capture in progress:" , update . paymentSessionId );
break ;
case "SUCCEEDED" :
// Final capture complete — funds in your treasury
await confirmSettlement ( update . paymentSessionId );
break ;
case "CANCELED" :
await handleCancellation ( update . paymentSessionId );
break ;
case "ERROR" :
await handlePaymentError ( update . paymentSessionId );
break ;
}
}
// Always return 200 to acknowledge receipt
return NextResponse . json ({ jsonrpc: "2.0" , result: "ok" });
}
Webhook Payload
{
"jsonrpc" : "2.0" ,
"id" : "1770947403693" ,
"method" : "cubepay_paymentStatusUpdate" ,
"params" : [
{
"merchantId" : "your-merchant-id" ,
"paymentSessionId" : "ps_abc123def456" ,
"status" : "FUNDED" ,
"updatedAt" : "2026-03-20T12:05:00Z"
}
]
}
Payment Status Lifecycle
CREATED → PENDING → FUNDED → CAPTURING → SUCCEEDED
| | |
↓ ↓ ↓
CANCELED CANCELED FUNDED (partial capture)
Status Meaning CREATEDSession created, customer has not connected a wallet yet PENDINGWallet connected, awaiting funding transaction FUNDEDFunds received at the one-time wallet — ready to capture CAPTURINGCapture in progress (funds being transferred) SUCCEEDEDFinal capture complete — funds settled to your treasury CANCELEDSession expired or was cancelled (not funded within 24 hours) ERRORAn error occurred during processing
Always return a 200 status from your webhook handler, even if your internal processing fails. This prevents unnecessary retries. Handle errors asynchronously.
Next Steps
Learn how to Handle Errors & Edge Cases for robust payment flows.