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. |
