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

# Vouchers and uploads

> Add codes and links, import files, revoke and reveal vouchers.

## Vouchers

A voucher is one redeemable item of a set: a code (`voucherKind: code`) or a link (`voucherKind: link`).

| Field | Rules |
| - | - |
| `value` | Code: 1-256 printable ASCII characters without spaces. Link: up to 2,048 characters, an https URL whose host is in the set's `allowedLinkHosts`, or a custom app scheme listed in `allowedLinkSchemes`; may contain [Link Template](https://docs-provider.voucher.allride.io/provider/link-templates.md) placeholders except `{{code}}`. Unique within the set. |
| `redemptionType` | `single` (once) or `multiple`. Default from `defaults`, else `single`. |
| `maxRedemptions` | `single`: 1 (absent or `null` is stored as 1; another value → `max-redemptions-not-allowed-for-single`). `multiple`: `null` for **unlimited**, or 2 up to 18446744073709551615; 0 or 1 → `max-redemptions-too-small`. |
| `validFrom`, `validUntil` | Optional instants with offset; `validUntil` after `validFrom` and in the future. |
| `metadata` | Optional JSON object up to 16 KiB, for your own references. |

Voucher `status`: `available`, `exhausted` (all redemptions used), `expired` (past `validUntil`) or `revoked`. Links are checked against the set's current content: the live revision, or the draft or pending content if the set was never approved.

You can add vouchers in every status except `suspended` and `archived`; no change request is needed. New vouchers are redeemable once the set is active. Stock figures update within about two minutes.

## Upload a file

For more than 1,000 vouchers, upload a CSV or XLSX file. The file goes directly to storage, never through the API:

**1. Create the upload**

```bash theme={null}
FILE=codes-november.csv
curl -sS -X POST "$VOUCHER_API_BASE_URL/provider/v1/voucher-sets/2030/uploads" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "fileName": "'"$FILE"'",
    "format": "csv",
    "sizeBytes": '"$(wc -c < "$FILE" | tr -d ' ')"',
    "sha256": "'"$(shasum -a 256 "$FILE" | cut -d' ' -f1)"'",
    "defaults": {"redemptionType": "single", "validUntil": "2026-12-31T22:59:59Z"}
  }' > upload.json
```

The answer is 201 with `status: pending-upload` and an `uploadTarget` (`url`, `method`, `headers`, `expiresAt`), valid for 1 hour.

**2. PUT the file to the upload target**

```bash theme={null}
# The upload target is a signed storage URL, not $VOUCHER_API_BASE_URL; send no API key to it.
UPLOAD_URL=$(jq -r '.data.uploadTarget.url' upload.json)
curl -sS -X PUT "$UPLOAD_URL" -H "Content-Type: text/csv" --data-binary @"$FILE"
```

Send exactly the method and headers of `uploadTarget` (`text/csv`, or `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` for XLSX).

**3. Start processing**

```bash theme={null}
UPLOAD_ID=$(jq -r '.data.id' upload.json)
curl -sS -X POST "$VOUCHER_API_BASE_URL/provider/v1/voucher-sets/2030/uploads/$UPLOAD_ID:start" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

202 with `Location` and `Retry-After: 2`. A missing file → 409 `upload-file-missing`; a size that differs from `sizeBytes` → 422 `upload-size-mismatch`.

**4. Wait for the result**

Poll `Location` (start after 2 seconds, double up to 30 seconds) or wait for the `voucher-upload-completed` / `voucher-upload-failed` webhook. You also get an email.

At most 10 uploads per set can be open at a time (409 `voucher-upload-limit-reached`). An upload not started within 1 hour is cancelled with `failureReason: upload-expired`. Cancel an upload in `pending-upload` or `validated` with `:cancel`.

## File format

* **CSV**: UTF-8 (a BOM is allowed), comma or semicolon separator (detected from the header), RFC 4180 quoting, LF or CRLF line ends.
* **XLSX**: the first worksheet only; strings, numbers and date cells (read as UTC).
* At most 100 MiB and 1,000,000 data rows.
* A **header row** is required. Column names are case-insensitive: `value` (required), `redemptionType`, `maxRedemptions`, `validFrom`, `validUntil`, `metadata` (a JSON object as text).
* An empty cell takes the upload's `defaults`. In `maxRedemptions`, the word `unlimited` means unlimited, even when the defaults set a maximum.

```text theme={null}
value,redemptionType,maxRedemptions,validUntil,metadata
ACME5-7KQ2-M9Q1,single,,2026-12-31T22:59:59Z,"{""batch"":""nov-2026""}"
ACME5-FAMILY-01,multiple,unlimited,,
ACME5-TEAM-02,multiple,25,2027-03-31T21:59:59Z,
```

A file without a `value` column fails with `failureReason: missing-value-column`. An **unknown column fails the whole upload** with `unknown-column` before any row is checked, so a misspelt `validUntill` never silently drops data. A row with more or fewer cells than the header is invalid (`column-count-mismatch`).

## Validation and import

Processing has two phases:

1. **Validation.** The file's SHA-256 is checked (`checksum-mismatch` fails the upload), then **every** row is validated before any voucher is written.
2. **Import.** With no invalid rows, the import starts automatically. With invalid rows, the upload stops in `validated`: inspect the errors, then confirm with `:import` and `{"skipInvalidRows": true}` to import only the valid rows, or `:cancel`. A `validated` upload left for 7 days is cancelled (`validation-expired`).

```bash theme={null}
curl -sS "$VOUCHER_API_BASE_URL/provider/v1/voucher-sets/2030/uploads/$UPLOAD_ID/errors?perPage=100" \
  -H "Authorization: Bearer $VOUCHER_API_KEY"
