> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grain.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle Payment Completion

> Process successful payments, capture funds, and confirm via webhooks

## 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.

```typescript theme={null}
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                                                  |
| ----------------- | -------- | ------------------------------------------------------------ |
| `transactionHash` | `string` | On-chain funding transaction hash                            |
| `session`         | `object` | Full session details (ID, status, network, token, addresses) |
| `details`         | `object` | Additional session metadata                                  |

<Info>
  `FUNDED` does not mean the payment is complete. You must **capture** the funds to transfer them from the OTW to your treasury wallet.
</Info>

***

## 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

```typescript theme={null}
// 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                                                                                                          |
| ---------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `requestId`      | `string`  | Yes      | Idempotency key to prevent duplicate captures                                                                        |
| `captureAmount`  | `string`  | Yes      | Amount to capture (e.g., `"50.00"`)                                                                                  |
| `isFinalCapture` | `boolean` | No       | Default `false`. When `true`, any remaining balance is refunded to the customer and the session moves to `SUCCEEDED` |

### Capture Response

```json theme={null}
{
  "captureId": "cap_xyz789",
  "captureAmount": "50.00",
  "refundAmount": "0.00",
  "status": "CAPTURING"
}
```

<Note>
  Capture is an **async operation**. The response returns immediately with status `CAPTURING`. You receive a webhook when the capture completes.
</Note>

### Partial vs Final Capture

<CardGroup cols={2}>
  <Card title="Partial Capture" icon="scissors">
    Capture a portion of the funded amount. The session returns to `FUNDED` and remains open for additional captures.

    ```json theme={null}
    {
      "captureAmount": "25.00",
      "isFinalCapture": false
    }
    ```
  </Card>

  <Card title="Final Capture" icon="circle-check">
    Capture the remaining amount (or a specific amount) and close the session. Any uncaptured balance is refunded to the customer.

    ```json theme={null}
    {
      "captureAmount": "25.00",
      "isFinalCapture": true
    }
    ```
  </Card>
</CardGroup>

### Check Capture Status

```typescript theme={null}
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:**

```json theme={null}
{
  "captureId": "cap_xyz789",
  "status": "CAPTURED",
  "captureAmount": "50.00",
  "refundAmount": "0.00",
  "captureTransferTransactionHash": "0xcapture123...",
  "refundTransferTransactionHash": null
}
```

| Field                            | Type             | Description                                    |
| -------------------------------- | ---------------- | ---------------------------------------------- |
| `captureId`                      | `string`         | Unique capture identifier                      |
| `status`                         | `string`         | `CAPTURING` or `CAPTURED`                      |
| `captureAmount`                  | `string`         | Amount captured                                |
| `refundAmount`                   | `string`         | Amount refunded to customer (on final capture) |
| `captureTransferTransactionHash` | `string \| null` | On-chain hash of the capture transfer          |
| `refundTransferTransactionHash`  | `string \| null` | On-chain hash of the refund transfer           |

***

## Fetch Session Details

After funding or capture, retrieve the full session state:

```typescript theme={null}
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

<Warning>
  Never rely solely on client-side results to fulfill orders. Always confirm payments server-side via webhooks.
</Warning>

### Register Your Webhook Endpoint

Configure your webhook URL in the [Grain Dashboard](https://dashboard.grain.inc) under **Settings > Webhooks**.

### Handle the Webhook

Grain sends payment status updates as **JSON-RPC 2.0** requests:

```typescript theme={null}
// 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

```json theme={null}
{
  "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                                                       |
| ----------- | ------------------------------------------------------------- |
| `CREATED`   | Session created, customer has not connected a wallet yet      |
| `PENDING`   | Wallet connected, awaiting funding transaction                |
| `FUNDED`    | Funds received at the one-time wallet — ready to capture      |
| `CAPTURING` | Capture in progress (funds being transferred)                 |
| `SUCCEEDED` | Final capture complete — funds settled to your treasury       |
| `CANCELED`  | Session expired or was cancelled (not funded within 24 hours) |
| `ERROR`     | An error occurred during processing                           |

<Tip>
  Always return a `200` status from your webhook handler, even if your internal processing fails. This prevents unnecessary retries. Handle errors asynchronously.
</Tip>

## Next Steps

Learn how to [Handle Errors & Edge Cases](/payment-sdk/integration-guide/handle-errors) for robust payment flows.
