---
title: "Panduan Integrasi & Contoh Kode"
summary: "Resep polling yang benar (backfill, updated_since, upsert, tombstone) plus contoh kode siap pakai untuk Google Apps Script, Python, dan Node.js."
slug: developer-api-panduan-integrasi
product: LENSIRO
source: https://lensiro.com/dokumentasi/developer-api-panduan-integrasi
updated: 2026-08-23
---

# 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

```bash
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.

```javascript
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 `id` yang 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

```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

```javascript
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 `limit` kecil, 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`, termasuk `id` berbentuk string untuk pergerakan stok.
- `include_deleted=true` dipakai, dan `deleted_at` ditindaklanjuti.
- `429` ditangani dengan `Retry-After`; `500` dicoba ulang dengan jeda bertingkat.
- Checkpoint `updated_at` hanya disimpan setelah seluruh halaman selesai.
- Tanggal dikonversi dari UTC ke WIB sebelum dikelompokkan per hari.
- Field JSON yang tidak dikenal diabaikan, bukan menyebabkan error.


---

Dokumentasi Lensiro · https://lensiro.com/dokumentasi/developer-api-panduan-integrasi · diperbarui 23 Agustus 2026
