> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Statement Credit Redemptions

> Cardholders can apply points as a credit toward their card balance.

Statement-credit redemptions let cardholders use their points as cardholder-facing credit, functionally similar to cashback. Rain handles this flow for Rain-Managed rewards programs. Partner-Managed Onchain programs handle statement credits in their own ledger, while Partner-Managed Offchain programs call Rain's API to debit the canonical points balance and then apply the credit in their own system.

The credit card industry has standardized around the convention that cash-like redemption options such as statement credits receive a lower redemption rate than others, like travel portal redemptions. This is due to the fact that the closer the redemption is to cash, the lower margin it provides the issuer.

## Rain-Managed

Rain-Managed statement credits use Rain's existing collateral payment flow. You still integrate the payment-signature and onchain payment UX; Rain records the rewards redemption automatically after the collateral payment settles.

Unlike travel redemptions—where the cardholder signs a withdrawal that burns points to the zero address—statement credits do not burn points. The cardholder signs a payment, and Rain calls `makePaymentFromCollateral` to apply the points against the card balance, increasing spending power and lowering the amount due.

### How It Works

1. The cardholder chooses to apply Raindrops towards statement credit in your app.
2. Your app requests a payment signature for the cardholder's collateral contract, passing the native token amount to redeem.
3. Your app creates a pending payment record with `POST /v1/issuing/users/{userId}/payments`. The response returns the `teamId` you pass to the onchain call.
4. The wallet submits the signed `makePaymentFromCollateral` transaction onchain.
5. Rain's payment event handler matches the onchain payment to the pending record, settles it, records a completed `STATEMENT_CREDIT` redemption, and reflects the discounted cent value in the balance response's `pendingStatementCredits` field.

### Code Recipe

This recipe walks through the Rain-Managed statement-credit flow end to end. You start from the dollars the cardholder wants to redeem and apply your program's discount rate to get the points to burn (see [Rain-Managed Discounted Value](#rain-managed-discounted-value)). Before you begin, fetch the user's collateral contract from the [get user contracts](/reference/contracts/get-smart-contract-information-for-a-user) endpoint. Your onchain points address is the `token` you pass to the signature request, and the `controllerAddress` is the coordinator contract you call onchain. The signature response returns the collateral proxy address and the rest of the onchain call arguments.

