Version 1Paket Enterprise

API Integrasi Cucimoo

REST API berformat JSON untuk menyambungkan data Cucimoo dengan sistem yang sudah Anda punya — akuntansi, gudang, dasbor internal, atau apa pun yang berjalan otomatis tanpa seseorang menekan tombol. Ini HTTP biasa, jadi bisa dipanggil dari bahasa dan platform apa pun.

Perlu langganan Enterprise
API Key hanya bisa dibuat oleh bisnis dengan paket Enterprise, lewat Pengaturan → API Key di aplikasi. Memakai key pada paket lain menghasilkan 402 PLAN_REQUIRED.
Base URLhttps://cucimoo.id/api/v1
Content typeapplication/json untuk request dan response. Tidak menerima kiriman formulir.
AmountsInteger dalam satuan mata uang terkecil — rupiah, jadi 99000.
TimestampsISO 8601 dalam UTC. Menampilkannya dalam jam cabang adalah tugas klien.
OpenAPI /api/v1/openapi.json — dibuat dari katalog yang sama dengan halaman ini, jadi keduanya tidak mungkin berbeda. Pakai untuk membuat client di bahasa Anda sendiri.
Catatan soal bahasa
Nama field, nilai enum, path endpoint dan error code ditulis dalam bahasa Inggris karena itulah yang benar-benar dikirim API. Isi `message` pada error ditulis dalam bahasa Indonesia dan aman ditampilkan apa adanya kepada pengguna akhir — itu memang disengaja.

Authentication

Integrasi memakai API Key — kredensial panjang umur yang tidak menumpang akun siapa pun. Kirim pada setiap request:

X-Api-Key: ck_live_…
  • Key dibuat pemilik bisnis di Pengaturan → API Key, dan hanya ditampilkan satu kali saat dibuat. Kami menyimpan sidik jarinya, bukan key-nya — key yang hilang tidak bisa dilihat lagi, hanya bisa dicabut dan dibuat ulang.
  • Tidak ada masa berlaku. Key hidup sampai dicabut — itulah gunanya untuk sistem yang berjalan sendiri.
  • Key bertindak setara pemilik bisnis dan menjangkau seluruh cabang. Simpan seperti kata sandi: di variabel lingkungan atau brankas rahasia, tidak pernah di dalam kode aplikasi atau repositori.
  • Setiap key punya izin sendiri. Key Baca data hanya boleh GET; POST, PATCH, PUT dan DELETE dijawab 403 FORBIDDEN. Untuk menulis, terbitkan key dengan izin Ubah data. Kalau integrasi Anda hanya menarik laporan, pakai key baca saja — kalau bocor, kerusakannya terbatas.
  • Prefiksnya menyebut lingkungan: ck_live_ untuk produksi, ck_test_ selain itu.
  • 401 UNAUTHENTICATED berarti key salah atau sudah dicabut. 402 PLAN_REQUIRED berarti paketnya bukan Enterprise lagi.
Memastikan key bekerja
curl https://cucimoo.id/api/v1/me \
  -H 'X-Api-Key: ck_live_xxxxxxxxxxxxxxxx'

Response envelope

Semua response punya bentuk yang sama, jadi klien cukup membukanya sekali lalu tidak perlu memikirkannya lagi.

Single resource

{
  "data": {
    "id": "clx…",
    "name": "Cabang Margonda"
  }
}

List

{
  "data": [
    "…"
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 137,
    "pageCount": 7
  }
}

Error

{
  "error": {
    "code": "LIMIT_REACHED",
    "message": "Langganan Free hanya mengizinkan 1 cabang. Upgrade untuk menambah lagi.",
    "status": 402
  }
}

Error codes

Bercabanglah pada error.code; tampilkan error.message kepada pengguna.

CodeHTTPArtinya
UNAUTHENTICATED401API Key tidak dikirim, salah, atau sudah dicabut. Periksa header `X-Api-Key`; key yang dicabut tidak bisa dipulihkan, harus dibuat baru.
FORBIDDEN403Key-nya sah, tetapi tindakan itu tidak diizinkan untuknya.
TENANT_SUSPENDED403Akun bisnis sedang ditangguhkan. Hubungi dukungan.
TENANT_DELETED403Akun bisnis sudah dihapus dan tidak dapat dipakai lagi. Tidak ada tindakan yang bisa memulihkannya dari sisi klien.
NOT_FOUND404Data tidak ada, atau milik tenant lain.
VALIDATION_FAILED400Request body tidak lolos validasi. `details.field` menyebut field yang bermasalah.
PLAN_REQUIRED402Paket bisnis ini belum mencakup fitur tersebut. Kalau ini muncul tiba-tiba pada integrasi yang tadinya jalan, langganan Enterprise-nya kemungkinan sudah berakhir.
LIMIT_REACHED402Batas angka pada paket sudah habis — misalnya jumlah cabang. `details.max` menyebut batasnya.
CONFLICT409Request bertentangan dengan kondisi saat ini (duplikat, perpindahan status yang tidak diizinkan).
UPGRADE_REQUIRED426Versi aplikasi mobile di bawah `minimumMobileVersion`. Hanya dikirim ke request yang membawa header `x-cucimoo-client`; tampilkan layar wajib perbarui. `details.minimumVersion` menyebut versi minimumnya.
RATE_LIMITED429Terlalu banyak percobaan. `details.retryAfterSeconds` menyebut berapa lama harus menunggu.
SERVER_ERROR500Kegagalan tak terduga di sisi kami. Aman untuk dicoba lagi dengan jeda.
402 bukan 403
402 berarti paket tenant tidak mengizinkannya — tawarkan peningkatan paket. 403 berarti peran ini memang tidak boleh melakukannya — meningkatkan paket tidak akan menolong.

Pagination & caching

Endpoint yang mengembalikan daftar menerima ?page, ?limit, ?search, ?branchId, ?dateFrom dan ?dateTo. Nilai bawaan limit adalah 20, dengan batas maksimal 100.

Endpoint bergaya katalog (cabang, staf, layanan, paket, pengaturan umum) mengirim ETag. Kirim balik sebagai If-None-Match dan Anda menerima 304 tanpa body — sangat berguna di koneksi seluler, dan cara termurah menyinkronkan data yang jarang berubah.

Rate limits