```

Row errors are `{rowNumber, column, code}` (never the value); up to 10,000 are stored. Codes: `value-missing`, `value-invalid`, `link-host-not-allowed`, `scheme-not-allowed`, `redemption-type-invalid`, `max-redemptions-invalid`, `max-redemptions-not-allowed-for-single`, `max-redemptions-too-small`, `validity-window-invalid`, `date-invalid`, `metadata-invalid`, `column-count-mismatch`.

**Duplicates are not errors.** A value repeated in the file (`duplicate-in-file`) or already in the set (`value-exists`) is a valid row that is skipped and counted in `duplicateRows`; it appears in the row list but never stops the upload. Uploading the same file twice imports nothing the second time.

`counts` follows: `totalRows = validRows + invalidRows`, and when completed `importedRows = validRows − duplicateRows`. Imports run in chunks of 5,000 rows and resume after interruptions without duplicates. A row whose `validUntil` passed between validation and import is skipped as `validity-window-invalid`. If AllRide suspends or you archive the set during an import, the upload fails with `voucher-set-unavailable`; chunks already imported stay.

## Batch API

Create up to 1,000 vouchers synchronously, all or nothing:

```bash theme={null}
curl -sS -X POST "$VOUCHER_API_BASE_URL/provider/v1/voucher-sets/2030/vouchers:batch" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "defaults": {"redemptionType": "single", "validUntil": "2026-12-31T22:59:59Z"},
    "vouchers": [
      {"value": "ACME5-7KQ2-M9Q1"},
      {"value": "ACME5-FAMILY-01", "redemptionType": "multiple", "maxRedemptions": null, "metadata": {"batch": "family"}}
    ]
  }'
```

* 201 with `{createdCount, items}`: compact items (`id`, `valueMask`, `redemptionType`, `maxRedemptions`, `status`) in request order.
* Any invalid item → 400 `data-failed-validation` with one entry per problem (`vouchers[17].value`); nothing is created.
* A value already in the set → 409 `voucher-value-exists`, naming each item; a value twice in the request → `duplicate-value`. Nothing is created.
* More than 1,000 items → 422 `batch-too-large`.
* Send an `Idempotency-Key`: a retry then replays the result and never duplicates vouchers.

## Revoking vouchers

Revoke vouchers that must never be redeemed (for example leaked codes):

```bash theme={null}
curl -sS -X POST "$VOUCHER_API_BASE_URL/provider/v1/voucher-sets/2030/vouchers:revoke" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"voucherIds": [5031, 5032], "reason": "Leaked on a forum."}'
```

* Only `available` vouchers can be revoked; others → 409 `voucher-not-revocable`. Up to 1,000 ids, all or nothing.
* One voucher: `POST /provider/v1/vouchers/{voucherId}:revoke` with `{"reason": …}`.
* Revoked vouchers are never redeemable and never deleted; past redemptions stay.

## Viewing voucher values

List and get responses always show **masked** values (`valueMask`: the first 2 and last 2 characters of a code, scheme and host of a link), for every principal. Dashboard downloads (CSV / Excel) contain the same masked values.

The clear value of one voucher comes only from the reveal operation (`reveal-provider-vouchers`, delegable; dashboard users need a sign-in within the last 15 minutes):

```bash theme={null}
curl -sS -X POST "$VOUCHER_API_BASE_URL/provider/v1/vouchers/5030:reveal" \
  -H "Authorization: Bearer $VOUCHER_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

* One voucher per call; there is no bulk reveal.
* Every reveal is recorded in your [audit log](https://docs-provider.voucher.allride.io/workspace/audit-log.md) as `voucher.revealed` (never with the value) and monitored by AllRide.
* An idempotent replay returns `"value": null` with `Idempotent-Replay-Redacted: true`.
* To find a voucher by its value, use `search` on the vouchers list: an exact match on the full value, available only to principals with `reveal-provider-vouchers`; results stay masked.

## Related

* [Voucher sets](https://docs-provider.voucher.allride.io/provider/voucher-sets.md)
* [Dynamic links](https://docs-provider.voucher.allride.io/provider/link-templates.md)
* [Stock and alerts](https://docs-provider.voucher.allride.io/provider/stock-and-alerts.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.