API v1

Dokumentasi API Reseller

Endpoint ini tersedia untuk member Gold dan Diamond. Semua request harus disertai header autentikasi HMAC-SHA256. API Key tersedia di Member Area.

Aktivasi api key? setelah daftar langsung WA kita: 0851-8864-4198

Base URL

https://isiin.id/api/v1

Autentikasi

Setiap request wajib menyertakan tiga header berikut:

X-Api-Key: {api_key}
X-Timestamp: {unix_timestamp_detik}
X-Signature: {hmac_sha256}

Formula Signature

payload = METHOD + "\n" + path + "\n" + timestamp + "\n" + SHA256(request_body)
X-Signature = HMAC-SHA256(payload, api_secret)

Timestamp harus dalam 5 menit dari waktu server. Body kosong = SHA256 dari string kosong.

Contoh (PHP)

$timestamp = time();
$body      = json_encode(['product_code' => 'TLP5000', 'target_number' => '08123456789']);
$bodyHash  = hash('sha256', $body);
$payload   = implode("\n", ['POST', '/api/v1/transaction', $timestamp, $bodyHash]);
$signature = hash_hmac('sha256', $payload, $apiSecret);

HTTP Status Codes

200 OK — pembayaran sync berhasil / status transaksi
202 Accepted — pembelian masuk antrian (async)
400 Bad Request — inquiry gagal (nomor tidak ditemukan, tagihan tidak ada)
401 Unauthorized — header tidak valid / timestamp kedaluwarsa
402 Payment Required — saldo tidak mencukupi
403 Forbidden — level bukan Gold/Diamond
404 Not Found
422 Unprocessable — field wajib tidak ada / tipe produk tidak sesuai / inquiry_id tidak valid atau expired / inquiry_total tidak cocok dengan data inquiry
429 Too Many Requests — melampaui batas 100 request/menit
500 Internal Server Error — gagal terbitkan tiket pesawat (saldo dikembalikan otomatis)
503 Service Unavailable — maintain mode aktif

Callback Notification

Saat transaksi berhasil (status=success), sistem akan mengirimkan HTTP POST ke Callback URL yang Anda daftarkan di Member Area → API Key.

Format Payload (JSON)

{
  "event": "transaction.success",
  "order_id": "TRX20260508143201A3F9C12B",
  "status": "success",
  "product_id": "uuid",
  "target_number": "08123456789",
  "amount": 6500,
  "provider_ref": "SN_OR_REF_ID",
  "processed_at": "2026-05-08 14:32:05"
}
  • Timeout pengiriman: 5 detik. Server Anda harus merespons dengan HTTP 2xx.
  • Kegagalan pengiriman dicatat di log sistem — transaksi tetap sukses.
  • Tidak ada retry otomatis. Gunakan GET /api/v1/transaction/{order_id} untuk memverifikasi status.
  • Callback URL dapat diatur di Member Area → API Key tanpa perlu regenerasi key.

Timestamp: Toleransi ±5 menit dari waktu server. Pastikan waktu sistem Anda sinkron (NTP). Kirim ulang request yang gagal karena 401 dengan timestamp baru.

Umum

GET
/api/v1/balance

Cek Saldo

Informasi saldo deposit dan level member.

Response

{
  "level": "gold",
  "balance": 500000,
  "currency": "IDR"
}
GET
/api/v1/products

Daftar Produk

Kembalikan semua produk aktif beserta harga untuk level member Anda. Field `code` digunakan sebagai `product_code` di endpoint transaksi. Filter opsional: `?category={slug}`.

Response

{
  "data": [
    {
      "code": "TLP5000",
      "name": "Telkomsel 5.000",
      "category": "Pulsa",
      "category_slug": "pulsa",
      "price": 6500,
      "is_active": true
    }
  ],
  "total": 1,
  "level": "gold"
}

Transaksi PPOB & Pulsa

POST
/api/v1/inquiry

Inquiry Tagihan (Pembayaran)

Cek data pelanggan sebelum melakukan transaksi pembayaran (PLN, BPJS, air, dll). Hanya tersedia untuk produk dengan tipe `pembayaran`. Tidak memotong saldo. Response menyertakan `inquiry_id` yang berlaku 15 menit — wajib disertakan saat memanggil POST /api/v1/transaction untuk produk pembayaran. Khusus produk **BPJS Kesehatan** (`code: BPJS1`), field `periode` wajib diisi angka 1-12 (jumlah bulan tagihan yang dibayar) — request akan ditolak `422` dengan pesan "periode pembayaran bpjs salah" jika kosong atau di luar rentang tersebut.

Request Body (JSON)

// Umum (PLN, air, dll)
{
  "product_code": "PLN20",
  "target_number": "530000123456"
}

// BPJS Kesehatan — periode wajib (1-12)
{
  "product_code": "BPJS1",
  "target_number": "0001234567890",
  "periode": "3"
}

Response

{
  "inquiry_id": "550e8400-e29b-41d4-a716-446655440000",
  "product_code": "PLN20",
  "target_number": "530000123456",
  "customer_name": "BUDI SANTOSO",
  "bill_amount": 250000,
  "admin_fee": 2500,
  "total_amount": 252500,
  "period": "202504",
  "message": "Berhasil"
}
POST
/api/v1/transaction

Buat Transaksi (Live)

Buat transaksi dan potong saldo langsung. Perilaku berbeda berdasarkan tipe produk: **pembelian** (pulsa, paket data) → tidak memerlukan inquiry, status awal `pending`, diproses async, response `202`; **pembayaran** (PLN, BPJS) → wajib sertakan `inquiry_id` dari hasil POST /api/v1/inquiry (berlaku 15 menit), diproses synchronous, response `200` dengan status final dan SN. Field `inquiry_total` bersifat opsional — jika dikirim harus cocok persis dengan `total_amount` dari inquiry (bill_amount + admin_fee).

Request Body (JSON)

// Pembelian (pulsa, paket data)
{
  "product_code": "TLP5000",
  "target_number": "08123456789"
}

// Pembayaran (PLN, BPJS, dll) — inquiry_id wajib
{
  "product_code": "PLN20",
  "target_number": "530000123456",
  "inquiry_id": "550e8400-e29b-41d4-a716-446655440000",
  "inquiry_total": 252500
}

Response

