Sabdopalon
Jombang
← Beranda
Login
📘 Dokumentasi API Sabdopalon
Backend Next.js —
api.sabdopalon.jombangkab.go.id
# Sabdopalon API — Developer Guide Backend Next.js untuk Sistem Layanan Surat Desa Sabdopalon (DPMD Kab. Jombang). Diakses oleh mobile app dan SPA frontend. --- ## Quick Start ```bash # 1. Login dapat JWT (base URL: https://sabdopalon.jombangkab.go.id/v2) curl -X POST https://sabdopalon.jombangkab.go.id/v2/api/auth/operator/login \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"username":"admin_desa","password":"yourpass"}' # 2. Pakai JWT untuk endpoint yang butuh identity user curl -H "Authorization: Bearer YOUR_JWT" \ https://sabdopalon.jombangkab.go.id/v2/api/auth/me ``` --- ## Two-layer Authentication Sabdopalon API pakai **dua lapis token**: | Layer | Header | Untuk | |---|---|---| | **API_TOKEN** (service) | `Authorization: Bearer xxx` atau `X-API-Key: xxx` | Identify aplikasi (mobile/SPA) yang boleh akses. **WAJIB di semua endpoint.** | | **JWT** (user) | `Authorization: Bearer eyJ.xxx.yyy` | Identify user yang sedang login. Dipakai untuk endpoint butuh user context. | JWT format adalah 3 part dot-separated (`xxx.yyy.zzz`). API_TOKEN format adalah 64-char hex polos. Server otomatis bedakan. **Catatan:** kalau pakai JWT, JWT juga di header `Authorization: Bearer ...`. Header ini di-prioritize untuk identify user. API_TOKEN harus dikirim via `X-API-Key` kalau JWT pakai Authorization. Contoh request dengan kedua token: ``` X-API-Key: 64charhexapitoken... Authorization: Bearer eyJhbGciOiJ.user.jwt ``` ### Cara dapat API_TOKEN Hubungi admin DPMD. Token disimpan di server `.env`, di-rotate via menu admin `Kabupaten → API Token`. --- ## CORS Allow-list via env `ALLOWED_ORIGINS` (comma-separated). Origin baru → hubungi devops untuk daftarkan. ## Rate Limit Default **120 req/menit per IP**. Exceeded → HTTP 429. Pakai exponential backoff. ## Base URL | Environment | URL | |---|---| | **Production** | `https://sabdopalon.jombangkab.go.id/v2` | | Local dev | `http://localhost:3000` (langsung ke Next.js, no proxy) | > ⚠️ **Catatan**: DNS `api.sabdopalon.jombangkab.go.id` belum dipublish per Juni 2026. Backend Next.js di-proxy via Apache `/v2/` subpath di vhost `sabdopalon.jombangkab.go.id`. Endpoint path `/api/...` di-preserve apa adanya — yang berubah cuma base URL. > > Kalau DNS subdomain `api.*` nanti dipublish, base URL tinggal diganti, **endpoint path tidak berubah**. ## Response Format **Success:** ```json { "success": true, "status": true, "data": [...], "msg": "..." } ``` **Error:** ```json { "success": false, "status": false, "msg": "Human-readable error" } ``` Beberapa endpoint legacy return raw array (`/surat/syarat`, `/surat/komponen`). --- ## Endpoints ### Authentication #### `POST /api/auth/operator/login` Login untuk admin/aparat/kades/sekdes/operator desa. **Body** JSON: ```json { "username": "admin_desa", "password": "passwordnya" } ``` **Response sukses (200):** ```json { "success": true, "msg": "Login berhasil", "token": "eyJhbGciOiJIUzI1NiIs...", "user": { "kodeUser": "U001", "username": "admin_desa", "nama": "Budi Santoso", "nip": "1234567890", "role": "Admin Desa", "jenisUnit": "DESA", "namaJabatan": "Kepala Desa", "kodeDesa": "3517070013", "kodeKec": "351707", "kodeKab": "3517", "kodeOpd": null, "isTTE": true } } ``` **Error:** 401 (cred salah), 403 (akun nonaktif), 400 (field kurang). #### `POST /api/auth/warga/request-otp` Trigger pengiriman OTP via WhatsApp untuk warga (NIK terdaftar). **Body:** `{ "nik": "3517123456789012" }` (16 digit) **Response sukses:** ```json { "success": true, "msg": "OTP terkirim ke WhatsApp Anda", "expires_in_seconds": 300, "nik_masked": "351712****12" } ``` OTP berlaku 5 menit. Disimpan di `datapenduduk.KodeOTP` server-side, satu kali pakai. **Error:** 400 (NIK invalid), 404 (NIK belum terdaftar), 500 (Waslah gagal). #### `POST /api/auth/warga/verify-otp` Verify OTP, dapat JWT. **Body:** `{ "nik": "3517...", "otp": "ABC123" }` **Response sukses:** ```json { "success": true, "msg": "Login berhasil", "token": "eyJhbGciOiJIUzI1NiIs...", "user": { "idPend": "1234567", "nik": "3517...", "nama": "Siti Aminah", "email": "siti@example.com", "noWa": "62812...", "kodeDesa": "...", "namaDesa": "...", "kodeKec": "...", "namaKec": "...", "kodeKab": "..." } } ``` OTP one-time use — setelah sukses, dihapus dari DB. **Error:** 401 (OTP salah / expired), 404 (NIK tidak ada). #### `GET /api/auth/me` Decode JWT, return payload untuk verify session masih valid. **Header:** `Authorization: Bearer <JWT>` **Response sukses:** ```json { "success": true, "user": { "sub": "...", "kind": "operator|warga", "role": "...", "jenisUnit": "...", "iat": 1234567890, "exp": 1234567890 } } ``` **Error:** 401 (JWT invalid/expired). --- ### Master Data #### `GET /api/master/jenis-surat` List jenis surat tersedia. No params. **Response:** ```json { "success": true, "data": [{ "JenisSurat": "Surat Keterangan Domisili" }, ...] } ``` --- ### Permohonan Surat #### `GET /api/sabdopalon/permohonan` List permohonan milik 1 pemohon. **Query:** - `IDPend` (required) — NIK pemohon - `cari` (opt) — substring filter `JenisSurat` atau `DeskripsiSurat` **Response:** ```json { "success": true, "data": [ { "NoTrMohon": "MHN-20260602-00001", "KodeDesa": "...", "KodeKec": "...", "KodeKab": "...", "IDPend": "...", "JenisSurat": "Surat Keterangan Domisili", "TglPermohonan": "2026-06-02T03:15:00.000Z", "EstimasiTglSelesai": "2026-06-05", "StatusPermohonan": "WAITING", "KeteranganDitolak": null, "FileHasilSurat": null } ] } ``` #### `GET /api/sabdopalon/permohonan/desa` **Inbox operator** — daftar permohonan di **wilayah operator** (bukan per-NIK). Untuk layar "Permohonan Masuk" di app operator (Operator Desa / Kades / Sekdes). **Auth:** API_TOKEN (service) **+ JWT operator**. Scope wilayah diambil **dari JWT** (bukan query) supaya operator tidak bisa melihat wilayah lain. **Scope otomatis by `jenisUnit` di JWT:** | jenisUnit | Filter | |---|---| | `DESA` | `KodeDesa` + `KodeKec` + `KodeKab` | | `KECAMATAN` | `KodeKec` + `KodeKab` | | `KABUPATEN` / `DINAS` / `CAPIL` / `KOMINFO` | `KodeKab` | **Header:** ``` X-API-Key: <API_TOKEN> Authorization: Bearer <JWT operator> ``` **Query (opsional):** - `status` — daftar `StatusPermohonan` dipisah koma, mis. `WAITING,ON PROGRESS` (default: semua status di wilayah). Nilai aktual di DB: `WAITING`, `ON PROGRESS`, `DONE`, `REJECTED`. - `cari` — substring `JenisSurat` / `DeskripsiSurat` (LIKE, di-escape). **Response:** sama bentuk dengan `GET /permohonan`, **+ `NamaPemohon`, `NoWa`, `NomorSurat`**: ```json { "success": true, "data": [ { "NoTrMohon": "MHN-20260604-00009", "KodeDesa": "...", "KodeKec": "...", "KodeKab": "...", "IDPend": "...", "NamaPemohon": "FATHUR ROMADHON", "JenisSurat": "Surat Keterangan Usaha", "TglPermohonan": "...", "EstimasiTglSelesai": "...", "StatusPermohonan": "ON PROGRESS", "KeteranganDitolak": null, "FileHasilSurat": null, "NomorSurat": null, "StatusProgress": "Verifikasi Berkas", "NoWa": "0857..." } ] } ``` > `StatusProgress` = tahap berjalan (mis. `Verifikasi Berkas`, `Penomoran Surat`, > `Pengesahaan Surat`) — dipakai app untuk tahu aksi mana yang tersedia. **Error:** 401 (JWT operator tidak ada/invalid), 403 (token bukan operator / scope JWT tidak lengkap), 500. #### `GET /api/sabdopalon/permohonan/dokumen` List berkas syarat yang diunggah warga untuk 1 permohonan (layar verifikasi operator). **Auth:** API_TOKEN + JWT operator. Scope wilayah dari JWT. **Query:** `NoTrMohon` (required). **Response:** ```json { "success": true, "data": [ { "NoUrutSyarat": 1, "NamaSyarat": "Pengantar RT RW", "Keterangan": null, "FileDoc": "MHN-20260604-00009_01.png", "IsVerified": 1, "JenisSurat": "Surat Pengantar Keterangan Catatan Kepolisian" } ] } ``` `IsVerified`: `1` = terverifikasi, `0` = ditolak, `null` = belum diverifikasi. File-nya diunduh via `GET /surat/download-syarat`. **Error:** 400 (NoTrMohon kosong), 401/403 (auth/scope), 500. #### `GET /api/sabdopalon/surat/download-syarat` Tampilkan/unduh file syarat warga. Nama file diambil dari DB berdasar `NoTrMohon` + `NoUrutSyarat` dalam **scope wilayah operator** (tak bisa tebak file wilayah lain). **Auth:** API_TOKEN + JWT operator. **Query:** `NoTrMohon`, `NoUrutSyarat` (required); `inline=1` → tampil di browser (default attachment). **Response sukses:** binary file (`image/png`, `image/jpeg`, atau `application/pdf`). **Error:** 400 (param kurang), 404 (dokumen/file tidak ada), 401/403 (auth/scope), 500. #### `POST /api/sabdopalon/surat/verifikasi` Verifikasi dokumen syarat **per-dokumen** (terima/tolak). Scope wilayah dari JWT operator. **Auth:** API_TOKEN + JWT operator. **Body** JSON: ```json { "NoTrMohon": "MHN-20260604-00009", "NoUrutSyarat": 1, "IsVerified": 0, "Keterangan": "KTP buram" } ``` - `IsVerified`: `1` = terima, `0` = tolak. `Keterangan` **wajib** saat menolak. **Efek:** - UPDATE `dokumensyaratmohon.IsVerified` + `Keterangan`. - Saat **tolak** (`0`): `trpermohonanmasy` → `StatusPermohonan='REJECTED'`, `StatusProgress='Pemohon/Masyarakat'`, `KeteranganDitolak`; + INSERT `progresssurat` (`StatusProgress='Verifikasi Berkas'`). Penolakan 1 dokumen = permohonan ditolak. - Saat **terima** (`1`): hanya tandai dokumen terverifikasi (pemindahan ke tahap berikut adalah aksi terpisah). **Response:** `{ success, msg, data: { NoTrMohon, NoUrutSyarat, IsVerified } }` **Error:** 400 (param kurang / alasan tolak kosong), 404 (permohonan/dokumen di luar scope), 401/403, 500. #### `POST /api/sabdopalon/permohonan/simpan` Buat permohonan baru atau update (kalau `NoTrMohon` di-set di body). **Body:** `multipart/form-data` Required: - `NamaPemohon`, `KodeDesa`, `KodeKec`, `KodeKab`, `IDPend`, `JenisSurat` - `NamaDesa`, `NamaKecamatan` — untuk display - `StandarWaktuPelayanan` — int hari untuk hitung estimasi Optional: - `NoTrMohon` — kalau ada → update mode - `Instansi` (default `DESA`) - `JumlahDetil` — int, jumlah detail field - `Field1..Field26` — field dinamis per komponen surat - `KeperluanSurat`, `DeskripsiSurat` - `FileDoc[]` + `NoUrutSyarat[]` + `NamaSyarat[]` — upload syarat dokumen **File restrictions:** - Format: `.png .jpg .jpeg` only - MIME: `image/png image/jpeg` - Max ~5MB (browser limit) - Filename ter-sanitize anti path traversal **Catatan keamanan:** - Field workflow (`StatusPermohonan`, `IsTTE`, `UserTTE`, `NomorSurat`, `TanggalSurat`, dst) **diabaikan server** — diset oleh workflow internal. **Response sukses:** ```json { "success": true, "msg": "Berhasil mengirim permohonan", "data": { "NoTrMohon": "MHN-...", "JenisSurat": "...", "IDPend": "..." } } ``` **Error:** 400 (data kurang), 409 (duplicate WAITING/ON PROGRESS), 500 (DB error). #### `DELETE /api/sabdopalon/permohonan/hapus` Hapus permohonan + cascade child tables. **Query:** `kode`, `kab`, `kec`, `desa` (semua required) **Response:** ```json { "success": true, "msg": "Berhasil menghapus data", "affected": 1 } ``` ⚠️ Destructive, no undo. Frontend wajib confirm user. --- ### Komponen & Syarat Surat #### `GET /api/sabdopalon/surat/komponen` List field form yang harus diisi pemohon per jenis surat. **Query:** `JenisSurat` (required) **Response (raw array, legacy format):** ```json [ { "JenisSurat": "...", "NoUrut": 1, "LabelInfo": "Alamat", "FieldDB": "Field1", "TipeDataInput": "text", "IsDiisiPemohon": 1 } ] ``` #### `GET /api/sabdopalon/surat/syarat` List syarat dokumen yang harus di-upload per jenis surat. **Query:** `JenisSurat` (required) **Response (raw array):** ```json [ { "KodeSyarat": "K01", "NamaSyarat": "KTP", "Keterangan": "Foto KTP yang masih berlaku" } ] ``` #### `POST /api/sabdopalon/surat/upload` Upload single file syarat (untuk tambahan setelah permohonan dibuat). **Body:** `multipart/form-data` - `file` (required, PNG/JPG only, MIME image/png|jpeg) - `NoTrMohon`, `KodeKab`, `KodeKec`, `KodeDesa`, `NoUrutSyarat` (required) - `NamaSyarat`, `IDPend`, `JenisSurat` (optional, untuk insert baru) **Response:** ```json { "success": true, "msg": "Upload dokumen berhasil", "file": "MHN-..._1.jpg" } ``` --- ### Progress Tracking #### `GET /api/sabdopalon/surat/progress` Timeline status permohonan. **Query (semua required):** `kode` (NoTrMohon), `kab`, `kec`, `desa` **Response:** ```json { "success": true, "data": [ { "NoUrutProgress": "1", "StatusProgress": "Pemohon/Masyarakat", "Tanggal": "2026-06-02T03:15:00.000Z", "Waktu": "2026-06-02T03:15:00.000Z", "UserSender": "BUDI" } ] } ``` --- ### Download File Hasil Surat #### `GET /api/sabdopalon/surat/download` Download file hasil surat (PDF/DOCX) dari folder output. File di-stream dengan header `Content-Disposition` sehingga langsung ter-download oleh client. **Query:** - `file` (required) — nama file output, mis. `MHN-20251007-00001_3517160001_20251118093835.pdf` - `inline` (opt) — `1`/`true` → tampilkan di browser (preview). Default `attachment` (paksa download). **Tipe file yang diizinkan:** `.pdf`, `.docx`, `.doc` (termasuk varian `_(nosigned).pdf`). **Contoh:** ```bash curl -H "X-API-Key: YOUR_API_TOKEN" \ -o surat.pdf \ "https://sabdopalon.jombangkab.go.id/v2/api/sabdopalon/surat/download?file=MHN-20251007-00001_3517160001_20251118093835.pdf" ``` **Response sukses (200):** binary file (bukan JSON) dengan header: - `Content-Type`: `application/pdf` / `...wordprocessingml.document` - `Content-Disposition`: `attachment; filename="..."` (atau `inline`) - `Cache-Control`: `private, no-store` **Error (JSON):** 400 (param kurang / nama file invalid / ext tidak diizinkan), 404 (file tidak ada), 500 (gagal baca). **Catatan keamanan:** - Anti path-traversal: tolak `/`, `\`, `..`, null-byte — nama wajib basename polos, di-resolve, lalu dipastikan tetap di dalam direktori output. - Direktori sumber: `OUTPUT_DIR` (env, default `/var/www/sabdopalon/assets/output`). --- ## Error Codes | HTTP | Meaning | Recommended action | |---|---|---| | 200 | Success | Process response | | 400 | Bad request — payload invalid/incomplete | Fix request, no retry | | 401 | Unauthorized — token/JWT invalid/expired | Re-login, no retry | | 403 | Forbidden — akun nonaktif / role tidak punya akses | Inform user | | 404 | Not found — resource (NIK/NoTrMohon) tidak ada | Inform user | | 409 | Conflict — duplicate (mis. permohonan WAITING sama) | Inform user | | 429 | Rate limit exceeded | Exponential backoff | | 500 | Internal error | Log, max 1-2x retry dengan delay | --- ## Complete Example: Warga Login → Submit Permohonan ```js const API_BASE = 'https://sabdopalon.jombangkab.go.id/v2'; const API_TOKEN = process.env.API_TOKEN; // 1. Request OTP await fetch(`${API_BASE}/api/auth/warga/request-otp`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ nik: '3517123456789012' }), }); // 2. User input OTP yang diterima di WA const otp = prompt('Masukkan OTP dari WA'); // 3. Verify OTP, dapat JWT const loginRes = await fetch(`${API_BASE}/api/auth/warga/verify-otp`, { method: 'POST', headers: { 'Authorization': `Bearer ${API_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ nik: '3517123456789012', otp }), }); const { token: jwt, user } = (await loginRes.json()); // 4. Submit permohonan dengan JWT const formData = new FormData(); formData.append('NamaPemohon', user.nama); formData.append('IDPend', user.nik); formData.append('KodeDesa', user.kodeDesa); formData.append('KodeKec', user.kodeKec); formData.append('KodeKab', user.kodeKab); formData.append('NamaDesa', user.namaDesa); formData.append('NamaKecamatan', user.namaKec); formData.append('JenisSurat', 'Surat Keterangan Domisili'); formData.append('StandarWaktuPelayanan', '3'); // + file syarat: formData.append('FileDoc[]', fileObject); etc. await fetch(`${API_BASE}/api/sabdopalon/permohonan/simpan`, { method: 'POST', headers: { 'X-API-Key': API_TOKEN, 'Authorization': `Bearer ${jwt}`, // JWT user }, body: formData, }); ``` --- ## Flutter/Dart Example ```dart import 'package:http/http.dart' as http; import 'dart:convert'; final apiToken = const String.fromEnvironment('API_TOKEN'); // Login operator final res = await http.post( Uri.parse('https://sabdopalon.jombangkab.go.id/v2/api/auth/operator/login'), headers: { 'Authorization': 'Bearer $apiToken', 'Content-Type': 'application/json', }, body: jsonEncode({'username': 'admin_desa', 'password': 'pwd'}), ); final data = jsonDecode(res.body); final jwt = data['token']; // simpan di secure storage ``` --- ## Versioning & Changelog API ini tidak punya versi explisit di URL. Breaking changes diumumkan via: - Email/WhatsApp ke kontak developer terdaftar - Update di file ini **Recent changes:** - 2026-06: Operator — verifikasi dokumen per-berkas (`POST /surat/verifikasi`) - 2026-06: Operator — list dokumen syarat (`/permohonan/dokumen`) + download syarat (`/surat/download-syarat`); inbox tambah `StatusProgress` - 2026-06: Tambah inbox operator (`GET /api/sabdopalon/permohonan/desa`) — scope wilayah dari JWT - 2026-06: Tambah endpoint download file hasil surat (`GET /api/sabdopalon/surat/download`) - 2026-06: Base URL diset ke `/v2` subpath via Apache reverse proxy (DNS `api.*` pending) - 2026-06: Tambah auth endpoints (operator login bcrypt, warga OTP WA) - 2026-06: API_TOKEN service-level auth, CORS allow-list, rate limit 120/min - 2026-06: File upload validation (MIME + extension whitelist) --- ## Contact - Repo private: `github.com/fathurroom/api_sabdopalon` - Issue/bug: kontak admin DPMD Kab. Jombang - Security report: jangan post issue publik, kirim encrypted ke admin