<Steps>
  <Step title="Request a payment signature">
    Convert the dollars the cardholder wants to redeem into points by applying your program's discount rate, then request a payment signature for those points. Rain sets the discount per program (30% by default) and can change it without a redeploy, so read it from your program configuration rather than hardcoding it. Round the points up so the redemption fully covers the requested credit, scale them to the native token amount (EVM has 18 decimals, Solana has 6), and pass that with `isAmountNative=true`. The endpoint returns `status: "ready"` with the signed `parameters` for the onchain call, or `status: "failed"` with an error, so check `status` before you use the response.

    ```ts theme={null}
    import axios from "axios";

    const BASE_URL = "https://api-dev.rain.xyz";

    // Rain applies a discount when converting the dollars the cardholder wants
    // to redeem into points. You confiugre this for your program and can
    // change it in the issuing dashboard, so be sure to update your implementation accordingly
    const DISCOUNT_RATE_PERCENTAGE = 30n;

    // Signed arguments for makePaymentFromCollateral, in call order:
    // [collateralProxy, assets[], amountsNative[], expiresAt, saltBytes[], signature]
    type PaymentSignatureParameters = [
      string,
      string[],
      string[],
      number,
      number[],
      string
    ];

    interface PaymentSignatureResponse {
      status: "ready" | "failed";
      signature?: { data: string; salt: string };
      expiresAt?: string; // ISO-8601
      sender?: string;
      chainId?: string;
      parameters?: PaymentSignatureParameters;
      error?: string;
    }

    async function getStatementCreditSignature(opts: {
      userId: string;
      apiKey: string;
      tenantTokenAddress: string;
      amountCents: bigint; // dollars the cardholder wants to redeem, in cents
      adminAddress: string; // the cardholder wallet making the payment
      chainId: string;
    }): Promise<PaymentSignatureResponse> {
      // Apply the discount to convert the dollars to redeem into points, rounding
      // up, then scale to the native amount (TenantToken has 18 decimals).
      const rate = 100n - DISCOUNT_RATE_PERCENTAGE;
      const amountPoints = (opts.amountCents * 100n + rate - 1n) / rate;
      const amountNative = (amountPoints * 10n ** 18n).toString();

      const response = await axios.get(
        `${BASE_URL}/v1/issuing/users/${opts.userId}/signatures/payments`,
        {
          headers: { "Api-Key": opts.apiKey },
          params: {
            token: opts.tenantTokenAddress,
            amount: amountNative,
            isAmountNative: true,
            adminAddress: opts.adminAddress,
            chainId: opts.chainId,
          },
        }
      );

      const data: PaymentSignatureResponse = response.data;
      if (data.status !== "ready") {
        throw new Error(`Signature not ready: ${data.error ?? data.status}`);
      }

      return data;
    }
    ```
  </Step>

  <Step title="Create a pending payment record">
    Create a pending payment record before you submit onchain. Rain's payment event handler matches the settled payment to this record by wallet address, team, chain, and amount, so the credit does not settle without it. Pass the dollars the cardholder is redeeming, in cents, as `amount`—the same value you converted to points in the previous step. Because you rounded the points up, this matches the cent value Rain derives from the settled onchain payment. The response returns the `teamId` you pass to `makePaymentFromCollateral`.

    ```ts theme={null}
    async function createPendingPayment(opts: {
      userId: string;
      apiKey: string;
      amountCents: bigint; // same dollars you converted to points
      walletAddress: string; // the cardholder wallet submitting the payment
      chainId: number;
    }): Promise<{ address: string; teamId: string }> {
      const response = await axios.post(
        `${BASE_URL}/v1/issuing/users/${opts.userId}/payments`,
        {
          amount: Number(opts.amountCents), // the statement credit, in cents
          walletAddress: opts.walletAddress,
          chainId: opts.chainId,
        },
        { headers: { "Api-Key": opts.apiKey } }
      );

      return response.data;
    }
    ```
  </Step>

  <Step title="Submit the collateral payment">
    The wallet signs and submits `makePaymentFromCollateral`. Unlike travel redemptions, this does not burn points to the zero address. Instead, it applies the points against the card balance, which increases spending power and lowers the amount due. Pass the signed `parameters` from the signature response, then pass the `teamId` from the pending payment record as the final argument.

    ```ts theme={null}
    import { ethers } from "ethers";
    // ABI Interface for V2 Collateral Coordinator Contract
    import { CoordinatorInterface } from "../lib/utils/abis/v2/Coordinator";

    async function submitStatementCredit(opts: {
      coordinatorAddress: string; // controllerAddress from the contracts endpoint
      signature: PaymentSignatureResponse;
      teamId: string; // from the pending payment record
      signer: ethers.Signer; // the cardholder wallet
    }) {
      const [proxyAddress, assets, amountsNative, expiresAt, saltBytes, signature] =
        opts.signature.parameters!;

      const coordinator = new ethers.Contract(
        opts.coordinatorAddress,
        CoordinatorInterface,
        opts.signer
      );

      const tx = await coordinator.makePaymentFromCollateral(
        proxyAddress, // _collateralProxy
        assets[0], // _asset (single asset)
        amountsNative[0], // _amountNative
        expiresAt, // _expiresAt (unix timestamp)
        ethers.hexlify(Uint8Array.from(saltBytes)), // _salt (bytes32)
        signature, // _signature
        opts.teamId // _teamId
      );

      await tx.wait();
    }
    ```
  </Step>

  <Step title="Rain records the redemption">
    After the payment settles onchain, Rain records a completed `STATEMENT_CREDIT` redemption and reflects the discounted cent value in the balance endpoint's `pendingStatementCredits` field. Poll the balance endpoint to confirm the credit landed.

    ```ts theme={null}
    async function getPendingStatementCredits(opts: {
      apiKey: string;
      userId: string;
    }): Promise<string> {
      const response = await axios.get(
        `${BASE_URL}/v1/issuing/raindrops/balance`,
        {
          headers: { "Api-Key": opts.apiKey },
          params: { userId: opts.userId },
        }
      );

      // Cent value of statement credits redeemed since the last statement.
      return response.data.pendingStatementCredits;
    }
    ```
  </Step>
</Steps>

<Info>
  Rain records the redemption idempotently against the settled payment, so a
  retried or re-observed onchain payment event does not create a duplicate
  `STATEMENT_CREDIT` redemption.
</Info>

## Partner-Managed

How statement credits work depends on whether your points balances live onchain or in Rain's database.

### Partner-Managed Onchain

