Version 1

Referensi API Cucimoo

REST API berformat JSON untuk Cucimoo, platform manajemen usaha cuci mobil dan motor. Ini HTTP biasa, jadi bisa dipanggil dari bahasa, framework atau platform apa pun. API ini dipakai aplikasi mobile Cucimoo dan terbuka untuk tenant Enterprise yang ingin membuat integrasi sendiri.

Base URLhttp://localhost:3000/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

Panggil POST /auth/login untuk menukar kredensial dengan session token, lalu kirimkan token itu pada setiap request berikutnya:

Authorization: Bearer <token>
  • Token berlaku 7 hari secara bawaan. Panggil POST /auth/refresh sebelum kedaluwarsa untuk memperpanjang sesi tanpa meminta kata sandi lagi.
  • Tidak ada cookie. Klien menyimpan sendiri tokennya — pakai penyimpanan aman bawaan platform, bukan penyimpanan preferensi biasa.
  • Tenant Enterprise boleh memakai API key berumur panjang lewat X-Api-Key: ck_live_…. Key dibuat di konsol tenant dan hanya ditampilkan sekali.
  • 401 UNAUTHENTICATED berarti tokennya hilang atau kedaluwarsa — kembalikan pengguna ke layar masuk.
Login
curl -X POST http://localhost:3000/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"identifier":"budi@example.com","password":"rahasia123"}'
Request dengan token
curl http://localhost:3000/api/v1/me \
  -H 'Authorization: Bearer <token>'

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
UNAUTHENTICATED401Token tidak ada, tidak valid, atau sudah kedaluwarsa. Arahkan pengguna kembali ke halaman masuk.
FORBIDDEN403Sudah masuk, tetapi peran ini tidak boleh melakukan tindakan tersebut.
TENANT_SUSPENDED403Akun bisnis sedang ditangguhkan. Hubungi dukungan.
NOT_FOUND404Data tidak ada, atau milik tenant lain.
VALIDATION_FAILED400Request body tidak lolos validasi. `details.field` menyebut field yang bermasalah.
PLAN_REQUIRED402Paket tenant belum mencakup fitur ini. Tawarkan peningkatan paket.
LIMIT_REACHED402Batas angka pada paket sudah habis. `details.max` adalah batasnya. Tawarkan peningkatan paket.
CONFLICT409Request bertentangan dengan kondisi saat ini (duplikat, perpindahan status yang tidak diizinkan).
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.

Rate limits

EndpointLimit
POST /auth/login5 percobaan gagal per identifier setiap 15 menit
POST /auth/register5 per alamat IP setiap jam
POST /auth/forgot-password5 per alamat IP setiap 15 menit

Melewati limit menghasilkan 429 RATE_LIMITED beserta details.retryAfterSeconds.

Authentication

Mengambil dan mengelola token sesi tenant yang dipakai semua endpoint lain.

POST/api/v1/auth/login Public

Masuk sebagai pemilik usaha atau kasir

Menerima alamat email atau nomor HP Indonesia sebagai `identifier`. Dibatasi 5 percobaan gagal per identifier setiap 15 menit.

Request body

  • identifierstringrequiredAlamat email atau nomor HP.
  • passwordstringrequiredKata sandi akun.

Response example

{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "expiresAt": "2026-08-18T04:41:38.000Z",
    "user": {
      "id": "clx…",
      "name": "Budi Santoso",
      "email": "budi@example.com",
      "phone": "628121111111",
      "tenantRole": "owner"
    },
    "tenant": {
      "id": "clx…",
      "name": "Cuci Kilat Depok",
      "slug": "cuci-kilat-depok"
    }
  }
}
Kemungkinan error:UNAUTHENTICATEDRATE_LIMITED
POST/api/v1/auth/register Public

Mendaftarkan pemilik sekaligus membuat bisnisnya

Membuat pengguna, tenant, cabang pertama dan langganan uji coba dalam satu request. Satu uji coba berlaku untuk satu nomor HP terverifikasi. Field bisnis boleh dikosongkan: tenant dibuat sementara dengan nama pemilik, lalu dilengkapi lewat alur penyiapan awal.