// Pembelian (async) — HTTP 202
{
  "order_id": "TRX20260508143201A3F9C12B",
  "status": "pending",
  "product_code": "TLP5000",
  "target_number": "08123456789",
  "amount": 6500,
  "saldo_terpotong": 6500,
  "sisa_saldo": 493500,
  "message": "Transaction queued. Poll GET /api/v1/transaction/{order_id} for status."
}

// Pembayaran (sync) — HTTP 200
{
  "order_id": "TRX20260508143201A3F9C12B",
  "status": "success",
  "product_code": "PLN20",
  "target_number": "530000123456",
  "amount": 252500,
  "saldo_terpotong": 252500,
  "sisa_saldo": 247500,
  "sn": "TOKEN:1234-5678-9012-3456",
  "message": "Transaksi berhasil"
}
GET
/api/v1/transaction/{order_id}

Status Transaksi

Ambil status transaksi live berdasarkan `order_id`. Gunakan untuk polling transaksi pembelian async.

Response

{
  "order_id": "TRX20260508143201A3F9C12B",
  "status": "success",
  "product_code": "TLP5000",
  "target_number": "08123456789",
  "amount": 6500,
  "sn": null,
  "created_at": "2026-05-08 14:32:01"
}
POST
/api/v1/demo/transaction?simulate=success

Demo — Buat Transaksi

Simulasi transaksi tanpa memotong saldo dan tanpa hit provider. Parameter `simulate`: `success`, `pending`, atau `failed`.

Request Body (JSON)

{
  "product_code": "TLP5000",
  "target_number": "08123456789"
}

Response

{
  "order_id": "DEMO20260508143201A3F9",
  "status": "success",
  "product_code": "TLP5000",
  "target_number": "08123456789",
  "amount": 6500,
  "demo": true,
  "message": "Demo mode: status simulated as 'success'. No balance deducted."
}
GET
/api/v1/demo/transaction/{order_id}

Demo — Status Transaksi

Ambil status transaksi demo berdasarkan `order_id` (prefix `DEMO`).

Response

{
  "order_id": "DEMO20260508143201A3F9",
  "status": "success",
  "product_code": "TLP5000",
  "demo": true,
  "created_at": "2026-05-08 14:32:01"
}

Tiket Pesawat

POST
/api/v1/flight/search

1. Cari Jadwal Penerbangan

Cari jadwal penerbangan berdasarkan rute, tanggal, dan jumlah penumpang. Response berisi array penerbangan berangkat (`depart`) dan pulang (`return`, hanya untuk `roundtrip`).

Request Body (JSON)

{
  "origin": "CGK",
  "destination": "SUB",
  "depart_date": "2026-06-10",
  "trip_type": "oneway",
  "pax_adult": 1,
  "pax_child": 0,
  "pax_infant": 0
}

Response

{
  "depart": [
    {
      "airline_code": "GA",
      "airline_name": "Garuda Indonesia",
      "flight_number": "GA-401",
      "depart_time": "06:00",
      "arrive_time": "08:10",
      "duration": "2j 10m",
      "origin": "CGK",
      "destination": "SUB",
      "depart_date": "2026-06-10",
      "cabin_class": "Ekonomi",
      "available": 9,
      "transit": 0,
      "base_price": 850000,
      "final_price": 885000,
      "final_price_per_pax": 885000,
      "search_key": "MOCK-SK-GA401-..."
    }
  ],
  "return": []
}
POST
/api/v1/flight/preview-addons

1b. Preview Bagasi & Kursi (Opsional)

Lihat katalog bagasi & meal asli dari maskapai, serta peta kursi, untuk sebuah penerbangan hasil `/search` — SEBELUM booking dibuat (tidak menyimpan apapun, tidak memotong saldo). Sama untuk semua maskapai, termasuk AirAsia (QZ/XT) — tidak ada endpoint terpisah per maskapai. Ketersediaan bergantung provider; jika `baggage`/`meal`/`seats` kosong berarti maskapai tersebut tidak menyediakan add-on ini. Field `flight` sama seperti field `flight` di `/book` (tanpa perlu `passengers`).

Request Body (JSON)

{
  "flight": {
    "airline_code": "GA",
    "airline_name": "Garuda Indonesia",
    "flight_number": "GA-401",
    "origin": "CGK",
    "destination": "SUB",
    "depart_date": "2026-06-10",
    "depart_time": "06:00",
    "arrive_time": "08:10",
    "cabin_class": "Ekonomi",
    "ticket_price": 850000,
    "sales_price": 850000,
    "search_key": "MOCK-SK-GA401-...",
    "trip_type": "oneway",
    "pax_adult": 1,
    "pax_child": 0,
    "pax_infant": 0
  }
}

Response

{
  "baggage": {
    "PBAA": {"desc": "Check-In Baggage - 15kg", "price": 0},
    "PBAB": {"desc": "Check-In Baggage - 20kg", "price": 88000}
  },
  "meal": {
    "NPCB": {"desc": "Nasi Padang", "price": 33900}
  },
  "seats": [
    {
      "origin": "CGK", "destination": "SUB",
      "departTime": "2026-06-10T06:00:00",
      "arrivalTime": "2026-06-10T08:10:00",
      "infos": [
        {"X":1,"Y":8,"seatDesignator":"1A","seatType":"NS","seatPrice":106900,"assignable":true,"isOpen":true}
      ]
    }
  ]
}

// Maskapai tidak menyediakan add-on ini — HTTP 200 dengan baggage/meal/seats kosong
{ "baggage": {}, "meal": {}, "seats": [] }
POST
/api/v1/flight/book

2. Booking (HOLD Kursi)

Booking Tiket: Endpoint ini untuk membuat booking tiket, dapat `booking_code` + `time_limit`, saldo BELUM dipotong dan tiket BELUM diterbitkan. Field `flight` berisi data penerbangan dari `/api/v1/flight/search` (termasuk `search_key`). Field `passengers` array sesuai jumlah `pax_adult` + `pax_child` + `pax_infant`. Setelah booking, opsional pilih kursi via <code>GET/PUT /seats</code> dan bagasi/meal via <code>GET/PUT /addons</code> — tersedia untuk semua maskapai, tapi katalognya bergantung provider (tidak semua maskapai menyediakan add-on ini). Lanjutkan ke `POST /api/v1/flight/{order_id}/issue` sebelum `time_limit` untuk menerbitkan tiket.

Request Body (JSON)