You handle statement credits entirely in your own system. Because your points balances are canonical onchain, Rain neither records nor orchestrates onchain statement-credit redemptions. You debit the points and apply the cardholder-facing credit yourself.

### Partner-Managed Offchain

Rain keeps the canonical points ledger in its database, so you cannot debit points yourself. To redeem points for a statement credit, call `POST /v1/issuing/raindrops/statement-credit` with the user and the number of points to redeem. Rain debits the cardholder's offchain points balance and records a completed `STATEMENT_CREDIT` redemption, which lowers the user's `availablePoints`.

You decide what a redemption is worth. Rain records the points deduction only—it does not derive a dollar value, move spending power, or touch the card balance—so you set how much statement credit each point is worth and apply the cardholder-facing credit in your own system. You can use any conversion you want. For example, you might credit 1 point as \$0.01 or use a 1:1 ratio.

#### How It Works

1. The cardholder requests a statement credit in your app.
2. Call `POST /v1/issuing/raindrops/statement-credit` with the user and the point amount.
3. Rain debits the offchain points ledger and records a completed `STATEMENT_CREDIT` redemption. The response confirms the redemption and the `raindropAmount` deducted.
4. You apply the cardholder-facing credit in your own system, using whatever value you assign to the redeemed points.

#### Request

Send the user and the point amount as a positive integer string. Include an `idempotency-key` header so that retries replay the same response.

```bash theme={null}
curl -X POST https://api.rain.xyz/v1/issuing/raindrops/statement-credit \
  -H "Api-Key: YOUR_API_KEY" \
  -H "idempotency-key: <UNIQUE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "1f2e3d4c-5b6a-7980-1234-567890abcdef",
    "raindropAmount": "10000"
  }'
```

| Field            | Type          | Description                                                   |
| ---------------- | ------------- | ------------------------------------------------------------- |
| `userId`         | string (UUID) | The cardholder redeeming points.                              |
| `raindropAmount` | string        | The number of points to redeem, as a positive integer string. |

#### Response

A successful request returns `201` with a completed `STATEMENT_CREDIT` redemption. The response confirms the redemption and the `raindropAmount` that was deducted. Rain does not return a dollar value—you decide what the redeemed points are worth and apply the credit in your own system.

```json theme={null}
{
  "redemptionId": "9a8b7c6d-5e4f-3210-fedc-ba0987654321",
  "userId": "1f2e3d4c-5b6a-7980-1234-567890abcdef",
  "type": "STATEMENT_CREDIT",
  "raindropAmount": "10000"
}
```

<Info>
  Reusing the same `idempotency-key` and body replays the cached `201` response,
  so a retried request never double-debits the balance. The key is limited to 64
  characters.
</Info>

#### Errors

| Status | Meaning                                                                                                                |
| ------ | ---------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request, an insufficient points balance, or a `raindropAmount` so small it rounds to 0 cents after conversion. |
| `403`  | Raindrops are not enabled for this tenant.                                                                             |
| `404`  | The user was not found in this tenant.                                                                                 |
| `405`  | The tenant's points mode does not support this endpoint. Only Partner-Managed Offchain programs can call it.           |
| `409`  | Another offchain redemption is already in progress for this user.                                                      |

## Rain-Managed Discounted Value

For Rain-Managed programs, points redeem for statement credit at a discount that Rain configures per program. You apply this discount when converting the dollars the cardholder wants to redeem into the points to burn:

```ts theme={null}
const amountPoints =
  (amountCents * 100n + (100n - DISCOUNT_RATE_PERCENTAGE) - 1n) /
  (100n - DISCOUNT_RATE_PERCENTAGE);
```

The default discount is 30%, so \$70 of statement credit costs `ceil(7000 × 100 / 70) = 10,000` points. Rain can change the discount at any time without a redeploy, so read your current rate from your program configuration rather than hardcoding it.

<Info>
  Log into your Rain issuing dashboard for your program's current
  statement-credit discount rate.
</Info>

## What You See

For Rain-Managed programs, the balance endpoint's `pendingStatementCredits` field shows the cent value of statement credits redeemed since the user's last statement.

For Partner-Managed Offchain programs, `GET /issuing/raindrops/balance` returns only `availablePoints`, which drops by the redeemed point amount once Rain records the redemption.

## Billing Impact

Rain-Managed statement credits are charged at redemption time. Partner-Managed statement credits carry no charge from Rain and are excluded from charge reconciliation—you own the liability and any funding for the credit you apply.
