Eatzino Connect API
Send a restaurant's menu to Eatzino straight from the website's code
One API call puts categories, dishes, prices, photos, sizes, add-ons and allergens into Eatzino. Change the website, send again – the menu stays in sync, guests order with 0 % commission. Made so that AI website builders can do it on their own.
No restaurant account yet? Sign up for free – then create the key in the partner dashboard.
curl -X PUT https://api.eatzino.com/api/v1/connect/menu \
-H "Authorization: Bearer $EATZINO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"categories": [{
"id": "pizza",
"name": {"en": "Pizza", "de": "Pizza"},
"dishes": [{
"id": "margherita",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 9.5,
"image": "https://example.com/margherita.jpg",
"labels": ["vegetarian"],
"allergens": ["gluten", "milk"],
"options": [{
"id": "size", "name": "Size",
"choices": [
{"name": "26 cm", "price": 0},
{"name": "32 cm", "price": 3}
]
}]
}]
}]
}'Quick start
- Register the restaurantThe owner signs up at eatzino.com/be-seller (free, about 5 minutes). Restaurants can't be created through the API.
- Create an API keyPartner dashboard → Restaurant → Developer API → Create key. Keep it on the server as
EATZINO_API_KEY. - Send the menu
PUT /menu?dry_run=1to check, thenPUT /menu. Add the ordering script to the website.
New restaurants are hidden from guests until Eatzino has verified them – you can build the whole menu before that.
For AI website builders
Building the site with Claude, ChatGPT, Cursor, Lovable, v0, Bolt or another AI tool? Paste this prompt into its chat. It points the agent to ai.md, a step-by-step guide written for coding agents. The restaurant owner finds the same prompt – with their key and id filled in – in the partner dashboard.
Connect this website's menu to Eatzino (restaurant id <RESTAURANT_ID>).
Read the API guide at https://eatzino.com/developers/ai.md and follow it exactly.
- API key: read it from the environment variable EATZINO_API_KEY. Never put it in browser code or commit it to git.
- Build the menu JSON from the menu data in this codebase: categories, dishes, prices, descriptions, photos, choices (sizes), add-ons, allergens. Use stable ids.
- Send it with ?dry_run=1 first and show me the summary. Then send it for real.
- Add Eatzino online ordering to the site with the embed script.
- Add a small script (e.g. "npm run eatzino:sync") so the menu can be sent again after changes.Authentication
Base URL https://api.eatzino.com/api/v1/connect. Send the restaurant's key on every request:
Authorization: Bearer ezk_…- One key belongs to one restaurant. The owner can create up to 10 and delete them at any time.
- A key can be read-only (GET only) or read & write.
- Server-side only. The key can change the menu – never put it in browser JavaScript, public HTML or a git repository.
Endpoints
| GET | /restaurant | Restaurant id, name, currency, status, page URL and ordering snippets |
| GET | /menu | The current menu – in exactly the format PUT /menu accepts |
| PUT | /menu | Create or update the whole menu in one call. ?dry_run=1 only checks |
| PATCH | /dishes/{id} | Change one dish: price, availability, texts, photo, allergens … |
| DELETE | /dishes/{id} | Remove one dish |
| POST | /images | Upload a photo (file, base64 or URL) → URL to use as a dish image |
| GET | /public/restaurants/{id}/menu | No key: the live menu guests can order (CORS open) |
How syncing works
- Fields you leave out stay as they are. If the owner added allergens in the dashboard and you don't send
allergens, they are kept. - Matching: your
id→ else anez-…id fromGET /menu→ else, on the first sync, the same name among dishes typed in the dashboard. No duplicates when a restaurant already has a menu. mergecreates and updates what you send.replacealso removes dishes, categories and add-ons that came through the API before and are no longer sent. Dishes added by hand in the dashboard are never removed.- Only real changes are written. Sending the same menu again reports everything as
unchanged– guests' carts are not touched. - Languages: the site shows English and German. A plain text is used for both until the other language is set; send
{"en": "…", "de": "…"}when you have both. - Dry run:
?dry_run=1checks everything and returns the same summary without saving.
{
"ok": true,
"dry_run": false,
"mode": "merge",
"summary": {
"categories": {"created": 1, "updated": 0, "unchanged": 0, "removed": 0},
"dishes": {"created": 1, "updated": 0, "unchanged": 0, "removed": 0},
"addons": {"created": 0, "updated": 0, "unchanged": 0, "removed": 0},
"images": {"downloaded": 1, "failed": 0, "not_checked": 0}
},
"warnings": [],
"menu_url": "https://eatzino.com/en/your-city/restaurant/your-restaurant"
}Small changes
PATCH /dishes/{id} takes any dish field plus category (a category id). To add a single dish, send PUT /menu in merge mode with just that category and dish.
# sold out
curl -X PATCH https://api.eatzino.com/api/v1/connect/dishes/margherita \
-H "Authorization: Bearer $EATZINO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"available": false}'
# new price – choices move with it
curl -X PATCH https://api.eatzino.com/api/v1/connect/dishes/margherita \
-H "Authorization: Bearer $EATZINO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"price": 10.5}'Photos
Public https:// image URLs can go straight into image – Eatzino downloads each URL once and shrinks it. Photos that only exist in the project (e.g. /public/images) are uploaded first:
curl -X POST https://api.eatzino.com/api/v1/connect/images \
-H "Authorization: Bearer $EATZINO_API_KEY" \
-F "file=@public/images/margherita.jpg"
# → {"ok": true, "url": "https://api.eatzino.com/storage/images/products/…jpg"}At most 25 new photos are downloaded per request; send the same menu again for the rest. Keep the returned URLs in your sync script so files aren't uploaded twice.
Errors & limits
Every response has ok. When a menu has problems, nothing is saved and each problem comes with the exact path:
{
"ok": false,
"error": {
"code": "invalid_menu",
"message": "Nothing was saved. Fix the fields listed in \"details\" and send again.",
"details": [
{"path": "categories[0].dishes[2].price",
"message": "Must be more than 0 – dishes without a price are not shown"},
{"path": "categories[1].dishes[0].allergens[0]",
"message": "Unknown allergen \"peanutbutter\". Allowed: gluten, crustaceans, …"}
]
}
}401 | missing_key, invalid_key |
403 | read_only_key |
404 | not_found – unknown dish id |
413 | too_large – body over 3 MB |
422 | invalid_menu, invalid_image |
429 | rate_limited – 120 requests and 30 writes per minute per key |
Size limits per request: 80 categories, 600 dishes, 300 add-ons; 4 options per dish, 30 choices per option.
Online ordering on the website
Eatzino handles cart, checkout, guest accounts and live order tracking. Add the script once; GET /restaurant returns these snippets with the right id. On WordPress use the Eatzino plugin instead.
<!-- once, before </body> – 123 = your restaurant id -->
<script src="https://eatzino.com/embed.js" data-shop="123" async></script>
<!-- any link or button opens ordering -->
<a href="#eatzino-order">Order online</a>
<!-- open one dish (eatzino_id from GET /menu) -->
<button data-eatzino-order data-eatzino-product="EATZINO_ID">Order</button>
<!-- the ordering menu inside your page -->
<div data-eatzino-menu data-height="800"></div>Script options: data-lang (en/de, default: the page language), data-label, data-color, data-position, data-floating="false". JavaScript: EatzinoWidget.open(), .close(), .reserve(). In React/Next.js load it once with next/script.
Public menu
Want the website to show the menu from Eatzino instead of its own data? GET /public/restaurants/{id}/menu needs no key, allows any origin and only returns live restaurants and dishes guests can order. Fetch it on the server or at build time and cache it for a few minutes.
Sync script
Put a small script into the project so the menu can be sent again whenever it changes:
// scripts/eatzino-sync.mjs – node scripts/eatzino-sync.mjs [--dry]
import menuData from "../data/menu.json" with { type: "json" }; // your menu data
const API = "https://api.eatzino.com/api/v1/connect";
const KEY = process.env.EATZINO_API_KEY;
if (!KEY) throw new Error("Set EATZINO_API_KEY");
const menu = {
mode: "replace",
categories: menuData.sections.map((s) => ({
id: s.slug,
name: s.title,
dishes: s.items.map((d) => ({
id: d.slug, name: d.name, description: d.description ?? "",
price: Number(d.price),
})),
})),
};
const dry = process.argv.includes("--dry") ? "?dry_run=1" : "";
const res = await fetch(`${API}/menu${dry}`, {
method: "PUT",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(menu),
});
const out = await res.json();
if (!out.ok) {
console.error(out.error.message, out.error.details ?? "");
process.exit(1);
}
console.log(out.dry_run ? "Dry run:" : "Synced:", out.summary);