---
title: "Konvensi Respons & Paginasi"
summary: "Aturan yang berlaku di semua endpoint: bentuk envelope, snake_case, tanggal UTC, paginasi cursor, sinkronisasi updated_since, dan penanda data terhapus."
slug: developer-api-konvensi
product: LENSIRO
source: https://lensiro.com/dokumentasi/developer-api-konvensi
updated: 2026-08-23
---

# Konvensi Respons & Paginasi

> Aturan yang berlaku di semua endpoint: bentuk envelope, snake_case, tanggal UTC, paginasi cursor, sinkronisasi updated_since, dan penanda data terhapus.

Aturan di halaman ini berlaku untuk **semua** endpoint. Kalau Anda hanya sempat membaca satu halaman teknis, baca yang ini.

## Format dasar

- Semua nama field `snake_case` — `sales_order_number`, bukan `salesOrderNumber`.
- Semua tanggal/waktu **ISO 8601 UTC**, contoh `2026-08-13T03:42:08.000Z`. Selalu berakhiran `Z`.
- Semua nilai uang berupa **angka Rupiah bulat** (integer), tanpa desimal dan tanpa pemisah ribuan. `1350000` berarti Rp 1.350.000.
- Field yang kosong bernilai `null`, bukan string kosong.
- Semua respons JSON, dengan `Content-Type: application/json`.

## Tiga bentuk respons

| Bentuk | Dipakai oleh | Struktur |
|---|---|---|
| **List berpaginasi** | `/sales`, `/sales/payments`, `/sales/facets`, `/sales/{id}/facets`, `/members`, `/members/{id}/sales`, `/members/{id}/point-logs`, `/warehouse/items`, `/warehouse/stocks`, `/warehouse/stock-movements` | `{ "data": [...], "pagination": {...} }` |
| **Objek tunggal** | `/sales/{id}`, `/members/{id}`, `/warehouse/items/{id}` | `{ "data": { ... } }` |
| **List pendek tanpa paginasi** | `/branches`, `/sales/{id}/items`, `/sales/{id}/payments`, `/warehouse/items/{id}/stocks` | `{ "data": [...] }` |

Satu pengecualian: `/ping` membalas `{ "ok": true, "key_name": "..." }` tanpa pembungkus `data`.

Contoh list berpaginasi:

```json
{
  "data": [ { "id": 18234, "...": "..." } ],
  "pagination": {
    "next_cursor": "eyJ1IjoiMjAyNi0wOC0xM1QwNTozMDoxMi4wMDBaIiwiaSI6MTgyMzR9",
    "has_more": true
  }
}
```

## Paginasi dengan cursor

- `limit` — jumlah baris per halaman. Default **100**, maksimal **500**, harus bilangan bulat minimal 1.
- `cursor` — penanda posisi, diambil dari `pagination.next_cursor` halaman sebelumnya.

Polanya:

```
halaman 1: GET /sales?limit=200
  → has_more = true, next_cursor = "AAA"
halaman 2: GET /sales?limit=200&cursor=AAA
  → has_more = true, next_cursor = "BBB"
halaman 3: GET /sales?limit=200&cursor=BBB
  → has_more = false, next_cursor = null   ← berhenti di sini
```

Aturan penting:

- **Bawa serta semua filter di setiap halaman.** Cursor hanya menyimpan posisi, bukan filter. Kalau halaman 1 memakai `updated_since`, halaman 2 harus memakainya juga.
- **Cursor bersifat opaque** — jangan diurai atau dibuat sendiri, cukup diteruskan kembali. Isinya bisa berubah sewaktu-waktu.
- **Tidak ada paginasi offset** (`page=` / `offset=`). Cursor aman terhadap data yang bertambah di tengah proses tarik.
- Cursor tidak punya masa kedaluwarsa, tapi tidak dimaksudkan disimpan lama. Untuk melanjutkan di lain waktu, simpan `updated_at` terbesar dan mulai lagi dengan `updated_since`.

### Urutan data

| Endpoint | Urutan |
|---|---|
| Sebagian besar list | `updated_at` menaik, lalu `id` menaik |
| `/warehouse/stocks` | `item_id` menaik, lalu `branch_id` menaik |
| `/warehouse/stock-movements` | `occurred_at` menaik, lalu `type`, lalu id sumber |

Karena `updated_at` hanya bergerak maju, baris yang berubah di tengah proses paginasi akan **pindah ke belakang** — jadi Anda mungkin menerimanya dua kali, tapi tidak akan pernah terlewat. Selama penyimpanan Anda melakukan upsert, ini tidak jadi masalah.

## Sinkronisasi inkremental: `updated_since`

Hampir semua list mendukung `updated_since=<ISO datetime>`: hanya kirim baris yang berubah sejak waktu tersebut.