Request body

  • namestringrequiredNama lengkap pemilik.
  • emailstringrequiredAlamat email, harus unik.
  • phonestringrequiredNomor HP Indonesia (08xx, +62xx atau 62xx).
  • passwordstringrequiredMinimal 8 karakter.
  • businessNamestringNama usaha cuci kendaraan. Sementara memakai nama pemilik sampai penyiapan awal mengisinya.
  • categorystringSalah satu dari `car`, `motorcycle`, `both`. Nilai bawaan `both`.
  • branchNamestringNama cabang pertama. Nilai bawaan `Cabang Utama`.
  • timezonestringSalah satu dari `Asia/Jakarta`, `Asia/Makassar`, `Asia/Jayapura`.
  • acceptTermsbooleanrequiredHarus `true` — mencatat persetujuan UU PDP.

Response example

{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "trialEndsAt": "2026-08-25T04:41:38.000Z"
  }
}
Kemungkinan error:VALIDATION_FAILEDCONFLICTRATE_LIMITED
POST/api/v1/auth/forgot-password Public

Meminta tautan atur ulang kata sandi

Selalu berhasil, ada atau tidaknya alamat itu di sistem — supaya daftar akun tidak bisa ditebak dari luar.

Request body

  • emailstringrequiredAlamat email pada akun.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:VALIDATION_FAILEDRATE_LIMITED
POST/api/v1/auth/reset-password Public

Menyetel kata sandi baru dengan token atur ulang

Request body

  • tokenstringrequiredToken mentah dari tautan yang dikirim lewat email.
  • passwordstringrequiredKata sandi baru, minimal 8 karakter.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:VALIDATION_FAILED
POST/api/v1/auth/refresh Bearer token

Menukar token yang masih berlaku dengan yang baru

Panggil sebelum token sekarang kedaluwarsa supaya sesi tetap hidup tanpa meminta kata sandi lagi.

Response example

{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "expiresAt": "2026-08-25T04:41:38.000Z"
  }
}
Kemungkinan error:UNAUTHENTICATED
POST/api/v1/auth/logout Bearer token

Mengakhiri sesi yang sedang berjalan

Klien sebaiknya membuang tokennya apa pun response yang diterima.

Response example

{
  "data": {
    "success": true
  }
}

Pengguna saat ini

Siapa yang sedang masuk, apa yang diizinkan paketnya, dan profilnya.

GET/api/v1/me Bearer token atau API key

Pengguna, tenant, peran, hak paket dan cabang

Satu-satunya request yang perlu dilakukan aplikasi klien saat dibuka. Semua yang dibutuhkan antarmuka untuk memutuskan apa yang ditampilkan. `tenant.timezone` adalah jam tampilan untuk layar yang tidak terikat cabang; kalau layarnya terikat satu cabang, pakai `timezone` cabang itu — 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"
    },
    "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
PATCH/api/v1/me Bearer token

Memperbarui profil pengguna yang sedang masuk

Request body

  • namestringNama lengkap, minimal 2 karakter.
  • phonestringNomor HP Indonesia.
  • avatarUrlstringURL hasil unggahan, atau null untuk mengosongkan.

Response example

{
  "data": {
    "user": {
      "id": "clx…",
      "name": "Budi Santoso",
      "email": "budi@example.com",
      "phone": "628121111111",
      "avatarUrl": null
    }
  }
}
Kemungkinan error:VALIDATION_FAILEDCONFLICT
POST/api/v1/me/change-password Bearer token

Mengganti kata sandi tanpa keluar dari akun

Request body

  • currentPasswordstringrequiredKata sandi yang dipakai sekarang.
  • newPasswordstringrequiredKata sandi baru, minimal 8 karakter.

Response example

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

Cabang

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

GET/api/v1/branches Bearer token atau 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",
      "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 Bearer token atau 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} Bearer token atau API key

Mengambil satu cabang

Parameters

  • idpath · stringrequiredId cabang.

Response example