Belum ada batas angka yang diumumkan untuk permintaan memakai API Key. Itu bukan izin memanggil sesering mungkin: tariklah secara berkala, pakai If-None-Match untuk data yang jarang berubah, dan saring dengan ?dateFrom alih-alih mengambil ulang seluruh riwayat. Kalau batas nanti diberlakukan, kami memberi tahu lebih dulu.

Melewati limit menghasilkan 429 RATE_LIMITED beserta details.retryAfterSeconds.

Identitas key

Bisnis mana yang diwakili key ini, dan apa yang diizinkan paketnya.

GET/api/v1/me API key

Tenant, hak paket dan daftar cabang milik key ini

Request paling murah untuk memastikan sebuah key masih berlaku dan mengetahui bisnis mana yang diwakilinya — berguna sebagai pemeriksaan kesehatan sebelum sinkronisasi berjalan. `tenant.timezone` adalah jam tampilan bawaan bisnis ini; kalau data yang Anda olah terikat satu cabang, pakai `timezone` cabang tersebut. Semua timestamp tetap dikirim dalam UTC.

Response example

{
  "data": {
    "user": {
      "id": "clx…",
      "name": "Budi Santoso",
      "email": "budi@example.com",
      "phone": "628121111111",
      "avatarUrl": null
    },
    "tenant": {
      "id": "clx…",
      "name": "Cuci Kilat Depok",
      "slug": "cuci-kilat-depok",
      "category": "both",
      "status": "active",
      "timezone": "Asia/Jakarta",
      "onboardingCompletedAt": "2026-02-01T04:12:00.000Z"
    },
    "membership": {
      "role": "owner",
      "abilities": [
        "billing.manage",
        "branches.manage",
        "staff.manage"
      ]
    },
    "entitlements": {
      "planSlug": "premium",
      "planName": "Premium",
      "status": "trialing",
      "trialDaysRemaining": 11,
      "limits": {
        "branches": 5,
        "staffTagsPerTransaction": -1,
        "customers": -1,
        "services": -1,
        "reportRetentionDays": 365
      },
      "features": {
        "export": true,
        "multiBranchRollup": true,
        "apiAccess": false,
        "prioritySupport": false
      }
    },
    "branches": [
      {
        "id": "clx…",
        "name": "Cabang Margonda",
        "timezone": "Asia/Jakarta",
        "isActive": true,
        "isPrimary": true
      }
    ]
  }
}
Kemungkinan error:UNAUTHENTICATED

Cabang

Outlet fisik milik bisnis. Membuat cabang melebihi batas paket menghasilkan 402 `LIMIT_REACHED`.

GET/api/v1/branches API key

Daftar cabang

Parameters

  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.
  • searchquery · stringDicocokkan dengan nama cabang.

Response example

{
  "data": [
    {
      "id": "clx…",
      "name": "Cabang Margonda",
      "code": "K7M2QD",
      "address": "Jl. Margonda Raya No. 12, Depok",
      "phone": "628121111111",
      "timezone": "Asia/Jakarta",
      "taxRate": 11,
      "isActive": true,
      "isPrimary": true,
      "staffCount": 3
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "pageCount": 1
  }
}
POST/api/v1/branches API key

Menambah cabang

Menghasilkan 402 `LIMIT_REACHED` kalau jatah cabang aktif pada paket sudah habis.

Request body

  • namestringrequiredNama cabang, minimal 2 karakter.
  • addressstringAlamat.
  • phonestringNomor telepon cabang.
  • timezonestring`Asia/Jakarta` | `Asia/Makassar` | `Asia/Jayapura`.
  • receiptFooterstringKalimat penutup yang dicetak di bawah struk cabang ini. Kepala struk memakai nama, alamat dan telepon cabang.
  • taxRatenumberPersen pajak yang dikenakan pada setiap transaksi di cabang ini, 0–100. Dihitung dari subtotal setelah diskon; 0 (bawaan) berarti tanpa pajak.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Cabang Kelapa Dua",
    "timezone": "Asia/Jakarta",
    "taxRate": 0,
    "isActive": true,
    "isPrimary": false
  }
}
Kemungkinan error:VALIDATION_FAILEDLIMIT_REACHEDFORBIDDEN
GET/api/v1/branches/{id} API key

Mengambil satu cabang

Parameters

  • idpath · stringrequiredId cabang.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Cabang Margonda",
    "code": "K7M2QD",
    "timezone": "Asia/Jakarta",
    "taxRate": 11,
    "isActive": true,
    "isPrimary": true,
    "staffCount": 3
  }
}
Kemungkinan error:NOT_FOUND
PATCH/api/v1/branches/{id} API key

Memperbarui cabang

Parameters

  • idpath · stringrequiredId cabang.

Request body

  • namestringNama cabang.
  • addressstringAlamat.
  • phonestringNomor telepon cabang.
  • timezonestringZona waktu cabang.
  • receiptFooterstringKalimat penutup yang dicetak di bawah struk cabang ini.
  • taxRatenumberPersen pajak yang dikenakan pada setiap transaksi di cabang ini, 0–100. Dihitung dari subtotal setelah diskon; 0 (bawaan) berarti tanpa pajak. Mengubahnya tidak pernah mengubah transaksi yang sudah tercatat.
  • isActivebooleanMengaktifkan kembali tetap tunduk pada batas paket.
  • isPrimarybooleanMenjadikan cabang ini pilihan utama.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Cabang Margonda Baru",
    "isActive": true
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILEDLIMIT_REACHED
DELETE/api/v1/branches/{id} API key

Menghapus cabang

Cabang terakhir yang tersisa tidak bisa dihapus. Daftar layanan cabang ikut terhapus bersamanya.

Parameters

  • idpath · stringrequiredId cabang.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:NOT_FOUNDCONFLICT

Tag staf

Nama orang-orang yang mengerjakan cucian. Ini label, bukan akun — staf tidak pernah masuk ke aplikasi.

GET/api/v1/branches/{id}/staff API key

Daftar tag staf di satu cabang

Parameters

  • idpath · stringrequiredId cabang.

Response example

{
  "data": [
    {
      "id": "clx…",
      "name": "Agus",
      "phone": null,
      "isActive": true,
      "branchId": "clx…"
    }
  ]
}
Kemungkinan error:NOT_FOUND
GET/api/v1/staff API key

Daftar staf seluruh bisnis