1. Simpan nilai `updated_at` **terbesar** yang pernah Anda terima.
2. Pada polling berikutnya, kirim nilai itu sebagai `updated_since`.
3. **Upsert** hasilnya berdasarkan `id` — jangan *insert* saja.

```
GET /sales?updated_since=2026-08-13T05:30:12.000Z&include_deleted=true&limit=200
```

Catatan penting:

- Filternya **inklusif** (`>=`). Baris tepat di batas waktu akan terkirim ulang setiap polling. Ini disengaja supaya tidak ada yang terlewat — upsert membuatnya tidak berbahaya.
- Pola ini otomatis menangkap **perubahan pada transaksi lama**: pembayaran yang menyusul, faktur yang terbit belakangan, atau pembatalan. Filter berdasarkan tanggal transaksi saja akan melewatkan semua itu.
- Endpoint yang **tidak** mendukung `updated_since`: `/warehouse/stocks` dan `/warehouse/stock-movements`. Alasannya dijelaskan di halaman **API Gudang**.

## Data terhapus (tombstone)

- Setiap objek yang bisa dihapus punya field `deleted_at` — `null` untuk data aktif.
- Secara default data terhapus **tidak** ikut. Tambahkan `include_deleted=true` untuk menerimanya.
- Penghapusan tetap mengubah `updated_at`, jadi kombinasi `updated_since` + `include_deleted=true` mengantarkan penghapusan lewat loop polling biasa.

> **Kalau Anda menyimpan salinan data, selalu poll dengan `include_deleted=true`.** Tanpa itu, transaksi yang dihapus berhenti terkirim tanpa pemberitahuan, dan salinan Anda diam-diam jadi lebih besar dari kenyataan. Baris dengan `deleted_at` terisi artinya: hapus, atau tandai batal, di sisi Anda.

## Transaksi yang dibatalkan

Berbeda dengan penghapusan: transaksi batal **tetap ada** dan ikut terkirim secara default (`include_canceled=true`), dengan `status: "CANCELED"` dan alasan di `canceled_reason`. Ini disengaja supaya pembatalan terlihat oleh sistem Anda. Kirim `include_canceled=false` kalau Anda hanya mau transaksi yang masih berlaku.

## Zona waktu

Semua tanggal keluar dalam UTC. WIB = UTC + 7 jam. Jadi "1 Agustus 2026 waktu Indonesia bagian barat" berarti:

```
transaction_date_from=2026-07-31T17:00:00Z
transaction_date_to=2026-08-01T16:59:59Z
```

Ini penyebab paling sering angka rekap Anda meleset dibanding laporan di aplikasi, yang memakai waktu lokal. Untuk WITA geser 8 jam, WIT 9 jam.

## Tipe dan validasi parameter

| Jenis | Contoh | Aturan | Error kalau salah |
|---|---|---|---|
| Bilangan bulat | `branch_id`, `item_id`, `category_id`, `brand_id` | bulat, minimal 0 | `400 invalid_parameter` |
| `limit` | `limit=200` | bulat, 1–500 | `400 invalid_limit` |
| Tanggal | `updated_since`, `paid_from` | ISO 8601, sebaiknya berakhiran `Z` | `400 invalid_date` |
| Boolean | `include_deleted` | aktif **hanya** bila persis `true` | — (nilai lain dianggap `false`) |
| `include_canceled` | `include_canceled=false` | nonaktif **hanya** bila persis `false` | — (default `true`) |
| Enum | `type`, `status` | harus salah satu nilai yang diizinkan | `400 invalid_parameter` |
| `cursor` | dari `next_cursor` | opaque | `400 invalid_cursor` |
| `{id}` di path | `/sales/18234` | bulat, minimal 1 | `400 invalid_id` |

Parameter yang tidak dikenal **diabaikan diam-diam** — salah ketik nama parameter tidak menimbulkan error, tapi juga tidak menyaring apa pun. Periksa lagi kalau hasil filter terasa aneh.

## Janji kompatibilitas

Selama masih **v1**:

- Field baru **bisa muncul kapan saja tanpa pemberitahuan.** Parser Anda harus mengabaikan field yang tidak dikenal.
- Field yang sudah ada **tidak akan** diganti nama, dihapus, atau berubah tipe.
- Perubahan yang merusak kompatibilitas akan terbit sebagai `/api/v2`, dan `/api/v1` tetap jalan.

## Ringkasan kode status

| HTTP | Arti |
|---|---|
| 200 | Berhasil |
| 400 | Parameter tidak valid |
| 401 | Masalah API key |
| 404 | Objek tidak ditemukan atau sudah dihapus |
| 405 | Metode selain `GET` |
| 429 | Melebihi batas pemakaian |
| 500 | Kesalahan server — coba ulang dengan jeda bertingkat |

Rincian setiap `error.code` ada di halaman **Kode Error & Pemecahan Masalah**.


---

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