Genel bakış
- REST ve JSON, UTF-8. Alan adları snake_case. Zamanlar ISO 8601, UTC:
2026-10-22T14:07:23Z. - Tutarlar kuruş cinsinden tam sayıdır (
18500= 185,00 ₺), yanındacurrency. - Her kaydın değişmeyen, string bir id'si vardır; silinen bir kaydın id'si yeniden kullanılmaz.
- Bir işletme bir mekândır (venue) ve şimdilik tek şubesi vardır; şube kimliği mekân kimliğiyle aynıdır. Şube kimliğini
GET /venue'dan alın. - Sürüm yoldadır (/v1). v1 içinde alan kaldırılmaz ve adı değişmez; yeni alanlar eklenebilir, bilmediğiniz alanları yok sayın. Uyumsuz değişiklik /v2 olur.
Erişim ve yetkiler
API anahtarlarını Ladom oluşturur: her işletme ve bağlanan her uygulama için ayrı bir anahtar. Anahtar için işletmenin adını, uygulamanızın adını ve gereken yetkileri hello@ladom.co adresine ya da WhatsApp'tan yazın.
Authorization: Bearer lqm_YOUR_KEY| Yetki | Neye izin verir |
|---|---|
menu:read | Tüm okuma uçları; webhook kaydetme. |
menu:write | "Tükendi" yazma uçları. |
Hız sınırı
Her anahtar dakikada en çok 120 istek yapabilir. Her yanıtta X-RateLimit-Limit, X-RateLimit-Remaining başlıkları vardır. Aşılırsa 429 + Retry-After: 60 döner.
Hatalar
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json; charset=utf-8
{ "error": { "code": "validation_error", "message": "is_sold_out: Invalid input: expected boolean, received undefined" } }| HTTP | code | Ne zaman |
|---|---|---|
| 401 | unauthorized | Anahtar yok, tanınmıyor ya da iptal edilmiş. |
| 403 | forbidden | Anahtarın bu yetkisi yok ya da şube başka bir işletmenin. |
| 404 | not_found | Bu şubede böyle bir kayıt yok. |
| 409 | conflict | Elle kapatılmış ürünü açmaya çalıştınız (bkz. Tükendi). |
| 422 | validation_error | Gövde JSON değil, bilinmeyen alan ya da geçersiz değer; geçersiz sorgu parametresi. |
| 429 | rate_limited | Dakikalık sınır aşıldı. |
| 500 | server_error | Bizim tarafımızda bir sorun. Tekrar deneyin. |
Diller
Metinler translations altındadır: { "tr": { "name": "…", "description": "…" }, "en": { … } }. Diller: tr, en, ar, ru.
- ?lang verilmezse dört dil de gelir.
?lang=trile sadece o dil gelir. - Çevirisi girilmemiş bir alan boş dönmez: o dilde yoksa İngilizcesi, o da yoksa işletmenin asıl dili (
default_language), o da yoksa Türkçesi verilir. Hiç girilmemiş alannullolur (ör. açıklama).
Sayfalama ve değişiklikler
- Liste uçları
?limit=100(1–500) ve?cursor=…alır. Yanıt:{ "data": [...], "next_cursor": "…" | null }. Sonraki sayfa için next_cursor'ı aynen gönderin; null ise son sayfadır. ?updated_since=2026-10-22T14:00:00Zo andan beri değişen kayıtları verir ve silinenleri de: silinen kayıt sadeceid,updated_at,deleted_atile gelir. updated_since olmadan silinenler gelmez.- Kayıtlar (updated_at, id) sırasıyla gelir. Eşitlemeden sonra gördüğünüz en büyük updated_at'i saklayıp bir sonrakinde updated_since olarak gönderin (sınır dahildir; aynı kaydı tekrar görebilirsiniz, işlem tekrar edilebilir olmalı).
GET /branches/8beb7e3e-852c-428c-828b-62658089a800/products?updated_since=2026-10-22T14:30:00Z&limit=100
{
"data": [
{ "id": "923fb739-…", "…": "…", "updated_at": "2026-10-22T14:30:36Z", "deleted_at": null },
{ "id": "1f3922e8-8018-48e4-bae6-e04a8801991d", "updated_at": "2026-10-22T14:31:15Z", "deleted_at": "2026-10-22T14:31:15Z" }
],
"next_cursor": null
}Veri modeli
Ürün (product)
{
"id": "af04129a-aa38-4625-8033-2deecf2fbc39",
"category_id": "6c118c2d-8693-411e-8885-83b345aa5f6a",
"sort_order": 0,
"sku": "BRG-001",
"translations": {
"tr": { "name": "Hamburger", "description": "El yapımı köfte", "ingredients": "Dana köfte, brioche ekmek, marul" },
"en": { "name": "Hamburger", "description": "Handmade patty", "ingredients": "Beef patty, brioche bun, lettuce" },
"ar": { "…": "…" },
"ru": { "…": "…" }
},
"price": 25000,
"currency": "TRY",
"tax_rate": 10,
"images": [{ "url": "https://ladomapps-media.ladom.co/menu/…/….webp", "is_primary": true }],
"portion": "220 g",
"calories": 650,
"ingredients": ["Dana köfte", "brioche ekmek", "marul"],
"allergens": ["gluten", "eggs", "milk", "sesame"],
"allergens_declared": true,
"contains_alcohol": false,
"contains_pork": false,
"option_group_ids": ["og_af04129a-aa38-4625-8033-2deecf2fbc39"],
"is_active": true,
"is_sold_out": false,
"sold_out_source": null,
"campaign": null,
"updated_at": "2026-10-22T14:30:47Z",
"deleted_at": null
}| Alan | Açıklama |
|---|---|
price | Güncel satış fiyatı (kuruş). Seçenekli üründe en ucuz seçeneğin fiyatı; diğer seçenekler price_delta kadar fazladır. |
tax_rate | KDV oranı (%): 0, 1, 10, 20 ya da işletme girmediyse null. |
sku | Kasadaki ürün kodu; işletme panelde girer. Eşleştirme için. |
translations.*.ingredients | İçindekiler (yönetmelik gereği menüde gösterilir), dillere göre metin. |
ingredients | İçindekilerin liste hali (istenen ya da asıl dilde, virgülle ayrılmış metinden). |
portion | Porsiyon metni, ör. "220 g", "330 ml", "6 adet"; yoksa null. |
allergens | Alerjen kodları (aşağıda). allergens_declared: false ise işletme henüz girmemiştir; boş liste o zaman "bilinmiyor" demektir. |
is_active | false ise müşteri menüsünde görünmez; siz de göstermeyin. |
is_sold_out | Tükendi. sold_out_source: manual (panelden elle) ya da pos; açıksa null. |
campaign | Şimdilik her zaman null (bkz. Henüz olmayanlar). |
Seçenek grubu (option group)
Bir ürünün option_group_ids listesinde iki tür grup olabilir; ikisi de aynı biçimdedir:
- Seçenek ve ekstra grupları (pişirme derecesi, ekstra malzeme, soslar…): işletme bir kez tanımlar, birçok ürüne bağlar.
selection:singleya damultiple;min/maxseçim sayısı (max null = sınır yok);price_deltaek ücret. - Boyutlar (küçük/büyük, tek/çift köfte): o ürüne özel, tek seçimli, zorunlu grup; kimliği
og_<product id>veproduct_idtaşır. Ürünün price'ı en ucuz boyutun fiyatıdır, diğerleri price_delta kadar fazladır.
Seçenek kimlikleri kalıcıdır; tükendi yazarken bunları kullanın.
{
"id": "og_af04129a-aa38-4625-8033-2deecf2fbc39",
"product_id": "af04129a-aa38-4625-8033-2deecf2fbc39",
"translations": { "tr": { "name": "Seçenek" }, "en": { "name": "Option" }, "…": "…" },
"selection": "single",
"required": true,
"min": 1,
"max": 1,
"options": [
{ "id": "93e8a968-dd2f-427c-bbc5-80b842874dec", "translations": { "tr": { "name": "Tek köfte" }, "…": "…" },
"price_delta": 0, "is_default": true, "is_sold_out": false, "sold_out_source": null, "sort_order": 0 },
{ "id": "944b2d3d-1a79-4a85-b817-3f296156a450", "translations": { "tr": { "name": "Çift köfte" }, "…": "…" },
"price_delta": 9000, "is_default": false, "is_sold_out": true, "sold_out_source": "pos", "sort_order": 1 }
],
"updated_at": "2026-10-22T14:30:47Z",
"deleted_at": null
}{
"id": "17468dea-c551-49e5-9d3c-3e155dada881",
"translations": { "tr": { "name": "Ekstralar" }, "en": { "name": "Extras" }, "…": "…" },
"selection": "multiple",
"required": false,
"min": 0,
"max": 3,
"options": [
{ "id": "5c0b…", "translations": { "tr": { "name": "Ekstra peynir" }, "…": "…" }, "price_delta": 1500,
"is_default": false, "is_sold_out": false, "sold_out_source": null, "sort_order": 0 },
{ "id": "285d1169-afd9-4330-bf38-39063626d609", "translations": { "tr": { "name": "Bacon" }, "…": "…" }, "price_delta": 3000,
"is_default": false, "is_sold_out": false, "sold_out_source": null, "sort_order": 1 }
],
"updated_at": "2026-10-22T14:52:10Z",
"deleted_at": null
}Alerjen kodları
Türk Gıda Kodeksi'nin 14 alerjen grubu (AB 1169/2011 ile aynı):
glutencrustaceanseggsfishpeanutssoymilknutscelerymustardsesamesulphiteslupinmolluscs
Okuma uçları
Hepsi menu:read yetkisi ister; listeler ?lang, ?limit, ?cursor ve ?updated_since alır.
GET/venue
İşletme, dilleri ve şubesi.
{
"id": "8beb7e3e-852c-428c-828b-62658089a800",
"name": "GG Burger",
"currency": "TRY",
"default_language": "tr",
"languages": ["tr", "en", "ar", "ru"],
"menu_url": "https://qr.ladom.co/ggburger",
"branches": [
{ "id": "8beb7e3e-852c-428c-828b-62658089a800", "name": "GG Burger", "timezone": "Europe/Istanbul" }
]
}GET/branches/{branch_id}/menu
Tüm menü tek yanıtta: kategoriler, ürünler, seçenek grupları (silinenler hariç). ETag döner; If-None-Match ile gönderirseniz menü değişmediyse 304 alırsınız. version menüdeki son değişikliğin zamanıdır.
curl https://qr.ladom.co/api/v1/branches/8beb7e3e-852c-428c-828b-62658089a800/menu \
-H "Authorization: Bearer lqm_YOUR_KEY" \
-H 'If-None-Match: "kq3…"'
{
"branch_id": "8beb7e3e-852c-428c-828b-62658089a800",
"version": "2026-10-22T14:31:15Z",
"categories": [ { "id": "6c118c2d-…", "parent_id": null, "sort_order": 0,
"translations": { "tr": { "name": "Burgerler" }, "…": "…" },
"image_url": null, "is_active": true, "updated_at": "…", "deleted_at": null } ],
"products": [ { "…": "the product above" } ],
"option_groups": [ { "…": "the option group above" } ]
}GET/branches/{branch_id}/categories
GET/branches/{branch_id}/products
Ek filtre: ?category_id=… (bu filtreyle silinen ürünler gelmez).
GET/branches/{branch_id}/products/{product_id}
GET/branches/{branch_id}/option-groups
Tükendi (yazma)
Kasanın menüye yazabildiği tek bilgi "tükendi"dir; menu:write yetkisi ister.
PATCH/branches/{branch_id}/products/{product_id}/availability
PATCH/branches/{branch_id}/options/{option_id}/availability
Gövde: { "is_sold_out": true, "source": "pos" } (source isteğe bağlı, verilirse "pos" olmalı).
curl -X PATCH https://qr.ladom.co/api/v1/branches/8beb7e3e-852c-428c-828b-62658089a800/products/923fb739-2339-41fe-b846-ba5c41ad21e5/availability \
-H "Authorization: Bearer lqm_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f1c2a9e-stock-out-0001" \
-d '{ "is_sold_out": true, "source": "pos" }'
{ "id": "923fb739-2339-41fe-b846-ba5c41ad21e5", "type": "product", "is_sold_out": true, "sold_out_source": "pos" }| Şu an | İstek | Sonuç |
|---|---|---|
| Satışta | is_sold_out: true | Tükendi, kaynak pos. |
| Tükendi (pos) | is_sold_out: false | Satışa açılır. |
| Tükendi (manual) | is_sold_out: false | 409 conflict |
| Tükendi (herhangi) | is_sold_out: true | Değişmez, 200. |
Kural: kasanın kapattığını sadece kasa açar; panelden elle kapatılanı kasa açamaz (409). Böylece yöneticinin bilerek kapattığı ürün stok girişiyle yanlışlıkla açılmaz. İşletme panelden her şeyi açabilir.
HTTP/1.1 409 Conflict
{ "error": { "code": "conflict", "message": "It was marked sold out by hand in the LQM panel; only the business can reopen it." } }POST/branches/{branch_id}/availability/batch
En çok 200 satır; satırlar sırayla, tek tek uygulanır ve her biri için sonuç döner (ok, not_found, conflict).
POST /branches/8beb7e3e-852c-428c-828b-62658089a800/availability/batch
{
"items": [
{ "type": "product", "id": "923fb739-2339-41fe-b846-ba5c41ad21e5", "is_sold_out": true },
{ "type": "option", "id": "944b2d3d-1a79-4a85-b817-3f296156a450", "is_sold_out": false }
],
"source": "pos"
}
200 OK
{
"results": [
{ "status": "ok", "id": "923fb739-…", "type": "product", "is_sold_out": true, "sold_out_source": "pos" },
{ "status": "conflict", "id": "944b2d3d-…", "type": "option", "message": "It was marked sold out by hand…" }
]
}Idempotency-Key
Yazma isteklerinde Idempotency-Key başlığı gönderebilirsiniz (en çok 200 karakter). Aynı anahtarla 24 saat içinde gelen istek ilk yanıtı aynen döner (Idempotent-Replayed: true); sunucu hataları saklanmaz, tekrar denenebilir.
Webhooks
Menüde bir şey değiştiğinde (işletmenin panelinden ya da API'den) adresinize bir POST gelir. Adresinizi ve gizli anahtarınızı kendiniz kaydedersiniz; anahtar başına bir webhook.
PUT/webhooks
Kaydeder ya da değiştirir (menu:read yeterli). events verilmezse tüm olaylar gelir. secret en az 16 karakter; adres https olmalı. Yeniden kaydetmek, kapanmış bir webhook'u açar.
curl -X PUT https://qr.ladom.co/api/v1/webhooks \
-H "Authorization: Bearer lqm_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://ladapp.example/api/webhooks/qr-menu",
"events": ["product.updated", "product.deleted", "category.updated", "category.deleted",
"option_group.updated", "option_group.deleted", "availability.changed"],
"secret": "whsec_a-long-random-secret-you-choose"
}'GET/webhooks
Kayıtlı webhook ve son teslim durumu (last_status, last_attempt_at) ya da { "webhook": null }.
DELETE/webhooks
| Olay | data | Ne zaman |
|---|---|---|
product.updated | { id, changed } | Ürün eklendi ya da herhangi bir alanı değişti (fiyat, metin, fotoğraf, tükendi, sıra…). |
product.deleted | { id } | Ürün silindi. |
category.updated | { id, changed } | Kategori eklendi ya da değişti. |
category.deleted | { id } | Kategori silindi. |
option_group.updated | { id, changed } | Bir grup eklendi ya da değişti (ad, kural, seçenekler, ek ücret, tükendi). Ürüne grup bağlanınca/çıkarılınca product.updated (option_group_ids) gelir. |
option_group.deleted | { id } | Grup silindi, ya da ürünün boyutları kaldırıldı / ürün silindi. |
availability.changed | { id, type, is_sold_out, source } | Ürün ya da seçenek tükendi/açıldı (kaynağı fark etmeksizin). type: product | option. |
POST /api/webhooks/qr-menu HTTP/1.1
Content-Type: application/json
User-Agent: Ladom-Webhooks/1
X-Event-Id: evt_3b8f6c2e-0d4a-4f61-9a57-2c1e9d7b4a10
X-Timestamp: 2026-10-22T14:30:47Z
X-Signature: sha256=5f2c0e…a91b
{
"id": "evt_3b8f6c2e-0d4a-4f61-9a57-2c1e9d7b4a10",
"type": "product.updated",
"branch_id": "8beb7e3e-852c-428c-828b-62658089a800",
"occurred_at": "2026-10-22T14:30:47Z",
"data": { "id": "af04129a-aa38-4625-8033-2deecf2fbc39", "changed": ["sku", "tax_rate"] }
}- Olay sadece değişen kaydın id'sini ve değişen alanları (
changed, ör.translations.en.name) taşır; güncel veriyi ilgili GET ucundan çekin. Böylece sırası bozuk gelen olaylar sorun çıkarmaz. - "Tükendi" değişince hem availability.changed hem product.updated (ya da option_group.updated) gelir.
- X-Event-Id benzersizdir; aynı olayı iki kez işlemeyin.
- 5 saniye içinde 2xx dönün; yönlendirme (3xx) izlenmez. Üst üste 50 başarısız teslimden sonra webhook kapanır; yeniden kaydedince açılır.
İmzayı doğrulama
X-Signature: sha256=<hex>, ham gövdenin gizli anahtarınızla HMAC-SHA256'sıdır. İmzası tutmayan isteği işlemeyin.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (before JSON.parse).
export function verifyLadomSignature(rawBody, signatureHeader, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const got = String(signatureHeader ?? "");
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}<?php
// $rawBody = file_get_contents('php://input');
// $header = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
function verify_ladom_signature(string $rawBody, string $header, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $header);
}import hashlib, hmac
def verify_ladom_signature(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")Örnek entegrasyon: kasa (POS)
- GET /venue ile şube kimliğini alın; GET /branches/{id}/menu ile menüyü çekin. Ürünleri id (ya da işletmenin girdiği sku) ile kendi ürünlerinizle eşleştirin.
- PUT /webhooks ile adresinizi kaydedin.
- Stok bitince PATCH …/availability { is_sold_out: true }, gelince false. Birden çok ürün için batch ucunu kullanın. 409 gelirse ürün elle kapatılmıştır; açmaya çalışmayın.
- Webhook gelince ilgili kaydı GET ile çekin; product.deleted gelince eşleşmeyi kaldırın.
- Her 10 dakikada bir updated_since ile eşitleyin; arada If-None-Match ile tam menü kontrolü de yapabilirsiniz.
Henüz olmayanlar
- Webhook teslimlerinin üstel gecikmeyle yeniden denenmesi.
- Birden çok şube ve şubeye göre fiyat; alt kategori (parent_id her zaman null); kategori görseli (image_url null).
- Kampanya fiyatı ve rozeti (campaign her zaman null); fiyat geçmişi ucu.
Sürümler
| Tarih | Değişiklik |
|---|---|
| 22 Ekim 2026 | Seçenek ve ekstra grupları (tek/çoklu seçim, min/max, ek ücret); option-groups listesinde ve ürünlerin option_group_ids'inde. Bu grupların seçeneklerine de tükendi yazılabilir. |
| 22 Ekim 2026 | v1: mekân ve şube, tam menü (ETag), listeler (updated_since, silinenler, sayfalama), tükendi yazma (tekil, seçenek, toplu, Idempotency-Key), webhook kaydı ve olayları. |
Sorular ve anahtar talepleri: hello@ladom.co