{
  "flight": {
    "airline_code": "GA",
    "airline_name": "Garuda Indonesia",
    "flight_number": "GA-401",
    "origin": "CGK",
    "destination": "SUB",
    "depart_date": "2026-06-10",
    "depart_time": "06:00",
    "arrive_time": "08:10",
    "cabin_class": "Ekonomi",
    "ticket_price": 850000,
    "sales_price": 850000,
    "search_key": "MOCK-SK-GA401-...",
    "trip_type": "oneway",
    "pax_adult": 1,
    "pax_child": 0,
    "pax_infant": 0
  },
  "passengers": [
    {
      "type": "adult",
      "title": "Tn.",
      "first_name": "Budi",
      "last_name": "Santoso",
      "nationality": "ID",
      "id_number": "3201234567890001"
    }
  ],
  "contact_email": "budi@email.com",
  "contact_phone": "08123456789"
}

Response

// Sukses — HTTP 201 (HOLD, saldo belum dipotong)
{
  "order_id": "FLT-XXXX260610-260602",
  "status": "booking",
  "booking_code": "DW-XXXXXX",
  "booking_code_airline": "ABCD12",
  "origin": "CGK",
  "destination": "SUB",
  "depart_date": "2026-06-10",
  "flight_number": "GA-401",
  "time_limit": "2026-06-02 11:30:00",
  "amount": 885000,
  "next_step": "POST /api/v1/flight/FLT-XXXX260610-260602/issue",
  "message": "Booking berhasil (HOLD). Tiket belum diterbitkan dan saldo belum dipotong. Terbitkan tiket via endpoint issue sebelum time_limit."
}

// Saldo kurang — HTTP 402
{
  "error": "Insufficient balance.",
  "balance": 100000,
  "required": 885000
}
GET
/api/v1/flight/{order_id}/seats

3. Lihat Seatmap (Opsional)

Ambil data peta kursi (seatmap) untuk booking yang masih berstatus <code>booking</code>. Response berisi layout kursi pesawat dengan koordinat X/Y, harga per kursi (informasi saja — sudah termasuk di harga tiket), dan status ketersediaan.

Request Body (JSON)

// Tidak ada body. order_id diambil dari path.
GET /api/v1/flight/FLT-XXXX260610-260602/seats

Response

{
  "order_id": "FLT-XXXX260610-260602",
  "seats": [
    {
      "origin": "SUB", "destination": "CGK",
      "departTime": "2026-07-24T05:00:00",
      "arrivalTime": "2026-07-24T06:25:00",
      "infos": [
        {"X":1,"Y":8,"seatDesignator":"1A","seatType":"NS","seatPrice":106900,"assignable":true,"isOpen":true},
        {"X":3,"Y":8,"seatDesignator":"1B","seatType":"NS","seatPrice":106900,"assignable":true,"isOpen":false}
      ]
    }
  ],
  "passengers": [
    {"type":"adult","name":"TN. BUDI SANTOSO","seat":"1A"},
    {"type":"child","name":"NN. PUTRI SANTOSO","seat":""}
  ]
}

// seatType: NS=kursi, LV=lavatory, LR=label, EX=exit, WG=wing, BH=bulkhead
// assignable+isOpen=true: kursi tersedia untuk dipilih
// assignable=true+isOpen=false: kursi sudah terisi
// X layout: 1=A, 3=B, 5=C, (aisle), 9=D, 11=E, 13=F
PUT
/api/v1/flight/{order_id}/seats

3b. Pilih Kursi (Opsional)

Update pilihan kursi penumpang sebelum terbitkan tiket. Tersedia untuk semua maskapai. Field <code>seats</code> berisi mapping index penumpang (non-infant, dimulai dari 0) ke kode kursi dari seatmap. Biaya kursi sudah termasuk di harga tiket, tidak ada biaya tambahan.

Request Body (JSON)

PUT /api/v1/flight/FLT-XXXX260610-260602/seats

{
  "seats": {
    "0": "10A",
    "1": "10B"
  }
}

Response

// Sukses — HTTP 200
{
  "order_id": "FLT-XXXX260610-260602",
  "message": "Pilihan kursi berhasil diperbarui.",
  "passengers": [
    {"type":"adult","name":"TN. BUDI SANTOSO","seat":"10A"},
    {"type":"child","name":"NN. PUTRI SANTOSO","seat":"10B"}
  ]
}

// Booking sudah issued — HTTP 400
{ "error": "Booking tidak valid." }
GET
/api/v1/flight/{order_id}/addons

3c. Lihat Katalog Bagasi & Meal (Opsional)

Ambil katalog bagasi & meal asli dari maskapai untuk booking yang masih berstatus <code>booking</code>. Ketersediaan bergantung pada maskapai — provider tidak selalu menyediakan add-on ini untuk semua maskapai; jika <code>baggage</code>/<code>meal</code> kosong berarti tidak ada pilihan tambahan untuk penerbangan tersebut.

Request Body (JSON)

// Tidak ada body. order_id diambil dari path.
GET /api/v1/flight/FLT-XXXX260610-260602/addons

Response

{
  "order_id": "FLT-XXXX260610-260602",
  "baggage": {
    "PBAA": {"desc": "Check-In Baggage - 15kg", "price": 0},
    "PBAB": {"desc": "Check-In Baggage - 20kg", "price": 88000}
  },
  "meal": {
    "NPCB": {"desc": "Nasi Padang", "price": 33900}
  },
  "passengers": [
    {"type":"adult","name":"TN. BUDI SANTOSO","seat":"10A","baggage_kg":0,"baggage_code":null,"baggage_description":null,"meal_code":null,"meal_description":null}
  ]
}

// Maskapai tidak menyediakan add-on ini — HTTP 200 dengan baggage/meal kosong
{ "order_id": "FLT-XXXX260610-260602", "baggage": {}, "meal": {}, "passengers": [...] }
PUT
/api/v1/flight/{order_id}/addons

3d. Pilih Bagasi & Meal (Opsional)

Update pilihan bagasi & meal penumpang sebelum terbitkan tiket, memakai kode dari <code>GET /addons</code>. Field <code>selections</code> berisi mapping index penumpang (non-infant, dimulai dari 0) ke <code>baggage_code</code>/<code>meal_code</code> — keduanya opsional per penumpang. Kode yang tidak ada di katalog akan ditolak.

Request Body (JSON)

PUT /api/v1/flight/FLT-XXXX260610-260602/addons

{
  "selections": {
    "0": {"baggage_code": "PBAB", "meal_code": "NPCB"}
  }
}

Response

