
APIs
7 Seiten
Stand: 6. November 2026
Inhalt
Revenue API
The Revenue API returns every fiscal transaction in a date range — with its positions, payments and tax breakdown — in a single call. Use it for accounting exports, revenue reconciliation, BI warehouses or ad-hoc reports.
Tip: Revenue API or SQL Views?
The Revenue API and the SQL Views expose the
same underlying data, but in different shapes for different runtimes:
- Revenue API — served over the public cloud endpoint
(my.apro.at). Returns a hierarchical JSON document per receipt
with positions, payments, tax, staff and table already nested. Ideal
for off-premise systems, SaaS integrations, mobile clients and any
consumer that wants a ready-to-use object tree.
- SQL Views — read-only views on the on-premise cash register
database. Flat, relational, queryable with any tool that speaks
SQL Server. Ideal for on-premise integrations, ETL pipelines and
existing BI workflows (Power BI, Tableau, Qlik, Metabase).
Pick the cloud API for real-time system-to-system integration, pick
the SQL views for bulk analytics and dashboards.
- Host:
my.apro.at - Auth: API key in
x-api-key - Content type:
application/json
Endpoints
Variant | Endpoint | Location derived from |
|---|---|---|
Path + key |
|
|
Key only |
| The API key itself. |
Both variants require a location-scoped API key. The key itself determines which location's data is returned — there is no tenant-wide variant. Pick whichever feels more explicit in your client code — both return the same data and require the same key.
Request body
Field | Type | Required | Description |
|---|---|---|---|
| ISO 8601 timestamp | ✅ | Return receipts after this point in time. |
| ISO 8601 timestamp | ✅ | Return receipts before this point in time. |
| integer | — | Cap the number of results returned. |
| GUID | — | Cursor — return transactions after this ID. Combine with |
Example request
POST /api/v1/reports/receipts
{
"after": "2026-01-01T00:00:00Z",
"before": "2026-02-01T00:00:00Z",
"limit": 100,
"afterTransactionID": "f03e11de-6330-4a03-a8a8-b6e5fd4e32a5"
}Download: revenue-request.json
Response
An array of receipts. Each element is one transaction with its positions, payments and metadata.
Top-level fields
Field | Type | Description |
|---|---|---|
| GUID | Unique transaction ID. |
| string | See Transaction types. |
| string |
|
| decimal | Gross total of the receipt. |
| decimal | Tip included in |
| decimal | Discount applied to the receipt. |
| ISO 8601 | When the transaction happened. |
| Tenant / operator record. | |
| The APRO location the data was queried for. | |
| Operator / waiter. | |
| Table or delivery spot (e.g. "Bar 1", "Room 301"). | |
| Room / zone the table belongs to (e.g. Garden, Bar). | |
| Customer · nullable | Invoice recipient. Always |
| Grouping the payment types roll up into. | |
| [Payment] | Payments settling the receipt. |
| [Item] | Line items. |
| integer | Number of guests, summed over the receipt's line items. |
Item
Field | Type | Description |
|---|---|---|
| string | Unique item ID. |
| integer | Product number (as used in APRO). |
| string | Product name. |
| string · nullable | Stable per-line identifier. Use it to deduplicate or reconcile lines across repeated queries. |
| string · nullable | Reference assigned by an external system. |
| ISO 8601 | Time the line was booked. |
| decimal | Gross price. |
| decimal | Net price. |
| decimal | Cost price of the line, for margin reporting. |
| decimal | Units sold. |
| integer · nullable | Course number in a multi-course service (Gang). |
| Applied tax rate. | |
| Product category (a.k.a. "Sparte"). | |
| [Side dish] · nullable | Side dishes / modifiers booked on this line. |
Side dish
Side dishes (Unterartikel) are the modifiers booked underneath a line item — the Pommes under a schnitzel, the ohne Zwiebel on a burger. They are returned nested inside their parent item, never as items of their own.
A side dish reuses the Item shape, but only the following fields are populated. Every other field — bookDate, externalId, purchasePrice, course, tax, category, sideDishes — is null or its zero value, and side dishes do not nest recursively.
Field | Type | Description |
|---|---|---|
| string | Unique side dish ID. |
| integer | Product number of the side dish. |
| string | Side dish name. |
| string | Stable per-side-dish identifier. Also the key the API deduplicates on. |
| decimal | Gross price of the side dish. |
| decimal | Net price of the side dish. |
| decimal | Units of the side dish. |
Caution: Do not add side dishes to your totals
A side dish that carries a surcharge is **already included in its
parent item'sgrossAmount/netAmount** and in the receipt'stotalAmount. SummingsideDishes[].grossAmounton top of the parent
line double-counts revenue. Treat the amounts as a breakdown for
display, not as separate positions.
sideDishes is null — not [] — when a line has none, and also on older query configurations that predate the feature. Test for null-or-empty rather than assuming an array is present.
Tax
Field | Type | Description |
|---|---|---|
| string | Unique tax rate ID. |
| decimal | Rate, e.g. |
| string | Display name, e.g. |
Category
Field | Type | Description |
|---|---|---|
| string | Unique category ID. |
| integer | Category number. |
| string | Display name. |
Payment
Field | Type | Description |
|---|---|---|
| string | Unique payment ID. Payments are deduplicated on this value. |
| decimal | Amount paid. |
| Method used. | |
| string · nullable | Financial-accounting account number the payment posts to. Use it to map payments onto ledger accounts in an export. |
Payment type
Field | Type | Description |
|---|---|---|
| integer · nullable | Unique payment method ID. |
| integer · nullable | Payment method number. Note the key is |
| string | Display name, e.g. |
Business
Field | Type | Description |
|---|---|---|
| string | Tenant ID from the POS database. |
| string | Legal name. |
Staff
Field | Type | Description |
|---|---|---|
| string | Unique staff ID. |
| integer | Personnel number. |
| string | Staff name. |
Table
Field | Type | Description |
|---|---|---|
| string | Unique table ID. |
| integer | Table number. |
| string | Table label, e.g. |
Location
Field | Type | Description |
|---|---|---|
| string | APRO location ID — the |
| string · nullable | Location name. |
Area
The room or zone a table sits in.
Field | Type | Description |
|---|---|---|
| string | Unique area ID. |
| integer | Area number. |
| string | Area label, e.g. |
Payment group
The grouping payment types roll up into — used for cash-book and settlement reporting.
Field | Type | Description |
|---|---|---|
| integer | Payment group ID. |
| string | Group label. |
Customer
Field | Type | Description |
|---|---|---|
| string | Unique customer ID. |
| integer | Customer number. |
| string | Customer name. |
Note: Always null today
The receipts endpoint never populatescustomer— it is reserved in the
response contract but not filled by the query. Do not build invoice-
recipient logic on it; resolve customers through their own endpoints
instead.
Example response
Response · two receipts settled back-to-back
[
{
"transactionId": "f03e11de-6330-4a03-a8a8-b6e5fd4e32a5",
"transactionType": "Abrechnen",
"receiptType": "Beleg",
"totalAmount": 3.2,
"tip": 0.0,
"discount": 0.0,
"dateTime": "03.10.2021 21:59:57",
"business": { "id": "1", "name": "APRO Kassensysteme" },
"location": { "id": "7236", "name": "Wien Haupthaus" },
"staff": { "id": "8", "number": 9500, "name": "Onlinekellner" },
"table": { "id": "76", "number": 76, "name": "Daniel Z" },
"area": { "id": "3", "number": 3, "name": "Garten" },
"customer": null,
"paymentGroup": { "id": 2, "name": "Kartenzahlung" },
"payments": [
{
"id": "04b2d2f5-8765-41b5-ad5c-aed802dc23bc",
"amount": 3.2,
"paymentType": { "id": 26, "nr": 200, "name": "Smorder Kreditkarte" },
"fibuAccountNr": "2751"
}
],
"items": [
{
"id": "38",
"number": 216,
"name": "Wiener Schnitzel",
"uniqueId": "0f6c2c14-3f8a-4f0b-9a1e-2b7d5c8e4411",
"externalId": null,
"bookDate": "03.10.2021 21:59:55",
"grossAmount": 3.2,
"netAmount": 3.2,
"purchasePrice": 1.1,
"quantity": 1.0,
"course": 2,
"tax": { "id": "5", "value": 5.0, "name": "Mwst 5 %" },
"category": { "id": "12", "number": 12, "name": "Hauptspeisen" },
"sideDishes": [
{
"id": "512",
"number": 9001,
"name": "Pommes",
"uniqueId": "b41d9a77-55c2-4e3d-8f10-6cc0a2b93d88",
"grossAmount": 1.5,
"netAmount": 1.36,
"quantity": 1.0
},
{
"id": "518",
"number": 9007,
"name": "ohne Zwiebel",
"uniqueId": "c9a70e31-1d44-4a92-b7e5-30f8c1d6a204",
"grossAmount": 0.0,
"netAmount": 0.0,
"quantity": 1.0
}
]
}
],
"guestCount": 2
},
{
"transactionId": "db2b90d1-ab37-4557-b169-2289c3004a0a",
"transactionType": "Abrechnen",
"receiptType": "Beleg",
"totalAmount": 3.2,
"tip": 0.0,
"discount": 0.0,
"dateTime": "03.10.2021 22:00:12",
"business": { "id": "1", "name": "APRO Kassensysteme" },
"location": { "id": "7236", "name": "Wien Haupthaus" },
"staff": { "id": "8", "number": 9500, "name": "Onlinekellner" },
"table": { "id": "76", "number": 76, "name": "Daniel Z" },
"area": { "id": "3", "number": 3, "name": "Garten" },
"customer": null,
"paymentGroup": { "id": 2, "name": "Kartenzahlung" },
"payments": [
{
"id": "bb3e6300-0cc3-4922-80c4-a8a4c3530d3c",
"amount": 3.2,
"paymentType": { "id": 26, "nr": 200, "name": "Smorder Kreditkarte" },
"fibuAccountNr": "2751"
}
],
"items": [
{
"id": "38",
"number": 216,
"name": "Cola 0,5",
"uniqueId": "7d2f3b90-6c11-4c8e-93aa-5e4419c7f0d3",
"externalId": null,
"bookDate": "03.10.2021 22:00:11",
"grossAmount": 3.2,
"netAmount": 3.2,
"purchasePrice": 0.7,
"quantity": 1.0,
"course": null,
"tax": { "id": "5", "value": 5.0, "name": "Mwst 5 %" },
"category": { "id": "20", "number": 20, "name": "Alkoholfreie Getränke" },
"sideDishes": null
}
],
"guestCount": 1
}
]Download the same payload as a standalone file: revenue-response.json
Transaction types
The transactionType discriminates what the row represents. For revenue-effective reports, filter on Abrechnen (and RechnungZurückholen for reversals).
Type | Meaning |
|---|---|
| Item booked on an open table. |
| Table change. |
| Settlement — a paid receipt. |
| Reversal of a booking. |
| Reversal of a settlement. |
| Debit booking. |
| Outgoing invoice lifecycle. |
| Day-end close / preview. |
| Waiter hand-over / settlement. |
| Legal fiscal receipts (RKSV). |
| Table lifecycle. |
| Order-manager events. |
| Cart lifecycle. |
| Data export markers. |
| Other operational events. |
Calculating revenue
To compute revenue-effective turnover over a response, keep only paid settlements and their reversals, and sum the net line totals:
response
.filter(x =>
x.paymentGroup.name === "Umsatzwirksam" &&
["Abrechnen", "RechnungZurückholen"].includes(x.transactionType)
)
.map(x =>
x.items.reduce((sum, item) => sum + item.netAmount * item.quantity, 0)
)
.reduce((sum, val) => sum + val, 0);Paging
There is no cursor-in-response — use the deterministic transactionId of the last row you saw as afterTransactionID on the next request, keeping after/before unchanged. Continue until the response contains fewer rows than your limit.
Errors
External Order API
The External Order API accepts a fully-formed order from any external system — a delivery aggregator, a white-label app, a kiosk, a voice agent — and routes it into the running APRO POS at the target location. A single POST is enough; the order appears on the order-manager screen immediately.
- Endpoint:
POST https://my.apro.at/api/v1/external/order - Auth: API key in
x-api-key - Content type:
application/json
Routing the order to a location
The order must be addressable to one concrete location. Provide one of the following — the first that is non-null wins:
LocationID— numeric IDLocationShortCode— short string identifier (e.g.vienna-main)SpotID— a specific table/room/pickup spot (location is derived)QRCode— a scanned APRO QR code (location and spot are derived)
If none is provided, the request is rejected.
Request body
Field | Type | Required | Description |
|---|---|---|---|
| string | — | Idempotency key. If the same |
| int? | ◻︎ | See routing above. |
| string | ◻︎ | See routing above. |
| int? | — | ID of the target spot (table, room, pickup, delivery — all are spots, just different types). Matches |
| int? | — | Spot number, when ID is unknown. Resolved by |
| string | — | Fallback when neither |
| int? | — | Number of the room spot for hotel / room-charge flows. Resolved against a |
| int? | — | Reserved for forward compatibility. Currently accepted but ignored — use |
| ✅ | Who placed the order. | |
| — | Delivery address. Required for home delivery. | |
| bool | ✅ |
|
| string | — | Free-form note from the guest (allergens, special requests). |
| string | — | See routing. |
| [Entry] | ✅ | Ordered items. |
| long? | — | Pre-order slot, Unix time in milliseconds UTC. |
| string | — | Pickup / delivery reference (e.g. |
| decimal? | — | Tip amount. |
| bool | — |
|
| decimal? | ✅ | Total of all entries + tip. Used to validate against server-side pricing. |
Entry
Field | Type | Required | Description |
|---|---|---|---|
| string | ✅ | Product number in APRO. |
| string | — | Display name — informational only; APRO trusts |
| long? | — | Per-item pre-order slot. |
| double | ✅ | Quantity ordered. |
| double? | — | Override the system price for this line (discounts, campaigns). |
| string | — | Item-level note (e.g. |
| [Addon] | — | Add-on selections. |
Addon
Field | Type | Required | Description |
|---|---|---|---|
| string | ✅ | Product number of the add-on. |
| string | — | Display name. |
| double | ✅ | Quantity. |
| double? | — | Override the system price. |
UserInfo
Field | Type | Required | Description |
|---|---|---|---|
| string | — | |
| string | — | |
| string | ✅ | Contact email for receipts and status updates. |
| string | — | |
| string | — | IETF locale, e.g. |
| string | — | Short name shown at pickup. |
DeliveryInfo
Field | Type | Description |
|---|---|---|
| string | Recipient label. |
| string | |
| string | |
| string | |
| string | |
| string | |
| string | |
| string | |
| string | |
| string | Delivery instructions. |
Examples
Minimal order
POST /api/v1/external/order
{
"locationShortCode": "vienna-main",
"takeaway": true,
"userInfo": { "email": "guest@example.com" },
"entries": [{ "productNr": "123", "quantity": 1 }],
"grandTotal": 9.99
}Download: external-order-minimal.json
Full order with delivery and add-ons
POST /api/v1/external/order
{
"guid": "123e4567-e89b-12d3-a456-426614174000",
"locationId": 101,
"spotId": 15,
"spotNumber": 10,
"shortCode": "zw1356",
"userInfo": {
"firstName": "Max",
"lastName": "Mustermann",
"email": "max.mustermann@example.com",
"phone": "+491234567890",
"locale": "de-DE",
"nickname": "Max"
},
"takeaway": false,
"note": "Bitte schnell liefern",
"qrCode": "QR1234567890",
"deliveryInfo": {
"name": "Max Mustermann",
"city": "Wien",
"zip": "1010",
"street": "Stephansplatz 1",
"country": "AT",
"floor": "3",
"stairway": "A",
"door": "12"
},
"entries": [
{
"productNr": "123",
"name": "Wiener Schnitzel",
"quantity": 2,
"price": 9.99,
"note": "Extra scharf",
"addons": [
{ "productNr": "501", "name": "Pommes", "quantity": 2, "price": 3.5 }
]
},
{ "productNr": "31231", "name": "Burger Classic", "quantity": 1, "note": "Ohne Zwiebeln" }
],
"preOrderTime": 1723574400000,
"tip": 1.2,
"isPaid": true,
"grandTotal": 28.18
}Download: external-order-full.json
Idempotency
Always set guid to a stable, client-generated UUID. APRO uses it to deduplicate retries — if a response is lost in transit and you retry, the second POST is a no-op and returns success.
Pricing
APRO re-prices every line server-side from productNr. price on an entry or add-on only takes effect when explicitly passed; grandTotal is checked against the server total and an inconsistent payload is rejected. Use it both as a safety net and as a mechanism to pass through campaign-specific discounts.
External Catalog API
The External Catalog API exposes a slim, read-only view of the master data inside an APRO location — the products, product categories and spots (tables, rooms, pickup points) — for use in white-label apps, kiosks, external menus and partner integrations.
The three endpoints share the same conventions: scoping by location, nested DTO shape, filtering via query parameters, and pagination.
- Host:
my.apro.at - Auth: API key in
x-api-key - Content type:
application/json
Tip: Try it in Postman
Download the ready-to-run collection:apro-external-catalog.postman_collection.json.
Import it into Postman, setapiKeyandlocationShortCodein the
collection's Variables tab, hit Send.
Caution: Filters use a bracketed query syntax
All filter fields — including the requiredlocationId/locationShortCode— must be passed asfilter['<name>']=<value>,
not as plain query parameters. Plain?locationShortCode=…is
silently ignored and the request fails with "location not resolved".
See Filtering for full rules.
Endpoints
Method | URL | Returns |
|---|---|---|
|
| Paged list of Product |
|
| Paged list of ProductCategory |
|
| Paged list of Spot |
Common conventions
Scoping to a location
Every request must address one concrete location. Provide one of:
locationId— numeric IDlocationShortCode— short string identifier (e.g.vienna-main)
If neither resolves to an existing location, the request is rejected with 400 Bad Request.
Pagination
Query param | Default | Description |
|---|---|---|
|
| Number of records to skip. |
|
| Page size. Capped at 500. |
Every response is wrapped in a paged envelope:
{
"page": { "offset": 0, "limit": 50, "total": 1234 },
"data": [ /* ... */ ]
}offsetandlimitecho the request.totalis the total number of records matching the filter, across all pages — use it to driveskip/takeloops.
Filtering
Filters are passed as bracketed query parameters under the filter key — filter['<name>']=<value>. Plain ?<name>=<value> is ignored. Pagination (skip, take) stays as plain query params.
List-valued filters (productIds, gtins, externalSpotIds, …) are comma-separated. Escape literal commas with \,.
GET /api/v1/external/product?filter['locationShortCode']=vienna-main
&filter['productNrs']=A-100,A-200
&filter['isVisible']=true
&filter['search']=schnitzel
&skip=0&take=100Notes:
- The bracket key is case-insensitive —
filter['LocationShortCode']andfilter['locationShortCode']are equivalent. filter["…"](double quotes, URL-encoded as%22) works too.- Unknown filter parameters are ignored.
Products
GET https://my.apro.at/api/v1/external/productQuery parameters
Param | Type | Description |
|---|---|---|
| int? | Required (or |
| string | Required (or |
| int? | Restrict to one cashbox. Must belong to the location. |
| string | Free-text match over name, external name, productNr, GTIN and external product ID. |
| int[] | Filter by APRO product ID. |
| string[] | Filter by |
| string[] | Filter by GTIN (barcode). |
| string[] | Filter by external product ID (third-party reference). |
| bool? | Filter by visibility flag. |
| bool? | Filter by locked flag. |
Product
Field | Type | Description |
|---|---|---|
| int | APRO Cloud product ID. |
| string | Identifier the order pipeline uses to resolve this product. Pass this value straight back as the entry's |
| string | Barcode / GTIN. |
| string | POS product ID — the ID assigned by the local POS system. Informational; not what External Order's |
| string | Display name. |
| string | Name for external surfaces — fallback to |
| string | Short subtitle. |
| string | Long-form description. |
| Price and tax info. | |
| Cashbox / parent / production-place wiring. | |
| Image URLs. | |
| Visibility, locking, review state. | |
| Amount, calories, allergens, age limit. | |
| string[] | External IDs of the product categories this product belongs to. |
| Maintained translations of the product's texts and image, one entry per locale. Empty when none exist. |
Pricing
Field | Type | Description |
|---|---|---|
| double? | Default price. |
| double? | Reference / strike-through price. |
| int? | APRO tax rate ID. |
| bool | Whether clients may override the price. |
ProductCashbox
Field | Type | Description |
|---|---|---|
| int | Owning cashbox. |
| int? | Kitchen / bar where the product is prepared. |
| int? | Set on variants / template-derived products. |
Media
Field | Type | Description |
|---|---|---|
| string | Public image URL. |
Flags
Field | Type | Description |
|---|---|---|
| bool | Visible in storefronts. |
| bool | Locked from automatic sync changes. |
| bool | Pending manual review. |
Nutrition
Field | Type | Description |
|---|---|---|
| double? | Net amount. |
| string | Unit, e.g. |
| double? | Calories per |
| int (flags) | Bit-flag of declared allergens. |
| int | Minimum buyer age ( |
| [Ingredient] | Declared ingredients list. Empty array when not maintained. |
| [NutritionalValue] | Structured nutritional values. Empty array when not maintained. |
Ingredient
Field | Type | Description |
|---|---|---|
| string | Ingredient name (e.g. |
NutritionalValue
Field | Type | Description |
|---|---|---|
| string | Value label (e.g. |
| string | Unit, free-form (e.g. |
| string | Numeric value, serialized as a string to preserve the source format. Parse client-side if needed. |
| int? | Optional reference to a sub-component / variant ( |
Localization
Field | Type | Description |
|---|---|---|
| string | Locale / culture key of the translation (e.g. |
| string | Translated display name. |
| string | Translated subtitle. |
| string | Translated long-form description. |
| string | Locale-specific image, when one is maintained. |
The top-level name, subtitle, description and media.imageUrl always carry the default (German) values — localizations never replaces them, it only adds the translations that exist. Entries without a locale key are omitted, and individual fields inside an entry may be null when that particular text was not translated. Resolve a display string by looking for the caller's locale in localizations and falling back to the top-level field.
Example
GET /api/v1/external/product?filter['locationShortCode']=vienna-main&filter['isVisible']=true&take=2{
"page": { "offset": 0, "limit": 2, "total": 1247 },
"data": [
{
"id": 9001,
"productNr": "A-100",
"gtin": "9001234567890",
"externalProductId": "wiener-schnitzel",
"name": "Wiener Schnitzel",
"externalName": "Wiener Schnitzel",
"subtitle": "Mit Erdäpfelsalat",
"description": "Klassisches Wiener Schnitzel vom Kalb.",
"pricing": {
"defaultPrice": 22.9,
"referencePrice": null,
"taxRateId": 3,
"allowClientPrice": false
},
"cashbox": { "cashboxId": 17, "productionPlaceId": 2, "parentProductId": null },
"media": { "imageUrl": "https://cdn.smorder.at/locations/7236/p/9001.jpg" },
"flags": { "isVisible": true, "isLocked": false, "queuedForReview": false },
"nutrition": {
"amount": 250,
"amountUnit": "g",
"calories": 720,
"allergens": 67,
"minimumAge": 0,
"ingredients": [{ "name": "Kalb" }, { "name": "Weizenmehl" }, { "name": "Ei" }],
"nutritionalValues": [
{ "name": "Gewicht", "unit": "g", "value": "250", "belongsTo": null },
{ "name": "Fett", "unit": "g", "value": "30", "belongsTo": null }
]
},
"productGroupExternalIds": ["mains", "austrian"],
"localizations": [
{
"locale": "en",
"name": "Viennese Schnitzel",
"subtitle": "With potato salad",
"description": "Classic Viennese veal schnitzel.",
"imageUrl": null
},
{
"locale": "it",
"name": "Cotoletta alla viennese",
"subtitle": null,
"description": null,
"imageUrl": null
}
]
},
{
"id": 9002,
"productNr": "A-110",
"gtin": null,
"externalProductId": null,
"name": "Tafelspitz",
"externalName": null,
"subtitle": null,
"description": null,
"pricing": { "defaultPrice": 24.5, "referencePrice": null, "taxRateId": 3, "allowClientPrice": false },
"cashbox": { "cashboxId": 17, "productionPlaceId": 2, "parentProductId": null },
"media": { "imageUrl": null },
"flags": { "isVisible": true, "isLocked": false, "queuedForReview": false },
"nutrition": { "amount": null, "amountUnit": null, "calories": null, "allergens": 0, "minimumAge": 0, "ingredients": [], "nutritionalValues": [] },
"productGroupExternalIds": ["mains"],
"localizations": []
}
]
}Download: external-products-response.json
Product categories
GET https://my.apro.at/api/v1/external/product-categoryCategories correspond to APRO product groups — the cashbox-scoped grouping that drives reports, tax rates and printing order. The productGroupExternalIds field on a product references the same externalId returned here, so you can join the two responses without extra lookups.
Query parameters
Param | Type | Description |
|---|---|---|
| int? | Required (or |
| string | Required (or |
| int? | Restrict to one cashbox. Must belong to the location. |
| string | Free-text match over name, external name and external ID. |
| int[] | Filter by APRO category ID. |
| string[] | Filter by external ID. |
ProductCategory
Field | Type | Description |
|---|---|---|
| int | APRO Cloud category ID. |
| string | POS category ID — the ID assigned by the local POS system. |
| string | Display name. |
| string | Name for external surfaces. |
| Cashbox / supplier wiring. | |
| Tax wiring. | |
| Maintained translations of the category's texts and image, one entry per locale. Empty when none exist. |
CategoryCashbox
Field | Type | Description |
|---|---|---|
| int? | Owning cashbox. |
| int? | Default supplier for products in this category. |
CategoryTax
Field | Type | Description |
|---|---|---|
| int? | Default tax rate for products in this category. |
| double | VAT percentage (e.g. |
CategoryLocalization
Field | Type | Description |
|---|---|---|
| string | Locale / culture key of the translation (e.g. |
| string | Translated display name. |
| string | Translated description. |
| string | Locale-specific image, when one is maintained. |
Same rules as on products: the top-level name stays the default (German) value and localizations only adds what exists.
Example
GET /api/v1/external/product-category?filter['locationShortCode']=vienna-main{
"page": { "offset": 0, "limit": 50, "total": 14 },
"data": [
{
"id": 41,
"externalId": "mains",
"name": "Hauptspeisen",
"externalName": "Mains",
"cashbox": { "cashboxId": 17, "supplierId": null },
"tax": { "taxRateId": 3, "vat": 10 },
"localizations": [
{ "locale": "en", "name": "Mains", "description": null, "imageUrl": null },
{ "locale": "it", "name": "Secondi", "description": null, "imageUrl": null }
]
},
{
"id": 42,
"externalId": "drinks-alcoholic",
"name": "Getränke (Alkohol)",
"externalName": "Alcoholic drinks",
"cashbox": { "cashboxId": 17, "supplierId": null },
"tax": { "taxRateId": 1, "vat": 20 },
"localizations": []
}
]
}Download: external-product-categories-response.json
Spots
GET https://my.apro.at/api/v1/external/spotSpots are the physical / logical destinations a guest can order to: tables, rooms, pickup counters, delivery zones, drive-in lanes.
Query parameters
Param | Type | Description |
|---|---|---|
| int? | Required (or |
| string | Required (or |
| int? | Restrict to one area / room. |
| string | Free-text match over name, alternative name, external spot ID and QR code. |
| int[] | Filter by APRO spot ID. |
| string[] | Filter by external spot ID. |
| enum |
|
| bool? | Filter on app visibility. |
| bool? | Filter on table-order capability. |
Spot
Field | Type | Description |
|---|---|---|
| int | APRO Cloud spot ID. |
| string | POS spot ID — the ID assigned by the local POS system. |
| int? | Numeric label (e.g. table number). |
| string | Display name. |
| string | Secondary name used by some surfaces. |
| string | Free-form description. |
| string | QR-code payload bound to the spot. |
| enum | Spot type. |
| int? | Number of seats (tables) / max guests (rooms). |
| Owning location. | |
| Owning area, if any. | |
| Geo coordinates, if any. Omitted when both are null. | |
| Capability flags. |
SpotLocation
Field | Type | Description |
|---|---|---|
| int | Numeric location ID. |
| string | Short code for cross-API joins. |
SpotArea
Field | Type | Description |
|---|---|---|
| int | APRO area ID. |
| string | Area display name. |
| string | External reference for the area. |
SpotGeo
Field | Type | Description |
|---|---|---|
| double? | WGS84 latitude. |
| double? | WGS84 longitude. |
SpotFlags
Field | Type | Description |
|---|---|---|
| bool | Visible in storefronts. |
| bool | Accepts in-house orders. |
| bool | Accepts cash. |
| bool | Default order mode is takeaway. |
| bool | Render as a button instead of a list item. |
Example
GET /api/v1/external/spot?filter['locationShortCode']=vienna-main&filter['type']=Table&filter['visibleInApp']=true&take=2{
"page": { "offset": 0, "limit": 2, "total": 38 },
"data": [
{
"id": 1501,
"externalSpotId": "table-01",
"number": 1,
"name": "Tisch 1",
"alternativeName": null,
"description": null,
"qrCode": "QR-01",
"type": "Table",
"capacity": 4,
"location": { "locationId": 7236, "locationShortCode": "vienna-main" },
"area": { "areaId": 11, "name": "Gastgarten", "externalAreaId": "garden" },
"geo": null,
"flags": {
"visibleInApp": true,
"supportsTableOrder": true,
"supportsCashPayment": true,
"defaultToTakeAway": false,
"displayAsButton": false
}
},
{
"id": 1502,
"externalSpotId": "table-02",
"number": 2,
"name": "Tisch 2",
"alternativeName": null,
"description": null,
"qrCode": "QR-02",
"type": "Table",
"capacity": 2,
"location": { "locationId": 7236, "locationShortCode": "vienna-main" },
"area": { "areaId": 11, "name": "Gastgarten", "externalAreaId": "garden" },
"geo": { "latitude": 48.2082, "longitude": 16.3738 },
"flags": {
"visibleInApp": true,
"supportsTableOrder": true,
"supportsCashPayment": true,
"defaultToTakeAway": false,
"displayAsButton": false
}
}
]
}Download: external-spots-response.json
Cross-API joins
These three endpoints are designed to be joined client-side:
product.productNr↔productNr(entry / addon) on the External Order API — case-insensitive, matched only against products on the location's primary cashbox. WhenproductNrdoesn't resolve, the order pipeline falls back to matching the entry'snameagainstproduct.name.product.productGroupExternalIds[i]↔productCategory.externalIdproduct.cashbox.cashboxId/productCategory.cashbox.cashboxId↔cashboxIdfilter on either endpointspot.location.locationShortCode↔locationShortCodefilter everywherespot.id↔SpotIDon the External Order API — pass the spot'sidstraight back to route an order to the same spot it was read from.spot.qrCodemaps to the External Order API'sQRCodefield.
Errors
Status | Meaning |
|---|---|
| Neither |
| Missing or invalid |
| Key not authorised for the requested location. |
| Rate-limited — back off and retry with jitter. |
Online Menu API
The Online Menu API serves the public menu structure for white-label apps, self-order kiosks and QR-ordering flows. Reading is unauthenticated; every mutating call requires authentication.
- Host:
my.apro.at - Content type:
application/json
Fetching a menu
GET https://my.apro.at/api/v1/locations/{locationId}/menus- Auth: none
- Example: my.apro.at/api/v1/locations/7236/menus
The response is a tree. Levels, from outermost to innermost:
- Menu — top-level grouping (e.g. Food, Drinks, Deals).
- Category — section inside a menu (e.g. Mains).
- MenuEntry — a concrete sellable item.
- MenuEntryFeature — add-on / modifier group on an item.
- MenuEntryFeatureValue — one option inside a feature.
- Hours — availability windows that can attach to menus and categories.
Example response (abridged)
GET /api/v1/locations/{locationId}/menus
[
{
"name": "Speisen",
"badge": 7,
"imageUrl": "https://cdn.smorder.at/locations/7236/menus/speisen.jpg",
"description": "Klassiker aus der österreichischen Küche.",
"priceName": "Hauspreis",
"orderableHours": [
{ "days": 254, "startTime": "11:00", "endTime": "22:00" }
],
"categories": [
{
"name": "Hauptspeisen",
"menuEntries": [
{
"menuEntryID": 4001,
"name": "Wiener Schnitzel",
"badge": 7,
"price": 22.9,
"features": [
{
"name": "Beilage",
"groupName": "Beilage",
"minValuesRequired": 1,
"maxValuesAllowed": 1,
"valuesForFree": 1,
"values": [
{ "name": "Erdäpfelsalat", "priceAdjustment": 0 },
{ "name": "Pommes frites", "priceAdjustment": 0 },
{ "name": "Reis", "priceAdjustment": 0 }
]
},
{
"name": "Ohne",
"isDeselectFeature": true,
"values": [
{ "name": "Preiselbeeren", "priceAdjustment": 0 }
]
}
]
}
]
}
]
}
]Download the full, unabridged sample: menu-response.json
1. Menu
Outermost grouping in the menu tree; contains categories. Examples: Food, Drinks, Deals.
Field | Type | Description |
|---|---|---|
| string | Display name. |
| enum | Visual badge — see Badges. |
| string | Header image URL. |
| string | Descriptive text. |
| Category[] | Categories inside this menu. |
| Hours[] | When ordering is allowed. |
| Hours[] | When pre-ordering is allowed. |
| Hours[] | When delivery is allowed. |
| string | Price level to use (e.g. House, Happy Hour). |
Mutations (auth required):
Action | Method | Path |
|---|---|---|
Create |
|
|
Update |
|
|
Delete |
|
|
2. Category
Sub-section of a menu. Examples: Non-alcoholic drinks, Beer, Mains, Desserts.
Field | Type | Description |
|---|---|---|
| string | Display name. |
| enum | See Badges. |
| string | Header image URL. |
| string | Descriptive text. |
| MenuEntry[] | Entries inside this category. |
| Hours[] | When ordering is allowed. |
| Hours[] | When pre-ordering is allowed. |
| Hours[] | When delivery is allowed. |
Mutations (auth required):
Action | Method | Path |
|---|---|---|
Create |
|
|
Update |
|
|
Delete |
|
|
3. MenuEntry
A concrete sellable item. Examples: Wiener Schnitzel, Kaiser Bier.
Field | Type | Description |
|---|---|---|
| integer | Unique entry ID; sent back with orders. |
| string | Item name. |
| enum | See Badges. |
| string | Item image URL. |
| string | Short tagline. |
| string | Descriptive text. |
| MenuEntryFeature[] | Modifier groups (sides, sizes…). |
| money | Selling price. |
Mutations (auth required):
Action | Method | Path |
|---|---|---|
Create |
|
|
Update |
|
|
Delete |
|
|
4. MenuEntryFeature
A modifier group for a menu entry. Examples: Size, Side for main.
Field | Type | Description |
|---|---|---|
| string | Feature label. |
| string | Descriptive text. |
| string | Grouping key for paginated rendering. |
| boolean |
|
| number | Max options selectable. |
| number | Min options required. |
| number | Options included at no extra charge. |
| MenuEntryFeatureValue[] | Available options. |
Mutations (auth required):
Action | Method | Path |
|---|---|---|
Create |
|
|
Update |
|
|
Delete |
|
|
5. MenuEntryFeatureValue
One concrete option inside a feature. Examples: Beer 0.3 l, Beer 0.5 l, Fries, Potatoes.
Field | Type | Description |
|---|---|---|
| string | Option label. |
| string | Descriptive text. |
| string | Option image URL. |
| number | Minimum quantity required when selected. |
| number | Maximum quantity allowed. |
| number | Quantity included at no extra charge. |
| number | Price change per selected unit. |
Mutations (auth required):
Action | Method | Path |
|---|---|---|
Create |
|
|
Update |
|
|
Delete |
|
|
6. Hours
Availability windows attached to menus or categories. They gate ordering, pre-ordering and delivery.
Field | Type | Description |
|---|---|---|
| enum bitmask | Bitmask of weekdays — see below. |
| date | Absolute start. |
| date | Absolute end. |
| daytime | Start time on matching days. |
| daytime | End time on matching days. |
| boolean | Invert the window — exclude instead of include. |
Weekday bitmask:
Day | Bit | Value |
|---|---|---|
Monday |
|
|
Tuesday |
|
|
Wednesday |
|
|
Thursday |
|
|
Friday |
|
|
Saturday |
|
|
Sunday |
|
|
Example: Mon–Fri is 2 + 4 + 8 + 16 + 32 = 62.
Badges
Visual flags for menus, categories and entries. Values:
Label | Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Hotel API (PMS)
The Hotel API is a two-way bridge between an APRO point-of-sale and any Property Management System (PMS). The PMS pushes reservations into the POS so that guests can charge to their room; the POS exposes those charges back to the PMS so they land on the final hotel invoice.
- Host:
cloudgateway.apro.at(path prefix/hotelservice/) - Auth: API key in
x-api-key - Content type:
application/json - Upstream reference: api-docs.apro.at → hotelservice ↗
Typical flow
PMS APRO POS
│ 1. POST /pms/reservations ─────────▶ │ (upsert guest rooms)
│ │
│ │ guests charge to room…
│ │
│ 2. GET /pms/charges?hotelId=… ─────▶│
│ ◀─── charges (and optionally receipts) │
│ │
│ 3. POST /pms/confirm-charges ───────▶│ (ack; won't be resent)
│ │
│ 4. DELETE /pms/reservations?… ───────▶│ (on check-out / close-out)Step 2 is the PMS's polling loop. Steps 1 and 4 are driven by events in the PMS. Step 3 must always follow a successful poll — otherwise the same charges will be returned again on the next poll.
1. Upsert reservations
Push the current list of active reservations into the POS. Keys are hotelId + Id + guestId; existing rows are updated in place.
POST https://cloudgateway.apro.at/hotelservice/api/v1/pms/reservationsRequest body
Field | Type | Required | Description |
|---|---|---|---|
| Reservation[] | ✅ | One or more reservations. |
Reservation
Field | Type | Required | Description |
|---|---|---|---|
| string | ✅ | PMS-side reservation ID. |
| string | ✅ | PMS tenant / property ID. |
| string | ✅ | PMS-side guest ID. |
| string | ✅ | Assigned room number. |
| string | — |
|
| string | — | |
| string | — | |
| ISO 8601 | ✅ | Check-in timestamp. |
| ISO 8601 | ✅ | Check-out timestamp. |
| integer | — | Guest count, used for covers / buffet rules. |
| Identifier[] | — | Keycard UIDs, NFC tags, room tablets etc. — any token the POS may receive when a guest wants to charge to a room. |
Identifier
Field | Type | Description |
|---|---|---|
| string | The identifier value (keycard UID, token). |
Example request
POST /hotelservice/api/v1/pms/reservations
{
"reservations": [
{
"Id": "Aabc123",
"hotelId": "1",
"guestId": "Abc123",
"roomNr": "Abc123",
"title": "Mr.",
"firstName": "Max",
"lastName": "Mustermann",
"arrival": "2026-04-02T14:00:00",
"departure": "2026-04-04T14:00:00",
"nrOfIndividuals": 1,
"identifiers": [{ "value": "keycard-uid" }]
}
]
}Download: hotel-reservations-upsert.json
Example response
200 OK
{
"newReservationsCount": 0,
"checkedOutReservationsCount": 0,
"updatedReservationsCount": 1,
"state": 1,
"isSuccessfull": true,
"errorCode": 0,
"message": null,
"context": null
}Download: hotel-reservations-response.json
Tip
Push the full active reservation set on every call, not deltas.checkedOutReservationsCountreflects reservations the POS was tracking
but which are no longer in your payload — it removes them automatically.
2. Poll charges
Fetch everything booked against rooms since the last confirmed poll.
GET https://cloudgateway.apro.at/hotelservice/api/v1/pms/charges
?hotelId=1
&excludeAlreadyExported=trueQuery parameters
Name | Type | Required | Description |
|---|---|---|---|
| string | ✅ | PMS tenant / property ID. |
| bool | — |
|
Response shape
Each item has a chargeType that controls which fields are populated:
| Meaning |
|
|---|---|---|
| An open line booked to the room (guest hasn't paid yet — POS is posting to the room folio). | no |
| A closed receipt (guest settled at the POS; PMS may ignore or reconcile). | yes |
Top-level envelope:
Field | Type | Description |
|---|---|---|
| integer | Page size applied. |
| integer | Offset into the backlog. |
| integer | Items still unconfirmed in total. |
| Item[] | Charges and receipts. |
Item
Field | Type | Description |
|---|
| `"Charge" | "Receipt"` | See table above. |
| ISO 8601 | When the line was booked in the POS. |
| integer | POS receipt number. |
| integer | Operator / waiter ID. |
| string | Operator name. |
| integer | Outlet ID (e.g. |
| integer | Table number. |
| string | Table label. |
| decimal | Gross total of the line. |
| decimal | Included tip. |
| GUID | APRO transaction ID — use this as the |
| string | null | PMS reservation ID, for |
| string | null | PMS guest ID. |
| string | null | Room number. |
| LineItem[] | Individual article rows. |
| Payment[] | Only on |
LineItem
Field | Type | Description |
|---|---|---|
| integer | Product number. |
| string | Product name. |
| integer | Category (Sparte) number. |
| string | Category name. |
| decimal | Quantity. |
| decimal | Gross unit price. |
| decimal | Applied VAT rate. |
Example response
200 OK · charge + two receipts
{
"page": { "limit": 3, "offset": 0, "total": 8096 },
"data": [
{
"chargeType": "Charge",
"bookDate": "2026-04-15T09:15:04.95",
"receiptNr": 90,
"userId": 20,
"userName": "Johannes",
"outlet": 1,
"tableNr": 2,
"table": "Bar 2",
"totalAmount": 9.00,
"tip": 0.30,
"businesscaseId": "253c225f-7c4a-4d3f-80cd-63de4dbfca9e",
"reservationId": " 15631",
"guestId": " 6158",
"roomNumber": " 414",
"charges": [
{ "articleNr": 129, "articleName": "Zipfer 0,2", "productgroupName": "Bier", "quantity": 1.00, "unitPrice": 1.10, "vatPercent": 20.00 },
{ "articleNr": 213, "articleName": "Apfel ltg 0,5", "productgroupName": "Flaschen AF", "quantity": 1.00, "unitPrice": 2.70, "vatPercent": 20.00 },
{ "articleNr": 1626, "articleName": "Würstel Pommes", "productgroupName": "Hauptspeise", "quantity": 1.00, "unitPrice": 4.90, "vatPercent": 10.00 }
]
}
]
}Download the full sample (Charge + two Receipts): hotel-charges-response.json
Caution
String fields likereservationId,guestId,roomNumbermay be left-
padded with spaces when the POS stores them as fixed-width columns
(" 15631"). Trim on the PMS side before matching.
3. Confirm charges
Acknowledge the list you've successfully ingested. Once confirmed, the POS won't return those businesscaseIds on a subsequent poll with excludeAlreadyExported=true.
POST https://cloudgateway.apro.at/hotelservice/api/v1/pms/confirm-chargesRequest body
Field | Type | Required | Description |
|---|---|---|---|
| string | ✅ | PMS tenant / property ID. |
| GUID[] | ✅ |
|
POST /hotelservice/api/v1/pms/confirm-charges
{
"hotelId": "1",
"transactionIds": [
"69F94B1A-3453-7017-8DDB-94E2175D0365",
"155FFA87-DC19-EB1F-3DD8-540EF62A115D"
]
}Download: hotel-confirm-charges.json
4. Remove a reservation
Explicit removal — useful at check-out or when the guest should no longer be able to charge to the room.
DELETE https://cloudgateway.apro.at/hotelservice/api/v1/pms/reservations
?hotelId=1
&reservationId=Abc123
&guestId=Abc123All three query parameters are required. Either the targeted reservation is removed, or the call is a no-op if it doesn't exist.
Note
In practice the full-set upsert on step 1 is enough — any reservation
no longer present in the payload is checked out automatically
(checkedOutReservationsCount). Use this endpoint only when you need
an explicit out-of-band removal.
Idempotency & retries
POST /pms/reservationsis idempotent on{ hotelId, Id, guestId }. Re-sending the same payload converges to the same state.POST /pms/confirm-chargesis idempotent ontransactionIds. Re-confirming an already-confirmed ID is a no-op.- Poll → confirm must be transactional in your own system: write the charges to your ledger first, then confirm. If the confirm fails, the same rows are returned next poll.
Errors
See Authentication → Error responses. On business-logic failures the response shape is:
{
"isSuccessfull": false,
"errorCode": 42,
"message": "No reservation found for guestId 'Abc123'.",
"context": { "guestId": "Abc123" }
}Voucher API (Gutscheine)
The Voucher API is the programmatic interface to APRO's central voucher ledger — the same system that powers in-store gift cards, online voucher shops, and loyalty credit. Balances are authoritative server-side; every operation returns the updated voucher so the caller never has to track state locally.
- Host:
cloudgateway.apro.at(path prefix/voucherservice/) - Auth: API key in
x-api-key - Content type:
application/json - Upstream reference: api-docs.apro.at → voucherservice ↗
Amounts
All monetary fields are integer minor units (cents) — 5000 means € 50.00. balance is the currently-available amount; amount is the original face value; maxAmount (optional) caps the ceiling for top-ups.
Voucher shape
The voucher object appears in every response. Fields:
Field | Type | Description |
|---|---|---|
| GUID | Internal APRO voucher ID. Use this in URL paths. |
| string | Human-readable code printed on the card/receipt. |
| integer | Original face value (minor units). |
| integer | Available balance (minor units). |
| integer | null | Ceiling for top-ups; |
| string | null | Optional secondary identifier. |
| ISO 8601 | null | Not valid before. |
| ISO 8601 | null | Not valid after. |
| bool |
|
| string | null | Free-form staff note. |
| integer | Voucher category (issuing channel). |
| string | Category display name. |
| integer | null | Category-level redemption-count cap. |
| string | null | Upstream payment-provider reference. |
| ISO 8601 | When the voucher was issued. |
| string |
|
1. List vouchers
GET https://cloudgateway.apro.at/voucherservice/api/v1/vouchersReturns every voucher visible to the API key. Use the validity endpoint instead when you just need to look up one code — it's indexed for that path.
2. Check validity
GET https://cloudgateway.apro.at/voucherservice/api/v1/vouchers/validity
?voucherCode=560620085Query parameters
Name | Type | Required | Description |
|---|---|---|---|
| string | ✅ | The code printed on the voucher. |
Example response
200 OK
{
"voucher": {
"gift": { "amount": 5000, "balance": 3880, "maxAmount": null },
"id": "f5f4d577-3764-4c25-12f8-08db73dad741",
"voucherCode": "560620085",
"codeId": null,
"startDate": null,
"expirationDate": null,
"enabled": true,
"comment": null,
"categoryId": 4,
"categoryName": "OnlineGutschein",
"categoryNrOfTimesRedeemablePerUnit": null,
"categoryExternalPaymentProviderId": null,
"createdAt": "2023-06-23T11:13:15.6386753",
"discriminator": "GiftVoucherDto"
},
"errors": [],
"isValid": true
}Download: voucher-validity-response.json
isValid: false + a populated errors array indicates why the voucher cannot be redeemed right now (expired, disabled, wrong date window, redemption cap reached). Common error codes:
Error | Meaning |
|---|---|
|
|
| Past |
| Before |
|
|
|
|
|
|
3. Issue a voucher
POST https://cloudgateway.apro.at/voucherservice/api/v1/vouchers/giftsCreates a new gift voucher. On success the HTTP status is 201 Created and the response Location header points at the new resource.
Request body
Field | Type | Required | Description |
|---|---|---|---|
| integer | ✅ | Which voucher category to issue under. |
| string | ✅ | Code to print on the voucher. Must be unique. |
| string | — | Staff note. |
| integer | ✅ | Face value in minor units. |
| integer | — | Top-up ceiling. |
| string | ✅ | Identifier of the calling system. Provided by APRO for your integration — see clientType. |
| string (JSON) | — | Serialised JSON for side-channel context (outlet etc.). |
| ISO 8601 | — | Not valid before. |
| ISO 8601 | — | Not valid after. |
Example request
POST /voucherservice/api/v1/vouchers/gifts
{
"categoryId": 1,
"voucherCode": "2131231",
"comment": "Gift card — reprint for a lost original",
"gift": { "amount": 50, "maxAmount": 10000 },
"clientType": "<provided-by-apro>",
"clientContext": "{\"outletId\":\"231231\"}",
"startDate": "2026-01-01T00:00:00",
"expirationDate": "2028-12-31T23:59:59"
}Download: voucher-issue-request.json
Example response
201 Created
{
"data": {
"gift": { "amount": 50, "balance": 50, "maxAmount": 10000 },
"id": "e961c17d-d037-4d27-734d-08ddda944e39",
"voucherCode": "2131231",
"categoryId": 1,
"categoryName": "Standard",
"enabled": true,
"comment": "Gift card — reprint for a lost original",
"startDate": "2026-01-01T00:00:00",
"expirationDate": "2028-12-31T23:59:59",
"createdAt": "2026-04-20T21:33:00.000+02:00",
"discriminator": "GiftVoucherDto"
}
}Download: voucher-issue-response.json
4. Redeem a voucher
POST https://cloudgateway.apro.at/voucherservice/api/v1/vouchers/{voucherId}/redemptionBurns amount minor units off the balance. Path parameter is the voucher's internal id (not the voucherCode) — you get it from /validity.
Request body
Field | Type | Required | Description |
|---|---|---|---|
| integer | ✅ | Amount to debit in minor units. |
| string | — | Opaque trace string (e.g. |
| string (JSON) | — | Side-channel context. |
| string | — | Staff note. |
| string | ✅ | Identifier of the calling system. Provided by APRO — see clientType. |
Example request
POST /voucherservice/api/v1/vouchers/{id}/redemption
{
"redemption": { "amount": 50 },
"clientReference": "{ \"waiterId\": 10 }",
"clientContext": "{\"outletId\":\"231231\"}",
"comment": "Redeemed at Bar 2 against receipt 5906",
"clientType": "<provided-by-apro>"
}Download: voucher-redeem-request.json
Example response
200 OK
{
"data": {
"redemption": { "amount": 50, "items": [] },
"success": true,
"errors": [],
"transactionId": 21142,
"voucher": {
"gift": { "amount": 5000, "balance": 3830, "maxAmount": null },
"id": "f5f4d577-3764-4c25-12f8-08db73dad741",
"voucherCode": "560620085",
"categoryId": 4,
"categoryName": "OnlineGutschein",
"enabled": true,
"discriminator": "GiftVoucherDto"
}
}
}Download: voucher-redeem-response.json
The transactionId (integer) is the authoritative ID for this redemption — persist it for reconciliation and refunds.
5. Top up a voucher
POST https://cloudgateway.apro.at/voucherservice/api/v1/vouchers/{voucherId}/bookingAdds amount minor units back onto the voucher, e.g. when a customer re-charges a multi-use card or a refund flows back.
Request body
Field | Type | Required | Description |
|---|---|---|---|
| integer | ✅ | Amount to credit in minor units. Must respect |
| string | — | Opaque trace string. |
| string | ✅ | Identifier of the calling system. Provided by APRO — see clientType. |
| string (JSON) | — | Side-channel context. |
| string | — | Staff note. |
Example request
POST /voucherservice/api/v1/vouchers/{id}/booking
{
"amount": 1000,
"clientReference": "{ \"waiterId\": 10 }",
"clientType": "<provided-by-apro>",
"clientContext": "{\"outletId\":\"231231\"}",
"comment": "Top-up after customer added credit"
}Download: voucher-booking-request.json
Example response
200 OK
{
"data": {
"newAmount": 4830,
"success": true,
"errors": [],
"transactionId": 21143,
"voucher": {
"gift": { "amount": 5000, "balance": 4830, "maxAmount": null },
"id": "f5f4d577-3764-4c25-12f8-08db73dad741",
"voucherCode": "560620085",
"categoryName": "OnlineGutschein",
"enabled": true,
"discriminator": "GiftVoucherDto"
}
}
}Download: voucher-booking-response.json
clientType
clientType identifies the calling system so the voucher ledger knows which integration created a transaction. APRO assigns a clientType value to every integration at onboarding time — the exact string is not self-selectable and is shared with you together with the API key. Always use the value provided by APRO; never hard-code a guess.
Error handling
- HTTP 4xx on validation failures (bad
voucherId, malformed body, missing auth). See Authentication → Error responses. - HTTP 200 with
success: falseon business rule failures (insufficient balance, expired, etc.). Inspect theerrorsarray. - Never replay a redemption on the client side. If a response times out, query /validity to confirm the balance before retrying — the operation may already be applied server-side.