JetWize Partner API
Integrasikan sistem Anda dengan platform pemesanan tiket penerbangan JetWize. API ini memungkinkan Anda mencari penerbangan, membuat booking, dan memproses pembayaran secara programatik.
Overview
| Base URL (Sandbox) | https://api.jetwize.com/api/v1/partner/v1 |
| Base URL (Production) | https://api.jetwize.com/api/v1/partner/v1 |
| Format | JSON (application/json) |
| Timeout | 30 detik |
Endpoint sandbox dan production menggunakan URL yang sama. Perbedaannya ditentukan oleh jenis API key yang digunakan (jwz_test_* atau jwz_live_*).
SDK & Tools
Impor koleksi Postman atau spesifikasi OpenAPI, set variabel apiKey ke sandbox key Anda (jwz_test_…), lalu jalankan dari atas ke bawah.
⬇ Postman Collection ⬇ OpenAPI 3.0 (JSON)
Di Postman: Import file koleksi di atas (sudah ada variabel {{baseUrl}} + {{apiKey}}), atau tempel URL partner-openapi.json untuk auto-generate koleksi dari OpenAPI.
Quickstart Sandbox — search → book → pay → e-ticket
Salin-tempel ke terminal. Sandbox mengembalikan inventori mock deterministik dan menyimulasikan pembayaran + penerbitan (tanpa uang/supplier nyata).
KEY=jwz_test_xxx # sandbox key Anda
BASE=https://api.jetwize.com/api/v1/partner/v1
# 1) Cari penerbangan (sandbox → offer mock)
curl -s -X POST $BASE/flights/search -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"origin":"CGK","destination":"DPS","departDate":"2026-07-15","passengers":{"adult":1,"child":0,"infant":0},"cabinClass":"ECONOMY"}'
# → salin results[0].offerId
# 2) Buat booking
curl -s -X POST $BASE/bookings -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"offerId":"OFFER_ID","contactEmail":"[email protected]","contactPhone":"+628123456789","passengers":[{"title":"MR","firstName":"Budi","lastName":"Santoso","dateOfBirth":"1990-01-15","nationality":"ID","type":"ADULT"}]}'
# → salin id booking
# 3) Bayar (sandbox: disimulasikan + tiket langsung terbit)
curl -s -X POST $BASE/bookings/BOOKING_ID/pay -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{}'
# 4) Ambil e-ticket
curl -s $BASE/bookings/BOOKING_ID/ticket -H "X-API-Key: $KEY"
Belum punya sandbox key? Buat di portal JetWize → API Keys → "Minta API Key" (sandbox instan), atau pakai Playground untuk menjalankan request ini langsung dari dashboard.
Environments
🧪 Sandbox (jwz_test_...)
- Menggunakan data penerbangan simulasi (tidak ada tiket nyata)
- Wallet saldo virtual: Rp 999.999.999 (tidak ada debit nyata)
- Booking dibuat tapi tidak diproses ke supplier
- Cocok untuk development dan testing integrasi
Semua transaksi nyata — menggunakan supplier flight aktif, debit wallet nyata. Pastikan integrasi sudah teruji di sandbox sebelum switch ke production.
Authentication
Semua request ke Partner API memerlukan API key. Gunakan salah satu metode berikut:
X-API-Key: jwz_test_a1b2c3d4e5f6...
Authorization: Bearer jwz_test_a1b2c3d4e5f6...
Mendapatkan API Key
API key dikelola melalui dashboard JetWize oleh admin agensi Anda:
- Login ke jetwize.com
- Buka Settings → API Keys
- Klik Buat API Key Baru
- Pilih environment: Sandbox atau Production
Common Errors
Semua error mengembalikan format JSON yang konsisten:
{
"error": {
"code": "BOOKING_EXPIRED",
"message": "Booking hold telah kadaluarsa",
"details": { "bookingId": "..." },
"traceId": "req-abc123"
}
}
Tabel di bawah ini berisi error yang bisa terjadi di endpoint mana pun (auth, validasi, server-side). Untuk error spesifik per-endpoint, lihat bagian Possible Errors di setiap endpoint.
| HTTP Status | Error Code | Keterangan |
|---|---|---|
| 401 | MISSING_API_KEY | Header X-API-Key tidak ada |
| 401 | INVALID_API_KEY | API key tidak valid atau dinonaktifkan |
| 401 | EXPIRED_API_KEY | API key sudah kadaluarsa |
| 401 | AGENCY_INACTIVE | Agensi tidak aktif / suspended |
| 403 | FORBIDDEN | API key tidak punya hak akses ke resource ini |
| 500 | INTERNAL_ERROR | Server error, retry dengan exponential backoff |
| 503 | SERVICE_UNAVAILABLE | Service sementara tidak tersedia (maintenance) |
GET /me — Info API Key
{
"apiKey": {
"id": "clx123...",
"name": "My Integration",
"environment": "SANDBOX"
},
"agency": {
"id": "clx456...",
"name": "PT Maju Travel"
},
"wallet": {
"balance": 999999999,
"holdBalance": 0,
"currency": "IDR"
},
"credit": {
"enabled": false,
"limit": 0,
"used": 0,
"available": 0
},
"sandbox": true
}
credit menampilkan limit kredit postpaid agensi (available = limit − used).
Flight Search
Cari penerbangan tersedia berdasarkan rute, tanggal, dan jumlah penumpang. Endpoint ini di-aggregate dari semua supplier maskapai aktif dan return list offer yang siap di-booking.
Request Body
| Field | Type | Keterangan |
|---|---|---|
| origin* | string | Kode IATA bandara asal. Contoh: CGK |
| destination* | string | Kode IATA bandara tujuan. Contoh: DPS |
| departDate* | string | Tanggal berangkat format YYYY-MM-DD |
| returnDate | string | Tanggal kembali (untuk pulang-pergi) |
| passengers* | object | { adult, child, infant } |
| cabinClass | string | ECONOMY / BUSINESS / FIRST. Default: ECONOMY |
POST /api/v1/partner/v1/flights/search
X-API-Key: jwz_test_...
{
"origin": "CGK",
"destination": "DPS",
"departDate": "2026-06-15",
"passengers": { "adult": 2, "child": 0, "infant": 0 },
"cabinClass": "ECONOMY"
}
{
"results": [
{
"id": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
"segments": [
{
"flightNumber": "GA405",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "CGK",
"destination": "DPS",
"departureTime": "2026-06-15T06:00:00.000Z",
"arrivalTime": "2026-06-15T08:55:00.000Z",
"duration": 115,
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
}
],
"stops": 0,
"totalDuration": 115,
"basePrice": 850000,
"tax": 93500,
"totalPrice": 943500,
"currency": "IDR",
"seatsAvailable": 9
}
],
"count": 5,
"sandbox": true
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | INVALID_QUERY | Origin / destination / departDate format salah atau tidak ada |
| 400 | INVALID_DATE | Tanggal berangkat di masa lalu, atau returnDate < departDate |
| 400 | INVALID_ROUTE | Origin = destination, atau IATA code tidak dikenali |
| 400 | INVALID_PASSENGER_COUNT | Total pax 0, atau infant > adult (rule lap-infant) |
| 404 | NO_FARE_FOUND | Tidak ada fare di rute+tanggal ini dari supplier aktif |
| 502 | SUPPLIER_ERROR | Semua supplier down — coba lagi nanti |
| 503 | SUPPLIER_UNAVAILABLE | Sebagian supplier rate-limited (LTB / quota circuit open) |
Airport Search
Autocomplete bandara/kota berdasarkan kode IATA, nama bandara, kota, atau negara. Berguna untuk membangun input pencarian penerbangan.
Query Parameters
| Field | Type | Keterangan |
|---|---|---|
| q* | string | Kata kunci: kode IATA, nama bandara, kota, atau negara. Contoh: DPS, Jakarta, bali |
| limit | number | Maksimum hasil (1–50). Default 20 |
GET /api/v1/partner/v1/airports?q=DPS&limit=10
X-API-Key: jwz_test_...
{
"query": "DPS",
"count": 1,
"airports": [
{
"code": "DPS",
"name": "Ngurah Rai International",
"city": "Bali",
"country": "Indonesia",
"countryCode": "ID",
"region": "Jawa"
}
]
}
Price Check
Verifikasi ulang harga + availability offer sebelum buat booking. Wajib dipanggil antara flights/search dan POST /bookings — harga supplier bisa berubah dalam hitungan menit. Kalau priceChanged: true, tampilkan harga baru ke user untuk konfirmasi sebelum booking.
Request Body
| Field | Type | Keterangan |
|---|---|---|
| offerId* | string | ID offer dari hasil /flights/search |
POST /api/v1/partner/v1/flights/price-check
X-API-Key: jwz_test_...
{ "offerId": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6" }
{
"offerId": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
"priceChanged": false,
"previousPrice": null,
"currentPrice": 943500,
"currency": "IDR",
"stillAvailable": true
}
Response Fields
| Field | Type | Keterangan |
|---|---|---|
| priceChanged | boolean | true kalau harga supplier berubah sejak search |
| previousPrice | number / null | Harga lama (kalau berubah) |
| currentPrice | number | Harga terkini, gunakan ini untuk booking |
| stillAvailable | boolean | false = seat habis / offer kadaluarsa, jangan lanjut booking |
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | INVALID_OFFER_ID | Token offer tidak valid / kedaluwarsa — lakukan search ulang |
| 404 | OFFER_NOT_FOUND | Offer expired / tidak ada di cache — re-search dulu |
| 410 | OFFER_EXPIRED | Cache TTL 5 menit terlewati — re-search dulu |
| 502 | SUPPLIER_ERROR | Supplier verify endpoint gagal — retry atau re-search |
Fare Rules
Ambil status refundable, fare brand, dan alokasi bagasi per segmen untuk satu offer — tampilkan ke pelanggan sebelum booking. Offer kedaluwarsa ~30 menit setelah search.
{
"offerId": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
"fareBrand": "Economy Basic",
"refundable": false,
"currency": "IDR",
"expiresAt": "2026-06-15T07:30:00.000Z",
"baggage": [
{
"segment": "CGK-DPS",
"flightNo": "GA405",
"cabinClass": "ECONOMY",
"allowance": "20kg"
}
]
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | OFFER_NOT_FOUND | Offer expired / tidak ada di cache — re-search dulu |
Bookings
Request Body
| Field | Type | Keterangan |
|---|---|---|
| offerId* | string | ID offer dari hasil search |
| passengers* | array | Data penumpang (lihat di bawah) |
| contactPhone* | string | Nomor telepon kontak, 7–20 karakter (flat field, bukan nested) |
| contactEmail | string | Email kontak (opsional, flat field) |
Passenger Object
| Field | Type | Keterangan |
|---|---|---|
| title* | string | MR / MRS / MS / MSTR |
| firstName* | string | Nama depan (huruf kapital, sesuai paspor) |
| lastName* | string | Nama belakang |
| dateOfBirth* | string | Format YYYY-MM-DD |
| nationality* | string | Kode negara ISO-2. Contoh: ID |
| passportNo | string | Nomor paspor (wajib untuk penerbangan internasional) |
| passportExpiry | string | Tanggal kadaluarsa paspor YYYY-MM-DD |
| type* | string | ADULT / CHILD / INFANT |
POST /api/v1/partner/v1/bookings
X-API-Key: jwz_test_...
{
"offerId": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
"passengers": [
{
"title": "MR",
"firstName": "BUDI",
"lastName": "SANTOSO",
"dateOfBirth": "1990-01-15",
"nationality": "ID",
"passportNo": "A1234567",
"passportExpiry": "2030-01-01",
"type": "ADULT"
}
],
"contactPhone": "+6281234567890",
"contactEmail": "[email protected]"
}
{
"id": "clx789...",
"bookingCode": "JWZ-20260615-ABC123",
"status": "PENDING_PAYMENT",
"expiresAt": "2026-06-15T07:30:00.000Z",
"totalAmount": 943500,
"currency": "IDR"
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | INVALID_PASSENGER_DATA | Field passenger missing/invalid (mis. passportExpiry < depart date) |
| 400 | PASSENGER_COUNT_MISMATCH | Jumlah passengers tidak match dengan yang di offer |
| 400 | PRICE_CHANGED | Harga supplier berubah — re-run price-check dulu |
| 400 | SINGLE_NAME_NOT_ALLOWED | Maskapai ini tidak menerima penumpang mononym (single-name) |
| 404 | OFFER_NOT_FOUND | offerId expired — re-search dulu |
| 409 | SEATS_UNAVAILABLE | Kursi habis sejak search |
| 502 | SUPPLIER_PNR_FAILED | Supplier reject PNR creation |
| HTTP | Error Code | Keterangan |
|---|---|---|
| 404 | BOOKING_NOT_FOUND | Booking ID tidak ada / sudah dihapus |
| 403 | BOOKING_ACCESS_DENIED | Booking milik agensi lain |
Query Parameters
| Param | Type | Keterangan |
|---|---|---|
| status | string | Filter: PENDING_PAYMENT, PAID, ISSUED, CANCELLED, REFUNDED |
| cursor | string | Pagination cursor dari response sebelumnya |
| limit | number | Default 20, max 100 |
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | INVALID_CURSOR | Cursor format invalid atau kadaluarsa |
| 400 | INVALID_STATUS_FILTER | Status filter tidak dikenali |
Request Body
| Field | Type | Keterangan |
|---|---|---|
| gateway* | string | Hanya WALLET di-support untuk partner API |
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | BOOKING_EXPIRED | Hold booking sudah habis (30 menit) — buat booking baru |
| 400 | INVALID_STATUS | Booking bukan status PENDING_PAYMENT |
| 402 | INSUFFICIENT_BALANCE | Saldo wallet kurang dari totalAmount |
| 409 | ALREADY_PAID | Booking sudah ke-bayar (idempotency) |
| 502 | SUPPLIER_TICKETING_FAILED | Pay ok di wallet tapi supplier issue gagal — refund otomatis dijalankan |
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | CANNOT_CANCEL | Booking sudah ISSUED — pakai refund flow, bukan cancel |
| 409 | ALREADY_CANCELLED | Booking sudah dibatalkan sebelumnya |
| 502 | SUPPLIER_CANCEL_FAILED | Supplier menolak cancel — hubungi support |
Refund (booking yang sudah ISSUED)
Untuk booking berstatus ISSUED. Cek estimasi dulu via refund-quote, lalu ajukan via refund. Hanya booking milik agensi Anda. Sebagian supplier memproses refund secara async — konfirmasi akhir lewat webhook booking.refunded.
{
"bookingId": "clx789...",
"bookingCode": "JWZ-20260615-ABC123",
"status": "ISSUED",
"refundable": true,
"currency": "IDR",
"maxRefundAmount": 943500,
"note": "Estimasi maksimum. Nilai final dikurangi biaya sesuai aturan tarif maskapai/supplier."
}
Request Body
| Field | Type | Keterangan |
|---|---|---|
| reason* | string | Alasan refund, minimal 10 karakter |
| amount | number | Opsional. Refund parsial; default seluruh totalAmount |
POST /api/v1/partner/v1/bookings/clx789.../refund
X-API-Key: jwz_live_...
{ "reason": "Pembatalan oleh penumpang karena perubahan jadwal" }
{
"bookingId": "clx789...",
"status": "ISSUED",
"message": "Refund request submitted to supplier. Confirmation will arrive via webhook.",
"refundId": "rfnd_..."
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | BOOKING_NOT_FOUND | Booking tidak ada atau bukan milik agensi Anda |
| 400 | REASON_REQUIRED | Alasan refund kurang dari 10 karakter |
| 409 | NOT_ISSUED | Hanya booking ISSUED yang bisa di-refund (pre-issue pakai cancel) |
{
"url": "https://storage.jetwize.com/eticket/...?signature=...&expires=...",
"expiresAt": "2026-06-15T12:15:00.000Z"
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 400 | TICKET_NOT_ISSUED | Booking belum status ISSUED — pay dulu |
| 404 | TICKET_NOT_FOUND | E-ticket PDF belum di-generate (job queue masih running) |
Ancillaries
Tambahkan bagasi, makanan, atau kursi ke booking yang sudah terbit
(status ISSUED). Ketersediaan tergantung dukungan maskapai/supplier — bila tidak didukung, respons katalog
mengembalikan supported: false beserta info maskapai; tampilkan fallback ke pengguna Anda dan arahkan ke
call center maskapai. Semua harga di-reprice di server dari katalog — harga yang Anda kirim diabaikan.
Query Parameters
| Param | Type | Keterangan |
|---|---|---|
| kind | string | Filter: BAGGAGE / MEAL / SEAT. Kosongkan untuk semua. |
{
"supported": true,
"airline": { "code": "JT", "name": "Lion Air" },
"reason": null,
"options": [
{ "id": "BAG-20", "kind": "BAGGAGE", "label": "Bagasi tambahan 20kg",
"price": 290000, "currency": "IDR", "scope": "PER_SEGMENT", "weightKg": 20 },
{ "id": "MEAL-VGML", "kind": "MEAL", "label": "Vegetarian (VGML)",
"price": 65000, "currency": "IDR", "scope": "PER_JOURNEY", "mealCode": "VGML" },
{ "id": "MEAL-MOML", "kind": "MEAL", "label": "Moslem Meal (MOML)",
"price": 0, "currency": "IDR", "scope": "PER_JOURNEY", "mealCode": "MOML" }
]
}
price: 0 berarti gratis (complimentary). Bila maskapai belum mendukung:
{ "supported": false, "reason": "UNSUPPORTED", "airline": { ... }, "options": [] }
— reason dapat berupa UNSUPPORTED atau NOT_ISSUED.
Pemilihan Kursi (Seat Booking)
Alur pesan kursi untuk booking yang sudah ISSUED:
- GET
/bookings/:id/seatmap— ambil peta kursi (kursiAVAILABLE/OCCUPIED+ harga). - Bentuk
optionId=SEAT-<seatNumber>(mis. kursi12A→SEAT-12A). Alternatif: ambil dariGET /ancillaries/catalog?kind=SEAT. - POST
/bookings/:id/ancillariesdengan item{ optionId: "SEAT-12A", kind: "SEAT", passengerId }— satu kursi per penumpang; harga di-reprice di server lalu debit wallet/kredit.
Bila maskapai belum mendukung pemilihan kursi, seatmap mengembalikan supported:false (reason UNSUPPORTED/NOT_ISSUED) — tampilkan fallback.
{
"supported": true,
"columns": ["A", "B", "C", "", "D", "E", "F"],
"rows": [
{ "row": 1, "seats": [
{ "seatNumber": "1A", "status": "AVAILABLE", "exitRow": false, "price": 100000, "currency": "IDR" },
{ "seatNumber": "1B", "status": "OCCUPIED", "exitRow": false, "price": 100000 }
]}
],
"currentSeats": [ { "passengerId": "clxpax1...", "seatNumber": null } ]
}
price > 0 = kursi berbayar (preferred/exit row). Kursi yang sudah dimiliki penumpang
pada booking ini ditandai OCCUPIED. Bila tidak didukung maskapai, tampilkan
"Seat selection not available for this flight".
Request Body
| Field | Type | Keterangan |
|---|---|---|
| payment | string | WALLET (default) / CREDIT (Kredit Tagihan, bila aktif) |
| items* | array | Daftar item (lihat di bawah), maks 50 |
Item Object
| Field | Type | Keterangan |
|---|---|---|
| optionId* | string | Dari katalog (BAG-20, MEAL-VGML) atau kursi: SEAT-12A |
| kind* | string | BAGGAGE / MEAL / SEAT |
| passengerId* | string | ID penumpang dari detail booking (satu kursi per penumpang) |
| segmentIndex | number | Indeks segmen (default 0) |
POST /api/v1/partner/v1/bookings/clx789.../ancillaries
X-API-Key: jwz_live_...
Idempotency-Key: 9f1c2e58-...
{
"payment": "WALLET",
"items": [
{ "optionId": "BAG-20", "kind": "BAGGAGE", "passengerId": "clxpax1..." },
{ "optionId": "MEAL-VGML", "kind": "MEAL", "passengerId": "clxpax1..." },
{ "optionId": "SEAT-12A", "kind": "SEAT", "passengerId": "clxpax1..." }
]
}
{
"purchaseId": "9b2f...",
"total": 505000,
"currency": "IDR",
"payment": "WALLET",
"items": [
{ "id": "anc1...", "kind": "BAGGAGE", "label": "Bagasi tambahan 20kg", "unitPrice": 290000, "ssrCode": "XBAG 20KG" },
{ "id": "anc2...", "kind": "MEAL", "label": "Vegetarian (VGML)", "unitPrice": 65000, "ssrCode": "VGML" },
{ "id": "anc3...", "kind": "SEAT", "label": "Kursi 12A", "unitPrice": 150000, "ssrCode": "RQST 12A" }
]
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 409 | INVALID_STATUS | Booking belum ISSUED |
| 422 | ANCILLARY_NOT_SUPPORTED | Maskapai belum mendukung — details.airline berisi info maskapai |
| 400 | ANCILLARY_OPTION_NOT_FOUND | optionId tidak ada di katalog |
| 409 | SEAT_UNAVAILABLE | Kursi sudah terisi — muat ulang seat map |
| 400 | SEAT_PER_PAX | Lebih dari satu kursi untuk penumpang yang sama |
| 400 | INSUFFICIENT_BALANCE | Saldo wallet tidak cukup |
| 502 | ANCILLARY_SUPPLIER_FAILED | Maskapai menolak — dana otomatis dikembalikan ke wallet |
Sandbox: di environment sandbox, pembelian disimulasikan (tidak ada debit nyata) dan respons
berisi { "sandbox": true }. Idempotency: kirim header Idempotency-Key —
retry dengan key yang sama mengembalikan respons yang sama tanpa debit ganda.
{
"data": [
{ "id": "anc1...", "kind": "BAGGAGE", "label": "Bagasi tambahan 20kg",
"passengerId": "clxpax1...", "unitPrice": "290000", "currency": "IDR",
"ssrCode": "XBAG 20KG", "postBooking": true, "createdAt": "2026-06-11T01:30:00.000Z" }
]
}
Reschedule
Pindahkan penumpang ke penerbangan lain pada rute yang sama (tanggal baru). Sistem menagih
max(0, selisih tarif) + biaya ubah sesuai fare rules, lalu membatalkan tiket lama dan menerbitkan
PNR baru. Perubahan tidak diizinkan dalam 24 jam sebelum keberangkatan. Bila tarif baru lebih
murah, selisihnya tidak dikembalikan — hanya biaya ubah yang ditagih.
Query Parameters
| Param | Type | Keterangan |
|---|---|---|
| date* | string | Tanggal keberangkatan baru YYYY-MM-DD (min H+24) |
{
"supported": true,
"airline": { "code": "JT", "name": "Lion Air" },
"reason": null,
"options": [
{ "id": "RSC-ABC123-2026-07-01-0", "flightNumber": "JT370",
"origin": "BTH", "destination": "CGK",
"departureTime": "2026-07-01T06:00:00.000Z", "arrivalTime": "2026-07-01T07:45:00.000Z",
"fareDifference": 120000, "changeFee": 250000,
"currency": "IDR", "seatsAvailable": 5 }
]
}
fareDifference negatif = penerbangan baru lebih murah (selisih tidak direfund).
reason bila supported:false: WINDOW_CLOSED (<24 jam sebelum berangkat),
UNSUPPORTED (maskapai belum mendukung), atau NOT_ISSUED.
Request Body
| Field | Type | Keterangan |
|---|---|---|
| optionId* | string | Dari reschedule/options |
| date* | string | Tanggal yang sama dengan saat mengambil options |
| payment | string | WALLET (default) / CREDIT |
POST /api/v1/partner/v1/bookings/clx789.../reschedule
X-API-Key: jwz_live_...
Idempotency-Key: 4d8a31c0-...
{
"optionId": "RSC-ABC123-2026-07-01-0",
"date": "2026-07-01",
"payment": "WALLET"
}
{
"oldPnr": "ABC123",
"newPnr": "XYZ789",
"flightNumber": "JT370",
"departureTime": "2026-07-01T06:00:00.000Z",
"arrivalTime": "2026-07-01T07:45:00.000Z",
"fareDifference": 120000,
"changeFee": 250000,
"amountCharged": 370000,
"currency": "IDR"
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 409 | RESCHEDULE_WINDOW_CLOSED | Perubahan tidak diizinkan dalam 24 jam keberangkatan |
| 409 | INVALID_STATUS | Booking belum ISSUED |
| 422 | RESCHEDULE_NOT_SUPPORTED | Maskapai belum mendukung jadwal ulang via API |
| 400 | RESCHEDULE_OPTION_NOT_FOUND | optionId kedaluwarsa — muat ulang options |
| 400 | INSUFFICIENT_BALANCE | Saldo wallet tidak cukup |
| 502 | RESCHEDULE_SUPPLIER_FAILED | Maskapai menolak — dana otomatis dikembalikan |
Sandbox: disimulasikan, respons berisi { "sandbox": true } tanpa perubahan nyata.
Setelah sukses, ambil ulang GET /bookings/:id — PNR dan jadwal sudah diperbarui, status tetap
ISSUED sehingga ancillary/reschedule berikutnya tetap tersedia.
Auto-Rebook
Pertahankan booking yang masih ditahan (PENDING_PAYMENT) secara otomatis: sistem
memperpanjang hold ke supplier sebelum kedaluwarsa — cek ketersediaan + harga, lalu
cancel hold lama dan rebook yang baru tiap siklus — hingga batas maksimum (default 24 jam).
Fitur ini tidak menerbitkan tiket dan tidak menarik pembayaran; hanya menjaga hold tetap hidup.
Opt-in per booking, dan harus diizinkan per supplier oleh operator. Di lingkungan SANDBOX siklus berjalan di supplier mock (aman, tanpa inventory nyata). Bila harga naik melebihi ambang yang Anda tetapkan, siklus PAUSE menunggu persetujuan.
Request Body (opsional)
| Field | Type | Keterangan |
|---|---|---|
| maxHoldMinutes | integer | Total jendela tahan, 60–2880 menit (1–48 jam). Default 1440 (24 jam). |
| priceThresholdPct | number | Jika harga renewal naik > persen ini, siklus PAUSE menunggu persetujuan. Kosongkan = tidak pernah pause. |
POST /api/v1/partner/v1/bookings/clx789.../auto-rebook/enable
X-API-Key: jwz_live_...
{
"maxHoldMinutes": 1440,
"priceThresholdPct": 10
}
{ "status": "ACTIVE", "cycleId": "clxcyc...", "nextAttemptAt": "2026-06-15T05:25:00.000Z" }
{
"cycle": {
"status": "ACTIVE",
"maxHoldMinutes": 1440,
"priceThresholdPct": 10,
"expiresAt": "2026-06-16T04:00:00.000Z",
"nextAttemptAt": "2026-06-15T05:25:00.000Z",
"attemptCount": 1,
"lastPrice": 1500000
},
"attempts": [
{ "attemptNumber": 1, "attemptedAt": "2026-06-15T04:55:00.000Z", "result": "SUCCESS",
"prevBookingRef": "BKG-AB12CD", "newBookingRef": "BKG-EF34GH",
"prevPrice": 1500000, "newPrice": 1500000, "priceDelta": 0, "priceDeltaPct": 0 }
]
}
result tiap percobaan: SUCCESS, SEAT_UNAVAILABLE (kursi habis → hold dibiarkan
kedaluwarsa, booking jadi HOLD_EXPIRED), PRICE_THRESHOLD_EXCEEDED (siklus PAUSE),
API_ERROR (gangguan sementara, dicoba lagi), atau HOLD_LOST (cancel berhasil tapi rebook gagal —
booking HOLD_LOST, perlu pesan ulang manual).
Dipakai saat siklus PAUSED karena kenaikan harga melebihi priceThresholdPct. Mengembalikan
{ "status": "ACTIVE", "nextAttemptAt": "..." }.
Mengembalikan { "status": "STOPPED", "bookingId": "...", "lastAttemptAt": "..." }.
| HTTP | Error Code | Keterangan |
|---|---|---|
| 409 | AUTO_REBOOK_NOT_ELIGIBLE | Booking tidak PENDING_PAYMENT (sudah dibayar/dibatalkan/terbit) |
| 409 | AUTO_REBOOK_ALREADY_ACTIVE | Siklus sudah aktif untuk booking ini |
| 409 | AUTO_REBOOK_SUPPLIER_DISABLED | Auto-rebook belum diaktifkan untuk supplier booking ini |
| 409 | AUTO_REBOOK_NOT_ACTIVE | disable: tidak ada siklus aktif/paused |
| 409 | AUTO_REBOOK_NOT_PAUSED | approve: siklus tidak sedang menunggu persetujuan harga |
Wallet
Saldo deposit + limit kredit agensi: balance = saldo deposit, credit.available = sisa kredit (limit − used).
{
"balance": 5000000,
"holdBalance": 943500,
"currency": "IDR",
"credit": {
"enabled": true,
"limit": 50000000,
"used": 12000000,
"available": 38000000
},
"sandbox": false
}
| HTTP | Error Code | Keterangan |
|---|---|---|
| 404 | WALLET_NOT_FOUND | Agensi belum punya wallet — hubungi admin |
Booking Flow
0. GET /airports?q=... → (opsional) autocomplete bandara untuk input
1. POST /flights/search → Dapat daftar offer + offerId
2. GET /flights/:offerId/fare-rules → (opsional) tampilkan bagasi & refundable
3. POST /flights/price-check → Verifikasi harga masih valid
4. POST /bookings → Buat booking (hold 30 menit)
└─ Response: bookingId, expiresAt, totalAmount
5. GET /wallet → Cek saldo / sisa kredit cukup
6. POST /bookings/:id/pay → Bayar dengan wallet
└─ Jika bayar dari luar → topup wallet dulu via dashboard
7. GET /bookings/:id → Poll status (PAID → ISSUED)
8. GET /bookings/:id/ticket → Download e-ticket
Pasca-issue (opsional):
GET /bookings/:id/refund-quote → Estimasi refund
POST /bookings/:id/refund → Ajukan refund (konfirmasi via webhook)
Skenario Booking (8 Kasus)
Panduan langkah demi langkah untuk membuat booking lewat Partner API untuk 8 skenario: OW/RT × Direct/Transfer × dengan/tanpa bagasi.
| # | Jenis | Bagasi |
|---|---|---|
| 1 | One-Way (OW) Direct | Dengan bagasi |
| 2 | One-Way (OW) Direct | Tanpa bagasi |
| 3 | Round-Trip (RT) Direct | Dengan bagasi |
| 4 | Round-Trip (RT) Direct | Tanpa bagasi |
| 5 | One-Way (OW) Transfer (transit) | Dengan bagasi |
| 6 | One-Way (OW) Transfer (transit) | Tanpa bagasi |
| 7 | Round-Trip (RT) Transfer (transit) | Dengan bagasi |
| 8 | Round-Trip (RT) Transfer (transit) | Tanpa bagasi |
search dan offer mana yang Anda pilih dari hasil pencarian. Langkah create → pay → e-ticket identik untuk semuanya.
Selain 8 skenario bentuk-offer di atas, ada 4 skenario operasional (9–12) di bagian Skenario Lanjutan di bawah: multi-penumpang, internasional (paspor), cancel & refund, dan ancillary.
Konektivitas ke sistem JetWize
Partner API sudah tersambung penuh ke inti JetWize secara code (bukan mock/stub):
POST /bookingsmemanggilBookingsService.createyang sama dengan dashboard internal — melakukan price-check nyata, menahan inventory (hold +expiresAt), memanggil suppliercreatePnr(), dan menyimpan rowBookingnyata.POST /bookings/:id/pay(production) menjalankanissueTicketSaganyata → debit wallet +issueTicket()supplier + nomor tiket, e-ticket & invoice PDF, email.- Kunci PRODUCTION (
jwz_live_...) menembak agregator supplier maskapai nyata. Kunci SANDBOX (jwz_test_...) menembakMockSupplieryang terisolasi — tidak pernah menyentuh supplier nyata / uang.
Cara mengenali jenis offer
Setiap hasil search adalah sebuah FlightOffer. Tiga sifat berikut menentukan skenario:
id berupa token off_… yang menyembunyikan supplier. Perlakukan sebagai string buram — simpan & kirim balik apa adanya pada price-check/booking; jangan mem-parsing prefiksnya.
1. Direct vs Transfer — pakai stops / segments
// DIRECT (nonstop): satu segmen, stops = 0
{ "stops": 0, "segments": [ { "origin": "CGK", "destination": "DPS", ... } ] }
// TRANSFER (transit): >1 segmen, stops = segments.length - 1
{ "stops": 1, "segments": [
{ "origin": "CGK", "destination": "SUB", ... }, // leg 1
{ "origin": "SUB", "destination": "DPS", ... } // leg 2 (transit di SUB)
] }
stops === 0→ penerbangan langsung.stops >= 1(atausegments.length > 1) → transfer/transit.- Untuk membatasi pencarian ke penerbangan langsung saja, kirim
"route": "DIRECT"di bodysearch.
2. Dengan vs Tanpa bagasi — pakai branded fare
Bagasi di JetWize adalah bagian dari fare brand, bukan add-on saat search. Satu penerbangan yang sama bisa muncul sebagai beberapa offer (masing-masing offerId berbeda) yang berbeda fareBrand dan segments[].baggageAllowance:
// offer "tanpa bagasi"
{ "id": "off_9pR3wLm4Xa7bY1cV2dU8eT0fS6gQ5hP2j", "fareBrand": "Economy Basic",
"segments": [ { "baggageAllowance": "Tanpa bagasi (kabin 7kg)", ... } ] }
// offer "dengan bagasi" (penerbangan sama, brand beda)
{ "id": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6", "fareBrand": "Economy Value",
"segments": [ { "baggageAllowance": "20kg", ... } ] }
baggageAllowanceadalah string bebas dari supplier (mis."20kg","1 pcs / 15kg","Tanpa bagasi","Kabin saja"). Tampilkan/parse string apa adanya — jangan asumsikan format tetap.- Untuk membandingkan brand dengan/tanpa bagasi per segmen, panggil
GET /flights/:offerId/fare-rules(mengembalikanfareBrand,refundable, danbaggage[]per segmen). - Pilih
offerIddari brand yang sesuai kebutuhan (dengan atau tanpa bagasi), lalu bookofferIditu.
POST /bookings/:id/ancillaries dengan kind: "BAGGAGE" (dihargai ulang oleh server).
3. One-Way vs Round-Trip — pakai returnDate
- Search: sertakan
returnDateuntuk round-trip; hilangkan untuk one-way. - Offer/booking: round-trip = satu offer dengan kedua leg digabung dalam
segments[](bukan dua offer terpisah) dan satu PNR. Urutansegments[]adalah outbound dulu lalu return.
// Round-trip DIRECT: 2 segmen (outbound + return), stops tetap 0 per arah
{ "segments": [
{ "origin": "CGK", "destination": "DPS", ... }, // outbound
{ "origin": "DPS", "destination": "CGK", ... } // return
] }
Alur Umum Booking
Berlaku untuk semua skenario. Tiap skenario di bawah hanya mengganti langkah search + pemilihan offer.
Langkah 1 — Search
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/flights/search \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15",
"passengers": { "adult": 1, "child": 0, "infant": 0 },
"cabinClass":"ECONOMY" }'
| Field | Wajib | Keterangan |
|---|---|---|
| origin / destination | ✓ | Kode IATA 3 huruf (uppercase) |
| departDate | ✓ | YYYY-MM-DD |
| returnDate | – | Isi untuk round-trip |
| passengers | ✓ | { adult, child, infant } |
| cabinClass | – | ECONOMY | BUSINESS | FIRST (default ECONOMY) |
| route | – | ALL (default) | DIRECT (hanya penerbangan langsung) |
| airlines | – | Array kode maskapai untuk memfilter |
{
"results": [
// lihat contoh FlightOffer lengkap ("id", "fareBrand", "stops",
// "segments[]", "totalPrice", dst.) di tiap skenario di bawah
{ "id": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6" }
],
"count": 1,
"sandbox": false
}
Langkah 2 — Pilih offer
Pilih offerId sesuai skenario (lihat aturan di bagian "Cara mengenali jenis offer" di atas).
Langkah 3 — Price-check
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/flights/price-check \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{ "offerId": "<OFFER_ID_TERPILIH>" }'
Respons memuat priceChanged, currentPrice, stillAvailable, baggageAllowance. Jika priceChanged: true, tampilkan harga baru ke pengguna sebelum lanjut.
{
"offerId": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
"priceChanged": false,
"currentPrice": 1250000,
"currency": "IDR",
"stillAvailable": true,
"baggageAllowance": "20kg"
}
Jika harga berubah, respons juga menyertakan "previousPrice":
{
"offerId": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
"priceChanged": true,
"previousPrice": 1250000,
"currentPrice": 1310000,
"currency": "IDR",
"stillAvailable": true,
"baggageAllowance": "20kg"
}
Langkah 4 — Create booking (hold)
contactPhone (WAJIB, 7–20 karakter) dan contactEmail (opsional) langsung di root body. Jangan kirim objek contact { } bertingkat — akan diabaikan dan menyebabkan error validasi (contactPhone hilang).
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{
"offerId": "<OFFER_ID_TERPILIH>",
"contactPhone": "+628123456789",
"contactEmail": "[email protected]",
"passengers": [
{ "title":"MR", "firstName":"BUDI", "lastName":"SANTOSO",
"dateOfBirth":"1990-01-15", "nationality":"ID", "type":"ADULT" }
]
}'
| Field passenger | Wajib | Keterangan |
|---|---|---|
| title | ✓ | MR | MRS | MS | MSTR |
| firstName | ✓ | |
| lastName | – | Opsional (nama tunggal/mononym) |
| dateOfBirth | ✓ | YYYY-MM-DD |
| nationality | ✓ | ISO 2 huruf, mis. ID |
| passportNo / passportExpiry | –* | Wajib untuk penerbangan internasional |
| type | ✓ | ADULT | CHILD | INFANT |
{
"id": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"pnr": null,
"status": "PENDING_PAYMENT",
"totalAmount": 1250000,
"currency": "IDR",
"markup": 50000,
"agentMarkup": 0,
"bookingFee": 10000,
"issuanceFee": 5000,
"commission": 0,
"flightData": {},
"expiresAt": "2026-07-07T10:30:00.000Z",
"issuedAt": null,
"passengers": [
{
"id": "clxpax1abcdef",
"title": "MR",
"firstName": "Budi",
"lastName": "Santoso",
"dateOfBirth": "1990-01-15",
"nationality": "ID",
"type": "ADULT",
"ticketNumber": null
}
],
"createdAt": "2026-07-07T09:00:00.000Z",
"updatedAt": "2026-07-07T09:00:00.000Z"
}
Langkah 5 — Pay (wallet) → issue
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/pay \
-H "X-API-Key: jwz_live_xxx"
- Production: debit wallet agensi + jalankan saga issuance → status
ISSUED. - Sandbox: simulasi pembayaran + issuance mock (tanpa debit) →
ISSUED, respons{ ok, sandbox, booking }.
{
"paymentId": "clxpay1abcdef",
"status": "SUCCESS",
"message": "Wallet payment successful"
}
Issuance (PAID → ISSUED, pengisian pnr & ticketNumber) berjalan sebagai bagian dari alur ini juga, tapi tidak ikut dikembalikan di respons /pay — ambil status & data terbaru via GET /bookings/:id:
{
"id": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"pnr": "XZ9K3P",
"status": "ISSUED",
"totalAmount": 1250000,
"currency": "IDR",
"markup": 50000,
"agentMarkup": 0,
"bookingFee": 10000,
"issuanceFee": 5000,
"commission": 0,
"flightData": {},
"expiresAt": "2026-07-07T10:30:00.000Z",
"issuedAt": "2026-07-07T09:05:00.000Z",
"passengers": [
{
"id": "clxpax1abcdef",
"title": "MR",
"firstName": "Budi",
"lastName": "Santoso",
"dateOfBirth": "1990-01-15",
"nationality": "ID",
"type": "ADULT",
"ticketNumber": "997-1234567890"
}
],
"createdAt": "2026-07-07T09:00:00.000Z",
"updatedAt": "2026-07-07T09:05:00.000Z"
}
Sandbox berbeda dari production — sandbox membungkus booking penuh langsung di respons /pay: { "ok": true, "sandbox": true, "message": "Pembayaran disimulasikan — tiket sandbox diterbitkan.", "booking": { /* booking status ISSUED seperti contoh GET /bookings/:id di atas */ } }.
Langkah 6 — Ambil e-ticket
curl -s https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/ticket \
-H "X-API-Key: jwz_live_xxx"
Production mengembalikan { "ticketUrl": "...", "bookingCode": "..." } (URL bertanda tangan, kedaluwarsa 15 menit). Cek status kapan saja: GET /bookings/:id.
{
"ticketUrl": "/api/v1/bookings/clxbkg1abcdef/eticket",
"bookingCode": "JWZ-20260707-ABC123"
}
Sandbox tidak menerbitkan e-ticket nyata: { "sandbox": true, "message": "E-ticket tidak tersedia di sandbox." }.
8 Skenario
Untuk tiap skenario: jalankan search berikut, lalu pilih offer sesuai aturan, lalu ikuti Alur Umum langkah 3–6 (price-check → create → pay → ticket).
Skenario 1 — OW Direct, dengan bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"DIRECT" }
Pilih offer: stops === 0 dan fareBrand/baggageAllowance menunjukkan ada bagasi (mis. "20kg"). Bandingkan brand via GET /flights/:offerId/fare-rules bila ragu.
{
"id": "off_5vT2kQx9Jd4mZ0aBc1De2Fg3Hh4Ii5Jj6",
"fareBrand": "Economy Value",
"stops": 0,
"segments": [
{
"flightNumber": "GA-402",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "CGK",
"destination": "DPS",
"departureTime": "2026-07-14T23:00:00.000Z",
"arrivalTime": "2026-07-15T01:55:00.000Z",
"duration": 115,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
}
],
"totalDuration": 115,
"basePrice": 1100000,
"tax": 150000,
"totalPrice": 1250000,
"currency": "IDR",
"seatsAvailable": 6,
"expiresAt": "2026-07-07T14:00:00.000Z"
// results[] juga berisi brand lain utk penerbangan sama — ini yg PUNYA bagasi
}
Skenario 2 — OW Direct, tanpa bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"DIRECT" }
Pilih offer: stops === 0 dan pilih brand tanpa bagasi (baggageAllowance seperti "Tanpa bagasi" / "Kabin saja") — biasanya fareBrand "Basic"/termurah untuk penerbangan yang sama.
{
"id": "off_9pR3wLm4Xa7bY1cV2dU8eT0fS6gQ5hP2j",
"fareBrand": "Economy Basic",
"stops": 0,
"segments": [
{
"flightNumber": "GA-402",
// sama seperti skenario 1 — penerbangan identik, brand lebih murah
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "CGK",
"destination": "DPS",
"departureTime": "2026-07-14T23:00:00.000Z",
"arrivalTime": "2026-07-15T01:55:00.000Z",
"duration": 115,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
}
],
"totalDuration": 115,
"basePrice": 920000,
"tax": 130000,
"totalPrice": 1050000,
"currency": "IDR",
"seatsAvailable": 9,
"expiresAt": "2026-07-07T14:00:00.000Z"
}
Skenario 3 — RT Direct, dengan bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15", "returnDate":"2026-07-20",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"DIRECT" }
Pilih offer: offer round-trip (segments[] berisi kedua leg, CGK→DPS lalu DPS→CGK), tiap arah stops 0, dan brand dengan bagasi. Satu offerId = satu booking = satu PNR untuk PP.
{
"id": "off_3kN8oI2uH5yG7fD1eC9bA0zX4wV6tU3s",
"fareBrand": "Economy Value",
"stops": 0,
"segments": [
{
"flightNumber": "GA-402",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "CGK",
"destination": "DPS",
"departureTime": "2026-07-14T23:00:00.000Z",
"arrivalTime": "2026-07-15T01:55:00.000Z",
"duration": 115,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
// outbound
},
{
"flightNumber": "GA-405",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "DPS",
"destination": "CGK",
"departureTime": "2026-07-19T23:30:00.000Z",
"arrivalTime": "2026-07-20T00:50:00.000Z",
"duration": 140,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
// return
}
],
"totalDuration": 255,
"basePrice": 2150000,
"tax": 300000,
"totalPrice": 2450000,
"currency": "IDR",
"seatsAvailable": 5,
"expiresAt": "2026-07-07T14:00:00.000Z"
}
Skenario 4 — RT Direct, tanpa bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15", "returnDate":"2026-07-20",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"DIRECT" }
Pilih offer: offer round-trip langsung dengan brand tanpa bagasi.
{
"id": "off_7qM1lK4jH8gF2eD5cB9aZ3yX6wV0uT4r",
"fareBrand": "Economy Basic",
"stops": 0,
"segments": [
{
"flightNumber": "GA-402",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "CGK",
"destination": "DPS",
"departureTime": "2026-07-14T23:00:00.000Z",
"arrivalTime": "2026-07-15T01:55:00.000Z",
"duration": 115,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
// outbound
},
{
"flightNumber": "GA-405",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "DPS",
"destination": "CGK",
"departureTime": "2026-07-19T23:30:00.000Z",
"arrivalTime": "2026-07-20T00:50:00.000Z",
"duration": 140,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
// return
}
],
"totalDuration": 255,
"basePrice": 1800000,
"tax": 250000,
"totalPrice": 2050000,
"currency": "IDR",
"seatsAvailable": 8,
"expiresAt": "2026-07-07T14:00:00.000Z"
}
Skenario 5 — OW Transfer, dengan bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"ALL" }
Pilih offer: stops >= 1 (mis. segments[] = CGK→SUB→DPS), brand dengan bagasi. baggageAllowance berlaku per segmen — cek semuanya via /fare-rules.
{
"id": "off_2aF6dS9gJ3kM7pQ1rT5vX8zB0cE4hL7n",
"fareBrand": "Economy Value",
"stops": 1,
"segments": [
{
"flightNumber": "QG-620",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "CGK",
"destination": "SUB",
"departureTime": "2026-07-14T22:00:00.000Z",
"arrivalTime": "2026-07-15T00:20:00.000Z",
"duration": 110,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
// leg 1
},
{
"flightNumber": "QG-740",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "SUB",
"destination": "DPS",
"departureTime": "2026-07-15T02:10:00.000Z",
"arrivalTime": "2026-07-15T03:35:00.000Z",
"duration": 85,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
// leg 2, transit di SUB
}
],
"totalDuration": 315,
"basePrice": 850000,
"tax": 130000,
"totalPrice": 980000,
"currency": "IDR",
"seatsAvailable": 4,
"expiresAt": "2026-07-07T14:00:00.000Z"
}
Skenario 6 — OW Transfer, tanpa bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"ALL" }
Pilih offer: stops >= 1 dan brand tanpa bagasi.
{
"id": "off_8wY4uR7tP1oL5jH9gF3eD6cB2aZ0xV5m",
"fareBrand": "Economy Basic",
"stops": 1,
"segments": [
{
"flightNumber": "QG-620",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "CGK",
"destination": "SUB",
"departureTime": "2026-07-14T22:00:00.000Z",
"arrivalTime": "2026-07-15T00:20:00.000Z",
"duration": 110,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
// leg 1
},
{
"flightNumber": "QG-740",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "SUB",
"destination": "DPS",
"departureTime": "2026-07-15T02:10:00.000Z",
"arrivalTime": "2026-07-15T03:35:00.000Z",
"duration": 85,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
// leg 2, transit di SUB
}
],
"totalDuration": 315,
"basePrice": 710000,
"tax": 110000,
"totalPrice": 820000,
"currency": "IDR",
"seatsAvailable": 7,
"expiresAt": "2026-07-07T14:00:00.000Z"
}
Skenario 7 — RT Transfer, dengan bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15", "returnDate":"2026-07-20",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"ALL" }
Pilih offer: offer round-trip yang minimal satu arahnya transit (stops >= 1), brand dengan bagasi. segments[] memuat leg outbound (bisa >1 segmen) lalu leg return.
{
"id": "off_4nC7bV1xZ5aS9dF3gH6jK0lM8pQ2rT6w",
"fareBrand": "Economy Value",
"stops": 1,
// stops mencerminkan arah yg transit; pisahkan arah via transisi origin/destination
"segments": [
{
"flightNumber": "QG-620",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "CGK",
"destination": "SUB",
"departureTime": "2026-07-14T22:00:00.000Z",
"arrivalTime": "2026-07-15T00:20:00.000Z",
"duration": 110,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
// outbound leg 1
},
{
"flightNumber": "QG-740",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "SUB",
"destination": "DPS",
"departureTime": "2026-07-15T02:10:00.000Z",
"arrivalTime": "2026-07-15T03:35:00.000Z",
"duration": 85,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
// outbound leg 2, transit di SUB
},
{
"flightNumber": "GA-405",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "DPS",
"destination": "CGK",
"departureTime": "2026-07-19T23:30:00.000Z",
"arrivalTime": "2026-07-20T00:50:00.000Z",
"duration": 140,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "20kg"
// return, direct
}
],
"totalDuration": 570,
"basePrice": 1900000,
"tax": 280000,
"totalPrice": 2180000,
"currency": "IDR",
"seatsAvailable": 4,
"expiresAt": "2026-07-07T14:00:00.000Z"
}
Skenario 8 — RT Transfer, tanpa bagasi
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15", "returnDate":"2026-07-20",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY", "route":"ALL" }
Pilih offer: offer round-trip dengan transit, brand tanpa bagasi.
{
"id": "off_6yU3iO8pL2kJ5hG9fD4sA1zX7cV0bN3q",
"fareBrand": "Economy Basic",
"stops": 1,
"segments": [
{
"flightNumber": "QG-620",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "CGK",
"destination": "SUB",
"departureTime": "2026-07-14T22:00:00.000Z",
"arrivalTime": "2026-07-15T00:20:00.000Z",
"duration": 110,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
// outbound leg 1
},
{
"flightNumber": "QG-740",
"carrier": "QG",
"carrierName": "Citilink",
"origin": "SUB",
"destination": "DPS",
"departureTime": "2026-07-15T02:10:00.000Z",
"arrivalTime": "2026-07-15T03:35:00.000Z",
"duration": 85,
"aircraft": "Airbus A320",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
// outbound leg 2, transit di SUB
},
{
"flightNumber": "GA-405",
"carrier": "GA",
"carrierName": "Garuda Indonesia",
"origin": "DPS",
"destination": "CGK",
"departureTime": "2026-07-19T23:30:00.000Z",
"arrivalTime": "2026-07-20T00:50:00.000Z",
"duration": 140,
"aircraft": "Boeing 737-800",
"cabinClass": "ECONOMY",
"baggageAllowance": "Tanpa bagasi (kabin 7kg)"
// return, direct
}
],
"totalDuration": 570,
"basePrice": 1560000,
"tax": 220000,
"totalPrice": 1780000,
"currency": "IDR",
"seatsAvailable": 6,
"expiresAt": "2026-07-07T14:00:00.000Z"
}
Skenario Lanjutan (9–12)
Selain 8 skenario bentuk-offer di atas, berikut 4 skenario operasional yang sering dijumpai partner: campuran tipe penumpang (dewasa/anak/bayi), penerbangan internasional yang mewajibkan dokumen paspor, pembatalan & refund pasca-booking, dan pembelian ancillary (bagasi/kursi/makan) setelah tiket terbit.
Skenario 9 — Multi-penumpang (Dewasa + Anak + Bayi)
{ "origin":"CGK", "destination":"DPS", "departDate":"2026-07-15",
"passengers":{ "adult":2,"child":1,"infant":1 }, "cabinClass":"ECONOMY", "route":"DIRECT" }
Pilih offer seperti Skenario 1 (stops === 0, brand dengan bagasi). Yang berbeda ada di langkah create: kirim 4 objek passengers[] — 2 ADULT, 1 CHILD (lahir sekitar 2018), 1 INFANT (lahir sekitar 2025, di bawah 2 tahun, lap infant tanpa kursi sendiri).
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{
"offerId": "<OFFER_ID_TERPILIH>",
"contactPhone": "+628123456789",
"contactEmail": "[email protected]",
"passengers": [
{ "title":"MR", "firstName":"BUDI", "lastName":"SANTOSO",
"dateOfBirth":"1990-01-15", "nationality":"ID", "type":"ADULT" },
{ "title":"MRS", "firstName":"SITI", "lastName":"SANTOSO",
"dateOfBirth":"1992-03-20", "nationality":"ID", "type":"ADULT" },
{ "title":"MSTR", "firstName":"ADI", "lastName":"SANTOSO",
"dateOfBirth":"2018-06-10", "nationality":"ID", "type":"CHILD" },
{ "title":"MSTR", "firstName":"BAYU", "lastName":"SANTOSO",
"dateOfBirth":"2025-09-01", "nationality":"ID", "type":"INFANT" }
]
}'
{
"id": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"pnr": null,
"status": "PENDING_PAYMENT",
// 2 dewasa + 1 anak + 1 bayi (base per-dewasa ~1.000.000 + tax/fee)
"totalAmount": 2850000,
"currency": "IDR",
"markup": 100000,
"bookingFee": 10000,
"issuanceFee": 5000,
"commission": 0,
"expiresAt": "2026-07-07T10:30:00.000Z",
"issuedAt": null,
"passengers": [
{ "id": "clxpax1abcdef", "type": "ADULT", "ticketNumber": null },
{ "id": "clxpax2abcdef", "type": "ADULT", "ticketNumber": null },
{ "id": "clxpax3abcdef", "type": "CHILD", "ticketNumber": null },
{ "id": "clxpax4abcdef", "type": "INFANT", "ticketNumber": null }
// tiap entri juga membawa title, firstName, lastName, dateOfBirth, nationality
],
"createdAt": "2026-07-07T09:00:00.000Z",
"updatedAt": "2026-07-07T09:00:00.000Z"
}
Skenario 10 — Internasional (paspor WAJIB)
{ "origin":"CGK", "destination":"SIN", "departDate":"2026-07-15",
"passengers":{ "adult":1,"child":0,"infant":0 }, "cabinClass":"ECONOMY" }
JetWize mendeteksi rute internasional bila ada segmen yang menyentuh bandara non-Indonesia (countryCode ≠ ID) — di sini CGK→SIN. Untuk rute internasional, setiap penumpang wajib menyertakan passportNo (≥ 6 karakter), passportExpiry, dan nationality saat create.
{
"error": {
"code": "PASSPORT_REQUIRED",
"message": "Rute internasional wajib menyertakan paspor + tanggal berlaku untuk semua penumpang. Lengkapi: JOHN DOE (no. paspor & tanggal berlaku paspor).",
"details": {
"route": "INTERNATIONAL",
"incompletePassengers": ["JOHN DOE (no. paspor & tanggal berlaku paspor)"]
},
"traceId": "req_abc123"
}
}
{
"error": {
"code": "PASSPORT_EXPIRED",
"message": "Paspor sudah kadaluarsa sebelum tanggal keberangkatan: JOHN DOE (kadaluarsa 2026-05-01). Perbarui paspor terlebih dahulu sebelum booking.",
"details": {
"departDate": "2026-07-15",
"expiredPassengers": [ { "name": "JOHN DOE", "expiry": "2026-05-01" } ],
"expiringSoonPassengers": []
}
}
}
Paspor yang berlaku < 6 bulan setelah tanggal keberangkatan muncul di expiringSoonPassengers — ini peringatan saja, tidak memblokir booking.
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{
"offerId": "<OFFER_ID_TERPILIH>",
"contactPhone": "+628123456789",
"contactEmail": "[email protected]",
"passengers": [
{ "title":"MR", "firstName":"JOHN", "lastName":"DOE",
"dateOfBirth":"1990-01-15", "nationality":"ID",
"passportNo":"A1234567", "passportExpiry":"2030-01-01", "type":"ADULT" }
]
}'
{
"id": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"pnr": null,
"status": "PENDING_PAYMENT",
"totalAmount": 1250000,
"currency": "IDR",
"expiresAt": "2026-07-07T10:30:00.000Z",
"issuedAt": null,
"passengers": [
{
"id": "clxpax1abcdef",
"title": "MR",
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": "1990-01-15",
"nationality": "ID",
"passportNo": "A1234567",
"passportExpiry": "2030-01-01",
"type": "ADULT",
"ticketNumber": null
}
],
"createdAt": "2026-07-07T09:00:00.000Z",
"updatedAt": "2026-07-07T09:00:00.000Z"
}
passportNo disimpan terenkripsi di database — di respons API tetap ditampilkan apa adanya untuk konfirmasi ke partner.
Skenario 11 — Cancel & Refund
Dua alur terpisah tergantung status booking.
Cancel (sebelum issued — status DRAFT / PENDING_PAYMENT / PAID)
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/cancel \
-H "X-API-Key: jwz_live_xxx"
{
"bookingId": "clxbkg1abcdef",
"status": "CANCELLED"
}
Hold dilepas dan dana (bila sudah didebit dari wallet) dikembalikan. Setelah ISSUED tidak bisa cancel — pakai alur refund di bawah.
Refund (setelah issued) — dua langkah
curl -s https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/refund-quote \
-H "X-API-Key: jwz_live_xxx"
{
"bookingId": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"status": "ISSUED",
"refundable": true,
"currency": "IDR",
"maxRefundAmount": 1065000,
"note": "Estimasi maksimum. Nilai final dikurangi biaya sesuai aturan tarif maskapai/supplier."
}
Bila booking belum ISSUED, respons menunjukkan "refundable": false, "maxRefundAmount": 0 dan note mengarahkan ke /cancel alih-alih refund.
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/refund \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{ "reason": "Pembatalan oleh penumpang karena perubahan jadwal" }'
reason wajib, minimal 10 karakter. amount opsional (default: estimasi maksimum dari refund-quote).
{
"bookingId": "clxbkg1abcdef",
"status": "ISSUED",
"message": "Refund request submitted to supplier. Confirmation will arrive via webhook.",
"refundId": "rfnd_xxx"
}
Refund via WALLET (atau supplier non-async) diselesaikan langsung. Sebagian supplier memproses refund secara async (konfirmasi via webhook) — status booking tetap ISSUED sampai konfirmasi webhook diterima, lalu berubah ke REFUNDED.
{
"ok": true,
"sandbox": true,
"message": "Refund disimulasikan di sandbox. Tidak ada dana yang dikembalikan."
}
Skenario 12 — Ancillary pasca-issue (bagasi / kursi / makan)
Hanya berlaku untuk booking berstatus ISSUED. Tiga langkah: lihat katalog, cek seatmap (untuk kursi), lalu beli.
Langkah 1 — Katalog ancillary
curl -s "https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/ancillaries/catalog?kind=BAGGAGE" \
-H "X-API-Key: jwz_live_xxx"
{
"supported": true,
"airline": { "code": "JT", "name": "Lion Air" },
"options": [
{
"id": "BAG-20",
"kind": "BAGGAGE",
"label": "Bagasi 20kg",
"price": 250000,
"currency": "IDR",
"scope": "PER_SEGMENT",
"available": true
}
]
}
Bila maskapai belum mendukung ancillary: { "supported": false, "reason": "UNSUPPORTED" } (atau "reason": "NOT_ISSUED" bila booking belum terbit).
Langkah 2 — Seatmap (khusus kursi)
curl -s https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/seatmap \
-H "X-API-Key: jwz_live_xxx"
{
"supported": true,
"columns": [ "A", "B", "C", "", "D", "E", "F" ],
"rows": [
{
"row": 12,
"seats": [
{ "seatNumber": "12A", "status": "AVAILABLE", "exitRow": false, "price": 150000, "currency": "IDR" },
{ "seatNumber": "12B", "status": "OCCUPIED", "exitRow": false }
]
}
]
}
"" di columns menandai posisi lorong. Bangun optionId kursi sebagai SEAT-<seatNumber> (mis. kursi 12A → SEAT-12A).
Langkah 3 — Beli ancillary
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/ancillaries \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{
"payment": "WALLET",
"items": [
{ "optionId": "BAG-20", "kind": "BAGGAGE", "passengerId": "clxpax1abcdef", "segmentIndex": 0 },
{ "optionId": "SEAT-12A", "kind": "SEAT", "passengerId": "clxpax1abcdef" }
]
}'
passengerId diambil dari GET /bookings/:id (field passengers[].id).
{
"purchaseId": "anc_xxx",
"total": 400000,
"currency": "IDR",
"items": [
{ "optionId": "BAG-20", "kind": "BAGGAGE", "passengerId": "clxpax1abcdef", "price": 250000 },
{ "optionId": "SEAT-12A", "kind": "SEAT", "passengerId": "clxpax1abcdef", "seatNumber": "12A", "price": 150000 }
]
}
{
"ok": true,
"sandbox": true,
"message": "Pembelian ancillary disimulasikan di sandbox. Tidak ada debit nyata."
}
Pola optionId per jenis: bagasi BAG-<n>, makan MEAL-<code> (mis. MEAL-VGML), kursi SEAT-<seatNumber>.
SEAT_UNAVAILABLE (kursi sudah terisi/dipesan orang lain) dan HTTP 400 SEAT_PER_PAX (maksimum 1 kursi per penumpang per segmen).
Skenario Lanjutan (13–16) — Harga, Idempotency & After-Sales
Empat skenario lanjutan berikutnya: markup agen & voucher diskon saat create, retry aman lewat idempotency key, jadwal ulang penerbangan (reschedule), dan perpanjangan hold otomatis (auto-rebook) sebelum booking kedaluwarsa.
Skenario 13 — Agent Markup & Voucher Diskon
Dua field opsional di body create booking (POST /bookings) yang memengaruhi harga akhir:
agentMarkup— profit milik agen sendiri, ditambahkan di atas harga platform. Bentuk:{ "type": "PERCENT", "value": 5, "max": 100000 }—typeadalahPERCENTatauFIXED;value= persen (untukPERCENT) atau nominal IDR flat (untukFIXED);maxopsional, batas atas nominal IDR khusus untukPERCENT.PERCENTdihitung dari harga yang ditampilkan;FIXEDadalah tambahan flat IDR.voucherCode— kode diskon yang mengurangi total. Bentuk: string, mis."voucherCode": "PROMO10".
Keduanya bisa dikombinasikan dalam satu request create.
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{
"offerId": "<OFFER_ID_TERPILIH>",
"contactPhone": "+628123456789",
"contactEmail": "[email protected]",
"passengers": [
{ "title":"MR", "firstName":"BUDI", "lastName":"SANTOSO",
"dateOfBirth":"1990-01-15", "nationality":"ID", "type":"ADULT" }
],
"agentMarkup": { "type": "PERCENT", "value": 5, "max": 100000 },
"voucherCode": "PROMO10"
}'
{
"id": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"status": "PENDING_PAYMENT",
"agentMarkup": 50000,
"voucherCode": "PROMO10",
"voucherDiscount": 100000,
"totalAmount": 950000,
// total = fare + fees + markup agen − diskon voucher
"currency": "IDR",
"expiresAt": "2026-07-07T10:30:00.000Z"
}
Markup menaikkan total (profit agen, ikut tertagih ke pelanggan); voucher menurunkan total (diskon). Bila voucherCode tidak valid/sudah kedaluwarsa, booking tetap dibuat — tanpa diskon ("voucherDiscount": 0) atau ditolak dengan error validasi voucher, tergantung jenis kegagalannya.
Skenario 14 — Idempotency (retry aman)
Semua POST yang memindahkan uang / membuat resource menerima header Idempotency-Key: <string unik Anda> — utamanya POST /bookings, POST /bookings/:id/pay, POST /bookings/:id/ancillaries, dan POST /bookings/:id/reschedule.
Kirim key unik per operasi logis. Bila request dengan key yang sama diulang (mis. karena timeout jaringan), server mengembalikan response yang sama persis dari yang pertama — server tidak membuat booking/charge ganda. Panggilan pertama → 201 Created; pengulangan identik → 200 OK (replay).
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-H "Idempotency-Key: idem-booking-2026-07-15-budi-001" \
-d '{
"offerId": "<OFFER_ID_TERPILIH>",
"contactPhone": "+628123456789",
"contactEmail": "[email protected]",
"passengers": [
{ "title":"MR", "firstName":"BUDI", "lastName":"SANTOSO",
"dateOfBirth":"1990-01-15", "nationality":"ID", "type":"ADULT" }
]
}'
{
"id": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"status": "PENDING_PAYMENT",
"totalAmount": 1000000,
"currency": "IDR",
"expiresAt": "2026-07-07T10:30:00.000Z"
}
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-H "Idempotency-Key: idem-booking-2026-07-15-budi-001" \
-d '{ /* body identik dengan panggilan pertama */ }'
{
"id": "clxbkg1abcdef",
"bookingCode": "JWZ-20260707-ABC123",
"status": "PENDING_PAYMENT",
"totalAmount": 1000000,
"currency": "IDR",
"expiresAt": "2026-07-07T10:30:00.000Z"
}
bookingCode/id identik dengan respons pertama — bukti tidak ada booking kedua yang tercipta.
Skenario 15 — Reschedule (ganti penerbangan, rute sama)
Hanya untuk booking berstatus ISSUED, dan lebih dari 24 jam sebelum keberangkatan. Dua langkah: lihat opsi, lalu eksekusi.
curl -s "https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/reschedule/options?date=2026-08-01" \
-H "X-API-Key: jwz_live_xxx"
{
"supported": true,
"reason": null,
"airline": { "code": "GA", "name": "Garuda Indonesia" },
"options": [
{
"optionId": "RSO-GA402-20260801",
"flightNumber": "GA-402",
"departureTime": "2026-08-01T01:00:00.000Z",
"arrivalTime": "2026-08-01T04:05:00.000Z",
"fareDifference": 150000,
"changeFee": 100000,
"currency": "IDR"
}
]
}
Bila booking belum ISSUED: { "supported": false, "reason": "NOT_ISSUED" }; bila kurang dari 24 jam sebelum keberangkatan: "reason": "WINDOW_CLOSED"; maskapai belum mendukung reschedule: "reason": "UNSUPPORTED". Tanggal tidak valid → error INVALID_DATE.
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/reschedule \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-H "Idempotency-Key: idem-reschedule-clxbkg1abcdef-001" \
-d '{
"optionId": "RSO-GA402-20260801",
"date": "2026-08-01",
"payment": "WALLET"
}'
Menagih max(0, fareDifference) + changeFee, void tiket lama, lalu terbitkan PNR baru.
{
"oldPnr": "FT12345K001824",
"newPnr": "FT98765K118240",
"flightNumber": "GA-402",
"departureTime": "2026-08-01T01:00:00.000Z",
"arrivalTime": "2026-08-01T04:05:00.000Z",
"fareDifference": 150000,
"changeFee": 100000,
"amountCharged": 250000,
"currency": "IDR"
}
Catatan: bila selisih tarif negatif (penerbangan baru lebih murah), selisih itu tidak dikembalikan — hanya changeFee yang tetap ditagih.
{
"ok": true,
"sandbox": true,
"message": "Jadwal ulang disimulasikan di sandbox. Tidak ada perubahan nyata."
}
{
"error": {
"code": "RESCHEDULE_WINDOW_CLOSED",
"message": "Reschedule window has closed",
"details": { "bookingId": "clxbkg1abcdef" },
"traceId": "..."
}
}
Skenario 16 — Auto-Rebook (perpanjang hold otomatis sebelum kedaluwarsa)
Untuk booking PENDING_PAYMENT yang belum dibayar; digerbang per-supplier (tidak semua supplier mendukung). Memperpanjang hold secara otomatis sebelum expiresAt — tiap siklus: cek ketersediaan + harga, lalu cancel → rebook. Fitur ini tidak menerbitkan tiket dan tidak menarik pembayaran. Opt-in per booking.
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/auto-rebook/enable \
-H "X-API-Key: jwz_live_xxx" -H "Content-Type: application/json" \
-d '{ "maxHoldMinutes": 1440, "priceThresholdPct": 10 }'
maxHoldMinutes antara 60–2880, default 1440 (24 jam). priceThresholdPct opsional — bila harga renewal naik lebih dari ambang ini, siklus PAUSE menunggu approval; dihilangkan berarti siklus tidak pernah pause karena harga.
{
"status": "ACTIVE",
"cycleId": "arc_abc123",
"nextAttemptAt": "2026-07-15T05:25:00.000Z"
}
{
"error": {
"code": "AUTO_REBOOK_NOT_ELIGIBLE",
"message": "Booking is not eligible for auto-rebook",
"details": { "bookingId": "clxbkg1abcdef" },
"traceId": "..."
}
}
Kode error 409 lain yang mungkin muncul: AUTO_REBOOK_ALREADY_ACTIVE (siklus sudah aktif) dan AUTO_REBOOK_SUPPLIER_DISABLED (supplier belum mendukung fitur ini). AUTO_REBOOK_NOT_ELIGIBLE muncul bila booking bukan PENDING_PAYMENT.
curl -s https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/auto-rebook/history \
-H "X-API-Key: jwz_live_xxx"
{
"cycle": {
"status": "ACTIVE",
"maxHoldMinutes": 1440,
"priceThresholdPct": 10,
"expiresAt": "2026-07-16T04:00:00.000Z",
"nextAttemptAt": "2026-07-15T05:25:00.000Z",
"attemptCount": 1,
"lastPrice": 1500000
},
"attempts": [
{
"attemptNumber": 1,
"attemptedAt": "2026-07-15T04:55:00.000Z",
"result": "SUCCESS",
"prevBookingRef": "BKG-AB12CD",
"newBookingRef": "BKG-EF34GH",
"prevPrice": 1500000,
"newPrice": 1500000,
"priceDelta": 0,
"priceDeltaPct": 0
}
]
}
Nilai result yang mungkin: SUCCESS, SEAT_UNAVAILABLE, API_ERROR, PRICE_THRESHOLD_EXCEEDED, HOLD_LOST.
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/auto-rebook/disable \
-H "X-API-Key: jwz_live_xxx"
{
"status": "STOPPED",
"bookingId": "clxbkg1abcdef",
"lastAttemptAt": "2026-07-15T04:55:00.000Z"
}
Hold aktif yang sudah ada tetap dipertahankan — disable hanya menghentikan siklus perpanjangan otomatisnya. Error (HTTP 409): AUTO_REBOOK_NOT_ACTIVE (siklus belum/tidak aktif).
curl -s -X POST https://api.jetwize.com/api/v1/partner/v1/bookings/<BOOKING_ID>/auto-rebook/approve \
-H "X-API-Key: jwz_live_xxx"
{
"status": "ACTIVE",
"nextAttemptAt": "2026-07-15T05:40:00.000Z"
}
Error (HTTP 409): AUTO_REBOOK_NOT_PAUSED (siklus tidak dalam status paused).
Sandbox vs Production — apa yang bisa diuji
MockSupplier (kunci jwz_test_...) sengaja disederhanakan agar deterministik untuk menguji mekanika API (auth, search, create, pay, e-ticket). Yang dikembalikan sandbox:
- Hanya penerbangan langsung (
stops: 0, satu segmen) — tidak ada offer transfer. - Hanya satu brand dengan bagasi
"20kg"— tidak ada varian tanpa bagasi. returnDatediabaikan pada offer sandbox — tidak membentuk leg return.- Offer id yang Anda terima berupa token opaque
off_…. Di sandbox token itu me-resolve ke inventory mock internal — cukup kirim balik token apa adanya; wallet sandbox bernilai besar tetap.
| Skenario | Sandbox | Production |
|---|---|---|
| 1. OW Direct + bagasi | ✅ bisa diuji end-to-end | ✅ |
| 2. OW Direct tanpa bagasi | ⚠️ brand tanpa bagasi tak muncul di sandbox | ✅ |
| 3–4. RT Direct | ⚠️ leg return tak muncul di sandbox | ✅ |
| 5–8. Transfer & RT Transfer | ⚠️ offer transfer tak muncul di sandbox | ✅ |
Error umum pada alur booking
| code | Arti | Tindakan |
|---|---|---|
MISSING_API_KEY | Header key tidak ada / bukan jwz_ | Kirim X-API-Key: jwz_... |
INVALID_API_KEY / EXPIRED_API_KEY | Key tidak valid/kadaluarsa | Regenerasi key |
AGENCY_INACTIVE | Agensi tidak aktif | Hubungi admin JetWize |
SANDBOX_OFFER_REQUIRED | Sandbox key hanya dapat membuat booking dari token offer hasil search sandbox | Search dulu dengan kunci sandbox, lalu book token offer hasil search tsb |
INVALID_OFFER_ID | Token offer tidak valid / kedaluwarsa | Lakukan search ulang lalu pakai token baru |
BOOKING_EXPIRED | Hold sudah lewat expiresAt | Ulangi dari search |
INSUFFICIENT_BALANCE | Wallet tidak cukup saat pay | Top-up wallet, lalu pay |
Referensi cepat endpoint booking
| Langkah | Endpoint |
|---|---|
| Search | POST /flights/search |
| Price-check | POST /flights/price-check |
| Inspeksi bagasi/brand | GET /flights/:offerId/fare-rules |
| Create booking | POST /bookings |
| Pay → issue | POST /bookings/:id/pay |
| E-ticket | GET /bookings/:id/ticket |
| Detail/status | GET /bookings/:id |
| Batal (pra-issue) | POST /bookings/:id/cancel |
| Bagasi ekstra (pasca-issue) | POST /bookings/:id/ancillaries (kind: BAGGAGE) |
SDK — 8 Skenario (Node.js & Python)
Contoh kode lengkap yang menjalankan semua 8 skenario secara loop. Setiap bahasa menyertakan client yang dapat digunakan ulang (helper search, priceCheck, createBooking, pay, getTicket) beserta helper pemilihan offer (direct/transfer, dengan/tanpa bagasi), lalu satu driver yang mengiterasi ke-8 kasus.
"20kg". Gunakan kunci sandbox (jwz_test_...) untuk memvalidasi mekanika integrasi, lalu kunci production (jwz_live_...) untuk menguji semua 8 skenario secara penuh.
Node.js (fetch)
// jetwize-client.js
// Reusable JetWize Partner API client (Node.js 18+ native fetch)
const BASE = 'https://api.jetwize.com/api/v1/partner/v1';
const API_KEY = process.env.JETWIZE_API_KEY; // jwz_test_... atau jwz_live_...
if (!API_KEY) throw new Error('Set JETWIZE_API_KEY environment variable');
// ---------------------------------------------------------------------------
// Low-level helper
// ---------------------------------------------------------------------------
async function request(method, path, body) {
const headers = { 'X-API-Key': API_KEY };
if (body !== undefined) headers['Content-Type'] = 'application/json';
const res = await fetch(`${BASE}${path}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const json = await res.json().catch(() => ({}));
if (!res.ok) {
const code = json?.error?.code ?? res.status;
throw new Error(`[${code}] ${json?.error?.message ?? res.statusText}`);
}
return json;
}
// ---------------------------------------------------------------------------
// API helpers
// ---------------------------------------------------------------------------
/** Search flights. Pass returnDate for round-trip; omit for one-way.
* route: "DIRECT" | "ALL" (default "ALL") */
async function search(params) {
return request('POST', '/flights/search', {
origin: params.origin,
destination: params.destination,
departDate: params.departDate,
returnDate: params.returnDate, // undefined = one-way
passengers: params.passengers ?? { adult: 1, child: 0, infant: 0 },
cabinClass: params.cabinClass ?? 'ECONOMY',
route: params.route ?? 'ALL',
});
}
/** Confirm price is still valid before booking. */
async function priceCheck(offerId) {
return request('POST', '/flights/price-check', { offerId });
}
/** Create a booking (hold, ~30 min).
* contactPhone is REQUIRED (flat field — never nested contact{}). */
async function createBooking({ offerId, contactPhone, contactEmail, passengers }) {
return request('POST', '/bookings', {
offerId,
contactPhone, // REQUIRED — flat field
...(contactEmail ? { contactEmail } : {}),
passengers,
});
}
/** Pay (wallet) and trigger issuance saga. */
async function pay(bookingId) {
return request('POST', `/bookings/${bookingId}/pay`);
}
/** Retrieve e-ticket signed URL (expires 15 min). */
async function getTicket(bookingId) {
return request('GET', `/bookings/${bookingId}/ticket`);
}
// ---------------------------------------------------------------------------
// Offer-selection predicates
// ---------------------------------------------------------------------------
/** true if offer is a nonstop flight (zero transit stops). */
const isDirect = (offer) => offer.stops === 0;
/** true if offer has at least one transit stop. */
const isTransfer = (offer) => offer.stops >= 1;
/** Heuristic: inspect each segment's baggageAllowance string.
* Treats values suggesting NO checked baggage as without-baggage.
* NOTE: baggageAllowance is a supplier free-text string — confirm edge
* cases via GET /flights/:offerId/fare-rules for authoritative data. */
function hasBaggage(offer) {
const NO_BAGGAGE = /tanpa|no.?baggage|0\s*kg|kabin.?saja|cabin.?only/i;
return offer.segments.every((seg) => {
const ba = seg.baggageAllowance ?? '';
return ba.trim() !== '' && !NO_BAGGAGE.test(ba);
});
}
module.exports = { search, priceCheck, createBooking, pay, getTicket,
isDirect, isTransfer, hasBaggage };
// run-all-cases.js — iterates over all 8 booking scenarios
'use strict';
const { search, priceCheck, createBooking, pay, getTicket,
isDirect, isTransfer, hasBaggage } = require('./jetwize-client');
// ---------------------------------------------------------------------------
// Case table — 8 scenarios: OW/RT × Direct/Transfer × with/without baggage
// ---------------------------------------------------------------------------
// Each case defines:
// label — human-readable name
// searchParams — params for search() (returnDate present = RT; route = DIRECT|ALL)
// pickOffer — predicate to select the correct offer from results[]
// ---------------------------------------------------------------------------
const CASES = [
{
label: '1. OW Direct — dengan bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
route: 'DIRECT',
},
pickOffer: (o) => isDirect(o) && hasBaggage(o),
},
{
label: '2. OW Direct — tanpa bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
route: 'DIRECT',
},
pickOffer: (o) => isDirect(o) && !hasBaggage(o),
},
{
label: '3. RT Direct — dengan bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
returnDate: '2026-07-20', route: 'DIRECT',
},
pickOffer: (o) => isDirect(o) && hasBaggage(o),
},
{
label: '4. RT Direct — tanpa bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
returnDate: '2026-07-20', route: 'DIRECT',
},
pickOffer: (o) => isDirect(o) && !hasBaggage(o),
},
{
label: '5. OW Transfer — dengan bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
route: 'ALL',
},
pickOffer: (o) => isTransfer(o) && hasBaggage(o),
},
{
label: '6. OW Transfer — tanpa bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
route: 'ALL',
},
pickOffer: (o) => isTransfer(o) && !hasBaggage(o),
},
{
label: '7. RT Transfer — dengan bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
returnDate: '2026-07-20', route: 'ALL',
},
pickOffer: (o) => isTransfer(o) && hasBaggage(o),
},
{
label: '8. RT Transfer — tanpa bagasi',
searchParams: {
origin: 'CGK', destination: 'DPS', departDate: '2026-07-15',
returnDate: '2026-07-20', route: 'ALL',
},
pickOffer: (o) => isTransfer(o) && !hasBaggage(o),
},
];
// Shared passenger used for all cases
const PASSENGERS = [
{ title: 'MR', firstName: 'BUDI', lastName: 'SANTOSO',
dateOfBirth: '1990-01-15', nationality: 'ID', type: 'ADULT' },
];
// ---------------------------------------------------------------------------
// Shared booking flow: priceCheck → createBooking → pay → getTicket
// (identical for all 8 cases — only the offer changes)
// ---------------------------------------------------------------------------
async function runFlow(offer, caseLabel) {
console.log(`\n [price-check] offerId=${offer.id}`);
const pc = await priceCheck(offer.id);
if (!pc.stillAvailable) throw new Error('Offer tidak tersedia lagi');
if (pc.priceChanged) {
console.log(` [price-check] HARGA BERUBAH: ${pc.currentPrice} — lanjut otomatis`);
}
console.log(' [create booking]');
const booking = await createBooking({
offerId: offer.id,
contactPhone: '+628123456789', // REQUIRED flat field
contactEmail: '[email protected]', // optional
passengers: PASSENGERS,
});
console.log(` bookingId=${booking.id} code=${booking.bookingCode} expires=${booking.expiresAt}`);
console.log(' [pay]');
const result = await pay(booking.id);
console.log(` status=${result.booking?.status ?? result.status}`);
console.log(' [e-ticket]');
const ticket = await getTicket(booking.id);
console.log(` ticketUrl=${ticket.ticketUrl}`);
return { booking, ticket };
}
// ---------------------------------------------------------------------------
// Main: loop over all 8 cases
// ---------------------------------------------------------------------------
(async () => {
for (const c of CASES) {
console.log(`\n=== ${c.label} ===`);
try {
const { results, sandbox } = await search(c.searchParams);
if (sandbox) {
console.log(' [sandbox] MockSupplier: skenario 2-8 mungkin tidak tersedia');
}
const offer = results.find(c.pickOffer);
if (!offer) {
console.warn(` SKIP — tidak ada offer yang cocok (normal di sandbox untuk kasus ini)`);
continue;
}
console.log(` offer: id=${offer.id} stops=${offer.stops} baggage=${offer.segments[0]?.baggageAllowance}`);
await runFlow(offer, c.label);
console.log(` DONE`);
} catch (err) {
console.error(` ERROR: ${err.message}`);
}
}
})();
Python (requests)
# jetwize_client.py
# Reusable JetWize Partner API client (Python 3.8+)
import os
import re
import requests as _requests
BASE = "https://api.jetwize.com/api/v1/partner/v1"
_API_KEY = os.environ.get("JETWIZE_API_KEY") # jwz_test_... atau jwz_live_...
if not _API_KEY:
raise RuntimeError("Set JETWIZE_API_KEY environment variable")
_SESSION = _requests.Session()
_SESSION.headers.update({"X-API-Key": _API_KEY, "Content-Type": "application/json"})
# ---------------------------------------------------------------------------
# Low-level helper
# ---------------------------------------------------------------------------
def _req(method: str, path: str, body: dict | None = None) -> dict:
r = _SESSION.request(method, f"{BASE}{path}", json=body, timeout=30)
try:
data = r.json()
except Exception:
data = {}
if not r.ok:
code = data.get("error", {}).get("code", r.status_code)
msg = data.get("error", {}).get("message", r.reason)
raise RuntimeError(f"[{code}] {msg}")
return data
# ---------------------------------------------------------------------------
# API helpers
# ---------------------------------------------------------------------------
def search(origin: str, destination: str, depart_date: str,
return_date: str | None = None,
route: str = "ALL",
cabin_class: str = "ECONOMY",
passengers: dict | None = None) -> dict:
"""Search flights. Pass return_date for round-trip; omit for one-way.
route: "DIRECT" | "ALL" (default "ALL")"""
body = {
"origin": origin,
"destination": destination,
"departDate": depart_date,
"passengers": passengers or {"adult": 1, "child": 0, "infant": 0},
"cabinClass": cabin_class,
"route": route,
}
if return_date:
body["returnDate"] = return_date # round-trip
return _req("POST", "/flights/search", body)
def price_check(offer_id: str) -> dict:
"""Confirm price is still valid before booking."""
return _req("POST", "/flights/price-check", {"offerId": offer_id})
def create_booking(offer_id: str, contact_phone: str,
passengers: list, contact_email: str | None = None) -> dict:
"""Create a booking (hold, ~30 min).
contact_phone is REQUIRED (flat field — never nested contact{})."""
body = {
"offerId": offer_id,
"contactPhone": contact_phone, # REQUIRED — flat field
"passengers": passengers,
}
if contact_email:
body["contactEmail"] = contact_email
return _req("POST", "/bookings", body)
def pay(booking_id: str) -> dict:
"""Pay (wallet) and trigger issuance saga."""
return _req("POST", f"/bookings/{booking_id}/pay")
def get_ticket(booking_id: str) -> dict:
"""Retrieve e-ticket signed URL (expires 15 min)."""
return _req("GET", f"/bookings/{booking_id}/ticket")
# ---------------------------------------------------------------------------
# Offer-selection predicates
# ---------------------------------------------------------------------------
def is_direct(offer: dict) -> bool:
"""True if offer is a nonstop flight (zero transit stops)."""
return offer.get("stops", 0) == 0
def is_transfer(offer: dict) -> bool:
"""True if offer has at least one transit stop."""
return offer.get("stops", 0) >= 1
_NO_BAGGAGE = re.compile(
r"tanpa|no.?baggage|0\s*kg|kabin.?saja|cabin.?only", re.IGNORECASE
)
def has_baggage(offer: dict) -> bool:
"""Heuristic: inspect each segment's baggageAllowance string.
Treats values implying NO checked baggage as without-baggage.
NOTE: baggageAllowance is a supplier free-text string — confirm edge
cases via GET /flights/:offerId/fare-rules for authoritative data."""
for seg in offer.get("segments", []):
ba = (seg.get("baggageAllowance") or "").strip()
if not ba or _NO_BAGGAGE.search(ba):
return False
return True
# run_all_cases.py — iterates over all 8 booking scenarios
from jetwize_client import (
search, price_check, create_booking, pay, get_ticket,
is_direct, is_transfer, has_baggage,
)
# ---------------------------------------------------------------------------
# Case table — 8 scenarios: OW/RT × Direct/Transfer × with/without baggage
# ---------------------------------------------------------------------------
# Each entry:
# label — human-readable name
# search_kw — kwargs for search() (return_date present = RT; route = DIRECT|ALL)
# pick — predicate to select the correct offer from results
# ---------------------------------------------------------------------------
CASES = [
{
"label": "1. OW Direct — dengan bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "route": "DIRECT"},
"pick": lambda o: is_direct(o) and has_baggage(o),
},
{
"label": "2. OW Direct — tanpa bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "route": "DIRECT"},
"pick": lambda o: is_direct(o) and not has_baggage(o),
},
{
"label": "3. RT Direct — dengan bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "return_date": "2026-07-20",
"route": "DIRECT"},
"pick": lambda o: is_direct(o) and has_baggage(o),
},
{
"label": "4. RT Direct — tanpa bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "return_date": "2026-07-20",
"route": "DIRECT"},
"pick": lambda o: is_direct(o) and not has_baggage(o),
},
{
"label": "5. OW Transfer — dengan bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "route": "ALL"},
"pick": lambda o: is_transfer(o) and has_baggage(o),
},
{
"label": "6. OW Transfer — tanpa bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "route": "ALL"},
"pick": lambda o: is_transfer(o) and not has_baggage(o),
},
{
"label": "7. RT Transfer — dengan bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "return_date": "2026-07-20",
"route": "ALL"},
"pick": lambda o: is_transfer(o) and has_baggage(o),
},
{
"label": "8. RT Transfer — tanpa bagasi",
"search_kw": {"origin": "CGK", "destination": "DPS",
"depart_date": "2026-07-15", "return_date": "2026-07-20",
"route": "ALL"},
"pick": lambda o: is_transfer(o) and not has_baggage(o),
},
]
# Shared passenger used for all cases
PASSENGERS = [
{"title": "MR", "firstName": "BUDI", "lastName": "SANTOSO",
"dateOfBirth": "1990-01-15", "nationality": "ID", "type": "ADULT"},
]
# ---------------------------------------------------------------------------
# Shared booking flow: price_check → create_booking → pay → get_ticket
# (identical for all 8 cases — only the offer changes)
# ---------------------------------------------------------------------------
def run_flow(offer: dict) -> dict:
print(f" [price-check] offerId={offer['id']}")
pc = price_check(offer["id"])
if not pc.get("stillAvailable"):
raise RuntimeError("Offer tidak tersedia lagi")
if pc.get("priceChanged"):
print(f" [price-check] HARGA BERUBAH: {pc['currentPrice']} — lanjut otomatis")
print(" [create booking]")
booking = create_booking(
offer_id=offer["id"],
contact_phone="+628123456789", # REQUIRED flat field
contact_email="[email protected]", # optional
passengers=PASSENGERS,
)
print(f" bookingId={booking['id']} code={booking['bookingCode']} expires={booking['expiresAt']}")
print(" [pay]")
result = pay(booking["id"])
status = (result.get("booking") or {}).get("status") or result.get("status")
print(f" status={status}")
print(" [e-ticket]")
ticket = get_ticket(booking["id"])
print(f" ticketUrl={ticket['ticketUrl']}")
return {"booking": booking, "ticket": ticket}
# ---------------------------------------------------------------------------
# Main: loop over all 8 cases
# ---------------------------------------------------------------------------
if __name__ == "__main__":
for c in CASES:
print(f"\n=== {c['label']} ===")
try:
data = search(**c["search_kw"])
results = data.get("results", [])
if data.get("sandbox"):
print(" [sandbox] MockSupplier: skenario 2-8 mungkin tidak tersedia")
offer = next((o for o in results if c["pick"](o)), None)
if offer is None:
print(" SKIP — tidak ada offer yang cocok (normal di sandbox untuk kasus ini)")
continue
ba0 = (offer["segments"][0].get("baggageAllowance") or "-")
print(f" offer: id={offer['id']} stops={offer['stops']} baggage={ba0}")
run_flow(offer)
print(" DONE")
except Exception as exc:
print(f" ERROR: {exc}")
Webhooks
JetWize mengirimkan notifikasi ke URL webhook Anda saat status booking berubah. Daftarkan webhook URL di dashboard Settings → Webhooks.
Event Types
| Event | Keterangan |
|---|---|
| booking.issued | Tiket berhasil diterbitkan |
| booking.cancelled | Booking dibatalkan |
| booking.failed | Penerbitan tiket gagal |
| booking.refunded | Refund diproses |
POST https://your-server.com/webhook
Content-Type: application/json
X-JetWize-Signature: sha256=...
{
"event": "booking.issued",
"bookingId": "clx789...",
"bookingCode": "JWZ-20260615-ABC123",
"status": "ISSUED",
"tickets": ["0123456789"],
"issuedAt": "2026-06-15T07:15:00.000Z"
}
Code Examples
JavaScript / Node.js
const API = 'https://api.jetwize.com/api/v1/partner/v1';
const KEY = process.env.JETWIZE_API_KEY; // jwz_test_... atau jwz_live_...
async function searchFlights(origin, destination, date) {
const res = await fetch(`${API}/flights/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': KEY,
},
body: JSON.stringify({
origin, destination,
departDate: date,
passengers: { adult: 1, child: 0, infant: 0 },
cabinClass: 'ECONOMY',
}),
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
// Full booking flow
async function bookFlight() {
// 1. Search
const { results } = await searchFlights('CGK', 'DPS', '2026-06-15');
const offer = results[0];
// 2. Price check
const priceCheck = await fetch(`${API}/flights/price-check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': KEY },
body: JSON.stringify({ offerId: offer.id }),
}).then(r => r.json());
if (!priceCheck.stillAvailable) throw new Error('Penerbangan tidak tersedia');
// 3. Create booking
const booking = await fetch(`${API}/bookings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': KEY },
body: JSON.stringify({
offerId: offer.id,
passengers: [{ title: 'MR', firstName: 'BUDI', lastName: 'SANTOSO',
dateOfBirth: '1990-01-15', nationality: 'ID', type: 'ADULT' }],
contactPhone: '+6281234567890',
contactEmail: '[email protected]',
}),
}).then(r => r.json());
// 4. Pay
const payment = await fetch(`${API}/bookings/${booking.id}/pay`, {
method: 'POST',
headers: { 'X-API-Key': KEY },
}).then(r => r.json());
return { booking, payment };
}
Python
import os, requests
API = 'https://api.jetwize.com/api/v1/partner/v1'
HEADERS = {
'Content-Type': 'application/json',
'X-API-Key': os.environ['JETWIZE_API_KEY']
}
def search_flights(origin, destination, depart_date):
r = requests.post(f'{API}/flights/search', json={
'origin': origin, 'destination': destination,
'departDate': depart_date,
'passengers': {'adult': 1, 'child': 0, 'infant': 0},
'cabinClass': 'ECONOMY'
}, headers=HEADERS)
r.raise_for_status()
return r.json()['results']
def create_booking(offer_id, passengers, contact_phone, contact_email=None):
body = {
'offerId': offer_id,
'passengers': passengers,
'contactPhone': contact_phone, # REQUIRED — flat field
}
if contact_email:
body['contactEmail'] = contact_email
r = requests.post(f'{API}/bookings', json=body, headers=HEADERS)
r.raise_for_status()
return r.json()
def pay_booking(booking_id):
r = requests.post(f'{API}/bookings/{booking_id}/pay', headers=HEADERS)
r.raise_for_status()
return r.json()
# Usage
offers = search_flights('CGK', 'DPS', '2026-06-15')
booking = create_booking(offers[0]['id'],
[{'title': 'MR', 'firstName': 'BUDI', 'lastName': 'SANTOSO',
'dateOfBirth': '1990-01-15', 'nationality': 'ID', 'type': 'ADULT'}],
contact_phone='+6281234567890', # REQUIRED flat field
contact_email='[email protected]' # optional
)
payment = pay_booking(booking['id'])
PHP
<?php
$API = 'https://api.jetwize.com/api/v1/partner/v1';
$KEY = getenv('JETWIZE_API_KEY');
function jetwize_request($method, $path, $body = null) {
global $API, $KEY;
$ch = curl_init("$API$path");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-API-Key: $KEY",
],
CURLOPT_POSTFIELDS => $body ? json_encode($body) : null,
CURLOPT_TIMEOUT => 30,
]);
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400) throw new Exception("API Error $code: $res");
return json_decode($res, true);
}
// Search
$result = jetwize_request('POST', '/flights/search', [
'origin' => 'CGK', 'destination' => 'DPS',
'departDate' => '2026-06-15',
'passengers' => ['adult' => 1, 'child' => 0, 'infant' => 0],
]);
$offer = $result['results'][0];
// Book & Pay
$booking = jetwize_request('POST', '/bookings', [
'offerId' => $offer['id'],
'passengers' => [['title'=>'MR','firstName'=>'BUDI','lastName'=>'SANTOSO',
'dateOfBirth'=>'1990-01-15','nationality'=>'ID','type'=>'ADULT']],
'contactPhone' => '+6281234567890', // REQUIRED — flat field
'contactEmail' => '[email protected]', // optional
]);
$payment = jetwize_request('POST', "/bookings/{$booking['id']}/pay");
JetWize Partner API v1 · jetwize.com · Support: [email protected]