---
title: "API Penjualan (Sales)"
summary: "Endpoint /sales beserta item dan pembayaran yang menyatu, cara membaca blok amounts, ledger pemasukan /sales/payments, dan pekerjaan faset /sales/facets."
slug: developer-api-penjualan
product: LENSIRO
source: https://lensiro.com/dokumentasi/developer-api-penjualan
updated: 2026-08-23
---

# API Penjualan (Sales)

> Endpoint /sales beserta item dan pembayaran yang menyatu, cara membaca blok amounts, ledger pemasukan /sales/payments, dan pekerjaan faset /sales/facets.

Ini domain yang paling sering dipakai. Satu objek `sale` = satu transaksi penjualan, **sudah lengkap dengan item dan pembayarannya** — Anda tidak perlu request tambahan per transaksi.

## `GET /sales`

Parameter lengkap ada di **Referensi Endpoint**. Yang paling sering dipakai:

```bash
# semua perubahan sejak polling terakhir, termasuk penghapusan
curl -H "Authorization: Bearer $KEY" \
  "$BASE/sales?updated_since=2026-08-13T05:30:12.000Z&include_deleted=true&limit=200"

# backfill satu bulan untuk satu cabang
curl -H "Authorization: Bearer $KEY" \
  "$BASE/sales?transaction_date_from=2026-07-31T17:00:00Z&transaction_date_to=2026-08-31T16:59:59Z&branch_id=2"
```

### Contoh objek

```json
{
  "id": 18234,
  "branch": { "id": 2, "code": "PST", "name": "Cabang Pusat" },
  "sales_order_number": "SO-PST-1042",
  "invoice_number": "INV-PST-987",
  "is_point_of_sale": false,
  "transaction_date": "2026-08-13T02:11:40.000Z",
  "invoice_date": "2026-08-13T05:30:12.000Z",
  "status": "COMPLETED",
  "canceled_reason": null,
  "customer": { "id": 511, "name": "Budi Santoso" },
  "salesperson": { "id": 7, "name": "Dewi" },
  "memo": null,
  "amounts": {
    "sub_total": 1500000,
    "item_discount_total": 50000,
    "sale_discount": 100000,
    "total": 1350000,
    "insurance_total": 300000,
    "net_total": 1050000,
    "paid_total": 1050000,
    "outstanding": 0
  },
  "items": [
    {
      "id": 90101,
      "item_id": 3321,
      "name": "Frame CONTOH X-01 Black",
      "brand": "CONTOH",
      "category": "Frame",
      "quantity": 1,
      "unit_price": 1500000,
      "discount": 50000,
      "line_total": 1450000
    }
  ],
  "payments": [
    {
      "id": 44210,
      "type": "ASSURANCE",
      "amount": 300000,
      "paid_at": "2026-08-13T02:11:40.000Z",
      "bank": null,
      "insurance": { "id": 3, "name": "BPJS Kesehatan", "card_number": "00012345" }
    },
    {
      "id": 44211,
      "type": "QRIS",
      "amount": 1050000,
      "paid_at": "2026-08-13T05:30:12.000Z",
      "bank": { "id": 1, "name": "BCA" },
      "insurance": null
    }
  ],
  "created_at": "2026-08-13T02:11:40.000Z",
  "updated_at": "2026-08-13T05:30:12.000Z",
  "deleted_at": null
}
```

### Field header transaksi

| Field | Tipe | Arti |
|---|---|---|
| `id` | int | Kunci utama. **Pakai ini untuk upsert.** |
| `branch` | objek | Cabang tempat transaksi dibuat |
| `sales_order_number` | string | Format `SO-{kode cabang}-{urutan}`; menjadi `POS-...` untuk transaksi kasir langsung |
| `invoice_number` | string / null | Format `INV-{kode cabang}-{urutan}`; `null` selama belum difakturkan |
| `is_point_of_sale` | bool | `true` untuk transaksi kasir langsung (tanpa pesanan) |
| `transaction_date` | datetime | Tanggal order — dasar rekap penjualan |
| `invoice_date` | datetime / null | Tanggal faktur terbit |
| `status` | enum | `OPEN`, `COMPLETED`, atau `CANCELED` |
| `canceled_reason` | string / null | Alasan pembatalan |
| `customer` | objek / null | Pelanggan; `null` untuk transaksi tanpa member |
| `salesperson` | objek | Pengguna Lensiro yang membuat transaksi |
| `memo` | string / null | Catatan bebas dari kasir |
| `created_at` / `updated_at` | datetime | Waktu dibuat / terakhir diubah |
| `deleted_at` | datetime / null | Terisi berarti transaksi sudah dihapus |