// Sukses — HTTP 200
{
  "order_id": "FLT-XXXX260610-260602",
  "message": "Pilihan bagasi & meal berhasil diperbarui.",
  "passengers": [
    {"type":"adult","name":"TN. BUDI SANTOSO","seat":"10A","baggage_kg":20,"baggage_code":"PBAB","baggage_description":"Check-In Baggage - 20kg","meal_code":"NPCB","meal_description":"Nasi Padang"}
  ]
}

// Kode tidak dikenal di katalog — HTTP 400
{ "error": "Kode bagasi XYZ tidak dikenal." }

// Booking sudah issued — HTTP 400
{ "error": "Booking tidak valid." }
POST
/api/v1/flight/{order_id}/issue

4. Terbitkan Tiket (Issued)

Langkah selanjutnya setelah booking (dan opsional pilih kursi): memotong saldo lalu menerbitkan tiket. Panggil dengan `order_id` dari response `/book`, sebelum `time_limit` habis. Jika penerbitan gagal setelah saldo dipotong, saldo dikembalikan otomatis. <strong>Idempotent</strong> — jika tiket sudah terbit, mengembalikan info tiket tanpa memotong saldo lagi. Tidak perlu request body.

Request Body (JSON)

// Tidak ada body. order_id diambil dari path.
POST /api/v1/flight/FLT-XXXX260610-260602/issue

Response

// Sukses — HTTP 200 (saldo dipotong, tiket terbit)
{
  "order_id": "FLT-XXXX260610-260602",
  "status": "issued",
  "booking_code": "DW-XXXXXX",
  "booking_code_airline": "ABCD12",
  "origin": "CGK",
  "destination": "SUB",
  "depart_date": "2026-06-10",
  "flight_number": "GA-401",
  "amount": 885000,
  "saldo_terpotong": 885000,
  "sisa_saldo": 4115000,
  "message": "Tiket berhasil diterbitkan"
}

// Booking tidak ditemukan / bukan milik Anda — HTTP 404
{ "error": "Booking not found." }

// Status bukan booking (mis. sudah failed) — HTTP 409
{ "error": "Booking tidak dapat diterbitkan (status: failed)." }

// Saldo kurang — HTTP 402
{ "error": "Insufficient balance.", "balance": 100000, "required": 885000 }
GET
/api/v1/flight/{order_id}

5. Status / Detail Tiket

Ambil detail booking tiket pesawat berdasarkan `order_id` (prefix `FLT-`). Hanya mengembalikan booking milik member yang terautentikasi.

Response

{
  "order_id": "FLT-XXXX260610-260602",
  "status": "issued",
  "booking_code": "DW-MOCK-XXXXXX",
  "booking_code_airline": "MOCKABCD",
  "origin": "CGK",
  "destination": "SUB",
  "trip_type": "oneway",
  "depart_date": "2026-06-10",
  "return_date": null,
  "airline": "GA",
  "flight_number": "GA-401",
  "cabin_class": "Ekonomi",
  "pax_adult": 1,
  "pax_child": 0,
  "pax_infant": 0,
  "amount": 885000,
  "issued_at": "2026-06-02 10:00:00",
  "created_at": "2026-06-02 10:00:00",
  "passengers": [
    {
      "type": "adult",
      "name": "TN. BUDI SANTOSO",
      "id_number": "3201234567890001",
      "seat": null,
      "baggage_kg": 0
    }
  ]
}

Hotel

GET
/api/v1/hotel/cities

Daftar Kota Hotel

Ambil daftar semua kota yang tersedia untuk pencarian hotel. Gunakan `city_code` dari response sebagai parameter `city_code` di endpoint pencarian hotel. Data di-cache selama 24 jam.

Response

{
  "cities": [
    { "city_code": "17193", "city_name": "Bali" },
    { "city_code": "18943", "city_name": "Bandung" },
    { "city_code": "8691",  "city_name": "Jakarta" },
    { "city_code": "12345", "city_name": "Medan" },
    { "city_code": "10779", "city_name": "Surabaya" },
    { "city_code": "11651", "city_name": "Yogyakarta" }
  ]
}
POST
/api/v1/hotel/search-by-name

1a. Cari Hotel Berdasarkan Nama

Cari hotel berdasarkan nama. Mengembalikan daftar hotel beserta `city_code`-nya dari seluruh kota. Gunakan `city_code` dari response untuk pencarian ketersediaan via endpoint `POST /api/v1/hotel/search`. Query minimal 2 karakter, hasil maksimal 10 hotel.

Request Body (JSON)

{
  "hotel_name": "Grand Hyatt"
}

Response

{
  "hotels": [
    {
      "hotel_code": "648932~||~10",
      "hotel_name": "Grand Hyatt Jakarta",
      "city_code": "8691",
      "city_name": "Jakarta",
      "stars": 5
    }
  ]
}
POST
/api/v1/hotel/search

1b. Cari Hotel Berdasarkan Kota

Cari hotel berdasarkan kota tujuan, tanggal check-in/out, jumlah kamar, dan tamu. Gunakan `city_code` dari `GET /api/v1/hotel/cities` atau dari response `POST /api/v1/hotel/search-by-name`. Simpan `hotel_code` & `internal_code` dari response untuk langkah berikutnya.

Request Body (JSON)

{
  "city_code": "8691",
  "check_in": "2026-07-10",
  "check_out": "2026-07-12",
  "rooms": 1,
  "adult_count": 2,
  "child_count": 0
}

Response

{
  "hotels": [
    {
      "hotel_code": "648932~||~10",
      "internal_code": "SUP",
      "hotel_name": "B Fashion Hotel",
      "hotel_stars": 3,
      "city_code": "8691",
      "city_name": "Jakarta",
      "address": "Jl. Aranda No.1, Jakarta",
      "logo": "https://.../hotel/logo?id=648932",
      "facilities": ["Laundry"],
      "rating": 5,
      "available": true,
      "price_per_night": 608162,
      "net_total": 1216324,
      "display_price_per_night": 632489
    }
  ]
}
POST
/api/v1/hotel/rooms

2. Kamar Tersedia

Ambil daftar tipe kamar tersedia untuk hotel dan tanggal yang dipilih. Sertakan `hotel_code`, `internal_code`, dan `city_code` dari response pencarian. Simpan `room_id`, `breakfast_raw`, dan `net_total` dari response untuk langkah booking.

Request Body (JSON)

{
  "hotel_code": "648932~||~10",
  "internal_code": "SUP",
  "city_code": "8691",
  "check_in": "2026-07-10",
  "check_out": "2026-07-12",
  "rooms": 1,
  "adult_count": 2,
  "child_count": 0
}