Nama staf lintas cabang, atau satu cabang saja dengan `branchId`. Jalur per-cabang di `/branches/{id}/staff` tetap ada; endpoint ini untuk layar yang menyaring transaksi berdasarkan siapa yang mengerjakan, yang tidak terbatas pada satu outlet.

Parameters

  • branchIdquery · stringBatasi ke satu cabang.
  • searchquery · stringCari berdasarkan nama.
  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.

Response example

{
  "data": [
    {
      "id": "clx…",
      "name": "Agus",
      "phone": null,
      "isActive": true,
      "branchId": "clx…"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 5,
    "pageCount": 1
  }
}
POST/api/v1/branches/{id}/staff API key

Menambah tag staf

Parameters

  • idpath · stringrequiredId cabang.

Request body

  • namestringrequiredNama staf, minimal 2 karakter.
  • phonestringNomor HP, boleh dikosongkan.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Rian",
    "isActive": true,
    "branchId": "clx…"
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILEDFORBIDDEN
PATCH/api/v1/staff/{id} API key

Memperbarui tag staf

Parameters

  • idpath · stringrequiredId tag staf.

Request body

  • namestringNama staf.
  • phonestringNomor HP.
  • isActivebooleanStaf nonaktif tidak lagi muncul saat menandai pekerjaan.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Rian Saputra",
    "isActive": true
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILED
DELETE/api/v1/staff/{id} API key

Menghapus tag staf

Parameters

  • idpath · stringrequiredId tag staf.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:NOT_FOUND

Daftar layanan

Apa yang dijual sebuah cabang dan berapa harganya. Satu layanan milik satu cabang: dua outlet yang sama-sama menjual cuci mobil kecil punya barisnya masing-masing, sehingga harga tiap cabang berdiri sendiri. Batas paket `services` menghitung seluruh bisnis, bukan per cabang. Semua nilai uang berupa bilangan bulat rupiah.

GET/api/v1/services API key

Daftar layanan

Isi `branchId` untuk mengambil daftar layanan satu cabang saja. Tanpa `branchId`, response-nya mencakup layanan seluruh cabang — setiap baris menyebut cabangnya sendiri lewat `branchId`. Tanpa `page` maupun `limit`, endpoint ini mengembalikan **seluruh** katalog dalam satu `data` tanpa `meta`, seperti sejak awal. Mengirim salah satunya membuat response-nya menjadi satu halaman lengkap dengan `meta`.

Parameters

  • branchIdquery · stringHanya layanan milik cabang ini.
  • searchquery · stringDicocokkan dengan nama layanan.
  • vehicleTypequery · string`car` | `motorcycle` | `truck` | `bus` | `both` | `addon`.
  • isActivequery · booleanHanya yang aktif, atau hanya yang nonaktif.
  • pagequery · integerNomor halaman, mulai dari 1. Mengaktifkan paging.
  • limitquery · integerJumlah per halaman, maksimal 100. Mengaktifkan paging.

Response example

{
  "data": [
    {
      "id": "clx…",
      "branchId": "clx…",
      "name": "Cuci mobil kecil (sedan/hatchback)",
      "description": null,
      "vehicleType": "car",
      "price": 40000,
      "durationMinutes": 30,
      "color": "sky",
      "isActive": true,
      "order": 0
    }
  ]
}
POST/api/v1/services API key

Menambah layanan ke sebuah cabang

Menghasilkan 402 `LIMIT_REACHED` kalau jatah layanan pada paket sudah habis. Jatah itu dihitung untuk seluruh bisnis, jadi layanan yang sama di tiga cabang memakai tiga jatah.

Request body

  • branchIdstringrequiredCabang pemilik layanan ini.
  • namestringrequiredNama layanan, minimal 2 karakter.
  • priceintegerrequiredHarga di cabang tersebut, dalam rupiah.
  • vehicleTypestring`car` | `motorcycle` | `truck` | `bus` | `both` (bawaan) | `addon`.
  • descriptionstringKeterangan singkat di bawah nama.
  • durationMinutesintegerPerkiraan waktu pengerjaan.
  • colorstringWarna kartu di layar kasir: `sky` | `teal` | `emerald` | `lime` | `amber` | `orange` | `rose` | `violet` | `slate`, atau null untuk tanpa warna.
  • isActivebooleanLayanan nonaktif tidak muncul di layar kasir.

Response example

{
  "data": {
    "id": "clx…",
    "branchId": "clx…",
    "name": "Poles bodi",
    "vehicleType": "addon",
    "price": 75000,
    "isActive": true
  }
}
Kemungkinan error:VALIDATION_FAILEDLIMIT_REACHEDNOT_FOUNDFORBIDDEN
PATCH/api/v1/services/{id} API key

Memperbarui layanan

Mengirim `branchId` memindahkan layanan ke cabang lain; harga dan urutannya ikut pindah.

Parameters

  • idpath · stringrequiredId layanan.

Request body

  • branchIdstringPindahkan layanan ini ke cabang lain.
  • namestringNama layanan.
  • priceintegerHarga di cabang tersebut, dalam rupiah.
  • vehicleTypestringJenis kendaraan yang dilayani.
  • durationMinutesintegerPerkiraan waktu, atau null untuk mengosongkan.
  • colorstringWarna kartu di layar kasir, atau null untuk mengosongkan.
  • isActivebooleanMenyembunyikan layanan dari layar kasir.

Response example

{
  "data": {
    "id": "clx…",
    "branchId": "clx…",
    "name": "Poles bodi premium",
    "price": 95000
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILED
DELETE/api/v1/services/{id} API key

Menghapus layanan

Menghasilkan 409 `CONFLICT` begitu layanan pernah terjual — nonaktifkan saja, supaya laporan lama tetap bisa dibaca.

Parameters

  • idpath · stringrequiredId layanan.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:NOT_FOUNDCONFLICT

Pelanggan & kendaraan

Catatan ringan untuk mengenali pelanggan yang kembali. Selalu opsional — mencatat transaksi tidak pernah mewajibkannya. Nama, nomor HP dan nomor pelat termasuk data pribadi menurut UU PDP; perlakukan sebagaimana mestinya.

GET/api/v1/customers API key

Daftar pelanggan

`search` dicocokkan dengan nama, nomor HP, atau nomor pelat yang tersimpan.

Parameters

  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.
  • searchquery · stringNama, nomor HP atau pelat.
  • sortquery · string`recent` (bawaan), `name` atau `spend`.

Response example

{
  "data": [
    {
      "id": "clx…",
      "name": "Ibu Sari",
      "phone": "628121234567",
      "notes": null,
      "totalVisits": 6,
      "totalSpend": 240000,
      "loyaltyBalance": 7,
      "lastVisitAt": "2026-08-11T02:14:00.000Z",
      "vehicles": [
        {
          "id": "clx…",
          "type": "car",
          "subType": "MPV",
          "plate": "B 1234 XYZ",
          "color": "Putih",
          "note": null
        }
      ]
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "pageCount": 1
  }
}
POST/api/v1/customers API key

Menambah pelanggan

Kendaraan boleh dibuat sekalian dalam satu request yang sama — begitulah layar kasir menyimpan pelanggan dadakan. Menghasilkan 402 `LIMIT_REACHED` saat batas paket tercapai.

Request body

  • namestringrequiredNama pelanggan, minimal 2 karakter.
  • phonestringNomor HP Indonesia; disimpan dalam bentuk `62…`.
  • notesstringCatatan bebas.
  • vehicleobject`{ type, subType?, plate?, color?, note? }` — sekalian membuat satu kendaraan.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Ibu Sari",
    "phone": "628121234567",
    "totalVisits": 0,
    "totalSpend": 0,
    "vehicles": []
  }
}
Kemungkinan error:VALIDATION_FAILEDLIMIT_REACHEDFORBIDDEN
GET/api/v1/customers/{id} API key

Mengambil satu pelanggan

Parameters

  • idpath · stringrequiredId pelanggan.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Ibu Sari",
    "totalVisits": 6,
    "totalSpend": 240000,
    "vehicles": []
  }
}
Kemungkinan error:NOT_FOUND
GET/api/v1/customers/{id}/transactions API key