Arti `status`:

| Nilai | Arti |
|---|---|
| `OPEN` | Sudah dibuat, belum difakturkan — pesanan masih berjalan |
| `COMPLETED` | Sudah difakturkan, `invoice_number` terisi |
| `CANCELED` | Dibatalkan; alasannya di `canceled_reason` |

### Field item

| Field | Arti |
|---|---|
| `id` | Id baris item, bukan id barang |
| `item_id` | Id barang di katalog — sambungkan ke `/warehouse/items` |
| `name`, `brand`, `category` | Nama, merek, dan kategori barang |
| `quantity` | Jumlah |
| `unit_price` | Harga satuan |
| `discount` | Diskon **untuk seluruh baris**, bukan per unit |
| `line_total` | `unit_price × quantity − discount` |

### Field pembayaran

| Field | Arti |
|---|---|
| `type` | `CASH`, `DEBIT`, `CREDIT`, `QRIS`, `ASSURANCE`, `POINT`, `REFUND` |
| `amount` | Nominal. **`REFUND` bernilai negatif**, jadi penjumlahan biasa sudah benar |
| `paid_at` | Waktu pembayaran diterima |
| `bank` | Bank untuk transaksi kartu/transfer, kalau ada |
| `insurance` | Penjamin untuk tipe `ASSURANCE`, berikut `card_number` |

## Membaca blok `amounts`

Semua angka sudah dihitung server memakai rumus yang sama persis dengan tampilan di aplikasi, jadi Anda tidak perlu menghitung ulang.

| Field | Rumus |
|---|---|
| `sub_total` | Jumlah `unit_price × quantity` seluruh item — sebelum diskon apa pun |
| `item_discount_total` | Jumlah `discount` seluruh item |
| `sale_discount` | Diskon tingkat transaksi, bukan per item |
| `total` | `sub_total − item_discount_total − sale_discount` |
| `insurance_total` | Jumlah pembayaran bertipe `ASSURANCE` — **inilah "potongan BPJS/asuransi"** |
| `net_total` | `total − insurance_total` — yang harus dibayar pelanggan sendiri |
| `paid_total` | Jumlah seluruh pembayaran dikurangi `insurance_total` |
| `outstanding` | `net_total − paid_total` — sisa tagihan |

Contoh angka dari objek di atas:

```
sub_total            1.500.000   (1 frame x Rp 1.500.000)
item_discount_total     50.000
sale_discount          100.000
                    -----------
total                1.350.000
insurance_total        300.000   (BPJS menanggung sebagian)
                    -----------
net_total            1.050.000   <- tagihan ke pelanggan
paid_total           1.050.000   (QRIS)
                    -----------
outstanding                  0   <- lunas
```

> **Untuk laporan omzet, pakai `net_total`** (pendapatan yang benar-benar ditagihkan ke pelanggan) atau `total` (nilai penjualan sebelum klaim asuransi) — pilih salah satu dan konsisten. `outstanding` lebih besar dari nol berarti piutang.

## Sub-resource satu transaksi

| Endpoint | Isi |
|---|---|
| `GET /sales/{id}` | Satu transaksi, bentuk persis sama dengan elemen list |
| `GET /sales/{id}/items` | Hanya array `items`, dibungkus `{ "data": [...] }` |
| `GET /sales/{id}/payments` | Hanya array `payments`, dibungkus `{ "data": [...] }` |
| `GET /sales/{id}/facets` | Pekerjaan faset transaksi itu, berpaginasi |

Semuanya membalas `404 not_found` kalau transaksi tidak ada atau sudah dihapus.

> Endpoint per-id ditujukan untuk **pencarian satu-satu**, misalnya menampilkan detail di layar Anda. Untuk tarik data massal, selalu pakai list `/sales` — item dan pembayaran sudah ikut di dalamnya, jadi menelusuri per id hanya memboroskan kuota rate limit.