Response

{
  "hotel_code": "648932~||~10",
  "check_in": "2026-07-10",
  "check_out": "2026-07-12",
  "nights": 2,
  "rooms": 1,
  "room_types": [
    {
      "room_id": "3055171|roomCateg...|648932~||~10~||~SUP",
      "room_type_name": "Deluxe King Bed",
      "breakfast": true,
      "breakfast_raw": "Breakfast",
      "on_request": false,
      "facilities": [],
      "price_per_night": 608162,
      "net_total": 1216324,
      "final_price_per_night": 644989,
      "final_total": 1289977
    }
  ]
}
GET
/api/v1/hotel/images/{hotel_code}

3. Gambar Hotel

Ambil daftar gambar/foto hotel berdasarkan `hotel_code` dari response pencarian. Berguna untuk menampilkan galeri foto hotel di aplikasi Anda.

Response

{
  "hotel_code": "648932~||~10",
  "images": [
    {
      "url": "https://image_source.com/hotel/logo?id=648932&type=img&idx=0",
      "caption": "Lobby"
    },
    {
      "url": "https://image_source.com/hotel/logo?id=648932&type=img&idx=1",
      "caption": "Deluxe Room"
    }
  ]
}
POST
/api/v1/hotel/book

4. Pesan & Terbitkan Voucher Hotel

Buat booking hotel dan langsung terbitkan voucher (synchronous). Saldo dipotong otomatis. Jika penerbitan gagal, saldo dikembalikan. Field `room` berisi data kamar yang dipilih dari `/api/v1/hotel/rooms` (wajib: `hotel_code`, `internal_code`, `room_id`, `breakfast_raw`, `net_total`, `city_code`, tanggal). Field `guests` adalah array data tamu (minimal 1 tamu utama).

Request Body (JSON)

{
  "room": {
    "hotel_code": "648932~||~10",
    "internal_code": "SUP",
    "hotel_name": "B Fashion Hotel",
    "hotel_stars": 3,
    "city_code": "8691",
    "city_name": "Jakarta",
    "room_id": "3055171|roomCateg...|648932~||~10~||~SUP",
    "room_type_name": "Deluxe King Bed",
    "breakfast": true,
    "breakfast_raw": "Breakfast",
    "check_in": "2026-07-10",
    "check_out": "2026-07-12",
    "nights": 2,
    "rooms": 1,
    "adult_count": 2,
    "child_count": 0,
    "net_total": 1216324
  },
  "guests": [
    {
      "title": "Tn.",
      "first_name": "Budi",
      "last_name": "Santoso",
      "nationality": "ID",
      "id_number": "3201234567890001"
    }
  ],
  "contact_email": "budi@email.com",
  "contact_phone": "08123456789"
}

Response

// Sukses — HTTP 200
{
  "order_id": "HTL-DPS1N2-260710-A3F9",
  "status": "issued",
  "booking_code": "DW-HTL-XXXXXXXX",
  "hotel_name": "Grand Hyatt Bali",
  "check_in": "2026-07-10",
  "check_out": "2026-07-12",
  "amount": 4994000,
  "saldo_terpotong": 4994000,
  "sisa_saldo": 5006000,
  "message": "Voucher hotel berhasil diterbitkan"
}

// Saldo kurang — HTTP 402
{
  "error": "Insufficient balance.",
  "balance": 1000000,
  "required": 4994000
}
GET
/api/v1/hotel/{order_id}

5. Status / Detail Voucher Hotel

Ambil detail booking hotel berdasarkan `order_id` (prefix `HTL-`). Hanya mengembalikan booking milik member yang terautentikasi.

Response

{
  "order_id": "HTL-DPS1N2-260710-A3F9",
  "status": "issued",
  "booking_code": "DW-HTL-XXXXXXXX",
  "hotel_name": "Grand Hyatt Bali",
  "hotel_stars": 5,
  "city": "Bali",
  "room_type": "Deluxe Room",
  "check_in": "2026-07-10",
  "check_out": "2026-07-12",
  "nights": 2,
  "rooms": 1,
  "guests_adult": 2,
  "guests_child": 0,
  "amount": 4994000,
  "issued_at": "2026-07-08 10:00:00",
  "guests": [
    {
      "name": "TN. BUDI SANTOSO",
      "nationality": "ID",
      "id_number": "3201...",
      "is_primary": true
    }
  ]
}

Tiket Kereta Api

POST
/api/v1/train/search

1. Cari Jadwal Kereta

Cari jadwal kereta berdasarkan stasiun asal, tujuan, tanggal keberangkatan, dan jumlah penumpang. `train_id` adalah operator kereta (opsional, default `KAI`; gunakan `RLK` untuk Railink/kereta bandara). Kode stasiun (mis. `GMR`, `BD`, `SGU`) mengikuti kode resmi KAI. Response berisi array `trains` — satu baris per sub-kelas yang tersedia pada tiap jadwal. Harga sudah termasuk markup + admin sesuai level member. **Simpan seluruh object train dari response untuk dipakai di `/book`** (tidak ada lagi `search_key`).

Request Body (JSON)

{
  "train_id": "KAI",
  "origin_code": "BD",
  "destination_code": "GMR",
  "depart_date": "2026-07-15",
  "pax_adult": 1,
  "pax_child": 0,
  "pax_infant": 0
}

Response

{
  "trains": [
    {
      "train_id": "KAI",
      "train_number": "101PRH",
      "train_name": "ARGO PARAHYANGAN",
      "origin_code": "BD",
      "origin_name": "Bandung",
      "destination_code": "GMR",
      "destination_name": "Gambir",
      "depart_date": "2026-07-15",
      "depart_time": "10:50",
      "arrive_time": "13:50",
      "availability_class": "EKS",
      "class_code": "EKS",
      "class_name": "Eksekutif",
      "sub_class": "A",
      "available": null,
      "available_label": "tersedia",
      "base_price": 123000,
      "markup_percent": 1.5,
      "markup_rupiah": 1845,
      "admin_fee": 15000,
      "price_per_pax": 124845,
      "total_price": 139845
    }
  ],
  "count": 4,
  "origin": "BD",
  "destination": "GMR",
  "date": "2026-07-15"
}
GET
/api/v1/train/seatmap

2. Peta Kursi (Opsional)

