### Generate a card series

`POST /api/v1/card-batches`

Stages an asynchronous generation job and returns immediately. The cards and the printable artifact are produced in the background; poll the series until status is ready, then fetch the print job.

- Authentication: manager session (JWT) or API token
- Permission: `prm_cards_generate_user` (Generate user cards)
- Risk: write
- Rate limit bucket: `t_mutate`
- Idempotent on `request_id`: retrying with the same id returns the original result

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `type` | body | enum | yes | refill or prepaid. |
| `quantity` | body | integer | yes | How many cards to mint. |
| `face_value` | body | number | yes | Value per card, major units. |
| `expiration` | body | string | yes | yyyy-MM-dd HH:mm:ss or yyyy-MM-dd. Series-level card lifetime. |
| `plan_profile_id` | body | integer | no | Required for prepaid, and for a refill card that auto-activates. |
| `code_length` | body | integer | no | The redemption PIN length. A MINIMUM, not an exact value — a mint may exceed it when the tenant's live code pool needs more entropy. |
| `code_charset` | body | enum | no | numeric, alpha, alnum or alnum_no_ambig. |
| `owner_manager_id` | body | integer | no | The series owner. Defaults to the creator. |
| `request_id` | body | string | yes | Idempotency key. |

#### Request

```json
{
  "type": "prepaid",
  "quantity": 500,
  "face_value": 150.00,
  "expiration": "2027-01-01",
  "plan_profile_id": 7,
  "code_length": 10,
  "code_charset": "alnum_no_ambig",
  "request_id": "5f2a7c18-2b0d-4a61-9f3e-7a4c8d2e1b09"
}
```

#### Response — 202 Accepted

```json
{
  "data": {
    "batch_id": 209,
    "mode": "voucher_account",
    "status": "generating",
    "replay": false
  }
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_VALIDATION` | 400 | bad expiration, an unworkable spec, or a request_id that is missing, too long or reserved |
| `ERR_VALIDATION` | 402 | the generating manager's wallet cannot cover the generation charge |
| `ERR_FORBIDDEN` | 403 | minting refill cards without the refill-generation permission, which is separate from the generate permission the route checks |

#### Note

A fresh stage answers 202 Accepted; a replay of the same request_id answers 200 OK with the original batch_id and replay true. The status field says generating in both cases, because it describes the job, not the outcome of this call. Branch on the HTTP status or on replay, not on status.