## `GET /sales/payments` — ledger pemasukan

Satu baris per **pembayaran**, bukan per transaksi. Ini padanan langsung Laporan Pemasukan di aplikasi, dan bentuk yang paling nyaman untuk rekap arus kas: cukup jumlahkan per `type` per hari di sisi Anda.

```bash
curl -H "Authorization: Bearer $KEY" \
  "$BASE/sales/payments?paid_from=2026-08-01T17:00:00Z&type=QRIS"
```

```json
{
  "id": 44211,
  "sale_id": 18234,
  "sales_order_number": "SO-PST-1042",
  "invoice_number": "INV-PST-987",
  "branch": { "id": 2, "code": "PST", "name": "Cabang Pusat" },
  "type": "QRIS",
  "amount": 1050000,
  "paid_at": "2026-08-13T05:30:12.000Z",
  "cashier": { "id": 7, "name": "Dewi" },
  "bank": { "id": 1, "name": "BCA" },
  "insurance": null,
  "created_at": "2026-08-13T05:30:12.000Z",
  "updated_at": "2026-08-13T05:30:12.000Z",
  "deleted_at": null
}
```

Bedanya dengan `payments` yang tertanam di objek sale: di sini setiap baris membawa konteks transaksinya (nomor SO, faktur, cabang) dan kasir yang menerima, serta bisa langsung difilter per tanggal bayar dan per metode.

> **Uang masuk bukan omzet.** Satu transaksi bisa dibayar dicicil lintas hari, dan tanggal bayar bisa jauh berbeda dari tanggal order. Untuk laporan penjualan pakai `/sales`; untuk laporan kas pakai `/sales/payments`. Ingat juga bahwa pembayaran `ASSURANCE` bukan uang tunai yang masuk hari itu, dan `POINT` bukan uang sama sekali — pisahkan keduanya kalau Anda merekap kas.

## `GET /sales/facets` — pekerjaan faset (Fasset)

Cerminan laporan Fasset: pekerjaan pemasangan dan pemotongan lensa ke frame.

```bash
curl -H "Authorization: Bearer $KEY" "$BASE/sales/facets?status=FAILED"
```

```json
{
  "id": 5521,
  "sale_id": 18234,
  "sales_order_number": "SO-PST-1042",
  "invoice_number": "INV-PST-987",
  "branch": { "id": 2, "code": "PST", "name": "Cabang Pusat" },
  "status": "SUCCESS",
  "order_date": "2026-08-13T02:11:40.000Z",
  "worked_at": "2026-08-13T04:02:00.000Z",
  "customer_name": "Budi Santoso",
  "worker_name": "Agus",
  "memo": null,
  "created_at": "2026-08-13T04:02:00.000Z",
  "updated_at": "2026-08-13T06:00:00.000Z",
  "deleted_at": null
}
```

| Field | Arti |
|---|---|
| `status` | `WORKING` (dikerjakan), `SUCCESS` (berhasil), `FAILED` (gagal atau pecah) |
| `order_date` | Tanggal transaksi induk |
| `worked_at` | Tanggal pekerjaan dicatat |
| `worker_name` | Pengguna Lensiro yang mengerjakan |
| `memo` | Catatan; untuk `FAILED` biasanya berisi penyebab kegagalan |

Nilai komisi pekerjaan sengaja tidak diekspos karena termasuk data penggajian internal.

## Hal-hal yang sering menjebak

- **Nomor SO bukan kunci penyimpanan.** Untuk upsert pakai `id`; nomor SO dan faktur untuk ditampilkan ke manusia.
- `invoice_number` dan `invoice_date` bernilai `null` sampai transaksi difakturkan. Kalau rekap Anda memerlukan nomor faktur, saring `status = "COMPLETED"`.
- **Transaksi bisa berubah setelah pertama Anda lihat**: pembayaran menyusul, faktur terbit belakangan, item diperbaiki, atau transaksi dibatalkan. Selalu upsert.
- Diskon per item adalah nilai **seluruh baris**, bukan per unit.
- Data HPP / harga modal dan resep kacamata sengaja tidak ada di v1 — lihat **Batasan, Versi & Rencana**.


---

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