Ambil peta kursi kereta. **Peta kursi KAI hanya tersedia SETELAH booking** (butuh `booking_code` + `booking_date` dari response `/book`), sehingga endpoint ini hanya relevan jika Anda ingin menukar kursi yang sudah di-assign otomatis. Response berisi array `wagons`, masing-masing dengan `wagonCode`, `wagonNumber`, `totalRow`, `totalColumn`, dan `detail` (tiap kursi: `seatNumber`, `seatRow`, `seatColumn`, `isAvailable`). **Pilih kursi bersifat opsional** — pada alur standar sistem meng-assign kursi otomatis saat booking.

Request Body (JSON)

// Query params (booking_code & booking_date WAJIB untuk KAI):
GET /api/v1/train/seatmap?train_id=KAI&train_number=101PRH&sub_class=A
    &origin_code=BD&destination_code=GMR&depart_date=2026-07-15
    &booking_code=ZON95ER&booking_date=2026-07-13T10:09:17.380+07:00

Response

{
  "train_number": "101PRH",
  "sub_class": "A",
  "depart_date": "2026-07-15",
  "wagons": [
    {
      "wagonCode": "EKS",
      "wagonNumber": "1",
      "totalRow": 5,
      "totalColumn": 4,
      "detail": [
        { "row": 1, "column": 1, "seatRow": "1", "seatColumn": "A", "seatNumber": "1A", "subClass": "A", "isAvailable": false },
        { "row": 1, "column": 2, "seatRow": "1", "seatColumn": "B", "seatNumber": "1B", "subClass": "A", "isAvailable": true }
      ]
    }
  ]
}
POST
/api/v1/train/book

3. Pesan & Terbitkan Tiket Kereta

Alur: Booking → (kursi di-assign otomatis) → Issued, dalam satu request. Saldo dipotong otomatis; jika tiket gagal terbit, saldo dikembalikan. Field `train` = object dari `/api/v1/train/search` (wajib: `train_id`, `train_number`, `availability_class`, `sub_class`, `origin_code`, `destination_code`, `depart_date`). Field `passengers` = array data penumpang (`type`: adult/child/infant). Bayi (`infant`) tidak dihitung biaya/kursi. `seat_number` opsional. `status` bisa `issued` (Ticketed) atau `processing` bila tiket sedang divalidasi admin (saldo tetap terpotong, cek ulang via status).

Request Body (JSON)

{
  "train": {
    "train_id": "KAI",
    "train_number": "101PRH",
    "train_name": "ARGO PARAHYANGAN",
    "origin_code": "BD",
    "origin_name": "Bandung",
    "destination_code": "GMR",
    "destination_name": "Gambir",
    "depart_date": "2026-07-15",
    "depart_time": "10:50",
    "arrive_time": "13:50",
    "availability_class": "EKS",
    "class_name": "Eksekutif",
    "sub_class": "A",
    "base_price": 123000,
    "pax_adult": 1,
    "pax_child": 0,
    "pax_infant": 0
  },
  "passengers": [
    {
      "type": "adult",
      "title": "Tn.",
      "first_name": "Budi",
      "last_name": "Santoso",
      "gender": "M",
      "birth_date": "1990-05-15",
      "nationality": "ID",
      "id_number": "3201011505900007",
      "phone": "081234567890"
    }
  ],
  "contact_email": "budi@email.com",
  "contact_phone": "081234567890"
}

Response

{
  "order_id": "TRN-BDGMR-260715-X7K2",
  "booking_code": "ZON95ER",
  "status": "issued",
  "train_name": "ARGO PARAHYANGAN",
  "depart_date": "2026-07-15",
  "depart_time": "10:50",
  "final_price": 139845,
  "saldo_terpotong": 139845,
  "sisa_saldo": 860155
}

// Field tidak valid — HTTP 422
{
  "error": "Field tidak valid: train.sub_class, train.availability_class"
}

// Saldo kurang — HTTP 402
{
  "error": "Saldo tidak mencukupi. Saldo Anda: Rp 100.000"
}
GET
/api/v1/train/{order_id}

4. Status / E-Tiket Kereta

Ambil detail booking kereta berdasarkan `order_id` (prefix `TRN-`). Hanya mengembalikan booking milik member yang terautentikasi.

Response

{
  "order_id": "TRN-BDGMR-260715-X7K2",
  "train_id": "KAI",
  "booking_code": "ZON95ER",
  "booking_code_train": "ZON95ER",
  "train_number": "101PRH",
  "train_name": "ARGO PARAHYANGAN",
  "class_code": "EKS",
  "class_name": "Eksekutif",
  "sub_class": "A",
  "origin_code": "BD",
  "origin_name": "Bandung",
  "destination_code": "GMR",
  "destination_name": "Gambir",
  "depart_date": "2026-07-15",
  "depart_time": "10:50",
  "arrive_time": "13:50",
  "pax_adult": 1,
  "pax_child": 0,
  "pax_infant": 0,
  "final_price": 139845,
  "status": "issued",
  "issued_at": "2026-07-13 10:00:00",
  "passengers": [
    {
      "type": "adult",
      "title": "Tn.",
      "first_name": "Budi",
      "last_name": "Santoso",
      "nationality": "ID",
      "seat_number": "2A",
      "wagon_code": "EKS",
      "wagon_number": "1"
    }
  ]
}

Tiket Kapal Laut

POST
/api/v1/ship/search

1. Cari Jadwal Kapal

Cari jadwal kapal Pelni berdasarkan pelabuhan asal, tujuan, tanggal keberangkatan, dan jumlah penumpang. Response berisi array jadwal kapal beserta kelas yang tersedia. Harga sudah termasuk markup sesuai level member. Simpan seluruh object ship dari response untuk digunakan di `/book`.

Request Body (JSON)

{
  "origin_port": "TPRIOK",
  "destination_port": "MKSSAR",
  "depart_date": "2026-07-20",
  "pax_adult": 2,
  "pax_child": 0,
  "pax_infant": 0
}

Response

{
  "ships": [
    {
      "ship_number": "KM-001",
      "ship_name": "KM Umsini",
      "origin_port": "TPRIOK",
      "origin_name": "Tanjung Priok (Jakarta)",
      "destination_port": "MKSSAR",
      "destination_name": "Makassar",
      "class_code": "EKO",
      "class_name": "Ekonomi",
      "sub_class": "Ekonomi",
      "depart_datetime": "2026-07-20 08:00:00",
      "arrival_datetime": "2026-07-23 14:00:00",
      "adult_fare": 350000,
      "child_fare": 280000,
      "infant_fare": 50000,
      "hpp_total": 700000,
      "markup_percent": 3,
      "markup_rupiah": 21000,
      "admin_fee": 50000,
      "total_price": 771000
    }
  ],
  "count": 5,
  "origin_port": "TPRIOK",
  "origin_name": "Tanjung Priok (Jakarta)",
  "destination_port": "MKSSAR",
  "destination_name": "Makassar",
  "depart_date": "2026-07-20"
}
POST
/api/v1/ship/book

