Bab 13 · Developer API
Panduan Integrasi & Contoh Kode
Resep polling yang benar (backfill, updated_since, upsert, tombstone) plus contoh kode siap pakai untuk Google Apps Script, Python, dan Node.js.
Halaman ini adalah cara yang didukung untuk menarik data Lensiro secara berkala. Kalau integrasi Anda mengikuti pola di bawah, ia tetap benar meski transaksi diedit, dibatalkan, atau dihapus.
Pola dasar: empat langkah
1. VALIDASI -> /ping dan /branches, sekali di awal
2. BACKFILL -> tarik riwayat per rentang bulanan, satu kali saja
3. POLLING -> tarik perubahan dengan updated_since, berkala
4. UPSERT -> simpan berdasarkan id; hapus baris yang punya deleted_at
1. Validasi
curl -H "Authorization: Bearer $KEY" "$BASE/ping"
curl -H "Authorization: Bearer $KEY" "$BASE/branches"
Simpan pemetaan cabang dari /branches supaya branch_id di data lain bisa diterjemahkan tanpa hardcode.
2. Backfill awal, sekali saja
Tarik riwayat per bulan supaya setiap request ringan dan mudah diulang kalau gagal di tengah jalan:
GET /sales?transaction_date_from=2026-01-01T00:00:00Z
&transaction_date_to=2026-01-31T23:59:59Z
&include_deleted=true
&limit=200
-> ikuti next_cursor sampai has_more = false
-> lanjut ke bulan berikutnya
Beri jeda sekitar satu detik antar halaman supaya tidak menyenggol batas 60 request per menit.
3. Polling berkala, misalnya tiap jam
last = nilai updated_at terbesar yang tersimpan
(kalau belum ada, pakai waktu selesai backfill)
GET /sales?updated_since={last}&include_deleted=true&limit=200
-> ikuti next_cursor sampai has_more = false
-> untuk setiap baris:
kalau deleted_at terisi -> hapus atau tandai batal di sistem Anda
kalau tidak -> UPSERT berdasarkan id
-> simpan updated_at terbesar yang baru
Pola yang sama berlaku untuk /sales/payments, /sales/facets, /members, dan /warehouse/items.
Kenapa harus upsert, bukan insert? Karena transaksi bisa berubah setelah pertama kali Anda lihat — pembayaran menyusul, faktur terbit belakangan, item diperbaiki, atau transaksi dibatalkan. updated_since akan mengirim transaksi itu lagi, dan salinan Anda harus menimpanya, bukan menduplikasinya.
Simpan updated_at terbesar hanya setelah seluruh halaman selesai ditarik. Kalau disimpan di tengah jalan lalu proses gagal, polling berikutnya akan melompati data yang belum sempat masuk.
4. Yang tidak mengikuti pola ini
| Data | Pola yang benar |
|---|---|
/warehouse/stocks | Snapshot — tarik ulang seluruhnya secara berkala, timpa tabel Anda |
/warehouse/stock-movements | Tarik ulang jendela 7 hari terakhir, upsert berdasarkan id |
| Saldo poin pelanggan | Tidak mengubah updated_at member — poll /members/{id}/point-logs atau full refresh berkala |
Resep siap pakai
| Kebutuhan | Cara |
|---|---|
| Omzet harian per cabang | /sales, kelompokkan net_total per tanggal (setelah konversi ke WIB) dan branch.id; buang status = "CANCELED" |
| Arus kas per metode bayar | /sales/payments, kelompokkan amount per paid_at dan type; pisahkan ASSURANCE dan POINT karena bukan uang tunai |
| Piutang berjalan | /sales, saring amounts.outstanding lebih besar dari nol dan status bukan CANCELED |
| Klaim BPJS atau asuransi | /sales/payments?type=ASSURANCE, atau amounts.insurance_total di objek sale |
| Barang terlaris | /sales, kelompokkan items[].quantity per item_id |
| Nilai persediaan | /warehouse/stocks dikalikan sell_price dari /warehouse/items (HPP tidak tersedia di v1) |
| Kartu stok satu barang | /warehouse/stock-movements?item_id=..., urut occurred_at |
| Sinkron pelanggan ke CRM | /members dengan updated_since; normalkan telephone_number di sisi Anda |
Contoh: Google Apps Script ke Google Sheets
Simpan key lewat Project Settings → Script Properties dengan nama LENSIRO_KEY, jangan ditulis langsung di kode.
const BASE = 'https://app-anda.lensiro.com/api/v1';
function tarikPenjualan() {
const props = PropertiesService.getScriptProperties();
const key = props.getProperty('LENSIRO_KEY');
const since = props.getProperty('last_updated_at') || '2026-01-01T00:00:00Z';
const sheet = SpreadsheetApp.getActive().getSheetByName('Penjualan');
let cursor = null;
let maxUpdated = since;
const rows = [];
do {
let url = BASE + '/sales?include_deleted=true&limit=200'
+ '&updated_since=' + encodeURIComponent(since);
if (cursor) url += '&cursor=' + encodeURIComponent(cursor);
const res = UrlFetchApp.fetch(url, {
headers: { Authorization: 'Bearer ' + key },
muteHttpExceptions: true,
});
if (res.getResponseCode() === 429) {
Utilities.sleep(1000 * (Number(res.getHeaders()['Retry-After']) || 5));
continue;
}
if (res.getResponseCode() !== 200) {
throw new Error(res.getContentText());
}
const body = JSON.parse(res.getContentText());
body.data.forEach(function (s) {
if (s.updated_at > maxUpdated) maxUpdated = s.updated_at;
rows.push([
s.id,
s.sales_order_number,
s.invoice_number,
s.transaction_date,
s.branch.name,
s.status,
s.amounts.net_total,
s.amounts.paid_total,
s.deleted_at ? 'DIHAPUS' : '',
]);
});
cursor = body.pagination.next_cursor;
} while (cursor);
if (rows.length) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
.setValues(rows);
}
props.setProperty('last_updated_at', maxUpdated);
}
Contoh di atas menambahkan baris, belum meng-upsert. Untuk pemakaian sungguhan, cari dulu baris dengan
idyang sama lalu timpa — kalau tidak, transaksi yang berubah akan muncul dua kali di sheet Anda.
Jadwalkan lewat Triggers → Add Trigger → Time-driven → Hour timer.
Contoh: Python
import os
import time
import requests
BASE = "https://app-anda.lensiro.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LENSIRO_API_KEY']}"}
def tarik(path, **params):
"""Generator yang menghasilkan seluruh baris dari sebuah endpoint list."""
cursor = None
while True:
query = {**params, "limit": 200}
if cursor:
query["cursor"] = cursor
res = requests.get(BASE + path, headers=HEADERS, params=query, timeout=30)
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", 5)))
continue
res.raise_for_status()
body = res.json()
yield from body["data"]
if not body["pagination"]["has_more"]:
return
cursor = body["pagination"]["next_cursor"]
terbaru = "2026-08-01T00:00:00Z"
for sale in tarik("/sales", updated_since=terbaru, include_deleted="true"):
if sale["deleted_at"]:
hapus(sale["id"]) # ganti dengan penyimpanan Anda
else:
upsert(sale) # ganti dengan penyimpanan Anda
terbaru = max(terbaru, sale["updated_at"])
simpan_checkpoint(terbaru)
Contoh: Node.js
const BASE = 'https://app-anda.lensiro.com/api/v1';
const KEY = process.env.LENSIRO_API_KEY;
async function* tarik(path, params = {}) {
let cursor = null;
for (;;) {
const query = new URLSearchParams({ ...params, limit: '200' });
if (cursor) query.set('cursor', cursor);
const res = await fetch(BASE + path + '?' + query, {
headers: { Authorization: 'Bearer ' + KEY },
});
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') || 5);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) throw new Error(await res.text());
const body = await res.json();
yield* body.data;
if (!body.pagination.has_more) return;
cursor = body.pagination.next_cursor;
}
}
for await (const sale of tarik('/sales', { updated_since: '2026-08-01T00:00:00Z' })) {
console.log(sale.id, sale.sales_order_number, sale.amounts.net_total);
}
Memakai API ini dengan agent AI
API ini cocok dijadikan alat (tool) untuk agent AI internal — misalnya asisten yang bisa ditanyai "omzet cabang Bandung minggu lalu berapa?". Beberapa saran:
- Berikan halaman Referensi Endpoint sebagai konteks; strukturnya memang dibuat ringkas untuk dibaca mesin.
- Batasi agent hanya ke endpoint list dengan
limitkecil, supaya satu pertanyaan tidak menghabiskan kuota rate limit. - Ingatkan agent bahwa semua tanggal UTC dan harus dikonversi ke WIB sebelum dijawab ke pengguna.
- Karena key memberi akses baca penuh, jangan tempelkan key ke agent yang bisa diakses pelanggan.
Checklist produksi
- Key disimpan sebagai secret, dan khusus untuk integrasi ini saja.
- Loop paginasi berhenti berdasarkan
has_more. - Semua filter dibawa ulang di setiap halaman berikutnya.
- Penyimpanan melakukan upsert berdasarkan
id, termasukidberbentuk string untuk pergerakan stok. include_deleted=truedipakai, dandeleted_atditindaklanjuti.429ditangani denganRetry-After;500dicoba ulang dengan jeda bertingkat.- Checkpoint
updated_athanya disimpan setelah seluruh halaman selesai. - Tanggal dikonversi dari UTC ke WIB sebelum dikelompokkan per hari.
- Field JSON yang tidak dikenal diabaikan, bukan menyebabkan error.