{
  "data": {
    "id": "clx…",
    "name": "Cabang Margonda",
    "timezone": "Asia/Jakarta",
    "taxRate": 11,
    "isActive": true,
    "isPrimary": true,
    "staffCount": 3
  }
}
Kemungkinan error:NOT_FOUND
PATCH/api/v1/branches/{id} Bearer token atau 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} Bearer token atau 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 Bearer token atau 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
POST/api/v1/branches/{id}/staff Bearer token atau 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} Bearer token atau 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} Bearer token atau 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 Bearer token atau 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`.

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.

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 Bearer token atau 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} Bearer token atau 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} Bearer token atau 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 Bearer token atau 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 Bearer token atau 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} Bearer token atau 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
PATCH/api/v1/customers/{id} Bearer token atau 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} Bearer token atau 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 Bearer token atau 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} Bearer token atau 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} Bearer token atau API key

Menghapus kendaraan

Parameters

  • idpath · stringrequiredId kendaraan.

Response example

{
  "data": {
    "success": true
  }
}
Kemungkinan error:NOT_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/cashier/context Bearer token atau API key

Semua kebutuhan layar kasir dalam satu request

Daftar layanan cabang ini beserta harganya, staf aktifnya, sesi kasir yang terbuka, total hari ini, dan jatah tag staf dari paket. `loyalty` berisi program yang sedang berjalan — `null` kalau bisnis ini tidak menjalankannya — lengkap dengan nilai hadiahnya, supaya layar kasir bisa menghitung total yang sama dengan yang akan disimpan server. Daftar pelanggan sengaja tidak ikut; cari lewat GET /customers. Dirancang untuk sekali jalan pada koneksi lambat — panggil saat masuk ke layar penjualan, lalu POST /transactions.

Parameters

  • branchIdquery · stringrequiredCabang tempat kasir bekerja.

Response example

{
  "data": {
    "businessName": "Cucimoo Wash",
    "branch": {
      "id": "clx…",
      "name": "Cabang Margonda",
      "timezone": "Asia/Jakarta",
      "address": "Jl. Margonda Raya No. 12, Depok",
      "phone": "0812-3456-7890",
      "receiptFooter": "Terima kasih atas kunjungan Anda."
    },
    "services": [
      {
        "id": "clx…",
        "name": "Cuci motor kecil",
        "vehicleType": "motorcycle",
        "price": 12000,
        "durationMinutes": 15,
        "color": "sky"
      }
    ],
    "staff": [
      {
        "id": "clx…",
        "name": "Agus"
      }
    ],
    "shift": {
      "id": "clx…",
      "openedAt": "2026-08-12T01:00:00.000Z",
      "openedByName": "Budi Santoso"
    },
    "summary": {
      "dateKey": "2026-08-12",
      "transactionCount": 14,
      "revenue": 320000,
      "byMethod": {
        "cash": 250000,
        "transfer": 0,
        "qris": 70000,
        "other": 0
      },
      "averageTicket": 22857
    },
    "maxStaffTags": -1,
    "loyalty": {
      "id": "clx…",
      "name": "Kartu Cuci Gratis",
      "type": "stamp",
      "unit": "stempel",
      "threshold": 10,
      "rewardName": "Gratis 1x Cuci",
      "rewardType": "free_service",
      "rewardValue": 25000,
      "rewardPercent": null,
      "maxDiscount": null
    },
    "recent": [
      {
        "id": "clx…",
        "code": "TRX-260812-0014",
        "total": 25000,
        "paymentMethod": "cash",
        "occurredAt": "2026-08-12T04:20:00.000Z"
      }
    ]
  }
}
Kemungkinan error:VALIDATION_FAILEDNOT_FOUND
GET/api/v1/transactions Bearer token atau 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.
  • dateFromquery · stringTimestamp ISO, termasuk batas ini.
  • dateToquery · stringTimestamp ISO, tidak termasuk batas ini.

Response example

{
  "data": [
    {
      "id": "clx…",
      "code": "TRX-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 Bearer token atau 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`.

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-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,
    "loyaltyLine": "Cuci ke-7 dari 10"
  }
}
Kemungkinan error:VALIDATION_FAILEDNOT_FOUNDLIMIT_REACHEDCONFLICTFORBIDDEN
GET/api/v1/transactions/{id} Bearer token atau API key