Riwayat cuci satu pelanggan

Parameters

  • idpath · stringrequiredId pelanggan.
  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.

Response example

{
  "data": [
    {
      "id": "clx…",
      "code": "TRX-260812-0014",
      "total": 25000,
      "status": "recorded",
      "occurredAt": "2026-08-12T04:20:00.000Z",
      "branchName": "Cabang Margonda",
      "services": [
        "Cuci mobil kecil"
      ]
    }
  ],
  "meta": {
    "page": 1,
    "limit": 10,
    "total": 6,
    "pageCount": 1
  }
}
Kemungkinan error:NOT_FOUND
PATCH/api/v1/customers/{id} API key

Memperbarui pelanggan

Parameters

  • idpath · stringrequiredId pelanggan.

Request body

  • namestringNama pelanggan.
  • phonestringNomor HP, atau null untuk mengosongkan.
  • notesstringCatatan bebas.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Ibu Sari Wulandari"
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILED
DELETE/api/v1/customers/{id} API key

Menghapus pelanggan

Penghapusan data pribadi sesuai UU PDP. Transaksinya tetap ada dengan tautan pelanggan dikosongkan — pemasukannya memang benar-benar terjadi.

Parameters

  • idpath · stringrequiredId pelanggan.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:NOT_FOUND
POST/api/v1/customers/{id}/vehicles API key

Menambah kendaraan milik pelanggan

Parameters

  • idpath · stringrequiredId pelanggan.

Request body

  • typestringrequired`car` | `motorcycle` | `truck` | `bus`.
  • subTypestringSedan, MPV, SUV, Matic, …
  • platestringNomor pelat; disimpan dalam huruf kapital.
  • colorstringWarna kendaraan.
  • notestringCatatan bebas.

Response example