2. Pesan & Terbitkan Tiket Kapal

Buat booking dan langsung terbitkan tiket kapal dalam satu request. Sistem otomatis melakukan GetRoom (reservasi kamar), Booking, dan Issue. Saldo dipotong otomatis. Field `ship` berisi seluruh object jadwal dari `/search`. Field `passengers` adalah array data penumpang. Response mencakup `order_id` untuk cek status.

Request Body (JSON)

{
  "ship": {
    "ship_number": "KM-001",
    "ship_name": "KM Umsini",
    "origin_port": "TPRIOK",
    "origin_call": 101,
    "origin_name": "Tanjung Priok (Jakarta)",
    "destination_port": "MKSSAR",
    "destination_call": 201,
    "destination_name": "Makassar",
    "class_code": "EKO",
    "class_name": "Ekonomi",
    "sub_class": "Ekonomi",
    "depart_datetime": "2026-07-20 08:00:00",
    "arrival_datetime": "2026-07-23 14:00:00",
    "adult_fare": 350000,
    "child_fare": 280000,
    "infant_fare": 50000,
    "hpp_total": 700000,
    "total_price": 771000,
    "admin_fee": 50000
  },
  "passengers": [
    {
      "type": "adult",
      "title": "Tn.",
      "first_name": "Budi",
      "last_name": "Santoso",
      "birth_date": "1990-05-15",
      "id_number": "3174xxxxxxxx0001",
      "phone": "08123456789",
      "nationality": "ID"
    }
  ],
  "contact_email": "budi@email.com",
  "contact_phone": "08123456789"
}

Response

{
  "order_id": "SHP-TPRIMKSS-260715-A3B2",
  "booking_number": "DW-SHP-ABCD1234",
  "status": "issued",
  "ship_name": "KM Umsini",
  "depart_datetime": "2026-07-20 08:00:00",
  "final_price": 771000,
  "saldo_terpotong": 771000,
  "sisa_saldo": 1229000
}
GET
/api/v1/ship/{order_id}

3. Status / E-Tiket Kapal

Ambil detail booking kapal berdasarkan `order_id` (prefix `SHP-`). Hanya mengembalikan booking milik member yang terautentikasi. Response mencakup detail penumpang beserta nomor tiket, nomor kamar (deck/cabin/bed), dan QR code tiket individual.

Response

{
  "order_id": "SHP-TPRIMKSS-260715-A3B2",
  "booking_number": "DW-SHP-ABCD1234",
  "ship_number": "KM-001",
  "ship_name": "KM Umsini",
  "class_code": "EKO",
  "class_name": "Ekonomi",
  "origin_port": "TPRIOK",
  "origin_name": "Tanjung Priok (Jakarta)",
  "destination_port": "MKSSAR",
  "destination_name": "Makassar",
  "depart_datetime": "2026-07-20 08:00:00",
  "arrival_datetime": "2026-07-23 14:00:00",
  "pax_adult": 1,
  "pax_child": 0,
  "pax_infant": 0,
  "final_price": 771000,
  "status": "issued",
  "issued_at": "2026-07-15 10:00:00",
  "passengers": [
    {
      "type": "adult",
      "first_name": "Budi",
      "last_name": "Santoso",
      "deck": "Dek 5",
      "cabin": "Kabin 01",
      "bed": "Kasur 1",
      "ticket_number": "TKT-ABCDEF1234",
      "ticket_qr_code": "QR-MOCK-ABCDEF123456789"
    }
  ]
}

Tiket Kapal DLU

POST
/api/v1/ship-dlu/search

1. Cari Jadwal Kapal DLU

Cari jadwal kapal DLU berdasarkan pelabuhan asal, tujuan, dan tanggal. Response berisi array jadwal beserta pilihan fare (kelas/tipe tiket: penumpang atau kendaraan). Harga sudah termasuk markup sesuai level member. Simpan seluruh object fare yang dipilih untuk digunakan di `/book`.

Request Body (JSON)

{
  "origin_port": "TPRIOK",
  "destination_port": "BALI",
  "depart_date": "2026-07-20"
}

Response

{
  "schedules": [
    {
      "ship_number": "DLU-001",
      "ship_name": "KMP Leuser",
      "ship_id": "DLU-S001",
      "depart_datetime": "2026-07-20 07:00:00",
      "arrival_datetime": "2026-07-22 15:00:00",
      "origin_port": "TPRIOK",
      "origin_name": "Tanjung Priok (Jakarta)",
      "destination_port": "BALI",
      "destination_name": "Benoa (Bali)",
      "fares": [
        {
          "data": "EKO|PAX",
          "ticketType": "Penumpang",
          "class": "Ekonomi",
          "hpp_price": 280000,
          "markup_rupiah": 11200,
          "admin_fee": 50000,
          "total_price": 341200
        }
      ]
    }
  ],
  "count": 1
}
POST
/api/v1/ship-dlu/book

2. Pesan & Terbitkan Tiket Kapal DLU

Buat booking dan langsung terbitkan tiket kapal DLU dalam satu request (tidak ada step GetRoom/Booking terpisah). Saldo dipotong otomatis. Field `ship` berisi object jadwal dari `/search` ditambah info fare yang dipilih. Field `passengers` adalah array data penumpang. Field `vehicles` (opsional) untuk kendaraan yang diangkut.

Request Body (JSON)

{
  "ship": {
    "ship_number": "DLU-001",
    "ship_name": "KMP Leuser",
    "ship_id": "DLU-S001",
    "origin_port": "TPRIOK",
    "origin_name": "Tanjung Priok (Jakarta)",
    "destination_port": "BALI",
    "destination_name": "Benoa (Bali)",
    "depart_datetime": "2026-07-20 07:00:00",
    "arrival_datetime": "2026-07-22 15:00:00",
    "ticket_type": "Penumpang",
    "class_type": "Ekonomi",
    "pax_adult": 1,
    "pax_child": 0,
    "pax_infant": 0
  },
  "passengers": [
    {
      "type": "adult",
      "first_name": "Budi",
      "last_name": "Santoso",
      "gender": "M",
      "birth_date": "1990-01-15",
      "id_number": "3271xxxxxxxxxx",
      "nationality": "ID",
      "ticket_type": "Penumpang",
      "class_type": "Ekonomi",
      "fare": 280000
    }
  ],
  "vehicles": []
}

