Poslite Service — API Reference
Port internal: 8096
Proxy publik: POST|GET /merchant/poslite/* via core_api (JWT required)
Auth internal: X-Internal-API-Key + X-Merchant-ID (di-inject core_api proxy)
Semua path di bawah ini adalah path setelah prefix /merchant distrip oleh core_api.
Mobile memanggil https://api-merchant.kesles.com/merchant/poslite/...; core_api forward ke
http://127.0.0.1:8096/poslite/....
Catalog — Outlets
GET /poslite/catalog/outlets
List semua outlet merchant.
Response 200:
{
"outlets": [
{
"id": "uuid",
"merchant_id": "uuid",
"code": "OUTLET-01",
"name": "Toko Pusat",
"address_line": "Jl. Sudirman 1",
"city": "Makassar",
"province": "Sulawesi Selatan",
"phone": "0411-123456",
"nmid": "ID1234567890123",
"qris_static_payload": "00020101...",
"is_active": true,
"created_at": "...",
"updated_at": "..."
}
]
}
POST /poslite/catalog/outlets
Buat outlet baru.
Body:
{
"code": "OUTLET-01",
"name": "Toko Pusat",
"address_line": "Jl. Sudirman 1",
"city": "Makassar",
"province": "Sulawesi Selatan",
"phone": "0411-123456"
}
code dan name wajib. Conflict uk_outlets_merchant_code → 409 code_conflict.
Response 201: Objek outlet.
GET /poslite/catalog/outlets/{id}
Ambil detail outlet.
Response 200: Objek outlet. 404 not_found jika tidak ada.
PATCH /poslite/catalog/outlets/{id}
Update outlet (semua field opsional).
Body:
{
"name": "Toko Baru",
"address_line": "...",
"city": "...",
"province": "...",
"phone": "...",
"is_active": false,
"nmid": "ID...",
"qris_static_payload": "00020101..."
}
nmid dan qris_static_payload di-pull dari merchant.merchant_outlets — update manual
hanya untuk sinkronisasi manual atau koreksi.
Response 200: Objek outlet terupdate.
DELETE /poslite/catalog/outlets/{id}
Soft-delete outlet (deleted_at = now()). Response 204.
Catalog — Categories
GET /poslite/catalog/categories
List semua kategori merchant.
Response 200: { "categories": [...] }
POST /poslite/catalog/categories
Buat kategori baru. Field name wajib.
Body: { "name": "...", "parent_id": null, "sort_order": 0, "icon_name": "category" }
Response 201: Objek kategori.
GET /poslite/catalog/categories/{id}
Ambil detail satu kategori.
Response 200: Objek kategori. 404 jika tidak ada.
PATCH /poslite/catalog/categories/{id}
Update kategori (semua field opsional).
Body: { "name": "...", "parent_id": "...", "sort_order": 1, "icon_name": "...", "is_active": true }
Response 200: Objek kategori.
DELETE /poslite/catalog/categories/{id}
Soft-delete. Response 204.
GET /poslite/catalog/categories/defaults?business_type_name=<nama>
Template kategori default dari db_reference berdasarkan jenis usaha.
Response 200: { "business_type_name": "...", "categories": [{ "name": "...", "icon_name": "...", "sort_order": 0 }] }
503 reference_db_unavailable jika DB_REFERENCE_POSTGRES_DSN tidak dikonfigurasi.
POST /poslite/catalog/categories/provision
Provisi kategori awal merchant dari daftar pilihan. Idempotent — jika merchant sudah punya kategori, return existing tanpa insert baru.
Body:
{
"selected": [{ "name": "Makanan", "icon_name": "food" }],
"custom": [{ "name": "Lainnya", "icon_name": "category" }]
}
Response 200: { "categories": [...] }
Catalog — Products
GET /poslite/catalog/products
List produk. Query params opsional: category_id, q, active=true, limit, offset.
Response 200: { "products": [...] } — setiap produk menyertakan barcode (dari
default variant, atau null jika belum di-set).
POST /poslite/catalog/products
Buat produk baru.
Body:
{
"name": "Nasi Goreng",
"sku": "NAGO-01",
"category_id": "uuid",
"product_type": "good",
"sell_price_amount": 15000,
"cost_price_amount": 8000,
"unit_label": "porsi",
"description": "",
"image_url": "",
"tax_rate_bps": 0,
"track_stock": false,
"barcode": "8999909096004"
}
name + sku wajib. product_type: good (default) atau service.
Auto-buat 1 default variant transparan (untuk referensi variant_id di sales).
Conflict:
409 sku_conflict— SKU sudah dipakai merchant409 barcode_conflict— Barcode sudah dipakai produk lain (unique global)
Response 201: Objek produk.
GET /poslite/catalog/products/{id}
Ambil produk termasuk barcode. Response 200. 404 jika tidak ada.
PATCH /poslite/catalog/products/{id}
Update produk (semua field opsional).
Body (semua opsional):
{
"name": "...",
"category_id": "...",
"description": "...",
"image_url": "...",
"unit_label": "...",
"sell_price_amount": 18000,
"cost_price_amount": 9000,
"tax_rate_bps": 0,
"track_stock": false,
"is_active": true,
"sort_order": 1,
"barcode": "8999909096004"
}
barcode: null → tidak ubah. barcode: "" → hapus barcode. barcode: "123..." → set baru.
Update barcode ditulis ke catalog.product_variants (default variant). Conflict → 409 barcode_conflict.
Response 200: Objek produk dengan barcode terbaru.
DELETE /poslite/catalog/products/{id}
Soft-delete. Response 204.
Sync
GET /poslite/sync/pull?entity=<e>&since=<millis>&limit=200
Tarik perubahan katalog ke device (incremental by updated_at).
Entity yang didukung: outlets | categories | products | variants | product_stock | customers
Response 200:
{
"entity": "products",
"rows": [{ "id": "...", "name": "...", ... }],
"server_time": 1749470000000,
"has_more": false
}
sincekosong → full pullhas_more: true→ loop dengansince=server_timedari response sebelumnya- Timestamp di
rows= epoch millis; bool = int (0/1) — disesuaikan format SQLite mobile
POST /poslite/sync/push
Kirim batch mutasi dari device ke server. Idempotent by outbox_id — pengiriman ulang aman.
Body:
{
"items": [
{
"outbox_id": "uuid",
"entity_type": "sales_order",
"entity_id": "uuid",
"op": "create",
"payload": { ... }
}
]
}
Entity type yang didukung: sales_order | stock_movement | customer
sales_order payload:
{
"id": "uuid", "merchant_id": "uuid", "outlet_id": "uuid",
"receipt_number": "RCP-00042", "status": "completed", "source": "offline",
"transacted_at": 1749470000000,
"subtotal_amount": 32000, "total_amount": 32000, "paid_amount": 50000, "change_amount": 18000,
"items": [
{ "id": "uuid", "line_no": 1, "variant_id": "uuid",
"sku_snapshot": "NAGO-01", "name_snapshot": "Nasi Goreng",
"quantity": 2, "unit_price_amount": 16000, "line_total_amount": 32000 }
],
"payments": [
{ "id": "uuid", "method": "cash", "amount": 50000 }
]
}
Response 200:
{
"accepted": ["outbox_id_1", "outbox_id_2"],
"rejected": { "outbox_id_3": "entity_type tidak dikenal: unknown" }
}
Max 500 items per batch.
Sales — Orders
GET /poslite/sales/orders
List order merchant. Query params opsional: outlet_id, status, date_from, date_to, limit (max 200, default 50), offset.
Status: open | completed | voided | refunded
Response 200: { "orders": [...] } — tanpa items dan payments (header saja).
GET /poslite/sales/orders/{id}
Detail order termasuk items dan payments. 404 jika tidak ada.
PATCH /poslite/sales/orders/{id}
Aksi pada order. Saat ini hanya mendukung void.
Body:
{
"action": "void",
"void_reason": "Pesanan dibatalkan pelanggan"
}
void_reason opsional — boleh string kosong. Mobile saat ini mengirim "".
Response 200: Objek order dengan status: "voided" dan voided_at terisi.
Error:
400 bad_request—actionbukan"void"404 not_found— order tidak ada500 internal_error: "order sudah dalam status voided"— sudah dibatalkan sebelumnya
Mobile flow: device memanggil PATCH endpoint ini, lalu menandai order sebagai voided di SQLite lokal dan me-reload daftar riwayat.
Kode Error Umum
| HTTP | Code | Keterangan |
|---|---|---|
| 400 | bad_request | Field wajib kosong atau body tidak valid |
| 401 | unauthorized | X-Internal-API-Key salah atau kosong |
| 404 | not_found | Resource tidak ditemukan atau bukan milik merchant |
| 405 | — | Method tidak didukung untuk path ini |
| 409 | sku_conflict | SKU sudah dipakai |
| 409 | barcode_conflict | Barcode sudah dipakai produk lain |
| 409 | code_conflict | Kode outlet sudah dipakai |
| 503 | reference_db_unavailable | db_reference tidak terhubung |
| 500 | internal_error | Error tak terduga — lihat log service |