{
  "data": {
    "id": "clx…",
    "type": "car",
    "subType": "MPV",
    "plate": "B 1234 XYZ"
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILED
PATCH/api/v1/vehicles/{id} API key

Memperbarui kendaraan

Parameters

  • idpath · stringrequiredId kendaraan.

Request body

  • typestring`car` | `motorcycle` | `truck` | `bus`.
  • subTypestringSub-jenis kendaraan.
  • platestringNomor pelat.
  • colorstringWarna kendaraan.

Response example

{
  "data": {
    "id": "clx…",
    "plate": "B 4321 ZYX"
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILED
DELETE/api/v1/vehicles/{id} API key

Menghapus kendaraan

Parameters

  • idpath · stringrequiredId kendaraan.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:NOT_FOUND

Loyalti

Satu programme per bisnis, aktif atau tidak. Kartu stempel dan kartu poin adalah mesin yang sama dengan satu tuas digeser (`accrualBasis`), jadi satu set endpoint melayani keduanya. Menukarkan hadiah terjadi di penjualan lewat `redeemLoyalty` pada `POST /transactions`, bukan di sini.

GET/api/v1/loyalty API key

Mengambil programme loyalti

`allowed` menyebut apakah paket saat ini mengizinkan loyalti. Paket yang tidak mengizinkan membekukan programme — tidak ada perolehan dan penukaran baru — tetapi saldo pelanggan tetap disimpan utuh.

Response example

{
  "data": {
    "program": {
      "id": "clx…",
      "name": "Kartu Cuci Gratis",
      "type": "stamp",
      "accrualRate": null,
      "minSpend": 0,
      "balanceExpiryDays": null,
      "crossBranch": true,
      "isActive": true,
      "activatedAt": "2026-07-01T02:00:00.000Z",
      "backfillMonths": 3,
      "serviceIds": [],
      "rule": {
        "id": "clx…",
        "name": "Gratis 1x Cuci",
        "threshold": 10,
        "rewardType": "free_service",
        "rewardServiceId": "clx…",
        "rewardPercent": null,
        "maxDiscount": null
      }
    },
    "allowed": true,
    "memberCount": 128,
    "backfillDefault": 3
  }
}
PUT/api/v1/loyalty API key

Menyimpan pengaturan programme

Mengganti `type` membekukan saldo lama, tidak menghapusnya — pelanggan hanya bisa membawa satu saldo, jadi hanya satu programme yang boleh aktif per bisnis.

Request body

  • namestringrequiredNama programme yang dilihat pelanggan.
  • typestringrequired`stamp` (per transaksi) atau `points` (per rupiah).
  • accrualRatenumberPoin per rupiah, untuk tipe `points`.
  • minSpendintegerBelanja minimal agar satu transaksi menghasilkan perolehan.
  • balanceExpiryDaysintegerUmur saldo dalam hari. Kosong berarti tidak expired.
  • crossBranchbooleanSaldo berlaku di semua cabang.
  • serviceIdsstring[]Batasi perolehan ke layanan tertentu. Kosong berarti semua layanan.
  • ruleobjectrequiredAmbang dan hadiahnya: `{ name, threshold, rewardType, rewardServiceId, rewardPercent, maxDiscount }`.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Kartu Cuci Gratis",
    "type": "stamp",
    "isActive": true
  }
}
Kemungkinan error:VALIDATION_FAILEDPLAN_REQUIREDCONFLICT
POST/api/v1/loyalty/activate API key

Menyalakan atau mematikan programme

Terpisah dari `PUT /loyalty` karena menyalakan mesin adalah tindakan yang berbeda dari menggeser tuasnya — dan karena penyalaan pertama menjalankan backfill: pelanggan diberi apa yang seharusnya sudah mereka kumpulkan, bawaannya tiga bulan ke belakang. Backfill hanya berjalan sekali.

Request body

  • isActivebooleanrequiredTrue untuk menyalakan.
  • backfillMonthsintegerBerapa bulan transaksi lama ikut dihitung saat penyalaan pertama.

Response example

{
  "data": {
    "isActive": true,
    "backfilled": 128
  }
}
Kemungkinan error:VALIDATION_FAILEDPLAN_REQUIREDNOT_FOUND
GET/api/v1/loyalty/members API key

Pelanggan yang punya saldo

Parameters

  • searchquery · stringCari berdasarkan nama atau nomor HP.
  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.

Response example

{
  "data": [
    {
      "id": "clx…",
      "name": "Ibu Sari",
      "phone": "628121234567",
      "balance": 7,
      "lastVisitAt": "2026-08-18T04:20:00.000Z",
      "state": {
        "goal": 10,
        "remaining": 3
      }
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 128,
    "pageCount": 7
  }
}
POST/api/v1/loyalty/adjust API key

Mengoreksi saldo satu pelanggan

Ditulis sebagai baris ledger baru, sama seperti setiap pergerakan lain — saldo tidak pernah diubah langsung di tempat, supaya buku besarnya tetap bisa dibaca mundur.

Request body

  • customerIdstringrequiredPelanggan yang saldonya dikoreksi.
  • amountintegerrequiredJumlah pergerakan. Negatif untuk mengurangi.
  • reasonstringAlasan koreksi, tersimpan di ledger.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:VALIDATION_FAILEDNOT_FOUND

Transaksi

Mencatat satu pekerjaan cuci. Cucimoo mencatat bahwa pembayaran terjadi; Cucimoo tidak pernah memprosesnya — `paymentMethod` hanyalah pembukuan. Nilai uang berupa bilangan bulat rupiah.

GET/api/v1/transactions API key

Daftar transaksi

Parameters

  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.
  • branchIdquery · stringBatasi ke satu cabang.
  • searchquery · stringDicocokkan dengan kode struk, nomor pelat atau nama pelanggan.
  • statusquery · string`recorded` atau `voided`.
  • paymentMethodquery · string`cash` | `transfer` | `qris` | `other`.
  • staffTagIdquery · stringHanya pekerjaan yang menandai staf ini.
  • customerIdquery · stringHanya cucian milik pelanggan ini.
  • shiftIdquery · stringHanya transaksi di dalam satu sesi kasir.
  • sourcequery · string`cashier` | `form` (formulir Tambah transaksi dan biaya member) | `import`.
  • dateFromquery · stringTimestamp ISO, termasuk batas ini.
  • dateToquery · stringTimestamp ISO, tidak termasuk batas ini.

Response example

{
  "data": [
    {
      "id": "clx…",
      "code": "TRX-A3F9K-260812-0014",
      "branch": {
        "id": "clx…",
        "name": "Cabang Margonda",
        "timezone": "Asia/Jakarta"
      },
      "status": "recorded",
      "paymentMethod": "cash",
      "subtotal": 25000,
      "discount": 0,
      "discountType": "fixed",
      "discountValue": 0,
      "taxRate": 0,
      "taxAmount": 0,
      "total": 25000,
      "paidAmount": 50000,
      "changeAmount": 25000,
      "vehicleType": "motorcycle",
      "plateNumber": "B 1234 XYZ",
      "occurredAt": "2026-08-12T04:20:00.000Z",
      "recordedByName": "Budi Santoso",
      "customer": null,
      "items": [
        {
          "id": "clx…",
          "serviceId": "clx…",
          "name": "Cuci motor kecil",
          "unitPrice": 12000,
          "quantity": 1,
          "lineTotal": 12000
        }
      ],
      "staff": [
        {
          "id": "clx…",
          "name": "Agus"
        }
      ]
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "pageCount": 1
  }
}
POST/api/v1/transactions API key

Mencatat satu cucian

Harga *dan pajak* ditentukan di sisi server: harga baris diambil dari daftar layanan cabang tersebut (kirim `unitPrice` pada satu baris hanya kalau memang ingin menimpanya, sebagai potongan manual) dan persen pajak diambil dari `taxRate` cabang, sehingga tidak bisa disetel per request. Keduanya disalin ke transaksi sebagai `taxRate`/`taxAmount`. Layanan milik cabang lain akan ditolak dengan 404 `NOT_FOUND`. Transaksi otomatis menempel pada sesi kasir cabang yang sedang terbuka. Menandai staf melebihi jatah paket menghasilkan 402 `LIMIT_REACHED`. **Diskon member diterapkan otomatis** kalau `customerId` sedang menjadi member di cabang ini (lihat `GET /cashier/membership`): hasilnya `discountType` bernilai `membership` dan `membershipName` berisi nama paketnya. Diskon tidak pernah ditumpuk — diskon yang diketik (`discountValue`/`discount` lebih dari 0) menggantikan diskon member, dan kalau `redeemLoyalty` dikirim, yang dipakai adalah potongan yang lebih besar; saldo loyalti hanya berkurang kalau hadiahnya yang dipakai. Transaksi yang menjadi gratis karena member tidak menghasilkan stempel atau poin.

Request body

  • branchIdstringrequiredCabang tempat cucian dikerjakan.
  • itemsarrayrequired`[{ serviceId?, name?, unitPrice?, quantity }]` — minimal satu baris; tiap baris wajib punya `serviceId` atau `name`.
  • staffTagIdsarrayId staf yang mengerjakan. Dibatasi paket lewat `staffTagsPerTransaction`.
  • customerIdstringPelanggan yang sudah tersimpan, kalau diketahui.
  • vehicleIdstringKendaraan yang sudah tersimpan; jenis dan pelatnya disalin dari catatan itu.
  • vehicleTypestring`car` | `motorcycle` | `truck` | `bus` untuk pelanggan dadakan tanpa kendaraan tersimpan.
  • plateNumberstringNomor pelat untuk pelanggan dadakan.
  • discountTypestring`fixed` (bawaan) membaca `discountValue` sebagai rupiah; `percent` membacanya sebagai persen dari subtotal, maksimal 100.
  • discountValueintegerDiskon sebagaimana diketik — rupiah kalau `discountType` bernilai `fixed`, persen kalau `percent`. Hasil rupiahnya dikembalikan sebagai `discount`.
  • discountintegerDiskon rupiah gaya lama, maksimal sebesar subtotal. Dipakai hanya kalau `discountValue` tidak dikirim.
  • redeemLoyaltybooleanTukarkan saldo loyalti pelanggan pada transaksi ini. Nilai hadiahnya ditentukan di sisi server dan menggantikan diskon yang diketik — hasilnya tetap muncul sebagai `discount`, dengan `discountType` bernilai `loyalty`. Diabaikan kalau tidak ada `customerId`, saldonya belum cukup, atau paket bisnis tidak mencakup loyalti.
  • paidAmountintegerUang tunai yang diserahkan pelanggan. `changeAmount` dihitung darinya di sisi server; kosongkan kalau tidak ada uang yang dihitung.
  • paymentMethodstring`cash` (bawaan) | `transfer` | `qris` | `other`.
  • notesstringCatatan bebas.
  • occurredAtstringTimestamp ISO untuk mencatat transaksi yang terlewat. Bawaannya sekarang.

Response example

{
  "data": {
    "id": "clx…",
    "code": "TRX-A3F9K-260812-0015",
    "status": "recorded",
    "subtotal": 25000,
    "discount": 2500,
    "discountType": "percent",
    "discountValue": 10,
    "taxRate": 11,
    "taxAmount": 2475,
    "total": 24975,
    "paidAmount": 25000,
    "changeAmount": 25,
    "paymentMethod": "cash",
    "shiftId": "clx…",
    "items": [
      {
        "id": "clx…",
        "name": "Cuci motor kecil",
        "unitPrice": 12000,
        "quantity": 1,
        "lineTotal": 12000
      }
    ],
    "staff": [
      {
        "id": "clx…",
        "name": "Agus"
      }
    ],
    "loyaltyRewardName": null,
    "membershipName": null,
    "membershipFee": null,
    "source": "cashier",
    "loyaltyLine": "Cuci ke-7 dari 10"
  }
}
Kemungkinan error:VALIDATION_FAILEDNOT_FOUNDLIMIT_REACHEDCONFLICTFORBIDDEN
GET/api/v1/transactions/{id} API key

Mengambil satu transaksi

Parameters

  • idpath · stringrequiredId transaksi.

Response example

{
  "data": {
    "id": "clx…",
    "code": "TRX-A3F9K-260812-0015",
    "total": 25000,
    "status": "recorded"
  }
}
Kemungkinan error:NOT_FOUND
PATCH/api/v1/transactions/{id} API key

Melengkapi keterangan setelah transaksi tersimpan

Menempelkan pelanggan, kendaraan, staf atau metode bayar yang dilewati Mode Kasir Cepat. **Nilai uang dan baris layanan tidak bisa diubah** — angka yang salah dibatalkan lalu dicatat ulang, supaya jejaknya tetap jujur.

Parameters

  • idpath · stringrequiredId transaksi.

Request body

  • customerIdstringMenempelkan pelanggan, atau (null) melepasnya.
  • vehicleIdstringMenempelkan kendaraan, atau (null) melepasnya.
  • vehicleTypestring`car` | `motorcycle` | `truck` | `bus`.
  • plateNumberstringNomor pelat.
  • paymentMethodstringMembetulkan cara pembayarannya.
  • staffTagIdsarrayMengganti seluruh daftar tag staf.
  • notesstringCatatan bebas.

Response example

{
  "data": {
    "id": "clx…",
    "code": "TRX-A3F9K-260812-0015",
    "staff": [
      {
        "id": "clx…",
        "name": "Agus"
      }
    ]
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILEDCONFLICTLIMIT_REACHED
POST/api/v1/transactions/{id}/void API key

Membatalkan transaksi

Tidak ada DELETE untuk transaksi. Pembatalan menyimpan barisnya, mencatat siapa dan mengapa, lalu mengeluarkannya dari semua total.

Parameters

  • idpath · stringrequiredId transaksi.

Request body

  • reasonstringrequiredAlasan pembatalan, minimal 3 karakter.

Response example

{
  "data": {
    "id": "clx…",
    "status": "voided",
    "voidedAt": "2026-08-12T05:00:00.000Z",
    "voidReason": "Salah input layanan"
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILEDCONFLICT

Sesi kasir & rekap uang tunai

Satu cabang paling banyak punya satu sesi kasir terbuka. Transaksi yang dicatat selagi sesi terbuka menempel padanya secara otomatis, dan menutup sesi membandingkan uang yang dihitung dengan yang tercatat.

GET/api/v1/shifts/current API key

Sesi kasir yang sedang terbuka di satu cabang

`data` bernilai null kalau tidak ada sesi terbuka — itu kondisi normal, bukan 404.

Parameters

  • branchIdquery · stringrequiredId cabang.

Response example

{
  "data": {
    "id": "clx…",
    "branch": {
      "id": "clx…",
      "name": "Cabang Margonda",
      "timezone": "Asia/Jakarta"
    },
    "status": "open",
    "openedAt": "2026-08-12T01:00:00.000Z",
    "openedByName": "Budi Santoso",
    "openingCash": 100000,
    "totals": {
      "transactionCount": 14,
      "revenue": 320000,
      "byMethod": {
        "cash": 250000,
        "transfer": 0,
        "qris": 70000,
        "other": 0
      },
      "cashSales": 250000,
      "cashExpenses": 50000
    }
  }
}
Kemungkinan error:VALIDATION_FAILEDNOT_FOUND
GET/api/v1/shifts API key

Daftar sesi kasir

Parameters

  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.
  • branchIdquery · stringBatasi ke satu cabang.
  • statusquery · string`open` atau `closed`.

Response example

{
  "data": [
    {
      "id": "clx…",
      "status": "closed",
      "openedAt": "2026-08-11T01:00:00.000Z",
      "closedAt": "2026-08-11T13:00:00.000Z",
      "openingCash": 100000,
      "countedCash": 445000,
      "expectedCash": 450000,
      "difference": -5000,
      "transactionCount": 21
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "pageCount": 1
  }
}
POST/api/v1/shifts API key

Membuka sesi kasir

Menghasilkan 409 `CONFLICT` kalau cabang tersebut sudah punya sesi terbuka.

Request body

  • branchIdstringrequiredCabang pemilik sesi ini.
  • openingCashintegerUang modal di laci saat dibuka, dalam rupiah.
  • notesstringCatatan bebas.

Response example

{
  "data": {
    "id": "clx…",
    "status": "open",
    "openedAt": "2026-08-12T01:00:00.000Z",
    "openingCash": 100000
  }
}
Kemungkinan error:VALIDATION_FAILEDCONFLICTNOT_FOUND
POST/api/v1/shifts/{id}/close API key

Menutup sesi kasir dan merekap uang tunai

Uang yang seharusnya ada adalah modal awal, ditambah penjualan **tunai** yang tercatat, dikurangi pengeluaran dengan `paidFrom` bernilai `cash_drawer` pada sesi ini. Transfer dan QRIS tidak pernah masuk laci. `difference` bernilai minus berarti uang di laci kurang.

Parameters

  • idpath · stringrequiredId sesi kasir.

Request body

  • countedCashintegerrequiredUang yang benar-benar dihitung, dalam rupiah.
  • notesstringPenjelasan kalau ada selisih.

Response example

{
  "data": {
    "id": "clx…",
    "status": "closed",
    "countedCash": 445000,
    "expectedCash": 450000,
    "difference": -5000
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILEDCONFLICT
GET/api/v1/shifts/{id} API key

Mengambil satu sesi kasir beserta totalnya

Parameters

  • idpath · stringrequiredId sesi kasir.

Response example

{
  "data": {
    "id": "clx…",
    "status": "closed",
    "difference": -5000,
    "totals": {
      "transactionCount": 21,
      "revenue": 520000,
      "cashSales": 350000,
      "cashExpenses": 50000
    }
  }
}
Kemungkinan error:NOT_FOUND

Pengeluaran

Catatan uang keluar: belanja bahan, tagihan, gaji, sewa. Tidak ada saldo yang dijaga — endpoint ini mencatat bahwa uang keluar, bukan berapa sisa uang usaha. Laporan menjumlahkannya per rentang waktu, dan tidak ada angka yang dibawa ke periode berikutnya. Satu field menghubungkannya ke bagian lain: `paidFrom`. Bernilai `cash_drawer`, pengeluaran itu menempel pada sesi kasir yang sedang terbuka di cabang tersebut dan ikut mengurangi uang laci yang seharusnya ada saat sesi ditutup. Bernilai `outside` (transfer, kartu, uang pribadi pemilik), pengeluaran hanya masuk laporan. Fitur ini tersedia di semua paket. Peran `cashier` boleh membuat catatan, tetapi hanya `cash_drawer` pada sesi yang sedang terbuka, dan hanya bisa membaca catatan sesi itu sendiri.

GET/api/v1/expenses API key

Daftar pengeluaran

Urut dari yang terbaru. Peran `owner` dan `manager` membaca seluruh usaha; `cashier` hanya menerima catatan pada sesi kasir yang sedang terbuka.

Parameters

  • pagequery · integerNomor halaman, mulai dari 1.
  • limitquery · integerJumlah per halaman, maksimal 100.
  • branchIdquery · stringBatasi ke satu cabang.
  • categoryquery · string`supplies` | `utilities` | `salary` | `rent` | `equipment` | `operational` | `other`.
  • paidFromquery · string`cash_drawer` atau `outside`.
  • searchquery · stringMencari pada field `notes`.
  • dateFromquery · stringTimestamp ISO 8601. Batas bawah `occurredAt`, inklusif.
  • dateToquery · stringTimestamp ISO 8601. Batas atas `occurredAt`, eksklusif.

Response example

{
  "data": [
    {
      "id": "clx…",
      "branchId": "clx…",
      "branch": {
        "id": "clx…",
        "name": "Cabang Margonda",
        "timezone": "Asia/Jakarta"
      },
      "shiftId": "clx…",
      "category": "supplies",
      "amount": 450000,
      "paidFrom": "cash_drawer",
      "notes": "Sampo wax 20 liter",
      "occurredAt": "2026-08-14T03:20:00.000Z",
      "recordedByName": "Budi Santoso",
      "editable": true,
      "createdAt": "2026-08-14T03:20:00.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "pageCount": 1
  }
}
POST/api/v1/expenses API key

Mencatat satu pengeluaran

Kalau `paidFrom` bernilai `cash_drawer` dan ada sesi kasir terbuka di cabang itu, catatan ini menempel pada sesi tersebut dan ikut mengurangi uang laci yang seharusnya ada. Untuk peran `cashier`, `paidFrom` selalu dipaksa menjadi `cash_drawer`, `occurredAt` selalu diisi waktu sekarang, dan request ditolak 409 `CONFLICT` kalau belum ada sesi kasir yang dibuka.

Request body

  • branchIdstringrequiredCabang yang mengeluarkan uang ini.
  • amountintegerrequiredJumlah dalam rupiah, harus lebih dari nol.
  • categorystring`supplies` | `utilities` | `salary` | `rent` | `equipment` | `operational` | `other`. Nilai bawaan `other`.
  • paidFromstring`cash_drawer` atau `outside`. Nilai bawaan `outside`.
  • notesstringKeterangan bebas, maksimal 300 karakter.
  • occurredAtstringTimestamp ISO 8601. Nilai bawaan waktu sekarang.

Response example

{
  "data": {
    "id": "clx…",
    "category": "supplies",
    "amount": 450000,
    "paidFrom": "cash_drawer",
    "shiftId": "clx…",
    "editable": true
  }
}
Kemungkinan error:VALIDATION_FAILEDNOT_FOUNDCONFLICT
PATCH/api/v1/expenses/{id} API key

Mengubah pengeluaran yang sudah dicatat

`branchId` tidak bisa diubah — memindahkan belanja antar cabang berarti menulis ulang riwayat. Catatan `cash_drawer` yang sesinya sudah ditutup menghasilkan 409 `CONFLICT`: sesi itu sudah menyimpan hasil rekap kasnya, jadi koreksinya dicatat sebagai pengeluaran baru. Field `editable` pada response daftar menyebutkan hal ini sebelum request dikirim.

Parameters

  • idpath · stringrequiredId pengeluaran.

Request body

  • amountintegerJumlah dalam rupiah.
  • categorystringKategori baru.
  • paidFromstring`cash_drawer` atau `outside`.
  • notesstringKeterangan bebas.
  • occurredAtstringTimestamp ISO 8601.

Response example

{
  "data": {
    "id": "clx…",
    "category": "utilities",
    "amount": 380000,
    "paidFrom": "outside",
    "editable": true
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILEDCONFLICT
DELETE/api/v1/expenses/{id} API key

Menghapus catatan pengeluaran

Benar-benar dihapus, tidak diarsipkan — catatan pengeluaran adalah nota internal usaha itu sendiri, bukan bukti yang dipegang pelanggan. Jejaknya tetap tersimpan di log aktivitas. Aturan sesi tertutup yang sama seperti pada PATCH berlaku di sini.

Parameters

  • idpath · stringrequiredId pengeluaran.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:NOT_FOUNDCONFLICT

Laporan

Satu endpoint melayani ringkasan harian sekaligus laporan bulanan — bedanya hanya rentang waktu. Batas hari dipotong memakai zona waktu cabang itu sendiri. Seberapa jauh ke belakang rentang boleh dimulai adalah batas paket (`reportRetentionDays`); request yang melewatinya dipangkas, bukan ditolak, dan response-nya menyebut di mana batas itu.

GET/api/v1/reports/summary API key

Pemasukan, pengeluaran, untung, layanan terlaris, staf dan komposisi pelanggan

Kosongkan `branchId` untuk tampilan gabungan seluruh cabang — gabungan itu memerlukan fitur paket `multiBranchRollup`, dan tanpa fitur tersebut laporan jatuh kembali ke cabang utama. `window.clamped` bernilai true kalau masa simpan paket memajukan tanggal mulainya. `totals.profit` adalah `totals.revenue` dikurangi `totals.expenses` — untung kotor, bukan untung bersih: Cucimoo tidak mengetahui pajak maupun penyusutan.

Parameters

  • rangequery · string`today` | `yesterday` | `last7` | `last30` (bawaan) | `thisMonth` | `lastMonth` | `custom`.
  • fromquery · string`YYYY-MM-DD`, dipakai saat `range=custom`.
  • toquery · string`YYYY-MM-DD`, dipakai saat `range=custom`.
  • branchIdquery · stringSatu cabang; kosongkan untuk gabungan semua cabang.

Response example

{
  "data": {
    "window": {
      "fromKey": "2026-07-14",
      "toKey": "2026-08-12",
      "timezone": "Asia/Jakarta",
      "branchId": null,
      "retentionFloorKey": "2025-08-13",
      "clamped": false
    },
    "totals": {
      "transactionCount": 412,
      "revenue": 9840000,
      "averageTicket": 23883,
      "discount": 120000,
      "voidedCount": 3,
      "dayCount": 30,
      "busiestDayKey": "2026-08-09",
      "expenses": 3120000,
      "profit": 6720000
    },
    "byMethod": {
      "cash": 7200000,
      "transfer": 640000,
      "qris": 2000000,
      "other": 0
    },
    "byVehicleType": {
      "car": 6100000,
      "motorcycle": 3740000,
      "truck": 900000,
      "bus": 0,
      "unknown": 0
    },
    "daily": [
      {
        "dateKey": "2026-08-12",
        "transactionCount": 14,
        "revenue": 320000,
        "expenses": 50000
      }
    ],
    "topServices": [
      {
        "id": "clx…",
        "name": "Cuci mobil kecil (sedan/hatchback)",
        "count": 118,
        "revenue": 4130000
      }
    ],
    "staff": [
      {
        "id": "clx…",
        "name": "Agus",
        "count": 190,
        "revenue": 4400000
      }
    ],
    "branches": [
      {
        "id": "clx…",
        "name": "Cabang Margonda",
        "count": 412,
        "revenue": 9840000
      }
    ],
    "hours": [
      {
        "hour": 9,
        "transactionCount": 48,
        "revenue": 1120000
      }
    ],
    "customers": {
      "newCount": 22,
      "returningCount": 61,
      "walkInCount": 329
    },
    "expensesByCategory": [
      {
        "category": "supplies",
        "amount": 1450000,
        "count": 9
      }
    ],
    "previous": {
      "transactionCount": 388,
      "revenue": 9120000,
      "expenses": 2980000,
      "profit": 6140000
    }
  }
}
Kemungkinan error:VALIDATION_FAILEDFORBIDDEN

Webhooks

Cucimoo menerima webhook dari payment gateway-nya di POST /api/webhooks/payments/{provider}. Endpoint itu bukan bagian dari API publik — pengamanannya berupa signature gateway, bukan token, dan setiap callback disimpan dengan deduplication key sehingga retry dari gateway tidak berdampak apa-apa.

Cucimoo belum mengirim webhook keluar. Sampai itu ada, integrasi perlu menarik sendiri secara berkala — saring dengan ?dateFrom supaya yang diambil hanya yang baru.

Reserved endpoints

Path berikut dipesan untuk kebutuhan usaha cuci kendaraan dan belum tersedia. Kami mencantumkannya supaya Anda bisa merencanakan ke depan; semuanya belum muncul di dokumen OpenAPI sampai benar-benar dirilis.

  • /api/v1/receipts/{id}Struk digital satu transaksi yang bisa dibagikan.
  • /api/v1/staff/{id}/commissionPerhitungan komisi dan upah per staf.
  • /api/v1/promosKode diskon dan kampanye promo berbatas waktu.