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

# Requests and responses

> JSON format, response envelopes, numeric ids, timestamps, money, localised text, concurrent changes, commands and asynchronous jobs.

Every operation of the Consumer API (`/consumer/v1`) and the Workspace API (`/partner/v1`) follows the conventions on this page. Learn them once and every reference page reads the same way.

## Format

* Requests and responses are JSON in UTF-8. Send `Content-Type: application/json` with every request that has a body.
* The API is HTTPS only (TLS 1.2 or later). Production is `https://api.voucher.allride.io`, the sandbox is `https://api.voucher-staging.allride.io`.
* Property names are `camelCase`; enum values are `kebab-case` (`deactivation-scheduled`).
* Nullable properties are **always present**. A value that is not set is `null`, never a missing key, so you can rely on the shape of every resource.
* Send `Accept-Language: de` or `en` to choose the language of server-formatted text such as money labels and error messages. The response carries `Content-Language`.
* Every response carries `X-Request-Id`. You can send your own `X-Request-Id` (1-100 characters of letters, digits and `._:-`) to correlate requests with your logs.

## Response envelopes

A response that returns one resource wraps it in `data`:

```json theme={null}
{
  "data": {
    "id": 6017,
    "resource": "redemption",
    "status": "completed",
    "voucherSetId": 2030,
    "redeemedAt": "2026-10-21T17:42:10.120Z",
    "_links": {
      "self": { "href": "https://api.voucher.allride.io/consumer/v1/redemptions/6017", "type": "application/json" },
      "voucherSet": { "href": "https://api.voucher.allride.io/consumer/v1/catalog/voucher-sets/2030", "type": "application/json" }
    }
  }
}
```

A list returns a page inside `data`:

```json theme={null}
{
  "data": {
    "data": [
      { "id": 6017, "resource": "redemption", "status": "completed", "_links": { "self": { "href": "https://api.voucher.allride.io/consumer/v1/redemptions/6017", "type": "application/json" } } }
    ],
    "page": 1,
    "perPage": 25,
    "totalPages": 5,
    "total": 112
  }
}
```

The other shapes:

