# MekanBook POS Connector API v1

**Türkçe rehber + English summary at the end.** Makine okunur tanım: `openapi.yaml`.

Her POS ve adisyon programı bu açık API ile MekanBook'a bağlanabilir. Bağlantı modelinde siparişleri MekanBook tutar, POS **çeker** (pull), işler ve sonucu **bildirir**. Ağ sorunlarında veri kaybolmaz: onaylanmayan sipariş tekrar gelir.

- **Taban adres:** `https://www.mekanbook.com/api/pos/v1`
- **Biçim:** JSON (UTF-8).
  - Başarılı yanıt: `{"data": ..., "meta": {...}?}`
  - Hatalı yanıt: `{"error": {"code": "...", "message": "..."}}`
- **Tutarlar:** kuruş cinsinden tamsayı (`12500` = 125,00 TL).
- **Zamanlar:** ISO-8601, UTC.

## 1. Anahtar ve imza

Restoran, **MekanBook Panel > POS entegrasyonu** ekranında markasını seçer ve "Anahtar oluştur"a basar. Ekranda iki değer **yalnız bir kez** gösterilir:

- **API anahtarı:** `mbpos_{bağlantı_no}_{40 karakter}`. MekanBook yalnız özetini saklar.
- **İmza sırrı:** 48 karakter.

Her istekte şu üç başlık gönderilir:

| Başlık | Değer |
|---|---|
| `Authorization` | `Bearer mbpos_…` |
| `X-MB-Timestamp` | Unix zaman damgası, saniye. Sunucu saatine göre ±300 sn içinde olmalı |
| `X-MB-Signature` | `hex( HMAC_SHA256( imza_sırrı, kanonik ) )` |

Kanonik metin, aralarında `\n` (LF) olan dört satırdır:

```
{X-MB-Timestamp}
{HTTP yöntemi, büyük harf}
{yol ve sorgu dizesi, ör. /api/pos/v1/orders?after=120}
{hex( SHA256( ham gövde ) )}   (gövde yoksa boş dizenin SHA256'sı)
```

Aynı imzayla gelen ikinci istek `409 replayed_request` alır. Her istekte yeni zaman damgası kullanın; aynı saniyede iki istek atmanız gerekirse gövde veya sorgu farklı olmalıdır.

**PHP**

```php
$ts = (string) time();
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);           // GET için ''
$canonical = $ts."\n".'POST'."\n".'/api/pos/v1/lines/45/status'."\n".hash('sha256', $body);
$sig = hash_hmac('sha256', $canonical, $secret);
$headers = ['Authorization: Bearer '.$key, 'X-MB-Timestamp: '.$ts, 'X-MB-Signature: '.$sig, 'Content-Type: application/json'];
```

**C# (.NET)**

```csharp
using System.Security.Cryptography; using System.Text;

static string Hex(byte[] b) => Convert.ToHexString(b).ToLowerInvariant();
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var body = JsonSerializer.Serialize(payload);                     // GET için ""
var bodyHash = Hex(SHA256.HashData(Encoding.UTF8.GetBytes(body)));
var canonical = $"{ts}\n{method.ToUpperInvariant()}\n{pathAndQuery}\n{bodyHash}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var sig = Hex(hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical)));
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
req.Headers.Add("X-MB-Timestamp", ts);
req.Headers.Add("X-MB-Signature", sig);
```

## 2. Akış

1. **Yoklama.** POS 5–15 saniyede bir `GET /orders?after={son_id}` çağırır. MekanBook'ta garsonun veya mutfağın kabul ettiği sipariş satırları gelir.
2. **Onay.** POS her olayı kaydedince `POST /orders/{id}/ack` gönderir; isterse kendi sipariş numarasını `pos_order_id` olarak ekler. Onaylanmayan olay sonraki yoklamada yine gelir (en az bir kez teslim). Aynı olayı iki kez işlememek için olay `id`'sini saklayın.
3. **Mutfak durumu.** Hazırlanınca `POST /lines/{line_id}/status {"status":"ready"}`. Misafirin ekranında ve MekanBook mutfak ekranında anında görünür.
4. **Kasa ödemesi.** Misafir kasada öderse `POST /tables/{masa}/payments`. MekanBook hesabı güncellenir ve aynı tutar uygulamadan ikinci kez alınamaz.
5. **Menü.** Menü değişince `PUT /menu`.

## 3. Uç noktalar

| Yöntem ve yol | Açıklama |
|---|---|
| `GET /ping` | Bağlantıyı doğrular. Yanıt: `{venue, sandbox, mode, server_time}` |
| `GET /orders?after=&limit=` | Onaylanmamış `order.accepted` olayları (en fazla 100). `meta.next_after`, `meta.has_more` |
| `GET /events?after=&limit=` | Tüm onaylanmamış olaylar: `order.accepted`, `line.cancelled`, `session.closed` |
| `POST /orders/{id}/ack` · `POST /events/{id}/ack` | Gövde `{"pos_order_id": "A-1023"}` isteğe bağlı. Idempotent |
| `POST /lines/{id}/status` | `{"status": "preparing|ready|served|cancelled", "reason": "..."}`. İptal için gerekçe yazın |
| `PUT /menu` | `{"mode": "full|delta", "items": [{"external_id", "name", "price", "category", "description", "is_available"}]}` |
| `GET /tables` | Açık adisyonlar: `[{session_id, table, opened_at, party_size, status, total, paid, due}]` |
| `POST /tables/{masa}/payments` | `Idempotency-Key` başlığı zorunlu. Gövde `{"amount", "tip", "method": "cash|card_pos", "reference"}` |
| `POST /checks/{session_id}/close` | Kalan borç yoksa adisyonu kapatır |
| `POST /sandbox/orders` | Yalnız deneme modunda örnek sipariş olayı üretir |