Response

{
  "order_id": "DLU-TPRIBALI-260715-A3B2",
  "booking_number": "DW-DLU-ABCD1234",
  "status": "issued",
  "ship_name": "KMP Leuser",
  "depart_datetime": "2026-07-20 07:00:00",
  "final_price": 341200,
  "saldo_terpotong": 341200,
  "sisa_saldo": 1658800
}
GET
/api/v1/ship-dlu/{order_id}

3. Status / E-Tiket Kapal DLU

Ambil detail booking kapal DLU berdasarkan `order_id` (prefix `DLU-`). Hanya mengembalikan booking milik member yang terautentikasi. Response mencakup detail penumpang beserta nomor tiket dan QR code, serta daftar kendaraan jika ada.

Response

{
  "order_id": "DLU-TPRIBALI-260715-A3B2",
  "booking_number": "DW-DLU-ABCD1234",
  "ship_number": "DLU-001",
  "ship_name": "KMP Leuser",
  "ticket_type": "Penumpang",
  "class_type": "Ekonomi",
  "origin_port": "TPRIOK",
  "origin_name": "Tanjung Priok (Jakarta)",
  "destination_port": "BALI",
  "destination_name": "Benoa (Bali)",
  "depart_datetime": "2026-07-20 07:00:00",
  "arrival_datetime": "2026-07-22 15:00:00",
  "pax_adult": 1,
  "pax_vehicle": 0,
  "final_price": 341200,
  "status": "issued",
  "issued_at": "2026-07-15 10:00:00",
  "passengers": [
    {
      "type": "adult",
      "first_name": "Budi",
      "last_name": "Santoso",
      "ticket_type": "Penumpang",
      "class_type": "Ekonomi",
      "ticket_number": "DLU-TKT-ABCDEF1234",
      "ticket_qr_code": "DLU-QR-ABCDEF123456789"
    }
  ],
  "vehicles": []
}

Tiket Bus

POST
/api/v1/bus/search

1. Cari Jadwal Bus

Cari jadwal bus berdasarkan terminal asal, tujuan, tanggal, dan jumlah penumpang. Harga yang dikembalikan sudah termasuk markup sesuai level member. Gunakan object schedule (termasuk `directCode`, `locationID`, `depart_id`, `arrival_id`, `selectedClass`) yang dipilih untuk request `/book`.

Request Body (JSON)

{
  "origin_terminal": "Jakarta Pulo Gadung",
  "destination_terminal": "Surabaya Bungurasih",
  "depart_date": "2026-07-20",
  "pax_adult": 1,
  "pax_child": 0,
  "pax_infant": 0,
  "bus": ""
}

Response

{
  "data": {
    "schedules": [
      {
        "directCode": "RI-JKT-SBY-001",
        "locationID": "LOC001",
        "operatorName": "Rosalia Indah",
        "busType": "Big Bus",
        "isAllowChooseSeat": true,
        "depart_time": "2026-07-20 07:00:00",
        "arrival_time": "2026-07-20 16:00:00",
        "depart_id": 1,
        "arrival_id": 2,
        "classes": [
          {
            "subClassFare": "EKS",
            "className": "Eksekutif",
            "adultHpp": 320000,
            "adultFareTotal": 349600
          }
        ]
      }
    ],
    "origin_terminal": "Jakarta Pulo Gadung",
    "destination_terminal": "Surabaya Bungurasih"
  }
}
POST
/api/v1/bus/book

2. Pesan & Terbitkan Tiket Bus

Booking dan terbitkan tiket bus dalam satu request. Sistem memanggil `Bus/Booking` lalu `Bus/Issued` secara atomik. Saldo dipotong otomatis. Field `bus` berisi data jadwal dari `/search`. Field `seats` adalah array nomor kursi (kosong jika bus tidak support pilih kursi). Field `passengers` adalah array data penumpang.

Request Body (JSON)

{
  "bus": {
    "bus_name": "Rosalia Indah",
    "origin_terminal": "Jakarta Pulo Gadung",
    "destination_terminal": "Surabaya Bungurasih",
    "direct_code": "RI-JKT-SBY-001",
    "location_id": "LOC001",
    "sub_class_fare": "EKS",
    "depart_date": "2026-07-20",
    "depart_id": 1,
    "arrival_id": 2,
    "pax_adult": 1,
    "pax_child": 0,
    "pax_infant": 0
  },
  "seats": ["A1"],
  "passengers": [
    {
      "pax_type": "adult",
      "title": "Tn.",
      "first_name": "Budi",
      "last_name": "Santoso",
      "identity": "3271xxxxxxxxxx",
      "identity_type": "KTP",
      "phone": "08123456789",
      "email": "budi@example.com",
      "address": "Jl. Contoh No. 1",
      "birth_date": "1990-01-15",
      "fare": 320000
    }
  ]
}

Response

{
  "data": {
    "order_id": "BUS-JAKASURA-260720-A3B2",
    "booking_code": "BK-ABCD1234EF",
    "reff_number": "REF-XYZ123456789",
    "final_price": 349600,
    "status": "issued",
    "saldo_terpotong": 349600,
    "sisa_saldo": 650400
  }
}
GET
/api/v1/bus/{order_id}

3. Status / E-Tiket Bus

Ambil detail booking bus berdasarkan `order_id` (prefix `BUS-`). Hanya mengembalikan booking milik member yang terautentikasi. Field `reff_number` adalah nomor tiket yang ditunjukkan ke petugas bus.

Response

{
  "data": {
    "order_id": "BUS-JAKASURA-260720-A3B2",
    "booking_code": "BK-ABCD1234EF",
    "reff_number": "REF-XYZ123456789",
    "bus_name": "Rosalia Indah",
    "origin_terminal": "Jakarta Pulo Gadung",
    "destination_terminal": "Surabaya Bungurasih",
    "depart_time": "2026-07-20 07:00:00",
    "seat_numbers": "A1",
    "final_price": 349600,
    "status": "issued",
    "issued_at": "2026-07-15 10:23:45",
    "passengers": [
      {
        "pax_type": "adult",
        "title": "Tn.",
        "first_name": "Budi",
        "last_name": "Santoso",
        "fare": 320000
      }
    ]
  }
}