| Response | Shape |
| - | - |
| `200` / `201` with one resource | `{"data": {…}}` |
| `200` list | `{"data": {"data": [...], "page", "perPage", "totalPages", "total"}}` |
| `202` asynchronous job | `{"data": {…job…}}` plus `Location` and `Retry-After` headers |
| `204` delete | No body |
| `4xx` / `5xx` error | `{"errors": [{"message", "statusCode", "errorCode", …}]}`, see [Errors](https://docs-consumer.voucher.allride.io/errors.md) |

Every resource has `id`, `resource` (its type, for example `redemption` or `redemption-link`) and `_links`. `_links` maps a relation name to `{href, type}`; `self` is always present. A link appears only when your key or user holds the permissions to follow it. Value objects and statistics rows (for example analytics rows) carry `resource` but no `id` or `_links`.

## Identifiers

* Ids are positive integers (`"id": 42`), unique per resource type. They are JSON numbers in bodies and plain numbers in paths (`/consumer/v1/redemptions/6017`).
* An id that is not a positive integer, in the path, the query or the body, is answered with `400 data-failed-validation`; the entry in `more.validationErrors` has the constraint [`invalid-id`](https://docs-consumer.voucher.allride.io/errors/catalog#invalid-id) and names the parameter or property.
* A well-formed id that does not exist **or belongs to another partner** is answered with `404` and the resource's not-found code, for example [`redemption-not-found`](https://docs-consumer.voucher.allride.io/errors/catalog#redemption-not-found). You never see another partner's data, not even its existence.
* Your own references use separate fields: `refId` on redemption entities (1-128 characters of letters, digits and `._:@/+=-`) and `externalRef` on catalog settings (at most 64 characters). Never put personal data into them.

## Dates and times

* **Timestamps** are RFC 3339 in UTC with `Z`, at millisecond precision, truncated: no fraction when the milliseconds are zero (`2026-11-01T00:00:00Z`), otherwise exactly three digits (`2026-10-21T17:42:10.120Z`). Accept both shapes.
* **Timestamps you send** must carry an offset (`Z` or `+01:00`); they are normalised to UTC. `2026-12-31T23:59:59+01:00` is stored and returned as `2026-12-31T22:59:59Z`. A timestamp without an offset fails with the constraint [`invalid-timestamp`](https://docs-consumer.voucher.allride.io/errors/catalog#invalid-timestamp).
* **Dates** are `YYYY-MM-DD`, **months** `YYYY-MM`.
* **Calendar periods** (analytics months and days, per-entity monthly limits) are evaluated in your partner `timezone`, an IANA id such as `Europe/Berlin`.
* **Exception for billing:** billing service months and billing dates (`issueDate`, `dueDate`, `collectionDate`) are calendar months and dates in `Europe/Berlin`, whatever your partner timezone. Billing resources that carry a month also carry `billingZone: "Europe/Berlin"`. A redemption at `2026-10-31T23:30:00Z` by a partner in `Europe/London` counts in analytics month `2026-10` and in billing service month `2026-11`. See [Billing](https://docs-consumer.voucher.allride.io/consumer/billing.md).
* **Durations** are integers whose unit is in the property name: `gracePeriodMinutes`, `expiresInMinutes`, `paymentTermsDays`.
* **Intervals** are half-open: `[from, to)`. Filter ranges with `gte` and `lt`.

## Money

All amounts are in euros (EUR). `value` is in cent.

Money is an object with the amount in cent and a formatted label:

```json theme={null}
{ "value": 2900, "label": "29,00 €" }
```

* `value` is an integer, never a float. `label` is formatted in the response language (`€29.00` in English, `29,00 €` in German).
* In requests send `{"value": 2900}`. A `label` you send is accepted and ignored, so you can send back what you read.
* Amounts that can be negative (Stornorechnungen, balances) use the same shape with a signed `value`, for example `{"value": -1250, "label": "-12,50 €"}`.
* Per-redemption **unit prices** in billing and pricing add a `scale` property: `{value, scale, label}` with `scale` always `4` and `value` in **ten-thousandths** of a euro. `1000` is €0.10 and `1250` is €0.1250:

```json theme={null}
{ "value": 1250, "scale": 4, "label": "0,1250 €" }
```

Totals on invoices are always ordinary money in cent. Tax rates are decimal strings with two fraction digits (`"19.00"`), never JSON numbers. See [Billing](https://docs-consumer.voucher.allride.io/consumer/billing.md) and [Prices and price acceptance](https://docs-consumer.voucher.allride.io/consumer/prices.md).

## Localised text

Texts that providers and partners write per language (voucher set names, terms, landing texts) are objects keyed by locale:

```json theme={null}
{ "de": "5 € Fahrguthaben", "en": "€5 ride credit" }
```

* Responses always return the full map. Pick the locale for your customer on your side.
* When you send localised text, include your partner's default locale.
* Rich text fields use a restricted Markdown subset (paragraphs, line breaks, bold, italic, lists, headings level 3-4 and https links). Render them with a sanitising Markdown renderer, never as raw HTML.

## Large integers

Ids and large counters (`maxRedemptions`, `redemptionCount`) are unsigned 64-bit integers. They are JSON numbers and can exceed 2^53, the largest integer a JavaScript `number` represents exactly. In JavaScript, parse responses with a BigInt-safe JSON parser (for example `json-bigint`) or keep these values as strings. In the OpenAPI document these fields have `format: uint64`; configure your client generator to map them to a 64-bit or arbitrary-precision type.

## Updating resources

* `PATCH` is a JSON Merge Patch: a property you leave out stays unchanged, `null` clears it, any other value replaces it.
* `PUT` replaces the whole resource (for example `PUT /partner/v1/smtp-configuration`).
* `DELETE` answers `204` without a body.
* Request bodies are strict: a property the operation does not define is refused with the constraint [`unknown-field`](https://docs-consumer.voucher.allride.io/errors/catalog#unknown-field), and an undeclared query parameter with [`unknown-parameter`](https://docs-consumer.voucher.allride.io/errors/catalog#unknown-parameter). Read-only properties such as `id` or `label` in money are the only values you may echo back.

## Concurrent changes

Updates are **last-writer-wins per field**. Resources carry no version number, responses carry no entity tag, and there are no conditional request headers: you never send a precondition with a write.

* An update writes only the properties you sent. Two clients that change different properties of the same resource both succeed, and neither overwrites the other.
* When two clients change the **same** property at the same time, the later write wins. If you need one writer, coordinate it on your side.
* A command that changes the state checks the current state inside the same transaction. If another request changed it in the meantime, the command answers `409` with a code that names the state it found (a **state race**). On this site these are, for example:

  | Code | When |
  | - | - |
  | [`invalid-state-transition`](https://docs-consumer.voucher.allride.io/errors/catalog#invalid-state-transition) | The resource is no longer in a state that allows the command (for example `:revoke` on a redemption link that another request revoked first) |
  | [`redemption-already-voided`](https://docs-consumer.voucher.allride.io/errors/catalog#redemption-already-voided) | Another request voided the redemption first |
  | [`redemption-entity-blocked`](https://docs-consumer.voucher.allride.io/errors/catalog#redemption-entity-blocked) / [`redemption-entity-erased`](https://docs-consumer.voucher.allride.io/errors/catalog#redemption-entity-erased) | The entity was blocked or erased while you redeemed or created a link for it |
  | [`redemption-link-exhausted`](https://docs-consumer.voucher.allride.io/errors/catalog#redemption-link-exhausted) | The last redemption of the link was used by a parallel request |
  | [`voucher-set-depleted`](https://docs-consumer.voucher.allride.io/errors/catalog#voucher-set-depleted) / [`voucher-set-unavailable`](https://docs-consumer.voucher.allride.io/errors/catalog#voucher-set-unavailable) | The set ran out of stock or became unavailable between your catalog read and the redemption |

  Read the resource again before you decide what to do; retrying the same request gives the same answer.
* If the database could not complete your write because of a concurrent one (a deadlock or a serialization failure), you get `409` [`concurrent-update`](https://docs-consumer.voucher.allride.io/errors/catalog#concurrent-update); retry with backoff and the same `Idempotency-Key`. This is the only transient conflict code; there is no per-resource concurrency error.

## Commands and batch operations

* Actions that are not plain updates are **commands**: `POST` to the resource path with a colon and a verb, for example `POST /consumer/v1/redemptions/{redemptionId}:void` or `POST /consumer/v1/redemption-links/{redemptionLinkId}:revoke`.
* Bulk operations are `POST` to the collection with `:batch`, for example [`POST /consumer/v1/redemptions:batch`](https://docs-consumer.voucher.allride.io/api-reference/consumer-redemptions-batch). Batches are **all or nothing**: if one item fails, nothing is created, and the error names the item (`items[2]`) in `more.validationErrors`.
* Batch results use the single-object envelope around one result resource whose `items` keep the order of your request.
* Every `POST`, commands and batches included, accepts an [`Idempotency-Key`](https://docs-consumer.voucher.allride.io/guides/idempotency).

## Asynchronous jobs

No operation on the consumer site runs as an asynchronous job: every request answers when its work is done, and there are no export jobs (to get a whole list, see [Downloading large lists](https://docs-consumer.voucher.allride.io/guides/pagination.md#downloading-large-lists)). Asynchronous jobs exist only on the provider site (voucher uploads) and follow this pattern:

**1. Start the job**

The command answers `202 Accepted` with the job in its initial status, a `Location` header with the job URL and `Retry-After: 2`.

**2. Poll the job**

`GET` the `Location` URL after `Retry-After` seconds. Double the interval on each poll, up to 30 seconds.

**3. Use the result**

When the job's status is final, read its result.

An idempotent replay of the `202` shows the job **as it was when it was accepted**. Always follow `Location` for the current state.

## Compatibility rules

Within `v1` the API changes only in backwards-compatible ways (see [Versioning and deprecation](https://docs-consumer.voucher.allride.io/guides/versioning.md)). To keep working when it does, your client must:

* ignore response properties and `_links` relations it does not know;
* treat enum values it does not know as "other" (every enum is an open set);
* ignore members of an error entry or its `more` object that it does not know;
* ignore webhook event types it has not subscribed to or does not know.

When you contact support about a failed request, include the `X-Request-Id` of the response (for server errors it is also in `requestId`).

## Related

* [Pagination, filtering and sorting](https://docs-consumer.voucher.allride.io/guides/pagination.md)
* [Idempotency](https://docs-consumer.voucher.allride.io/guides/idempotency.md)
* [Errors](https://docs-consumer.voucher.allride.io/errors.md) and the [error catalog](https://docs-consumer.voucher.allride.io/errors/catalog.md)
* [Versioning and deprecation](https://docs-consumer.voucher.allride.io/guides/versioning.md)
* [Billing](https://docs-consumer.voucher.allride.io/consumer/billing.md)


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