---
title: "Wallet Redeem Codes"
canonical_url: "https://ainode.run/docs/wallet-redeem-codes"
type: "doc"
locale: "en"
description: "Admin batch generation, revocation, and user wallet redemption APIs."
---

# Wallet Redeem Codes
> Admin batch generation, revocation, and user wallet redemption APIs.

APay provides built-in wallet redeem codes (`redeem_codes` and `redeem_redemptions`) that credit a user's `cash` balance pool.

- **Currency alignment**: Every redeem code's currency is locked to the site's accounting currency (`getTopupRules().accountingCurrency`, backed by `settings.currency`). Both code creation and redemption reject mismatched currencies with HTTP `409`.
- **Idempotency & concurrency gate**: `redeem_redemptions.code_id` (unique) acts as the concurrency gate so each code can only be claimed once; `redeem_redemptions.event_id` (`redeem:`) is the deterministic ledger idempotency key; `(batch_id, user_id)` enforces one redemption per user within the same non-null batch.
- **Wallet backend routing**: Local wallet sites (`APAY_WALLET_BACKEND=local`) credit via `changeBalance` (`actionType: 'redeem'`, `balanceType: 'cash'`); external wallet sites (`APAY_WALLET_BACKEND=ainode`) credit via the theme's `walletAdapter` (`transaction.credit`, `kind: 'redeem'`).

## User Redemption (`POST /api/users/redeem`)

Requires an authenticated user session. Rate-limited to 10 requests per minute per user.

### Request Body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | `string` | yes | Redeem code string (whitespace is stripped and normalized to uppercase) |

```json
{
  "code": "RC-2345-6789-ABCD"
}
```

### Response

```json
{
  "code": 0,
  "message": "兑换成功，已到账 10 USD",
  "data": {
    "amount": 10,
    "currency": "USD",
    "balance": 25.5,
    "alreadyProcessed": false
  }
}
```

If the same user submits a code they have already completed redeeming, the endpoint short-circuits without calling downstream wallet services and returns `alreadyProcessed: true`.

## Admin Endpoints

Protected by the `redeem` (`redeem:view` / `redeem:edit`) admin permission.

### List Redeem Codes (`GET /api/admin/redeem-codes`)

Query parameters: `page`, `pageSize` (max 200), `status` (`all` | `active` | `used` | `disabled`), `batchId`, `keyword`.
Effective `status`, `usedBy`, and `usedAt` are reconciled with `redeem_redemptions`.

### Batch Generate Redeem Codes (`POST /api/admin/redeem-codes`)

Requires `redeem:edit`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `count` | `number` | yes | Number of codes to generate (1–1000) |
| `amount` | `number` | yes | Face value per code (`> 0`) in the site's accounting currency |
| `currency` | `string` | no | Optional; if provided, must match the site's accounting currency |
| `batchId` | `string` | no | Optional batch identifier (limits each user to 1 code per batch) |
| `expiresAt` | `string` | no | Optional expiration timestamp; timezone-less strings are parsed in `settings.timezone` |
| `remark` | `string` | no | Optional operator note (max 200 chars) |

### Disable Redeem Code (`DELETE /api/admin/redeem-codes/:id`)

Requires `redeem:edit`. Disables an unused (`active`) redeem code. Returns HTTP `409` if the code has already been claimed in `redeem_redemptions` or marked `used`.
