Skip to main content

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

FieldTypeDescription
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

ParameterTypeRequiredDescription
requestIdstringYesIdempotency key to prevent duplicate captures
captureAmountstringYesAmount to capture (e.g., "50.00")
isFinalCapturebooleanNoDefault 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
}
FieldTypeDescription
captureIdstringUnique capture identifier
statusstringCAPTURING or CAPTURED
captureAmountstringAmount 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)
StatusMeaning
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.