On this page
menu-item API · v1
The search engine of dishes
The menu-item API is a read-only REST interface over the menu-item.com catalogue: Belgian hospitality venues, the dishes extracted from their menus and regional price statistics. Every response is JSON and every request carries a bearer token.
This page is generated from the same OpenAPI contract that validates the live API, so what you read here is exactly what the server returns. Point your integration at the base URL and go.
Every endpoint is a GET. Nothing here can change data, so you are
free to explore.
curl "https://menu-item.com/api/v1/dishes/stats?name=spareribs" \
-H "Authorization: Bearer YOUR_TOKEN"
Authentication
Authenticate every request with a Laravel Sanctum personal access
token, sent as a bearer token in the Authorization header. You can
create and revoke tokens from your account dashboard.
Keep your token secret and use it only from server-side code. A
missing or invalid token returns a 401 response.
curl "https://menu-item.com/api/v1/restaurants" \
-H "Authorization: Bearer YOUR_TOKEN"
{
"message": "Unauthenticated."
}
Base URL
All endpoint paths are relative to the base URL. Versioned resources
live under /v1; the /user health check is intentionally
unversioned.
Use the production host for live data. A local host is available when you run the API yourself.
# Production
https://menu-item.com/api
# Local development
http://localhost:8000/api
Rate limits
The API is rate limited per token so it stays fast for everyone. If
you exceed the limit you receive a 429 Too Many Requests response;
wait a moment and retry.
Be a good citizen: cache responses where you can and avoid tight polling loops.
Price statistics and menus change slowly. Caching for a few minutes is usually plenty.
Errors
The API uses conventional HTTP status codes. Any 4xx or 5xx
response carries a JSON body with a message, and validation errors
add a field-keyed errors object.
200OK: the request succeeded401Unauthorized: the bearer token is missing or invalid404Not Found: no resource matched the request422Unprocessable Entity: a parameter failed validation429Too Many Requests: you are being rate limited500Server Error: something went wrong on our end
{
"message": "The name field is required.",
"errors": {
"name": [
"The name field is required."
]
}
}
Pagination
List endpoints are paginated. Control the window with per_page
(1 to 100, default 25) and page. Each response carries a meta
block with the current position and totals, and a links block with
ready-made URLs for the first, last, previous and next pages.
To walk every page, follow links.next until it is null.
{
"links": {
"first": "https://menu-item.com/api/v1/restaurants?page=1",
"last": "https://menu-item.com/api/v1/restaurants?page=6",
"prev": null,
"next": "https://menu-item.com/api/v1/restaurants?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 6,
"per_page": 25,
"to": 25,
"total": 146
}
}
Restaurants
Venues discovered in the catalogue.
List restaurants
Returns a paginated list of venues. Combine the filters below to narrow the catalogue; results default to alphabetical order. Each item carries the venue, its city and a menu_items_count.
Parameters
search
string
city
string
type
string
has_menu
boolean
sort
string
name, price_level, created_at.
Default: name
per_page
integer
25
page
integer
1
curl "https://menu-item.com/api/v1/restaurants?city=brugge" \
-H "Authorization: Bearer YOUR_TOKEN"
const params = new URLSearchParams({
city: "brugge",
});
const response = await fetch(
`https://menu-item.com/api/v1/restaurants?${params}`,
{
headers: { Authorization: "Bearer YOUR_TOKEN" },
},
);
const data = await response.json();
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://menu-item.com/api/',
]);
$response = $client->get('v1/restaurants', [
'headers' => ['Authorization' => 'Bearer YOUR_TOKEN'],
'query' => [
'city' => 'brugge',
],
]);
$data = json_decode((string) $response->getBody(), true);
import requests
response = requests.get(
"https://menu-item.com/api/v1/restaurants",
headers={"Authorization": "Bearer YOUR_TOKEN"},
params={
"city": "brugge",
},
)
data = response.json()
{
"data": [
{
"id": 42,
"name": "De Gastro",
"address": "Langestraat 15, 8000 Brugge",
"latitude": 51.209,
"longitude": 3.2247,
"website": "https://degastro.be",
"type": "restaurant",
"price_level": 2,
"city": {
"name": "Brugge",
"slug": "brugge",
"country": "BE"
},
"menu_items_count": 48,
"menu_document": {
"id": 108,
"source_url": "https://degastro.be/menukaart.pdf",
"document_date": "2025-11-02",
"status": "extracted",
"extracted_at": "2025-11-04T09:12:00Z"
}
}
],
"links": {
"first": "https://menu-item.com/api/v1/restaurants?page=1",
"last": "https://menu-item.com/api/v1/restaurants?page=6",
"prev": null,
"next": "https://menu-item.com/api/v1/restaurants?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 6,
"per_page": 25,
"to": 25,
"total": 146
}
}
Retrieve a restaurant
Returns a single venue with its full latest menu (menu), the source document and, for rated venues, a value-for-money score.
Parameters
restaurant
integer
required
curl "https://menu-item.com/api/v1/restaurants/42" \
-H "Authorization: Bearer YOUR_TOKEN"
const response = await fetch("https://menu-item.com/api/v1/restaurants/42", {
headers: { Authorization: "Bearer YOUR_TOKEN" },
});
const data = await response.json();
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://menu-item.com/api/',
]);
$response = $client->get('v1/restaurants/42', [
'headers' => ['Authorization' => 'Bearer YOUR_TOKEN'],
]);
$data = json_decode((string) $response->getBody(), true);
import requests
response = requests.get(
"https://menu-item.com/api/v1/restaurants/42",
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
data = response.json()
{
"data": {
"id": 42,
"name": "De Gastro",
"address": "Langestraat 15, 8000 Brugge",
"latitude": 51.209,
"longitude": 3.2247,
"website": "https://degastro.be",
"type": "restaurant",
"price_level": 2,
"city": {
"name": "Brugge",
"slug": "brugge",
"country": "BE"
},
"menu_document": {
"id": 108,
"source_url": "https://degastro.be/menukaart.pdf",
"document_date": "2025-11-02",
"status": "extracted",
"extracted_at": "2025-11-04T09:12:00Z"
},
"score": {
"quality_score": 4.32,
"quality_basis": "bayesiaans gewogen op 300 reviews",
"rating": 4.5,
"ratings_count": 300,
"avg_dish_price": 22.5,
"value_index": 78,
"value_label": "sterke prijs-kwaliteit",
"price_position": "gemiddeld",
"confidence": "hoog",
"anomaly": null,
"sample_size": 48,
"disclaimer": "Indicatief en enkel gebaseerd op openbare Google-ratings en onze eigen menuprijzen. Geen oordeel over de echtheid van reviews."
},
"menu": [
{
"id": 5001,
"section": "Voorgerechten",
"name": "Tomatensoep met balletjes",
"description": null,
"price": 8.5,
"currency": "EUR",
"allergens": [
"selderij"
],
"dietary_tags": []
},
{
"id": 5002,
"section": "Hoofdgerechten",
"name": "Vlaamse stoverij",
"description": "met huisgemaakte friet",
"price": 22,
"currency": "EUR",
"allergens": [
"gluten"
],
"dietary_tags": []
}
]
}
}
Dishes
Menu items across every restaurant; the search engine of dishes.
Search dishes
Searches dishes across every restaurant: the search engine of dishes. Each hit carries the dish and a compact reference to its restaurant and city.
Parameters
dish
string
name is also accepted.
name
string
dish.
section
string
city
string
min_price
number
max_price
number
dietary
string
sort
string
name, price, section.
Default: name
per_page
integer
25
page
integer
1
curl "https://menu-item.com/api/v1/menu-items?dish=stoverij" \
-H "Authorization: Bearer YOUR_TOKEN"
const params = new URLSearchParams({
dish: "stoverij",
});
const response = await fetch(
`https://menu-item.com/api/v1/menu-items?${params}`,
{
headers: { Authorization: "Bearer YOUR_TOKEN" },
},
);
const data = await response.json();
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://menu-item.com/api/',
]);
$response = $client->get('v1/menu-items', [
'headers' => ['Authorization' => 'Bearer YOUR_TOKEN'],
'query' => [
'dish' => 'stoverij',
],
]);
$data = json_decode((string) $response->getBody(), true);
import requests
response = requests.get(
"https://menu-item.com/api/v1/menu-items",
headers={"Authorization": "Bearer YOUR_TOKEN"},
params={
"dish": "stoverij",
},
)
data = response.json()
{
"data": [
{
"id": 5002,
"section": "Hoofdgerechten",
"name": "Vlaamse stoverij",
"description": "met huisgemaakte friet",
"price": 22,
"currency": "EUR",
"allergens": [
"gluten"
],
"dietary_tags": [],
"restaurant": {
"id": 42,
"name": "De Gastro",
"city": "brugge"
}
},
{
"id": 6110,
"section": "Hoofdgerechten",
"name": "Stoverij van 't huis",
"description": null,
"price": 24.5,
"currency": "EUR",
"allergens": [
"gluten",
"selderij"
],
"dietary_tags": [],
"restaurant": {
"id": 57,
"name": "Brasserie Nord",
"city": "brugge"
}
}
],
"links": {
"first": "https://menu-item.com/api/v1/menu-items?page=1",
"last": "https://menu-item.com/api/v1/menu-items?page=3",
"prev": null,
"next": "https://menu-item.com/api/v1/menu-items?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 3,
"per_page": 25,
"to": 25,
"total": 61
}
}
Dish price statistics
Aggregates the price of a named dish across the region: sample size, average, min and max in EUR, plus up to ten of the cheapest examples. Scope it to a city with city.
Parameters
name
string
required
city
string
curl "https://menu-item.com/api/v1/dishes/stats?name=spareribs&city=brugge" \
-H "Authorization: Bearer YOUR_TOKEN"
const params = new URLSearchParams({
name: "spareribs",
city: "brugge",
});
const response = await fetch(
`https://menu-item.com/api/v1/dishes/stats?${params}`,
{
headers: { Authorization: "Bearer YOUR_TOKEN" },
},
);
const data = await response.json();
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://menu-item.com/api/',
]);
$response = $client->get('v1/dishes/stats', [
'headers' => ['Authorization' => 'Bearer YOUR_TOKEN'],
'query' => [
'name' => 'spareribs',
'city' => 'brugge',
],
]);
$data = json_decode((string) $response->getBody(), true);
import requests
response = requests.get(
"https://menu-item.com/api/v1/dishes/stats",
headers={"Authorization": "Bearer YOUR_TOKEN"},
params={
"name": "spareribs",
"city": "brugge",
},
)
data = response.json()
{
"query": {
"dish": "spareribs",
"city": "brugge"
},
"sample_size": 11,
"currency": "EUR",
"average_price": 24.8,
"min_price": 19.5,
"max_price": 31,
"examples": [
{
"name": "Spareribs",
"price": 19.5,
"restaurant": "'t Ribhuis",
"city": "brugge"
},
{
"name": "Spareribs (baby back)",
"price": 22,
"restaurant": "Brasserie Nord",
"city": "brugge"
}
]
}
Account
The authenticated token holder.
Current token holder
Returns the account the bearer token belongs to. Handy as a health check to confirm a token is valid. Note this endpoint is unversioned: it sits at /api/user, not under /v1.
curl "https://menu-item.com/api/user" \
-H "Authorization: Bearer YOUR_TOKEN"
const response = await fetch("https://menu-item.com/api/user", {
headers: { Authorization: "Bearer YOUR_TOKEN" },
});
const data = await response.json();
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://menu-item.com/api/',
]);
$response = $client->get('user', [
'headers' => ['Authorization' => 'Bearer YOUR_TOKEN'],
]);
$data = json_decode((string) $response->getBody(), true);
import requests
response = requests.get(
"https://menu-item.com/api/user",
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
data = response.json()
{
"id": 1,
"name": "Ada Lovelace",
"email": "[email protected]"
}
Prefer a machine-readable contract? The full API is described in an OpenAPI 3.0 spec you can import into Postman, Insomnia or your code generator. Questions? Email [email protected].