Mengambil satu transaksi

Parameters

  • idpath · stringrequiredId transaksi.

Response example

{
  "data": {
    "id": "clx…",
    "code": "TRX-260812-0015",
    "total": 25000,
    "status": "recorded"
  }
}
Kemungkinan error:NOT_FOUND
PATCH/api/v1/transactions/{id} Bearer token atau 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-260812-0015",
    "staff": [
      {
        "id": "clx…",
        "name": "Agus"
      }
    ]
  }
}
Kemungkinan error:NOT_FOUNDVALIDATION_FAILEDCONFLICTLIMIT_REACHED
POST/api/v1/transactions/{id}/void Bearer token atau 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 Bearer token atau 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 Bearer token atau 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 Bearer token atau 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 Bearer token atau 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} Bearer token atau 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 Bearer token atau 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 Bearer token atau 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} Bearer token atau 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} Bearer token atau 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 Bearer token atau 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

Notifikasi

Notifikasi di dalam aplikasi untuk pengguna yang sedang masuk.

GET/api/v1/notifications Bearer token

Daftar notifikasi

Parameters

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

Response example

{
  "data": [
    {
      "id": "clx…",
      "title": "Uji coba Premium aktif",
      "body": "Semua fitur Premium terbuka sampai 25 Agustus 2026.",
      "actionUrl": "/app/billing",
      "level": "info",
      "readAt": null,
      "createdAt": "2026-08-11T04:41:38.000Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "pageCount": 1
  }
}
POST/api/v1/notifications/{id}/read Bearer token

Menandai satu notifikasi sudah dibaca

Parameters

  • idpath · stringrequiredId notifikasi.

Response example

{
  "data": {
    "success": true
  }
}
POST/api/v1/notifications/read-all Bearer token

Menandai semua notifikasi sudah dibaca

Response example

{
  "data": {
    "success": true,
    "count": 4
  }
}

Paket & langganan

Daftar paket yang terbuka untuk umum, langganan tenant saat ini, dan cara memulai pembayaran.

GET/api/v1/plans Public

Daftar paket untuk umum

Tanpa autentikasi. Harga berupa teks desimal; batas bernilai -1 berarti "tanpa batas".

Response example

{
  "data": [
    {
      "slug": "premium",
      "name": "Premium",
      "description": "Untuk usaha yang tumbuh: banyak cabang, banyak staf, dan laporan yang bisa diekspor.",
      "priceMonthly": "99000",
      "priceYearly": "990000",
      "currency": "IDR",
      "trialDays": 14,
      "limits": {
        "branches": 5,
        "staffTagsPerTransaction": -1,
        "customers": -1,
        "services": -1,
        "reportRetentionDays": 365
      },
      "features": {
        "export": true,
        "multiBranchRollup": true,
        "apiAccess": false,
        "prioritySupport": false
      }
    }
  ]
}
GET/api/v1/subscription Bearer token atau API key

Kondisi langganan saat ini

Termasuk sisa hari uji coba dan tenggat masa tenggang ketika pembayaran gagal.

Response example

{
  "data": {
    "status": "trialing",
    "period": "monthly",
    "plan": {
      "slug": "premium",
      "name": "Premium"
    },
    "trialEndsAt": "2026-08-25T04:41:38.000Z",
    "trialDaysRemaining": 11,
    "currentPeriodEnd": null,
    "graceEndsAt": null,
    "needsBranchSelection": false
  }
}
POST/api/v1/subscription/checkout Bearer token

Memulai pembayaran langganan

Mengembalikan URL halaman pembayaran yang perlu dibuka pengguna. `token` dan `clientKey` ikut terkirim kalau gateway yang aktif mendukung checkout tertanam — jangan menuliskan nama gateway secara tetap di kode, gateway yang aktif bisa berganti. Tidak tersedia di dalam sesi impersonasi.

Request body

  • planSlugstringrequiredPaket tujuan, misalnya `premium`.
  • periodstring`monthly` (bawaan) atau `yearly`.

Response example

{
  "data": {
    "orderId": "CM-M0X2K1-AB12",
    "redirectUrl": "https://pay.example-gateway.com/redirect/66e4fa55…",
    "token": "66e4fa55-fdac-4ef9-91b5-733b97d1b862",
    "clientKey": "SB-Mid-client-…",
    "amount": 99000,
    "expiresAt": "2026-08-11T05:41:38.000Z"
  }
}
Kemungkinan error:VALIDATION_FAILEDNOT_FOUNDFORBIDDEN

Pengaturan aplikasi

Identitas merek dan konfigurasi klien yang dibaca aplikasi mobile saat dibuka.

GET/api/v1/settings/public Public

Pengaturan aplikasi untuk umum

Tanpa autentikasi. Baca ini sebelum layar masuk supaya aplikasi bisa menampilkan identitas merek, kontak dukungan, dan meminta pembaruan wajib ketika versinya di bawah `minimumMobileVersion`.

Response example

{
  "data": {
    "company": {
      "name": "Cucimoo",
      "tagline": "Kelola usaha cuci kendaraan Anda tanpa ribet",
      "logoUrl": "",
      "email": "halo@cucimoo.id",
      "whatsapp": "628120000000"
    },
    "minimumMobileVersion": "1.0.0",
    "maintenanceMode": false,
    "maintenanceMessage": "",
    "registrationOpen": true,
    "timezones": [
      "Asia/Jakarta",
      "Asia/Makassar",
      "Asia/Jayapura"
    ]
  }
}

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 ke tenant. Polling GET /subscription setelah pembayaran untuk melihat perubahan statusnya; langganan yang terbayar biasanya beres dalam hitungan detik setelah gateway mengonfirmasi.

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 program loyalitas.

Versioning & changelog

Versinya ada di dalam path. Setelah aplikasi mobile dirilis, /api/v1 dibekukan: kami hanya akan menambah endpoint dan menambah field opsional, tanpa menghapus field, mengganti namanya, atau mengubah arti nilai yang sudah ada. Breaking change akan terbit sebagai /api/v2.

1.2.0

2026-08-14
  • Perubahan pada daftar layanan di bawah ini memutus kompatibilitas. Ini dilakukan sebelum aplikasi mobile dirilis, jadi path-nya tetap /api/v1; setelah rilis, perubahan sejenis akan terbit sebagai /api/v2.
  • Layanan kini milik satu cabang. `POST /services` mewajibkan `branchId`, dan `GET /services?branchId=` mengembalikan daftar layanan cabang itu saja.
  • `basePrice`, `hasBranchPrice` dan `branchPrices` hilang dari objek layanan. `price` tetap ada dan sekarang berarti harga di cabang tersebut.
  • `POST /services/{id}/price` dihapus karena tidak ada lagi harga khusus cabang yang perlu ditimpa. Ubah `price` layanan cabang itu langsung lewat `PATCH /services/{id}`.
  • `POST /transactions` menolak layanan milik cabang lain dengan 404 `NOT_FOUND`.
  • Halaman referensi ini kini berbahasa Indonesia. Nama field, nilai enum, error code dan seluruh contoh payload tetap seperti apa adanya di jaringan.

1.1.0

2026-08-12
  • Menambahkan domain usaha cuci: daftar layanan dengan harga khusus per cabang, pelanggan beserta kendaraannya, transaksi dengan baris layanan dan penandaan staf, sesi kasir dengan rekap uang tunai, dan pelaporan.
  • Menambahkan GET /cashier/context — satu request yang menyiapkan seluruh layar kasir.
  • Nilai uang di endpoint-endpoint ini berupa bilangan bulat rupiah, bukan teks desimal.
  • Hanya menambah: tidak ada endpoint, field atau error code lama yang berubah.

1.0.0

2026-08-11
  • Rilis publik pertama /api/v1.
  • Autentikasi, pengguna saat ini, cabang, tag staf, notifikasi, paket, langganan dan pengaturan untuk umum.