**Olay örneği** (`GET /orders`)

```json
{"data": [{"id": 812, "type": "order.accepted", "created_at": "2026-11-03T18:42:10Z", "test": false,
  "data": {"order_id": 5531, "session_id": 977, "table": "12", "note": null,
    "lines": [{"line_id": 20441, "external_id": "P-117", "name": "Izgara köfte", "qty": 2, "unit_price": 38000,
               "options": [{"group": "Pişme", "name": "İyi pişmiş", "price": 0}], "note": "soğansız", "station": "kitchen"}]}}],
 "meta": {"next_after": 812, "has_more": false}}
```

**Menü kuralları**

- `price` kuruş cinsindendir. `null` gönderilirse ürün "fiyat belirtilmemiş" olur ve MekanBook'ta sipariş edilemez. Bu, sürpriz fiyatı önlemek içindir.
- Fiyat değişiklikleri kayda geçer ve menüde "fiyat güncellendi" tarihi yenilenir. Açık adisyonlardaki satırlar sipariş anındaki fiyatı korur.
- `full` modunda listede olmayan ve daha önce POS'tan gelmiş ürünler "şu an yok" yapılır, silinmez. `delta` modunda yalnız gönderilen ürünler değişir.

**Ödeme kuralları**

- `amount` kalan hesabı aşamaz; aşarsa `422 amount_exceeds_due` döner.
- Aynı `Idempotency-Key` tekrar gönderilirse ilk yanıt döner ve `Idempotent-Replay: true` başlığı eklenir.

## 4. Deneme (sandbox) modu

Restoran panelden "Deneme moduna al" der. Bu modda:

- `GET /orders` yalnız deneme olaylarını döndürür. `POST /sandbox/orders` ile örnek sipariş üretebilirsiniz.
- Yazma istekleri (durum, menü, ödeme, kapanış) gerçek kuralla doğrulanır, sonucu hesaplanır ve **geri alınır**. Yanıtta `"test": true` olur.

Canlıya geçmek için restoran deneme modunu kapatır.

## 5. Anlık bildirim (isteğe bağlı)

Yoklama yerine itme isteyen POS, panelde bir HTTPS adresi ve en az 24 karakterlik imza anahtarı tanımlar. MekanBook olayları bu adrese POST eder:

- `X-MekanBook-Event: order.accepted`
- `X-MekanBook-Signature: sha256=HMAC_SHA256(gövde, anahtar)`
- Gövde: `{"event", "sent_at", "data"}`

Başarısız gönderim üstel beklemeyle 8 kez denenir. Olay ack edilene kadar `GET /events`'te de durur.

## 6. Hata kodları

| HTTP | Kod | Anlamı |
|---|---|---|
| 401 | `invalid_key` | Anahtar yok, yanlış, yenilenmiş ya da bağlantı kapatılmış |
| 401 | `timestamp_out_of_window` | Saat farkı 5 dakikadan fazla |
| 401 | `invalid_signature` | İmza tutmuyor |
| 409 | `replayed_request` | Aynı imza tekrar kullanıldı |
| 404 | `not_found` | Kayıt yok veya başka restorana ait |
| 422 | `invalid_transition` | Satır bu duruma geçemez (ör. servis edilmiş satır "hazırlanıyor" olamaz) |
| 422 | `amount_exceeds_due` · `payment_rejected` | Ödeme kabul edilmedi |
| 422 | `idempotency_key_required` | Ödeme isteğinde başlık yok |
| 404 | `no_open_check` | Masada açık adisyon yok |
| 409 | `balance_due` · `close_rejected` | Kapanış yapılamadı |
| 422 | `validation_failed` | Alan hataları (`fields`) |
| 429 | `rate_limited` | Bağlantı başına dakikada 600 istek aşıldı |

## 7. Güvenlik notları

- Anahtar ve sırrı yalnız POS sunucusunda saklayın. Kasa bilgisayarlarına ya da istemci uygulamaya koymayın.
- Anahtar sızarsa restoran panelden "Anahtarı yenile" ile eskisini anında geçersiz kılar.
- Bir anahtar yalnız kendi restoranının verisine erişir. Başka restoranın satır numarası `404` döner.

---

## English summary

- **Base URL:** `https://www.mekanbook.com/api/pos/v1`. JSON. Amounts are integer kuruş (1/100 TRY), times are UTC ISO-8601.
- **Auth:** `Authorization: Bearer mbpos_{id}_{40}`, plus `X-MB-Timestamp` (unix seconds, ±300 s) and `X-MB-Signature`.
  - The signature is the lowercase hex of HMAC-SHA256(secret, `ts \n METHOD \n path?query \n hex(sha256(body))`).
  - Replays are rejected with `409`.
  - The key and secret are generated per venue in the MekanBook panel and shown once.
- **Flow:**
  1. Poll `GET /orders?after=`.
  2. Acknowledge with `POST /orders/{id}/ack`. Delivery is at-least-once; deduplicate by event id.
  3. Report kitchen progress with `POST /lines/{id}/status`.
  4. Sync the menu with `PUT /menu` (full or delta). Price changes are audited, and open checks keep their prices.
  5. Report till payments with `POST /tables/{label}/payments` (needs the `Idempotency-Key` header).
  6. List open checks with `GET /tables`; close one with `POST /checks/{id}/close`.
- **Sandbox:** writes are validated and rolled back, and responses carry `"test": true`. `POST /sandbox/orders` creates a sample order event.
- **Optional push:** signed webhooks to your HTTPS URL (`X-MekanBook-Signature: sha256=…`), retried with backoff.
- **Rate limit:** 600 requests/min per connection.
