
Developer Documentation
15 Seiten
Stand: 6. November 2026
Inhalt
Start
Introduction
APRO is the modular point-of-sale platform for restaurants, hotels, food trucks and system gastronomy — built to connect. Hotel software, accounting, payments, delivery services, reservation systems, access control, digital signage: APRO plugs into your existing stack for end-to-end processes and maximum automation.
This site is the developer documentation for teams building on that surface: partners wiring up reservation or inventory systems, delivery marketplaces routing orders into the POS, BI tools reading the ledger, or in-house teams automating reports.
What you will find here
- REST APIs. Stable, JSON-over-HTTPS endpoints for the domains below. Every endpoint is authenticated with a single API key — no SDK required, no OAuth dance to orchestrate.
- SQL views. Read-only views for direct database access, useful for BI pipelines and data warehouses.
- Payload samples. Every example on this site is backed by a standalone JSON file under
/samples/that you can download and replay. - Full API reference. The machine-readable contract is published at api-docs.apro.at.
Pick your starting point
Goal | Start here |
|---|---|
Pull revenue, receipts and payments | |
Push orders from an external system | |
Read or edit menu cards | |
Export data for BI / warehouse | |
Sign requests |
Status and versioning
Endpoints under /api/v1/… are considered stable. Breaking changes are announced in the release notes on apro.at. New optional fields and non-breaking response additions can land at any time — parsers should tolerate unknown keys.
Support
- Commercial & onboarding: apro.at/support
- Integration questions: reach out through your APRO account manager.
Authentication
All APRO REST endpoints use a single API key, passed in the x-api-key request header. There is no OAuth flow and no token refresh — keys are long-lived and scoped to either a tenant or a single location.
Header
POST /api/v1/external/order HTTP/1.1
Host: my.apro.at
x-api-key: sk_live_pLpXZq8S…
content-type: application/jsonKey scope
Every APRO REST key is scoped to a single location. A request succeeds only if the location derived from the endpoint matches the location bound to the key.
Endpoint style | Location is derived from |
|---|---|
| the API key itself. |
|
|
A location-scoped key fails closed if reused against another location and is safe to hand to a partner or a single deployment.
Obtaining a key
Keys are issued by the APRO account team. Contact your account manager or apro.at/support to request one. Rotation is supported; the retired key is kept valid for a grace period so you can roll over without downtime.
Storing keys
- Never commit a key to source control.
- Store keys in a secret manager (Azure Key Vault, 1Password, HashiCorp Vault, GitHub Actions / Azure DevOps secrets).
- Inject them at runtime as environment variables (
APRO_API_KEY).
Error responses
Status | Meaning |
|---|---|
| Missing or invalid |
| Key valid but not authorised for the requested location or action. |
| You are being rate-limited. Back off and retry with exponential jitter. |
APIs
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.
Web widgets
Voucher Widget Embedding
The voucher widget is a <giftcard-widget> web component that loads the APRO voucher shop for a single location in an iframe. Drop two lines into any page and customers can browse, buy and personalise gift cards without leaving the host site.
- Script:
https://smorderfrontend.azureedge.net/embed/widget.js - Tag:
<giftcard-widget location-id="…"> - Three modes: inline list (the voucher list fills its container), inline button (a button is rendered in the page flow and opens a panel), or floating button (a fixed FAB opens a panel)
Quick start
The minimum embed — load the script once, then add the tag wherever the shop should appear:
<script src="https://smorderfrontend.azureedge.net/embed/widget.js"></script>
<giftcard-widget location-id="535"></giftcard-widget>That's it. The widget queries the location's voucher categories, renders the shop, and handles the customer flow through to checkout. The host page only needs to provide layout space.
Live sample — inline button
Rendered with presentation="inline-button". The trigger sits in the page flow exactly where the <giftcard-widget> tag is placed; clicking it opens the voucher shop in a modal panel.
<script src="https://smorderfrontend.azureedge.net/embed/widget.js"></script>
<giftcard-widget
location-id="535"
locale="en"
presentation="inline-button"
expand-icon="redeem"
expand-label="Vouchers"
headline="Buy a voucher"
max-height="700"
></giftcard-widget><div style="padding:1rem; margin:1.25rem 0;">
<giftcard-widget location-id="535" locale="en" presentation="inline-button" expand-icon="redeem" expand-label="Vouchers" headline="Buy a voucher" max-height="700" ></giftcard-widget>
</div>
Live sample — inline list
Same widget without presentation set — the full voucher list drops into the page flow up to a fixed max-height.
<script src="https://smorderfrontend.azureedge.net/embed/widget.js"></script>
<giftcard-widget
location-id="535"
locale="en"
max-height="700"
></giftcard-widget><style>{giftcard-widget:not([presentation]) { display: block; min-height: 320px; }}</style>
<div style="border:1px dashed var(--sl-color-gray-5); border-radius:10px; padding:1rem; margin:1.25rem 0; min-height:740px;">
<giftcard-widget location-id="535" locale="en" max-height="700" style="display:block; height:700px;" ></giftcard-widget>
</div>
Interactive builder
Tweak any attribute below — the markup updates instantly and the preview re-mounts the widget with the new configuration. Copy the final snippet straight into your CMS or template.
Inline-button mode
The recommended default for most pages. Set presentation="inline-button" to render the trigger in the page flow exactly where the <giftcard-widget> tag is placed. Clicking it opens the voucher shop inside a modal panel. The host page's layout stays untouched until the customer opts in.
<giftcard-widget
location-id="535"
locale="en"
presentation="inline-button"
expand-icon="redeem"
expand-label="Vouchers"
headline="Buy a voucher"
max-height="700"
></giftcard-widget>Floating-button mode
Set presentation="expandable" to switch to a fixed floating-action button: a circular pill in one of six corners opens a docked panel with the shop inside. Useful when you want a persistent entry point that follows the visitor down the page.
<giftcard-widget
location-id="535"
locale="en"
presentation="expandable"
position="right-bottom"
max-height="700"
max-width="420"
expand-label="Gift cards"
expand-icon="card_giftcard"
headline="Buy a voucher"
></giftcard-widget>position controls both the FAB anchor and which edge the panel docks to. Allowed values:
right-bottom(default),left-bottom,center-bottomright-top,left-top,center-top
expand-icon is a Material Icons ligature name (e.g. redeem, card_giftcard, celebration). Bundled via @fontsource/material-icons inside the embed — image URLs are not supported.
expand-theme switches between dark (default) and light styling for the button. The panel header follows the same theme — dark in default mode, light when expand-theme="light".
Inline-list mode
Leave presentation empty (or omit it) to drop the voucher list itself into the page flow. The widget takes the width of its parent and grows to the iframe's natural height, capped at max-height. Use this when you want a dedicated "Gift cards" landing section.
<giftcard-widget
location-id="535"
locale="en"
max-height="700"
></giftcard-widget>Attributes
Attribute | Required | Default | Description |
|---|---|---|---|
| ✅ | — | Numeric APRO location ID. The widget refuses to mount without it. |
| — |
| UI language inside the iframe: |
| — | `` (inline list) |
|
| — | — | Pixel cap. Inline list: iframe height. Panel modes: panel height (also bounded by viewport). |
| — | — | Pixel cap for the panel width. Panel modes only. |
| — |
| One of six corner keys; controls the panel anchor (and FAB position when |
| — | locale default | Text on the trigger button. Empty = automatic by |
| — |
|
|
| — | — | Material Icons ligature shown left of the label. Panel modes only. |
| — | — | Title bar above the iframe in the panel; also forwarded as |
| — | — | Any CSS color ( |
| — |
| When |
Loading the script
Load widget.js once per page — anywhere in <head> or before the closing </body>. It registers the <giftcard-widget> custom element globally; subsequent <giftcard-widget> tags on the same page activate automatically.
<script src="https://smorderfrontend.azureedge.net/embed/widget.js"></script>Multiple widgets on one page are supported (e.g. an inline shop plus an expandable FAB elsewhere). Each iframe is isolated.
Data Export
SQL Views
When you need direct SQL access to the POS — usually because APRO is installed on-premise alongside existing systems — the cash register database exposes a stable set of read-only views. They are intentionally shaped for the two most common consumers:
- On-premise integrations — accounting bridges, kitchen displays, stock management, loyalty engines — anything that runs inside the same network and wants a low-latency feed without going through REST.
- Existing BI workflows — Power BI, Tableau, Qlik, Metabase, Excel, native SQL Server Analysis Services — any tool that already understands SQL Server connections. Point the connector at the views and you get receipts, line items, payments and product master in the shape a data warehouse expects.
Views are read-only, queryable over the standard SQL Server protocol, and never join to operational tables that mutate under load — they are safe to drive dashboards and scheduled ETL jobs against.
Tip: SQL Views or Revenue API?
The SQL views and the Revenue API expose the same
underlying data — pick whichever fits the runtime:
- SQL Views (this page) — flat, relational, on-premise.
Perfect for ETL, Power BI / Tableau / Qlik / Metabase connectors and
any workflow that wants bulk access via SQL.
- Revenue API — served over the cloud endpointmy.apro.at, returns
a hierarchical JSON document with positions, payments and tax
already nested per receipt. Better for real-time system-to-system
integration, SaaS consumers and anything outside the local network.
Note
The read-only SQL user is provisioned per deployment. Ask your APRO
account manager to enable access for your tenant and to share the
server name, database, and credentials.
Overview
View | What it contains |
|---|---|
One row per business transaction. | |
Line items. Join on | |
Payments. Join on | |
Light transaction index. | |
Transaction-type lookup. | |
Denormalized receipt × item + payment summary — start here for Power BI. | |
Product master with category. |
1. vw_Receipts
One row per business transaction. The TransactionType column distinguishes what the row represents; amounts are signed — reversal transactions (TransactionType = 4) return the booking with a negated amount so a plain SUM(TotalAmount) already nets them out.
Column | Type | Description |
|---|---|---|
| GUID | Unique transaction record ID. |
| GUID | Transaction ID (join key for |
| enum | See Transaction types. |
| string | Human-readable transaction type. |
| string | Receipt / invoice number. |
| decimal | Gross total (signed — negative on reversals). |
| decimal | Tip (signed). |
| decimal | Discount applied to the whole receipt (signed). |
| datetime | Booking timestamp. |
| date | Virtual fiscal business day. |
| GUID | null | Predecessor receipt (e.g. retrieved settlement → original). |
| integer | null | Payment group ID (e.g. Umsatzwirksam). |
| string | null | Payment group name — useful as a turnover filter. |
| integer | Operating company ID. |
| string | Operating company name. |
| integer | Table ID. |
| integer | Table number. |
| string | Table label. |
| integer | Area / section ID ( |
| integer | Area number. |
| string | Area name (e.g. Restaurant, Bar, Terrasse). |
| integer | Staff ID. |
| integer | Personnel number. |
| string | Staff name. |
| integer | Customer ID. |
| integer | Customer number. |
| string | Customer name. |
| integer | Device that performed the transaction. |
Transaction types
Only transactions with a TransactionType in (1, 2, 3, 4, 5, 14, 24, 25) are exposed through this view. The authoritative mapping lives in vw_TransactionTypes and is also joined onto every row via TransactionTypeName.
| Meaning |
|---|---|
| Booking — order at the table or self-service. |
| Move — table change. |
| Settlement — paid receipt. |
| Reversal — undoes a booking (amounts negated). |
| Retrieve receipt — undoes a settlement. |
| Outgoing invoice created. |
| Outgoing invoice — ancillary (see |
| Outgoing invoice paid. |
2. vw_Receipt_Items
Line items. Join to vw_Receipts on ReceiptId / TransactionId. Values are signed the same way as the parent receipt.
Column | Type | Description |
|---|---|---|
| GUID | Transaction ID. |
| GUID | Receipt ID (join to |
| GUID | Unique line item ID. |
| GUID | Underlying booking ID (shared by bookings and their reversals). |
| datetime | Booking timestamp. |
| integer | Category (Sparte) ID. |
| integer | Category number. |
| string | Category name. |
| string | Accounting account number for the category. |
| integer | Product ID. |
| integer | Product number. |
| string | Product name. |
| decimal | Quantity (signed on reversals / retrievals). |
| decimal | Unit price at the time of sale (signed on reversals). |
| decimal | Discounted unit price (signed on reversals). |
| decimal |
|
| decimal |
|
| decimal | Discount amount in money (signed on reversals / retrievals). |
| decimal | Discount as % of the original price. |
| string |
|
| integer | Tax rate ID. |
| string | Tax rate name. |
| decimal | Tax rate as decimal. |
| enum | Same codes as |
3. vw_Payments
Payments recorded against receipts. Only settlements and their reversals are exposed (TransactionType in (3, 4, 5, 14, 24, 25)).
Column | Type | Description |
|---|---|---|
| GUID | Transaction ID. |
| GUID | Receipt ID. |
| GUID | Unique payment ID. |
| decimal | Amount paid (signed). |
| integer | Payment method ID. |
| integer | Payment method number. |
| string | Payment method name (e.g. Bar, Kreditkarte, Gutschein). |
| datetime | Payment timestamp. |
| GUID | null | When a settlement is retrieved, the original receipt it cancels. |
| string | null | Free-form payment metadata (e.g. card masked PAN, voucher code). |
4. vw_Transactions
Light index of every transaction. Useful for watermark-based ETL (pulling only new rows since the last checkpoint).
Column | Type | Description |
|---|---|---|
| GUID | Transaction ID. |
| enum | Transaction type. |
| string | Human-readable name. |
| datetime | Timestamp of the transaction. |
| integer | Device that performed it. |
5. vw_TransactionTypes
Lookup for every transaction type the POS emits.
Column | Type | Description |
|---|---|---|
| integer | Transaction-type ID. |
| string | Human-readable name (German). |
6. vw_Bi_Export
The one-stop view for BI tools. Denormalized join of every turnover-relevant receipt with every line item, plus a comma-separated summary of the payment methods used on the receipt. Point Power BI or Tableau at this view and you have everything needed for revenue, margin, mix and payment-method dashboards without writing joins.
Columns are the union of vw_Receipts and vw_Receipt_Items (minus TransactionTypeId, which lives only on the receipt side), plus:
Column | Type | Description |
|---|---|---|
| string | Comma-separated list of |
Power BI example
- Get data → SQL Server database — server + database as provided.
- Import or DirectQuery mode, using the read-only user.
- Pick
dbo.vw_Bi_Exportas the fact table. - Optionally add
dbo.vw_Transactions,dbo.vw_Payments,dbo.vw_products_and_categoriesas companion tables and relate onTransactionId/ProductId.
Import mode is normally sufficient — the views are already aggregated to transaction grain and refresh in seconds for a typical venue.
7. vw_products_and_categories
Product master with category. Useful as a dimension alongside vw_Bi_Export when the BI tool wants product names to stay stable even if historical rows predate a rename.
Column | Type | Description |
|---|---|---|
| integer | Product ID. |
| integer | Product number. |
| string | Product name. |
| integer | Category ID. |
| integer | Category number. |
| string | Category name. |
| decimal | Current list price ( |
Only products where Geloescht = 0 (not soft-deleted) are included.
Example queries
Turnover per business day, split by tax rate — for a monthly KPI chart:
SELECT
r.BusinessDay,
ri.TaxName,
SUM(ri.EffectiveAmount) AS net_turnover
FROM vw_Receipts AS r
JOIN vw_Receipt_Items AS ri ON ri.ReceiptId = r.Id
WHERE r.TransactionType IN (3, 4, 5) -- settlements + reversals net out
AND r.BusinessDay BETWEEN @from AND @to
GROUP BY r.BusinessDay, ri.TaxName
ORDER BY r.BusinessDay, ri.TaxName;Top-10 products for the current month, using the BI view directly:
SELECT TOP 10
ProductName,
SUM(Quantity) AS units_sold,
SUM(EffectiveAmount) AS net_turnover
FROM vw_Bi_Export
WHERE BusinessDay >= DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)
AND TransactionType IN (3, 4, 5)
GROUP BY ProductName
ORDER BY net_turnover DESC;Cash vs. card split for a single business day:
SELECT
PaymentTypeName,
SUM(TotalAmount) AS total
FROM vw_Payments
WHERE CAST(DateTime AS date) = @businessDay
GROUP BY PaymentTypeName;