{
  "openapi": "3.1.0",
  "info": {
    "title": "OngkirHub API",
    "version": "1.0.0",
    "summary": "Cek tarif, buat resi, lacak paket, dan baca saldo OngkirHub dari sistem kamu.",
    "description": "REST API OngkirHub untuk cek tarif, membuat resi, melacak paket, dan membaca saldo dari sistem kamu sendiri.\n\n## Base URL\n\n`https://api.ongkirhub.com/v1`. Semua permintaan dan respons memakai JSON (UTF-8). Uang selalu dalam rupiah bilangan bulat, berat dalam kg, waktu dalam ISO 8601 (UTC, kecuali jadwal jemput yang memakai +07:00).\n\n## Autentikasi\n\nBuat kunci API di menu Developer aplikasi OngkirHub, lalu kirim di setiap permintaan:\n\n```\nAuthorization: Bearer ohk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nKunci hanya ditampilkan sekali saat dibuat. Simpan di server kamu, jangan di aplikasi browser atau aplikasi mobile: API ini tidak mengirim header CORS, jadi panggil dari backend kamu.\n\nSetiap kunci punya akses (scope). Endpoint menolak kunci tanpa akses yang dibutuhkan dengan `403 forbidden`:\n\n- `locations:read`: Cari lokasi\n- `couriers:read`: Daftar kurir\n- `rates:read`: Cek ongkir\n- `shipments:read`: Lihat pengiriman\n- `shipments:write`: Buat dan batalkan pengiriman\n- `tracking:read`: Lacak resi\n- `wallet:read`: Lihat saldo dan mutasi\n\n## Mode test dan live\n\n- Kunci `ohk_test_...` memakai simulasi: tarif, resi, dan pelacakan dibuat oleh simulator OngkirHub, tidak ada kurir yang datang dan saldo sungguhan tidak tersentuh. Mode test memakai saldo uji Rp10.000.000 yang berkurang oleh pengiriman uji 30 hari terakhir.\n- Kunci `ohk_live_...` membuat pesanan sungguhan ke kurir dan memotong saldo OngkirHub kamu.\n\nData kedua mode terpisah: kunci test hanya melihat pengiriman test, kunci live hanya melihat pengiriman live, dan penawaran dari satu mode tidak bisa dipakai di mode lain. Setiap objek punya field `livemode`.\n\nDi mode test, resi simulasi terbit sekitar 20 detik sampai beberapa menit setelah pesanan diterima, lalu riwayat pelacakan bergerak selama sekitar 1 jam sampai terkirim. Untuk mencoba skenario lain, tulis salah satu penanda ini di nama atau alamat penerima (hanya berlaku untuk kunci test):\n\n- `[mock:courier_rejected]`: kurir menolak pesanan, status `failed` dan saldo uji dikembalikan.\n- `[mock:problem]`, `[mock:lost]`, `[mock:failed_pickup]`, `[mock:cancelled]`: pelacakan berakhir dengan kendala (status `problem`).\n- `[mock:returned]`: paket diretur ke pengirim (status `returned`).\n\nPembatalan dari kunci test langsung disetujui: pengiriman berpindah ke `cancel_requested` lalu `cancelled`, event `shipment.status_updated` dan `shipment.cancelled` terkirim, dan saldo uji kembali. Di mode live, pembatalan tetap ditinjau tim OngkirHub.\n\n## Alur membuat resi\n\n1. Cari wilayah asal dan tujuan dengan `GET /v1/locations`.\n2. Cek tarif dengan `POST /v1/rates`. Respons berisi `id` penawaran (berlaku 30 menit) dan daftar layanan.\n3. Buat pengiriman dengan `POST /v1/shipments`: kirim isi yang sama dengan langkah 2, ditambah `quoteId`, `rateCode`, dan `pickupTime` untuk PICKUP. Total harga ditahan dari saldo dan pengiriman berstatus `submitting`.\n4. Pesanan diteruskan ke kurir beberapa detik setelah respons. Status berubah menjadi `booked`, lalu `awb_ready` saat resi terbit, atau `failed` jika kurir menolak (saldo yang ditahan dikembalikan). Pantau lewat webhook atau `GET /v1/shipments/{id}`.\n\n## Idempotency-Key\n\nSemua `POST` menerima header `Idempotency-Key` (1 sampai 100 karakter: huruf, angka, `.`, `_`, `:`, `-`). Permintaan ulang dengan kunci dan isi yang sama dalam 24 jam mendapat respons yang tersimpan, dengan header `Idempotent-Replayed: true`, tanpa diproses dua kali. Kunci yang sama dengan isi berbeda ditolak dengan `idempotency_key_reused`; selama permintaan pertama masih diproses, ulangan mendapat `409 idempotency_conflict`. Respons error 5xx dan 429 tidak disimpan, jadi aman diulang. Kirim header ini di setiap `POST /v1/shipments`: di sana kunci juga tersimpan di pengiriman tanpa batas waktu, jadi setelah 24 jam kunci dan isi yang sama mengembalikan pengiriman yang sudah ada, sedangkan isi berbeda atau mode lain mendapat `409 idempotency_conflict`. Satu penawaran hanya untuk satu pengiriman; memakainya lagi dengan kunci baru juga mendapat `409 idempotency_conflict` dengan `details.shipmentId`.\n\n## Paginasi\n\nDaftar memakai cursor: `{ \"data\": [...], \"hasMore\": true, \"nextCursor\": \"...\" }`. Kirim `nextCursor` sebagai parameter `cursor` untuk halaman berikutnya, dengan filter yang sama. `limit` 1 sampai 100, default 20. Daftar tidak menyertakan jumlah total: lanjutkan selama `hasMore` bernilai true.\n\n## Batas permintaan\n\n120 permintaan per 60 detik per kunci, dan 30 per 60 detik untuk membuat pengiriman dan pembatalan. Setiap respons membawa `RateLimit-Limit`, `RateLimit-Remaining`, dan `RateLimit-Reset` (detik). Saat terlampaui, API menjawab `429 rate_limited` dengan `Retry-After`. Body maksimal 64 KB.\n\nLog API di menu Developer (disimpan 30 hari) mencatat paling banyak 60 permintaan per kunci per 60 detik, ditambah penolakan `429` pertama di jendela itu. Permintaan lain tetap diproses seperti biasa, hanya tidak muncul di log.\n\n## Error\n\n```json\n{ \"error\": { \"code\": \"validation_error\", \"message\": \"...\", \"request_id\": \"req_...\", \"details\": { \"fields\": [{ \"path\": \"receiver.phone\", \"message\": \"...\" }] } } }\n```\n\n`code` stabil dan aman dipakai di logika integrasi; `message` berbahasa Indonesia dan aman ditampilkan ke pengguna. Setiap respons membawa header `X-Request-Id`; sertakan nilainya saat menghubungi tim OngkirHub.\n\n## Webhook\n\nDaftarkan URL HTTPS di menu Developer dan pilih event yang ingin diterima. OngkirHub mengirim `POST` JSON dengan header `OngkirHub-Signature: t=<unix>,v1=<hex>`, di mana `v1` adalah HMAC-SHA256 dengan secret endpoint (`whsec_...`) atas teks `<t>.<body mentah>`. Verifikasi tanda tangan, tolak `t` yang lebih dari 5 menit, lalu jawab 2xx dalam 10 detik. Pengiriman yang gagal dicoba ulang setelah 1 menit, 5 menit, 30 menit, 2 jam, 6 jam, 24 jam. Event bisa terkirim lebih dari sekali dan tidak selalu berurutan: pakai `id` event untuk mengabaikan duplikat dan `data.shipment.updatedAt` untuk urutan. Panduan lengkap: https://www.ongkirhub.com/docs/webhooks\n\nDokumentasi lengkap: https://www.ongkirhub.com/docs/api",
    "contact": {
      "name": "OngkirHub",
      "url": "https://www.ongkirhub.com/kontak"
    },
    "termsOfService": "https://www.ongkirhub.com/legal/syarat-ketentuan"
  },
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "servers": [
    {
      "url": "https://api.ongkirhub.com",
      "description": "Produksi (kunci test dan live)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Lokasi",
      "description": "Wilayah asal dan tujuan."
    },
    {
      "name": "Kurir",
      "description": "Kurir dan layanan."
    },
    {
      "name": "Tarif",
      "description": "Cek ongkir yang bisa dipesan."
    },
    {
      "name": "Pengiriman",
      "description": "Buat, lihat, dan batalkan pengiriman."
    },
    {
      "name": "Pelacakan",
      "description": "Lacak resi."
    },
    {
      "name": "Saldo",
      "description": "Saldo dan mutasi."
    },
    {
      "name": "Webhook",
      "description": "Event yang dikirim OngkirHub ke URL kamu."
    },
    {
      "name": "Spesifikasi",
      "description": "Dokumen OpenAPI."
    }
  ],
  "paths": {
    "/v1/locations": {
      "get": {
        "operationId": "searchLocations",
        "tags": [
          "Lokasi"
        ],
        "summary": "Cari wilayah",
        "description": "Cari kelurahan, kecamatan, kota, atau kode pos. Pakai hasilnya sebagai location pengirim dan penerima agar cocok dengan data kurir.\n\nAkses (scope): `locations:read`.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Kata kunci, minimal 3 karakter: nama wilayah atau kode pos.",
            "schema": {
              "type": "string",
              "minLength": 3,
              "maxLength": 100
            },
            "example": "coblong"
          },
          {
            "name": "province",
            "in": "query",
            "required": false,
            "description": "Batasi ke satu provinsi (nama persis, contoh Jawa Barat).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah hasil.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Wilayah yang cocok.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationList"
                },
                "example": {
                  "data": [
                    {
                      "province": "Jawa Barat",
                      "city": "Bandung",
                      "district": "Coblong",
                      "subdistrict": "Cipaganti",
                      "zipcode": "40131"
                    },
                    {
                      "province": "Jawa Barat",
                      "city": "Bandung",
                      "district": "Coblong",
                      "subdistrict": "Dago",
                      "zipcode": "40135"
                    },
                    {
                      "province": "Jawa Barat",
                      "city": "Bandung",
                      "district": "Coblong",
                      "subdistrict": "Lebak Gede",
                      "zipcode": "40132"
                    }
                  ],
                  "hasMore": false,
                  "nextCursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "x-ongkirhub-scope": "locations:read"
      }
    },
    "/v1/couriers": {
      "get": {
        "operationId": "listCouriers",
        "tags": [
          "Kurir"
        ],
        "summary": "Daftar kurir",
        "description": "Kurir dan layanan di OngkirHub, termasuk layanan yang bisa dipesan saat ini dan dukungan pelacakan. Layanan yang muncul saat cek tarif tetap tergantung rute dan berat.\n\nAkses (scope): `couriers:read`.",
        "responses": {
          "200": {
            "description": "Daftar kurir.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CourierList"
                },
                "example": {
                  "data": [
                    {
                      "code": "jne",
                      "slug": "jne",
                      "name": "JNE",
                      "logoUrl": "https://www.ongkirhub.com/couriers/jne.png",
                      "bookable": true,
                      "tracking": {
                        "ongkirhubShipments": true,
                        "anyResi": true,
                        "requiresVerification": true
                      },
                      "services": [
                        {
                          "rateCode": "REG23",
                          "name": "Reguler",
                          "category": "regular",
                          "handover": [
                            "PICKUP",
                            "DROP"
                          ],
                          "requiresCoordinates": false,
                          "enabled": true
                        },
                        {
                          "rateCode": "YES23",
                          "name": "YES (Yakin Esok Sampai)",
                          "category": "express",
                          "handover": [
                            "PICKUP",
                            "DROP"
                          ],
                          "requiresCoordinates": false,
                          "enabled": true
                        }
                      ]
                    }
                  ],
                  "hasMore": false,
                  "nextCursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-ongkirhub-scope": "couriers:read"
      }
    },
    "/v1/rates": {
      "post": {
        "operationId": "createRates",
        "tags": [
          "Tarif"
        ],
        "summary": "Cek tarif",
        "description": "Hitung ongkir semua layanan yang tersedia untuk paket ini, termurah dulu. Respons adalah penawaran yang bisa dipesan selama 30 menit. Untuk PICKUP, setiap layanan membawa pickupSlots.\n\nAkses (scope): `rates:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RateRequest"
              },
              "examples": {
                "drop": {
                  "summary": "Antar ke gerai (DROP)",
                  "value": {
                    "sender": {
                      "name": "Toko Contoh Jaya",
                      "phone": "081200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      }
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "081300000102",
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      }
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        }
                      }
                    ],
                    "pickupType": "DROP",
                    "insured": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Penawaran.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                },
                "example": {
                  "id": "6709f1c2a1b2c3d4e5f60718",
                  "livemode": false,
                  "pickupType": "DROP",
                  "insured": false,
                  "expiresAt": "2026-10-11T03:30:00.000Z",
                  "rates": [
                    {
                      "rateCode": "JAGOPACK",
                      "courierCode": "lion",
                      "courierName": "Lion Parcel",
                      "serviceName": "JAGOPACK",
                      "etd": {
                        "min": 2,
                        "max": 5,
                        "unit": "DAY"
                      },
                      "price": {
                        "courierPrice": 9500,
                        "insurancePrice": 0,
                        "serviceFee": 0,
                        "total": 9500
                      },
                      "chargeableWeightKg": 1,
                      "pickupToday": false,
                      "pickupSlots": null
                    },
                    {
                      "rateCode": "REG23",
                      "courierCode": "jne",
                      "courierName": "JNE",
                      "serviceName": "Reguler",
                      "etd": {
                        "min": 1,
                        "max": 2,
                        "unit": "DAY"
                      },
                      "price": {
                        "courierPrice": 12000,
                        "insurancePrice": 0,
                        "serviceFee": 0,
                        "total": 12000
                      },
                      "chargeableWeightKg": 1,
                      "pickupToday": false,
                      "pickupSlots": null
                    },
                    {
                      "rateCode": "EZ",
                      "courierCode": "jnt",
                      "courierName": "J&T Express",
                      "serviceName": "Reguler",
                      "etd": {
                        "min": 2,
                        "max": 3,
                        "unit": "DAY"
                      },
                      "price": {
                        "courierPrice": 15000,
                        "insurancePrice": 0,
                        "serviceFee": 0,
                        "total": 15000
                      },
                      "chargeableWeightKg": 1,
                      "pickupToday": false,
                      "pickupSlots": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Permintaan dengan Idempotency-Key yang sama masih diproses.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "idempotency_conflict": {
                    "summary": "Permintaan sama masih diproses",
                    "value": {
                      "error": {
                        "code": "idempotency_conflict",
                        "message": "Permintaan dengan Idempotency-Key yang sama masih diproses. Tunggu sebentar, lalu kirim ulang.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "description": "Isi belum valid, tidak ada layanan untuk rute ini, atau Idempotency-Key dipakai untuk isi lain.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "validation_error": {
                    "summary": "Isian belum valid",
                    "value": {
                      "error": {
                        "code": "validation_error",
                        "message": "Ada data yang belum lengkap atau belum sesuai. Periksa lagi isianmu.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                        "details": {
                          "fields": [
                            {
                              "path": "items.0.weightKg",
                              "message": "Berat harus lebih dari 0 kg"
                            }
                          ]
                        }
                      }
                    }
                  },
                  "no_rates_available": {
                    "summary": "Tidak ada layanan",
                    "value": {
                      "error": {
                        "code": "no_rates_available",
                        "message": "Belum ada layanan kurir untuk rute dan paket ini. Coba ubah alamat atau berat paket.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  },
                  "idempotency_key_reused": {
                    "summary": "Idempotency-Key untuk isi lain",
                    "value": {
                      "error": {
                        "code": "idempotency_key_reused",
                        "message": "Idempotency-Key ini sudah dipakai untuk permintaan lain. Pakai kunci baru untuk permintaan yang berbeda.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "x-ongkirhub-scope": "rates:read"
      }
    },
    "/v1/shipments": {
      "post": {
        "operationId": "createShipment",
        "tags": [
          "Pengiriman"
        ],
        "summary": "Buat pengiriman",
        "description": "Pesan layanan dari penawaran. Total harga ditahan dari saldo (mode test: saldo uji) dan pengiriman langsung dikembalikan dengan status submitting; pesanan diteruskan ke kurir beberapa detik kemudian. Satu penawaran hanya bisa dipakai untuk satu pengiriman, kecuali pengiriman sebelumnya gagal.\n\nAkses (scope): `shipments:write`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentRequest"
              },
              "examples": {
                "drop": {
                  "summary": "Antar ke gerai (DROP)",
                  "value": {
                    "sender": {
                      "name": "Toko Contoh Jaya",
                      "phone": "081200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      }
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "081300000102",
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      }
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        }
                      }
                    ],
                    "pickupType": "DROP",
                    "insured": false,
                    "quoteId": "6709f1c2a1b2c3d4e5f60718",
                    "rateCode": "REG23",
                    "externalRef": "INV-2026-10-0412"
                  }
                },
                "pickup": {
                  "summary": "Dijemput kurir (PICKUP)",
                  "value": {
                    "sender": {
                      "name": "Toko Contoh Jaya",
                      "phone": "081200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      }
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "081300000102",
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      }
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        }
                      }
                    ],
                    "pickupType": "PICKUP",
                    "insured": false,
                    "quoteId": "6709f1c2a1b2c3d4e5f60719",
                    "rateCode": "REG23",
                    "pickupTime": "2026-10-12T09:00:00+07:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pengiriman dibuat dan sedang diproses.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Shipment"
                },
                "example": {
                  "id": "OH0HZ3K7Q2M4XB",
                  "livemode": true,
                  "status": "submitting",
                  "statusLabel": "Sedang diproses",
                  "stage": null,
                  "courier": {
                    "code": "jne",
                    "name": "JNE"
                  },
                  "service": {
                    "rateCode": "REG23",
                    "name": "Reguler",
                    "etd": {
                      "min": 1,
                      "max": 2,
                      "unit": "DAY"
                    }
                  },
                  "awb": null,
                  "shipper": {
                    "name": "Toko Contoh Jaya",
                    "phone": "6281200000001",
                    "email": "gudang@contoh.test",
                    "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                    "location": {
                      "province": "DKI Jakarta",
                      "city": "Jakarta Selatan",
                      "district": "Setia Budi",
                      "subdistrict": "Setiabudi",
                      "zipcode": "12910"
                    },
                    "latlng": null
                  },
                  "receiver": {
                    "name": "Dimas Pratama",
                    "phone": "6281300000102",
                    "email": null,
                    "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                    "location": {
                      "province": "Jawa Barat",
                      "city": "Bandung",
                      "district": "Coblong",
                      "subdistrict": "Lebak Gede",
                      "zipcode": "40132"
                    },
                    "latlng": null
                  },
                  "items": [
                    {
                      "name": "Kaos katun",
                      "sku": "KAOS-HITAM-L",
                      "qty": 2,
                      "weightKg": 0.5,
                      "value": 85000,
                      "dimensions": {
                        "lengthCm": 25,
                        "widthCm": 20,
                        "heightCm": 5
                      },
                      "itemType": "REGULAR"
                    }
                  ],
                  "parcel": {
                    "weightKg": 1,
                    "chargeableWeightKg": 1,
                    "declaredValue": 170000
                  },
                  "insurance": {
                    "insured": false,
                    "price": 0
                  },
                  "pricing": {
                    "courierPrice": 12000,
                    "insurancePrice": 0,
                    "serviceFee": 0,
                    "total": 12000
                  },
                  "pickup": {
                    "type": "DROP",
                    "time": null
                  },
                  "tracking": {
                    "lastCheckedAt": null,
                    "origin": null,
                    "destination": null,
                    "courierStatus": null,
                    "events": []
                  },
                  "timeline": [
                    {
                      "status": "submitting",
                      "label": "Sedang diproses",
                      "at": "2026-10-11T03:01:00.000Z",
                      "note": null
                    }
                  ],
                  "error": null,
                  "externalRef": "INV-2026-10-0412",
                  "notes": null,
                  "payment": null,
                  "source": "api",
                  "createdAt": "2026-10-11T03:01:00.000Z",
                  "updatedAt": "2026-10-11T03:01:00.000Z",
                  "bookedAt": null,
                  "deliveredAt": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Saldo tidak cukup.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "wallet_insufficient_funds": {
                    "summary": "Saldo kurang",
                    "value": {
                      "error": {
                        "code": "wallet_insufficient_funds",
                        "message": "Saldo OngkirHub kamu belum cukup untuk pengiriman ini. Isi saldo dulu, lalu coba lagi.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                        "details": {
                          "available": 5000,
                          "required": 12000
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "Penawaran kedaluwarsa atau harganya berubah, penawaran sudah dipakai, Idempotency-Key sudah dipakai pengiriman lain, atau permintaan yang sama masih diproses.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "quote_expired": {
                    "summary": "Penawaran kedaluwarsa",
                    "value": {
                      "error": {
                        "code": "quote_expired",
                        "message": "Harga ongkir sudah diperbarui. Cek ongkir lagi sebelum membuat resi.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  },
                  "quote_used": {
                    "summary": "Penawaran sudah dipakai",
                    "value": {
                      "error": {
                        "code": "idempotency_conflict",
                        "message": "Penawaran ini sudah dipakai untuk pengiriman OH0HZ3K7Q2M4XB. Cek tarif lagi untuk membuat pengiriman lain.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                        "details": {
                          "shipmentId": "OH0HZ3K7Q2M4XB"
                        }
                      }
                    }
                  },
                  "key_used": {
                    "summary": "Idempotency-Key sudah dipakai",
                    "value": {
                      "error": {
                        "code": "idempotency_conflict",
                        "message": "Idempotency-Key ini sudah dipakai untuk pengiriman lain (isi berbeda atau mode test/live lain). Pakai kunci baru.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  },
                  "in_progress": {
                    "summary": "Permintaan sama masih diproses",
                    "value": {
                      "error": {
                        "code": "idempotency_conflict",
                        "message": "Permintaan dengan Idempotency-Key yang sama masih diproses. Tunggu sebentar, lalu kirim ulang.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "description": "Isi belum valid, melebihi batas per pengiriman, layanan tidak tersedia, atau Idempotency-Key dipakai untuk isi lain.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "validation_error": {
                    "summary": "Isian belum valid",
                    "value": {
                      "error": {
                        "code": "validation_error",
                        "message": "Ada data yang belum lengkap atau belum sesuai. Periksa lagi isianmu.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                        "details": {
                          "fields": [
                            {
                              "path": "pickupTime",
                              "message": "Pilih jadwal penjemputan"
                            }
                          ]
                        }
                      }
                    }
                  },
                  "amount_limit_exceeded": {
                    "summary": "Melebihi batas per pengiriman",
                    "value": {
                      "error": {
                        "code": "amount_limit_exceeded",
                        "message": "Total biaya melebihi batas per pengiriman untuk akunmu.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  },
                  "courier_service_unavailable": {
                    "summary": "Layanan kurir tidak tersedia",
                    "value": {
                      "error": {
                        "code": "courier_service_unavailable",
                        "message": "Layanan kurir ini sedang tidak tersedia. Pilih layanan lain.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  },
                  "idempotency_key_reused": {
                    "summary": "Idempotency-Key untuk isi lain",
                    "value": {
                      "error": {
                        "code": "idempotency_key_reused",
                        "message": "Idempotency-Key ini sudah dipakai untuk permintaan lain. Pakai kunci baru untuk permintaan yang berbeda.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Batas permintaan atau batas pengiriman harian tercapai.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "Batas permintaan terlampaui",
                    "value": {
                      "error": {
                        "code": "rate_limited",
                        "message": "Terlalu banyak permintaan. Tunggu sebentar, lalu coba lagi.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                        "details": {
                          "retryAfterSec": 21
                        }
                      }
                    }
                  },
                  "daily_limit_reached": {
                    "summary": "Batas harian tercapai",
                    "value": {
                      "error": {
                        "code": "daily_limit_reached",
                        "message": "Batas pengiriman harian akunmu sudah tercapai. Coba lagi besok atau hubungi kami.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "x-ongkirhub-scope": "shipments:write"
      },
      "get": {
        "operationId": "listShipments",
        "tags": [
          "Pengiriman"
        ],
        "summary": "Daftar pengiriman",
        "description": "Pengiriman di mode kunci ini, terbaru dulu.\n\nAkses (scope): `shipments:read`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Satu atau beberapa status, dipisah koma.",
            "schema": {
              "type": "string"
            },
            "example": "awb_ready,in_transit",
            "x-enum": [
              "quoted",
              "submitting",
              "booked",
              "awb_ready",
              "picked_up",
              "in_transit",
              "out_for_delivery",
              "delivered",
              "failed",
              "unknown",
              "problem",
              "returned",
              "cancel_requested",
              "cancelled",
              "expired"
            ]
          },
          {
            "name": "awb",
            "in": "query",
            "required": false,
            "description": "Nomor resi.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "externalRef",
            "in": "query",
            "required": false,
            "description": "Referensi milikmu.",
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          },
          {
            "$ref": "#/components/parameters/CreatedFrom"
          },
          {
            "$ref": "#/components/parameters/CreatedTo"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman pengiriman.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShipmentList"
                },
                "example": {
                  "data": [
                    {
                      "id": "OH0HZ3K7Q2M4XB",
                      "livemode": true,
                      "status": "awb_ready",
                      "statusLabel": "Resi terbit",
                      "stage": "manifested",
                      "courier": {
                        "code": "jne",
                        "name": "JNE"
                      },
                      "service": {
                        "rateCode": "REG23",
                        "name": "Reguler",
                        "etd": {
                          "min": 1,
                          "max": 2,
                          "unit": "DAY"
                        }
                      },
                      "awb": "CGK0123456789012",
                      "shipper": {
                        "name": "Toko Contoh Jaya",
                        "phone": "6281200000001",
                        "email": "gudang@contoh.test",
                        "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                        "location": {
                          "province": "DKI Jakarta",
                          "city": "Jakarta Selatan",
                          "district": "Setia Budi",
                          "subdistrict": "Setiabudi",
                          "zipcode": "12910"
                        },
                        "latlng": null
                      },
                      "receiver": {
                        "name": "Dimas Pratama",
                        "phone": "6281300000102",
                        "email": null,
                        "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                        "location": {
                          "province": "Jawa Barat",
                          "city": "Bandung",
                          "district": "Coblong",
                          "subdistrict": "Lebak Gede",
                          "zipcode": "40132"
                        },
                        "latlng": null
                      },
                      "items": [
                        {
                          "name": "Kaos katun",
                          "sku": "KAOS-HITAM-L",
                          "qty": 2,
                          "weightKg": 0.5,
                          "value": 85000,
                          "dimensions": {
                            "lengthCm": 25,
                            "widthCm": 20,
                            "heightCm": 5
                          },
                          "itemType": "REGULAR"
                        }
                      ],
                      "parcel": {
                        "weightKg": 1,
                        "chargeableWeightKg": 1,
                        "declaredValue": 170000
                      },
                      "insurance": {
                        "insured": false,
                        "price": 0
                      },
                      "pricing": {
                        "courierPrice": 12000,
                        "insurancePrice": 0,
                        "serviceFee": 0,
                        "total": 12000
                      },
                      "pickup": {
                        "type": "DROP",
                        "time": null
                      },
                      "tracking": {
                        "lastCheckedAt": "2026-10-11T04:10:00.000Z",
                        "origin": "JAKARTA",
                        "destination": "BANDUNG",
                        "courierStatus": "MANIFESTED",
                        "events": [
                          {
                            "at": "2026-10-11T04:05:00.000Z",
                            "rawDate": "2026-10-11 11:05:00",
                            "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                          }
                        ]
                      },
                      "timeline": [
                        {
                          "status": "submitting",
                          "label": "Sedang diproses",
                          "at": "2026-10-11T03:01:00.000Z",
                          "note": null
                        },
                        {
                          "status": "booked",
                          "label": "Menunggu resi",
                          "at": "2026-10-11T03:01:04.000Z",
                          "note": null
                        },
                        {
                          "status": "awb_ready",
                          "label": "Resi terbit",
                          "at": "2026-10-11T03:02:10.000Z",
                          "note": null
                        }
                      ],
                      "error": null,
                      "externalRef": "INV-2026-10-0412",
                      "notes": null,
                      "payment": null,
                      "source": "api",
                      "createdAt": "2026-10-11T03:01:00.000Z",
                      "updatedAt": "2026-10-11T04:10:00.000Z",
                      "bookedAt": "2026-10-11T03:01:04.000Z",
                      "deliveredAt": null
                    }
                  ],
                  "hasMore": false,
                  "nextCursor": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-ongkirhub-scope": "shipments:read"
      }
    },
    "/v1/shipments/{id}": {
      "get": {
        "operationId": "getShipment",
        "tags": [
          "Pengiriman"
        ],
        "summary": "Detail pengiriman",
        "description": "Status, resi, harga, riwayat status, dan data pelacakan terakhir.\n\nAkses (scope): `shipments:read`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ShipmentId"
          }
        ],
        "responses": {
          "200": {
            "description": "Pengiriman.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Shipment"
                },
                "example": {
                  "id": "OH0HZ3K7Q2M4XB",
                  "livemode": true,
                  "status": "awb_ready",
                  "statusLabel": "Resi terbit",
                  "stage": "manifested",
                  "courier": {
                    "code": "jne",
                    "name": "JNE"
                  },
                  "service": {
                    "rateCode": "REG23",
                    "name": "Reguler",
                    "etd": {
                      "min": 1,
                      "max": 2,
                      "unit": "DAY"
                    }
                  },
                  "awb": "CGK0123456789012",
                  "shipper": {
                    "name": "Toko Contoh Jaya",
                    "phone": "6281200000001",
                    "email": "gudang@contoh.test",
                    "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                    "location": {
                      "province": "DKI Jakarta",
                      "city": "Jakarta Selatan",
                      "district": "Setia Budi",
                      "subdistrict": "Setiabudi",
                      "zipcode": "12910"
                    },
                    "latlng": null
                  },
                  "receiver": {
                    "name": "Dimas Pratama",
                    "phone": "6281300000102",
                    "email": null,
                    "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                    "location": {
                      "province": "Jawa Barat",
                      "city": "Bandung",
                      "district": "Coblong",
                      "subdistrict": "Lebak Gede",
                      "zipcode": "40132"
                    },
                    "latlng": null
                  },
                  "items": [
                    {
                      "name": "Kaos katun",
                      "sku": "KAOS-HITAM-L",
                      "qty": 2,
                      "weightKg": 0.5,
                      "value": 85000,
                      "dimensions": {
                        "lengthCm": 25,
                        "widthCm": 20,
                        "heightCm": 5
                      },
                      "itemType": "REGULAR"
                    }
                  ],
                  "parcel": {
                    "weightKg": 1,
                    "chargeableWeightKg": 1,
                    "declaredValue": 170000
                  },
                  "insurance": {
                    "insured": false,
                    "price": 0
                  },
                  "pricing": {
                    "courierPrice": 12000,
                    "insurancePrice": 0,
                    "serviceFee": 0,
                    "total": 12000
                  },
                  "pickup": {
                    "type": "DROP",
                    "time": null
                  },
                  "tracking": {
                    "lastCheckedAt": "2026-10-11T04:10:00.000Z",
                    "origin": "JAKARTA",
                    "destination": "BANDUNG",
                    "courierStatus": "MANIFESTED",
                    "events": [
                      {
                        "at": "2026-10-11T04:05:00.000Z",
                        "rawDate": "2026-10-11 11:05:00",
                        "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                      }
                    ]
                  },
                  "timeline": [
                    {
                      "status": "submitting",
                      "label": "Sedang diproses",
                      "at": "2026-10-11T03:01:00.000Z",
                      "note": null
                    },
                    {
                      "status": "booked",
                      "label": "Menunggu resi",
                      "at": "2026-10-11T03:01:04.000Z",
                      "note": null
                    },
                    {
                      "status": "awb_ready",
                      "label": "Resi terbit",
                      "at": "2026-10-11T03:02:10.000Z",
                      "note": null
                    }
                  ],
                  "error": null,
                  "externalRef": "INV-2026-10-0412",
                  "notes": null,
                  "payment": null,
                  "source": "api",
                  "createdAt": "2026-10-11T03:01:00.000Z",
                  "updatedAt": "2026-10-11T04:10:00.000Z",
                  "bookedAt": "2026-10-11T03:01:04.000Z",
                  "deliveredAt": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-ongkirhub-scope": "shipments:read"
      }
    },
    "/v1/shipments/{id}/cancel": {
      "post": {
        "operationId": "cancelShipment",
        "tags": [
          "Pengiriman"
        ],
        "summary": "Ajukan pembatalan",
        "description": "Ajukan pembatalan untuk pengiriman berstatus booked, awb_ready, atau problem. Mode live: pengiriman berubah menjadi cancel_requested dan tim OngkirHub meninjau permintaan bersama kurir. Jika disetujui, status menjadi cancelled dan saldo dikembalikan (event shipment.cancelled); jika ditolak, status kembali seperti semula. Mode test: permintaan langsung disetujui, respons berstatus refunded dan pengiriman menjadi cancelled.\n\nAkses (scope): `shipments:write`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ShipmentId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelRequest"
              },
              "examples": {
                "reason": {
                  "summary": "Alasan",
                  "value": {
                    "reason": "Pembeli membatalkan pesanan sebelum paket dijemput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Permintaan pembatalan dibuat (live) atau langsung disetujui (test).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cancellation"
                },
                "examples": {
                  "live": {
                    "summary": "Kunci live: menunggu tinjauan",
                    "value": {
                      "id": "6709f8a0c4d5e6f708192a3b",
                      "shipmentId": "OH0HZ3K7Q2M4XB",
                      "status": "requested",
                      "reason": "Pembeli membatalkan pesanan sebelum paket dijemput",
                      "reviewNote": null,
                      "refundAmount": null,
                      "createdAt": "2026-10-11T05:00:00.000Z",
                      "reviewedAt": null
                    }
                  },
                  "test": {
                    "summary": "Kunci test: langsung disetujui",
                    "value": {
                      "id": "6709f8a0c4d5e6f708192a3b",
                      "shipmentId": "OH0HZ3K7Q2M4XB",
                      "status": "refunded",
                      "reason": "Pembeli membatalkan pesanan sebelum paket dijemput",
                      "reviewNote": "Disetujui otomatis di mode test",
                      "refundAmount": 12000,
                      "createdAt": "2026-10-11T05:00:00.000Z",
                      "reviewedAt": "2026-10-11T05:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Pengiriman sudah tidak bisa dibatalkan, atau permintaan yang sama masih diproses.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "cancellation_not_allowed": {
                    "summary": "Tidak bisa dibatalkan",
                    "value": {
                      "error": {
                        "code": "cancellation_not_allowed",
                        "message": "Pengiriman ini sudah tidak bisa dibatalkan.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-ongkirhub-scope": "shipments:write"
      }
    },
    "/v1/tracking/{awb}": {
      "get": {
        "operationId": "trackAwb",
        "tags": [
          "Pelacakan"
        ],
        "summary": "Lacak resi",
        "description": "Resi yang dibuat lewat OngkirHub di mode kunci ini dilacak tanpa parameter tambahan; datanya diperbarui berkala. Resi dari luar OngkirHub butuh parameter courier dan hanya didukung untuk kurir dengan tracking.anyResi di GET /v1/couriers (JNE, J&T Express, AnterAja). JNE juga butuh verification.\n\nAkses (scope): `tracking:read`.",
        "parameters": [
          {
            "name": "awb",
            "in": "path",
            "required": true,
            "description": "Nomor resi.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9 -]{6,60}$"
            },
            "example": "CGK0123456789012"
          },
          {
            "name": "courier",
            "in": "query",
            "required": false,
            "description": "Kode kurir, untuk resi dari luar OngkirHub.",
            "schema": {
              "type": "string",
              "enum": [
                "jne",
                "jnt",
                "jtc",
                "sicepat",
                "spx",
                "lion",
                "pos",
                "paxel",
                "gosend",
                "anteraja"
              ]
            }
          },
          {
            "name": "verification",
            "in": "query",
            "required": false,
            "description": "5 digit terakhir nomor HP penerima (wajib untuk resi JNE dari luar OngkirHub).",
            "schema": {
              "type": "string",
              "pattern": "^\\d{5}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status dan riwayat pelacakan.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tracking"
                },
                "example": {
                  "awb": "CGK0123456789012",
                  "courier": {
                    "code": "jne",
                    "name": "JNE"
                  },
                  "shipmentId": "OH0HZ3K7Q2M4XB",
                  "shipmentStatus": "in_transit",
                  "shipmentStatusLabel": "Dalam perjalanan",
                  "stage": "in_transit",
                  "stageLabel": "Dalam perjalanan",
                  "delivered": false,
                  "origin": "JAKARTA",
                  "destination": "BANDUNG",
                  "courierStatus": "ON PROCESS",
                  "shippedAt": "2026-10-11T04:05:00.000Z",
                  "deliveredAt": null,
                  "lastCheckedAt": "2026-10-11T09:00:00.000Z",
                  "events": [
                    {
                      "at": "2026-10-11T08:40:00.000Z",
                      "rawDate": "2026-10-11 15:40:00",
                      "description": "SHIPMENT FORWARDED TO DESTINATION [BANDUNG]"
                    },
                    {
                      "at": "2026-10-11T04:05:00.000Z",
                      "rawDate": "2026-10-11 11:05:00",
                      "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "Resi tidak ditemukan.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "tracking_not_found": {
                    "summary": "Resi tidak ditemukan",
                    "value": {
                      "error": {
                        "code": "tracking_not_found",
                        "message": "Nomor resi tidak ditemukan. Pastikan nomor resi dan kurirnya sudah benar.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Parameter belum valid, kurir belum didukung, atau verifikasi dibutuhkan.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "tracking_verification_required": {
                    "summary": "Butuh verifikasi",
                    "value": {
                      "error": {
                        "code": "tracking_verification_required",
                        "message": "Masukkan 5 digit terakhir nomor HP penerima untuk melacak resi ini.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  },
                  "tracking_unsupported_courier": {
                    "summary": "Kurir belum didukung",
                    "value": {
                      "error": {
                        "code": "tracking_unsupported_courier",
                        "message": "Resi dari kurir ini belum bisa dilacak di OngkirHub.",
                        "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "x-ongkirhub-scope": "tracking:read"
      }
    },
    "/v1/wallet": {
      "get": {
        "operationId": "getWallet",
        "tags": [
          "Saldo"
        ],
        "summary": "Saldo",
        "description": "Saldo tersedia dan saldo yang ditahan. Kunci test membaca saldo uji.\n\nAkses (scope): `wallet:read`.",
        "responses": {
          "200": {
            "description": "Saldo.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Wallet"
                },
                "example": {
                  "livemode": true,
                  "currency": "IDR",
                  "available": 488000,
                  "held": 12000,
                  "updatedAt": "2026-10-11T03:01:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-ongkirhub-scope": "wallet:read"
      }
    },
    "/v1/wallet/transactions": {
      "get": {
        "operationId": "listWalletTransactions",
        "tags": [
          "Saldo"
        ],
        "summary": "Mutasi saldo",
        "description": "Riwayat perubahan saldo, terbaru dulu. Kunci test membaca mutasi saldo uji.\n\nAkses (scope): `wallet:read`.",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Jenis mutasi.",
            "schema": {
              "type": "string",
              "enum": [
                "topup",
                "hold",
                "release",
                "capture",
                "refund",
                "adjustment",
                "bonus"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/CreatedFrom"
          },
          {
            "$ref": "#/components/parameters/CreatedTo"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman mutasi.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletTransactionList"
                },
                "example": {
                  "data": [
                    {
                      "id": "6709f1c9a1b2c3d4e5f6071a",
                      "type": "hold",
                      "amount": -12000,
                      "availableAfter": 488000,
                      "heldAfter": 12000,
                      "ref": {
                        "type": "shipment",
                        "id": "OH0HZ3K7Q2M4XB"
                      },
                      "note": null,
                      "createdAt": "2026-10-11T03:01:00.000Z"
                    },
                    {
                      "id": "6709e0a2a1b2c3d4e5f60701",
                      "type": "topup",
                      "amount": 500000,
                      "availableAfter": 500000,
                      "heldAfter": 0,
                      "ref": {
                        "type": "topup",
                        "id": "TU0HZ2Y1X0W9VA"
                      },
                      "note": null,
                      "createdAt": "2026-10-10T02:00:00.000Z"
                    }
                  ],
                  "hasMore": true,
                  "nextCursor": "WzE3OTE1OTc2MDAwMDAsIjY3MDllMGEyYTFiMmMzZDRlNWY2MDcwMSJd"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-ongkirhub-scope": "wallet:read"
      }
    },
    "/v1/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "tags": [
          "Spesifikasi"
        ],
        "summary": "Spesifikasi OpenAPI",
        "description": "Dokumen ini, tanpa autentikasi.",
        "responses": {
          "200": {
            "description": "Dokumen OpenAPI 3.1.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": []
      }
    }
  },
  "webhooks": {
    "shipment.booked": {
      "post": {
        "operationId": "webhook_shipment_booked",
        "tags": [
          "Webhook"
        ],
        "summary": "Pesanan diterima kurir",
        "description": "Pengiriman berpindah ke booked: kurir menerima pesanan dan saldo yang ditahan dipakai.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentEvent"
              },
              "example": {
                "id": "evt_3f2a1b0c9d8e7f6a5b4c3d2e",
                "type": "shipment.booked",
                "createdAt": "2026-10-11T03:02:10.000Z",
                "livemode": true,
                "data": {
                  "shipment": {
                    "id": "OH0HZ3K7Q2M4XB",
                    "livemode": true,
                    "status": "booked",
                    "statusLabel": "Menunggu resi",
                    "stage": "manifested",
                    "courier": {
                      "code": "jne",
                      "name": "JNE"
                    },
                    "service": {
                      "rateCode": "REG23",
                      "name": "Reguler",
                      "etd": {
                        "min": 1,
                        "max": 2,
                        "unit": "DAY"
                      }
                    },
                    "awb": null,
                    "shipper": {
                      "name": "Toko Contoh Jaya",
                      "phone": "6281200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      },
                      "latlng": null
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "6281300000102",
                      "email": null,
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      },
                      "latlng": null
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        },
                        "itemType": "REGULAR"
                      }
                    ],
                    "parcel": {
                      "weightKg": 1,
                      "chargeableWeightKg": 1,
                      "declaredValue": 170000
                    },
                    "insurance": {
                      "insured": false,
                      "price": 0
                    },
                    "pricing": {
                      "courierPrice": 12000,
                      "insurancePrice": 0,
                      "serviceFee": 0,
                      "total": 12000
                    },
                    "pickup": {
                      "type": "DROP",
                      "time": null
                    },
                    "tracking": {
                      "lastCheckedAt": "2026-10-11T04:10:00.000Z",
                      "origin": "JAKARTA",
                      "destination": "BANDUNG",
                      "courierStatus": "MANIFESTED",
                      "events": [
                        {
                          "at": "2026-10-11T04:05:00.000Z",
                          "rawDate": "2026-10-11 11:05:00",
                          "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                        }
                      ]
                    },
                    "timeline": [
                      {
                        "status": "submitting",
                        "label": "Sedang diproses",
                        "at": "2026-10-11T03:01:00.000Z",
                        "note": null
                      },
                      {
                        "status": "booked",
                        "label": "Menunggu resi",
                        "at": "2026-10-11T03:01:04.000Z",
                        "note": null
                      },
                      {
                        "status": "awb_ready",
                        "label": "Resi terbit",
                        "at": "2026-10-11T03:02:10.000Z",
                        "note": null
                      }
                    ],
                    "error": null,
                    "externalRef": "INV-2026-10-0412",
                    "notes": null,
                    "payment": null,
                    "source": "api",
                    "createdAt": "2026-10-11T03:01:00.000Z",
                    "updatedAt": "2026-10-11T04:10:00.000Z",
                    "bookedAt": "2026-10-11T03:01:04.000Z",
                    "deliveredAt": null
                  },
                  "previousStatus": "submitting"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    },
    "shipment.awb_ready": {
      "post": {
        "operationId": "webhook_shipment_awb_ready",
        "tags": [
          "Webhook"
        ],
        "summary": "Resi terbit",
        "description": "Nomor resi sudah ada di data.shipment.awb dan label siap dicetak.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentEvent"
              },
              "example": {
                "id": "evt_3f2a1b0c9d8e7f6a5b4c3d2e",
                "type": "shipment.awb_ready",
                "createdAt": "2026-10-11T03:02:10.000Z",
                "livemode": true,
                "data": {
                  "shipment": {
                    "id": "OH0HZ3K7Q2M4XB",
                    "livemode": true,
                    "status": "awb_ready",
                    "statusLabel": "Resi terbit",
                    "stage": "manifested",
                    "courier": {
                      "code": "jne",
                      "name": "JNE"
                    },
                    "service": {
                      "rateCode": "REG23",
                      "name": "Reguler",
                      "etd": {
                        "min": 1,
                        "max": 2,
                        "unit": "DAY"
                      }
                    },
                    "awb": "CGK0123456789012",
                    "shipper": {
                      "name": "Toko Contoh Jaya",
                      "phone": "6281200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      },
                      "latlng": null
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "6281300000102",
                      "email": null,
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      },
                      "latlng": null
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        },
                        "itemType": "REGULAR"
                      }
                    ],
                    "parcel": {
                      "weightKg": 1,
                      "chargeableWeightKg": 1,
                      "declaredValue": 170000
                    },
                    "insurance": {
                      "insured": false,
                      "price": 0
                    },
                    "pricing": {
                      "courierPrice": 12000,
                      "insurancePrice": 0,
                      "serviceFee": 0,
                      "total": 12000
                    },
                    "pickup": {
                      "type": "DROP",
                      "time": null
                    },
                    "tracking": {
                      "lastCheckedAt": "2026-10-11T04:10:00.000Z",
                      "origin": "JAKARTA",
                      "destination": "BANDUNG",
                      "courierStatus": "MANIFESTED",
                      "events": [
                        {
                          "at": "2026-10-11T04:05:00.000Z",
                          "rawDate": "2026-10-11 11:05:00",
                          "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                        }
                      ]
                    },
                    "timeline": [
                      {
                        "status": "submitting",
                        "label": "Sedang diproses",
                        "at": "2026-10-11T03:01:00.000Z",
                        "note": null
                      },
                      {
                        "status": "booked",
                        "label": "Menunggu resi",
                        "at": "2026-10-11T03:01:04.000Z",
                        "note": null
                      },
                      {
                        "status": "awb_ready",
                        "label": "Resi terbit",
                        "at": "2026-10-11T03:02:10.000Z",
                        "note": null
                      }
                    ],
                    "error": null,
                    "externalRef": "INV-2026-10-0412",
                    "notes": null,
                    "payment": null,
                    "source": "api",
                    "createdAt": "2026-10-11T03:01:00.000Z",
                    "updatedAt": "2026-10-11T04:10:00.000Z",
                    "bookedAt": "2026-10-11T03:01:04.000Z",
                    "deliveredAt": null
                  },
                  "previousStatus": "booked"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    },
    "shipment.status_updated": {
      "post": {
        "operationId": "webhook_shipment_status_updated",
        "tags": [
          "Webhook"
        ],
        "summary": "Status perjalanan berubah",
        "description": "Perpindahan ke picked_up, in_transit, out_for_delivery, problem, returned, atau cancel_requested.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentEvent"
              },
              "example": {
                "id": "evt_3f2a1b0c9d8e7f6a5b4c3d2e",
                "type": "shipment.status_updated",
                "createdAt": "2026-10-11T03:02:10.000Z",
                "livemode": true,
                "data": {
                  "shipment": {
                    "id": "OH0HZ3K7Q2M4XB",
                    "livemode": true,
                    "status": "in_transit",
                    "statusLabel": "Dalam perjalanan",
                    "stage": "in_transit",
                    "courier": {
                      "code": "jne",
                      "name": "JNE"
                    },
                    "service": {
                      "rateCode": "REG23",
                      "name": "Reguler",
                      "etd": {
                        "min": 1,
                        "max": 2,
                        "unit": "DAY"
                      }
                    },
                    "awb": "CGK0123456789012",
                    "shipper": {
                      "name": "Toko Contoh Jaya",
                      "phone": "6281200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      },
                      "latlng": null
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "6281300000102",
                      "email": null,
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      },
                      "latlng": null
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        },
                        "itemType": "REGULAR"
                      }
                    ],
                    "parcel": {
                      "weightKg": 1,
                      "chargeableWeightKg": 1,
                      "declaredValue": 170000
                    },
                    "insurance": {
                      "insured": false,
                      "price": 0
                    },
                    "pricing": {
                      "courierPrice": 12000,
                      "insurancePrice": 0,
                      "serviceFee": 0,
                      "total": 12000
                    },
                    "pickup": {
                      "type": "DROP",
                      "time": null
                    },
                    "tracking": {
                      "lastCheckedAt": "2026-10-11T04:10:00.000Z",
                      "origin": "JAKARTA",
                      "destination": "BANDUNG",
                      "courierStatus": "MANIFESTED",
                      "events": [
                        {
                          "at": "2026-10-11T04:05:00.000Z",
                          "rawDate": "2026-10-11 11:05:00",
                          "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                        }
                      ]
                    },
                    "timeline": [
                      {
                        "status": "submitting",
                        "label": "Sedang diproses",
                        "at": "2026-10-11T03:01:00.000Z",
                        "note": null
                      },
                      {
                        "status": "booked",
                        "label": "Menunggu resi",
                        "at": "2026-10-11T03:01:04.000Z",
                        "note": null
                      },
                      {
                        "status": "awb_ready",
                        "label": "Resi terbit",
                        "at": "2026-10-11T03:02:10.000Z",
                        "note": null
                      }
                    ],
                    "error": null,
                    "externalRef": "INV-2026-10-0412",
                    "notes": null,
                    "payment": null,
                    "source": "api",
                    "createdAt": "2026-10-11T03:01:00.000Z",
                    "updatedAt": "2026-10-11T04:10:00.000Z",
                    "bookedAt": "2026-10-11T03:01:04.000Z",
                    "deliveredAt": null
                  },
                  "previousStatus": "picked_up"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    },
    "shipment.delivered": {
      "post": {
        "operationId": "webhook_shipment_delivered",
        "tags": [
          "Webhook"
        ],
        "summary": "Paket terkirim",
        "description": "Paket diterima penerima.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentEvent"
              },
              "example": {
                "id": "evt_3f2a1b0c9d8e7f6a5b4c3d2e",
                "type": "shipment.delivered",
                "createdAt": "2026-10-11T03:02:10.000Z",
                "livemode": true,
                "data": {
                  "shipment": {
                    "id": "OH0HZ3K7Q2M4XB",
                    "livemode": true,
                    "status": "delivered",
                    "statusLabel": "Terkirim",
                    "stage": "delivered",
                    "courier": {
                      "code": "jne",
                      "name": "JNE"
                    },
                    "service": {
                      "rateCode": "REG23",
                      "name": "Reguler",
                      "etd": {
                        "min": 1,
                        "max": 2,
                        "unit": "DAY"
                      }
                    },
                    "awb": "CGK0123456789012",
                    "shipper": {
                      "name": "Toko Contoh Jaya",
                      "phone": "6281200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      },
                      "latlng": null
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "6281300000102",
                      "email": null,
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      },
                      "latlng": null
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        },
                        "itemType": "REGULAR"
                      }
                    ],
                    "parcel": {
                      "weightKg": 1,
                      "chargeableWeightKg": 1,
                      "declaredValue": 170000
                    },
                    "insurance": {
                      "insured": false,
                      "price": 0
                    },
                    "pricing": {
                      "courierPrice": 12000,
                      "insurancePrice": 0,
                      "serviceFee": 0,
                      "total": 12000
                    },
                    "pickup": {
                      "type": "DROP",
                      "time": null
                    },
                    "tracking": {
                      "lastCheckedAt": "2026-10-11T04:10:00.000Z",
                      "origin": "JAKARTA",
                      "destination": "BANDUNG",
                      "courierStatus": "MANIFESTED",
                      "events": [
                        {
                          "at": "2026-10-11T04:05:00.000Z",
                          "rawDate": "2026-10-11 11:05:00",
                          "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                        }
                      ]
                    },
                    "timeline": [
                      {
                        "status": "submitting",
                        "label": "Sedang diproses",
                        "at": "2026-10-11T03:01:00.000Z",
                        "note": null
                      },
                      {
                        "status": "booked",
                        "label": "Menunggu resi",
                        "at": "2026-10-11T03:01:04.000Z",
                        "note": null
                      },
                      {
                        "status": "awb_ready",
                        "label": "Resi terbit",
                        "at": "2026-10-11T03:02:10.000Z",
                        "note": null
                      }
                    ],
                    "error": null,
                    "externalRef": "INV-2026-10-0412",
                    "notes": null,
                    "payment": null,
                    "source": "api",
                    "createdAt": "2026-10-11T03:01:00.000Z",
                    "updatedAt": "2026-10-11T04:10:00.000Z",
                    "bookedAt": "2026-10-11T03:01:04.000Z",
                    "deliveredAt": "2026-10-12T06:30:00.000Z"
                  },
                  "previousStatus": "out_for_delivery"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    },
    "shipment.failed": {
      "post": {
        "operationId": "webhook_shipment_failed",
        "tags": [
          "Webhook"
        ],
        "summary": "Pesanan gagal dibuat",
        "description": "Kurir menolak pesanan. Saldo yang ditahan sudah dikembalikan; alasan ada di data.shipment.error.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentEvent"
              },
              "example": {
                "id": "evt_3f2a1b0c9d8e7f6a5b4c3d2e",
                "type": "shipment.failed",
                "createdAt": "2026-10-11T03:02:10.000Z",
                "livemode": true,
                "data": {
                  "shipment": {
                    "id": "OH0HZ3K7Q2M4XB",
                    "livemode": true,
                    "status": "failed",
                    "statusLabel": "Gagal",
                    "stage": null,
                    "courier": {
                      "code": "jne",
                      "name": "JNE"
                    },
                    "service": {
                      "rateCode": "REG23",
                      "name": "Reguler",
                      "etd": {
                        "min": 1,
                        "max": 2,
                        "unit": "DAY"
                      }
                    },
                    "awb": null,
                    "shipper": {
                      "name": "Toko Contoh Jaya",
                      "phone": "6281200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      },
                      "latlng": null
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "6281300000102",
                      "email": null,
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      },
                      "latlng": null
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        },
                        "itemType": "REGULAR"
                      }
                    ],
                    "parcel": {
                      "weightKg": 1,
                      "chargeableWeightKg": 1,
                      "declaredValue": 170000
                    },
                    "insurance": {
                      "insured": false,
                      "price": 0
                    },
                    "pricing": {
                      "courierPrice": 12000,
                      "insurancePrice": 0,
                      "serviceFee": 0,
                      "total": 12000
                    },
                    "pickup": {
                      "type": "DROP",
                      "time": null
                    },
                    "tracking": {
                      "lastCheckedAt": null,
                      "origin": null,
                      "destination": null,
                      "courierStatus": null,
                      "events": []
                    },
                    "timeline": [
                      {
                        "status": "submitting",
                        "label": "Sedang diproses",
                        "at": "2026-10-11T03:01:00.000Z",
                        "note": null
                      }
                    ],
                    "error": {
                      "code": "courier_rejected",
                      "message": "Kurir belum bisa memproses pesanan ini. Coba layanan kurir lain. Saldomu tidak terpotong."
                    },
                    "externalRef": "INV-2026-10-0412",
                    "notes": null,
                    "payment": null,
                    "source": "api",
                    "createdAt": "2026-10-11T03:01:00.000Z",
                    "updatedAt": "2026-10-11T03:01:00.000Z",
                    "bookedAt": null,
                    "deliveredAt": null
                  },
                  "previousStatus": "submitting"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    },
    "shipment.cancelled": {
      "post": {
        "operationId": "webhook_shipment_cancelled",
        "tags": [
          "Webhook"
        ],
        "summary": "Pembatalan disetujui",
        "description": "Permintaan pembatalan disetujui dan saldo dikembalikan.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentEvent"
              },
              "example": {
                "id": "evt_3f2a1b0c9d8e7f6a5b4c3d2e",
                "type": "shipment.cancelled",
                "createdAt": "2026-10-11T03:02:10.000Z",
                "livemode": true,
                "data": {
                  "shipment": {
                    "id": "OH0HZ3K7Q2M4XB",
                    "livemode": true,
                    "status": "cancelled",
                    "statusLabel": "Dibatalkan",
                    "stage": "manifested",
                    "courier": {
                      "code": "jne",
                      "name": "JNE"
                    },
                    "service": {
                      "rateCode": "REG23",
                      "name": "Reguler",
                      "etd": {
                        "min": 1,
                        "max": 2,
                        "unit": "DAY"
                      }
                    },
                    "awb": "CGK0123456789012",
                    "shipper": {
                      "name": "Toko Contoh Jaya",
                      "phone": "6281200000001",
                      "email": "gudang@contoh.test",
                      "address": "Jl. Contoh Raya No. 1, RT 01/RW 02, Setiabudi",
                      "location": {
                        "province": "DKI Jakarta",
                        "city": "Jakarta Selatan",
                        "district": "Setia Budi",
                        "subdistrict": "Setiabudi",
                        "zipcode": "12910"
                      },
                      "latlng": null
                    },
                    "receiver": {
                      "name": "Dimas Pratama",
                      "phone": "6281300000102",
                      "email": null,
                      "address": "Jl. Penerima No. 2, dekat masjid, Lebak Gede",
                      "location": {
                        "province": "Jawa Barat",
                        "city": "Bandung",
                        "district": "Coblong",
                        "subdistrict": "Lebak Gede",
                        "zipcode": "40132"
                      },
                      "latlng": null
                    },
                    "items": [
                      {
                        "name": "Kaos katun",
                        "sku": "KAOS-HITAM-L",
                        "qty": 2,
                        "weightKg": 0.5,
                        "value": 85000,
                        "dimensions": {
                          "lengthCm": 25,
                          "widthCm": 20,
                          "heightCm": 5
                        },
                        "itemType": "REGULAR"
                      }
                    ],
                    "parcel": {
                      "weightKg": 1,
                      "chargeableWeightKg": 1,
                      "declaredValue": 170000
                    },
                    "insurance": {
                      "insured": false,
                      "price": 0
                    },
                    "pricing": {
                      "courierPrice": 12000,
                      "insurancePrice": 0,
                      "serviceFee": 0,
                      "total": 12000
                    },
                    "pickup": {
                      "type": "DROP",
                      "time": null
                    },
                    "tracking": {
                      "lastCheckedAt": "2026-10-11T04:10:00.000Z",
                      "origin": "JAKARTA",
                      "destination": "BANDUNG",
                      "courierStatus": "MANIFESTED",
                      "events": [
                        {
                          "at": "2026-10-11T04:05:00.000Z",
                          "rawDate": "2026-10-11 11:05:00",
                          "description": "SHIPMENT RECEIVED BY JNE COUNTER OFFICER AT [JAKARTA]"
                        }
                      ]
                    },
                    "timeline": [
                      {
                        "status": "submitting",
                        "label": "Sedang diproses",
                        "at": "2026-10-11T03:01:00.000Z",
                        "note": null
                      },
                      {
                        "status": "booked",
                        "label": "Menunggu resi",
                        "at": "2026-10-11T03:01:04.000Z",
                        "note": null
                      },
                      {
                        "status": "awb_ready",
                        "label": "Resi terbit",
                        "at": "2026-10-11T03:02:10.000Z",
                        "note": null
                      }
                    ],
                    "error": null,
                    "externalRef": "INV-2026-10-0412",
                    "notes": null,
                    "payment": null,
                    "source": "api",
                    "createdAt": "2026-10-11T03:01:00.000Z",
                    "updatedAt": "2026-10-11T04:10:00.000Z",
                    "bookedAt": "2026-10-11T03:01:04.000Z",
                    "deliveredAt": null
                  },
                  "previousStatus": "cancel_requested"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    },
    "topup.paid": {
      "post": {
        "operationId": "webhook_topup_paid",
        "tags": [
          "Webhook"
        ],
        "summary": "Isi saldo dibayar",
        "description": "Pembayaran isi saldo dikonfirmasi dan saldo bertambah.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopupPaidEvent"
              },
              "example": {
                "id": "evt_9a8b7c6d5e4f3a2b1c0d9e8f",
                "type": "topup.paid",
                "createdAt": "2026-10-10T02:00:00.000Z",
                "livemode": true,
                "data": {
                  "topup": {
                    "code": "TU0HZ2Y1X0W9VA",
                    "amount": 500000,
                    "fee": 723,
                    "total": 500723,
                    "method": "manual_transfer",
                    "status": "paid",
                    "statusLabel": "Berhasil",
                    "expiresAt": "2026-10-11T01:30:00.000Z",
                    "paidAt": "2026-10-10T02:00:00.000Z",
                    "createdAt": "2026-10-10T01:30:00.000Z",
                    "purpose": null,
                    "payment": null,
                    "manual": {
                      "bankAccounts": [],
                      "senderName": null,
                      "senderBank": null,
                      "uniqueCode": 723,
                      "submittedAt": "2026-10-10T01:42:00.000Z",
                      "rejectionReason": null,
                      "proofUploadId": null
                    }
                  },
                  "balance": {
                    "available": 500000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    },
    "webhook.test": {
      "post": {
        "operationId": "webhook_webhook_test",
        "tags": [
          "Webhook"
        ],
        "summary": "Tes webhook",
        "description": "Dikirim saat kamu menekan Kirim tes di menu Developer. Tidak perlu dipilih sebagai event.",
        "parameters": [
          {
            "name": "OngkirHub-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix detik>,v1=<hex HMAC-SHA256(secret, t + \".\" + body mentah)>.",
            "schema": {
              "type": "string"
            },
            "example": "t=1791763330,v1=5d0b1f..."
          },
          {
            "name": "OngkirHub-Event",
            "in": "header",
            "required": true,
            "description": "Jenis event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "OngkirHub-Delivery",
            "in": "header",
            "required": true,
            "description": "ID pengiriman webhook (sama untuk setiap percobaan ulang).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TestEvent"
              },
              "example": {
                "id": "evt_0a1b2c3d4e5f6a7b8c9d0e1f",
                "type": "webhook.test",
                "createdAt": "2026-10-11T06:00:00.000Z",
                "livemode": false,
                "data": {
                  "message": "Tes webhook dari OngkirHub"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Diterima. Jawab dalam 10 detik; status lain dianggap gagal dan dicoba ulang."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ohk_live_... atau ohk_test_...",
        "description": "Kunci API dari menu Developer."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "method_not_allowed",
                  "payload_too_large",
                  "unsupported_media_type",
                  "idempotency_key_reused",
                  "validation_error",
                  "unauthenticated",
                  "forbidden",
                  "not_found",
                  "rate_limited",
                  "idempotency_conflict",
                  "quote_expired",
                  "no_rates_available",
                  "pickup_time_required",
                  "coordinates_required",
                  "wallet_insufficient_funds",
                  "daily_limit_reached",
                  "amount_limit_exceeded",
                  "booking_disabled",
                  "courier_rejected",
                  "courier_service_unavailable",
                  "order_reconciling",
                  "service_unavailable",
                  "service_timeout",
                  "tracking_not_found",
                  "tracking_courier_required",
                  "tracking_verification_required",
                  "tracking_unsupported_courier",
                  "topup_below_minimum",
                  "topup_above_limit",
                  "payment_method_unavailable",
                  "payment_expired",
                  "shipment_payment_expired",
                  "shipment_needs_rebooking",
                  "claim_not_allowed",
                  "claim_already_open",
                  "upload_invalid",
                  "cancellation_not_allowed",
                  "mail_unavailable",
                  "network_error",
                  "internal_error"
                ],
                "description": "Kode error yang stabil. Gunakan kode ini di logika integrasimu, bukan teks pesannya."
              },
              "message": {
                "type": "string",
                "description": "Penjelasan dalam Bahasa Indonesia, aman ditampilkan ke pengguna."
              },
              "request_id": {
                "type": "string",
                "description": "ID permintaan, sama dengan header X-Request-Id. Sertakan saat menghubungi tim OngkirHub.",
                "pattern": "^req_[0-9a-f]{24}$"
              },
              "details": {
                "type": "object",
                "description": "Detail tambahan sesuai jenis error.",
                "properties": {
                  "fields": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/FieldIssue"
                    },
                    "description": "Untuk validation_error: field yang perlu diperbaiki."
                  },
                  "requiredScope": {
                    "type": "string",
                    "description": "Untuk forbidden: akses yang dibutuhkan endpoint ini."
                  },
                  "retryAfterSec": {
                    "type": "integer",
                    "description": "Untuk rate_limited: detik sampai boleh mencoba lagi."
                  },
                  "quoteId": {
                    "type": "string",
                    "description": "Untuk quote_expired: penawaran baru yang bisa dipakai."
                  },
                  "shipmentId": {
                    "type": "string",
                    "description": "Untuk idempotency_conflict: pengiriman yang sudah memakai penawaran ini."
                  },
                  "available": {
                    "type": "integer",
                    "description": "Untuk wallet_insufficient_funds: saldo yang tersedia. Rupiah, bilangan bulat."
                  },
                  "required": {
                    "type": "integer",
                    "description": "Untuk wallet_insufficient_funds: saldo yang dibutuhkan. Rupiah, bilangan bulat."
                  },
                  "maxBytes": {
                    "type": "integer",
                    "description": "Untuk payload_too_large: ukuran body maksimal."
                  }
                }
              }
            },
            "required": [
              "code",
              "message",
              "request_id"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false
      },
      "FieldIssue": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Lokasi field, contoh receiver.location.zipcode."
          },
          "message": {
            "type": "string",
            "description": "Apa yang perlu diperbaiki."
          }
        },
        "required": [
          "path",
          "message"
        ],
        "additionalProperties": false
      },
      "Location": {
        "type": "object",
        "properties": {
          "province": {
            "type": "string",
            "description": "Provinsi, ditulis persis seperti hasil GET /v1/locations.",
            "examples": [
              "Jawa Barat"
            ]
          },
          "city": {
            "type": "string",
            "description": "Kota atau kabupaten.",
            "examples": [
              "Bandung"
            ]
          },
          "district": {
            "type": "string",
            "description": "Kecamatan.",
            "examples": [
              "Coblong"
            ]
          },
          "subdistrict": {
            "type": "string",
            "description": "Kelurahan atau desa.",
            "examples": [
              "Lebak Gede"
            ]
          },
          "zipcode": {
            "type": "string",
            "description": "Kode pos 5 digit.",
            "pattern": "^\\d{5}$",
            "examples": [
              "40132"
            ]
          }
        },
        "required": [
          "province",
          "city",
          "district",
          "subdistrict",
          "zipcode"
        ],
        "description": "Wilayah tujuan atau asal. Ambil dari GET /v1/locations agar cocok dengan data kurir."
      },
      "LatLng": {
        "type": "object",
        "properties": {
          "lat": {
            "type": "number",
            "description": "Latitude, di dalam wilayah Indonesia.",
            "minimum": -11.5,
            "maximum": 6.5
          },
          "lng": {
            "type": "number",
            "description": "Longitude, di dalam wilayah Indonesia.",
            "minimum": 94,
            "maximum": 141.5
          }
        },
        "required": [
          "lat",
          "lng"
        ],
        "description": "Titik lokasi. Hanya wajib untuk layanan dengan requiresCoordinates."
      },
      "Sender": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nama pengirim.",
            "minLength": 2,
            "maxLength": 100
          },
          "phone": {
            "type": "string",
            "description": "Nomor HP Indonesia. Diterima 08..., +62... atau 62...; disimpan sebagai 62 diikuti angka.",
            "examples": [
              "081234567890"
            ]
          },
          "email": {
            "type": "string",
            "description": "Email pengirim, wajib untuk pembuatan resi.",
            "format": "email",
            "maxLength": 254
          },
          "address": {
            "type": "string",
            "description": "Alamat lengkap: jalan, nomor, RT/RW, patokan.",
            "minLength": 10,
            "maxLength": 500
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "latlng": {
            "$ref": "#/components/schemas/LatLng"
          }
        },
        "required": [
          "name",
          "phone",
          "email",
          "address",
          "location"
        ]
      },
      "Receiver": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nama penerima.",
            "minLength": 2,
            "maxLength": 100
          },
          "phone": {
            "type": "string",
            "description": "Nomor HP Indonesia. Diterima 08..., +62... atau 62...; disimpan sebagai 62 diikuti angka.",
            "examples": [
              "081234567890"
            ]
          },
          "email": {
            "type": "string",
            "description": "Email penerima (opsional).",
            "format": "email",
            "maxLength": 254
          },
          "address": {
            "type": "string",
            "description": "Alamat lengkap: jalan, nomor, RT/RW, patokan.",
            "minLength": 10,
            "maxLength": 500
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "latlng": {
            "$ref": "#/components/schemas/LatLng"
          }
        },
        "required": [
          "name",
          "phone",
          "address",
          "location"
        ]
      },
      "Dimensions": {
        "type": "object",
        "properties": {
          "lengthCm": {
            "type": "number",
            "description": "Panjang dalam cm.",
            "exclusiveMinimum": 0,
            "maximum": 1000
          },
          "widthCm": {
            "type": "number",
            "description": "Lebar dalam cm.",
            "exclusiveMinimum": 0,
            "maximum": 1000
          },
          "heightCm": {
            "type": "number",
            "description": "Tinggi dalam cm.",
            "exclusiveMinimum": 0,
            "maximum": 1000
          }
        },
        "required": [
          "lengthCm",
          "widthCm",
          "heightCm"
        ]
      },
      "Item": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nama barang.",
            "minLength": 1,
            "maxLength": 200
          },
          "sku": {
            "type": "string",
            "description": "SKU (opsional).",
            "maxLength": 64
          },
          "qty": {
            "type": "integer",
            "description": "Jumlah.",
            "minimum": 1,
            "maximum": 1000
          },
          "weightKg": {
            "type": "number",
            "description": "Berat satu unit dalam kg.",
            "exclusiveMinimum": 0,
            "maximum": 1000
          },
          "dimensions": {
            "$ref": "#/components/schemas/Dimensions"
          },
          "value": {
            "type": "integer",
            "description": "Nilai satu unit dalam rupiah, dipakai untuk asuransi.",
            "minimum": 0,
            "maximum": 1000000000
          },
          "itemType": {
            "type": "string",
            "enum": [
              "REGULAR",
              "ORGANIC",
              "LIQUID",
              "ELECTRONIC"
            ],
            "default": "REGULAR",
            "description": "Jenis barang."
          }
        },
        "required": [
          "name",
          "qty",
          "weightKg",
          "value"
        ]
      },
      "RateRequest": {
        "type": "object",
        "properties": {
          "sender": {
            "$ref": "#/components/schemas/Sender"
          },
          "receiver": {
            "$ref": "#/components/schemas/Receiver"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Item"
            },
            "minItems": 1,
            "maxItems": 50
          },
          "pickupType": {
            "type": "string",
            "enum": [
              "PICKUP",
              "DROP"
            ],
            "description": "PICKUP: kurir menjemput. DROP: kamu antar ke gerai kurir."
          },
          "insured": {
            "type": "boolean",
            "default": false,
            "description": "Tambahkan asuransi. Biaya asuransi per layanan ada di price.insurancePrice."
          }
        },
        "required": [
          "sender",
          "receiver",
          "items",
          "pickupType"
        ],
        "description": "Data paket untuk cek tarif. Simpan isi ini: kirim lagi persis sama saat membuat pengiriman."
      },
      "ShipmentRequest": {
        "type": "object",
        "properties": {
          "sender": {
            "$ref": "#/components/schemas/Sender"
          },
          "receiver": {
            "$ref": "#/components/schemas/Receiver"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Item"
            },
            "minItems": 1,
            "maxItems": 50
          },
          "pickupType": {
            "type": "string",
            "enum": [
              "PICKUP",
              "DROP"
            ],
            "description": "PICKUP: kurir menjemput. DROP: kamu antar ke gerai kurir."
          },
          "insured": {
            "type": "boolean",
            "default": false,
            "description": "Tambahkan asuransi. Biaya asuransi per layanan ada di price.insurancePrice."
          },
          "quoteId": {
            "type": "string",
            "description": "id dari respons POST /v1/rates.",
            "examples": [
              "6709f1c2a1b2c3d4e5f60718"
            ]
          },
          "rateCode": {
            "type": "string",
            "description": "rateCode layanan yang dipilih.",
            "pattern": "^[A-Za-z0-9_-]{1,64}$",
            "examples": [
              "REG23"
            ]
          },
          "pickupTime": {
            "type": "string",
            "format": "date-time",
            "description": "Wajib untuk PICKUP: startAt salah satu pickupSlots dari rate yang dipilih."
          },
          "notes": {
            "type": "string",
            "description": "Catatan untuk kurir.",
            "maxLength": 500
          },
          "externalRef": {
            "type": "string",
            "description": "Referensi milikmu, misalnya nomor pesanan toko. Bisa dipakai untuk mencari pengiriman.",
            "pattern": "^[A-Za-z0-9._/-]{1,64}$"
          }
        },
        "required": [
          "sender",
          "receiver",
          "items",
          "pickupType",
          "quoteId",
          "rateCode"
        ],
        "description": "Isi yang sama persis dengan permintaan POST /v1/rates, ditambah penawaran dan layanan yang dipilih. Jika isinya berubah atau penawaran sudah lebih dari 10 menit, harga dicek ulang; jika harganya berubah, permintaan ditolak dengan quote_expired."
      },
      "CancelRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "description": "Alasan pembatalan.",
            "minLength": 5,
            "maxLength": 500
          }
        },
        "required": [
          "reason"
        ]
      },
      "Etd": {
        "type": "object",
        "properties": {
          "min": {
            "type": "integer",
            "description": "Estimasi tercepat."
          },
          "max": {
            "type": "integer",
            "description": "Estimasi terlama."
          },
          "unit": {
            "type": "string",
            "enum": [
              "DAY",
              "HOUR",
              "MINUTE"
            ]
          }
        },
        "required": [
          "min",
          "max",
          "unit"
        ],
        "additionalProperties": false,
        "description": "Estimasi waktu sampai dari kurir."
      },
      "PriceBreakdown": {
        "type": "object",
        "properties": {
          "courierPrice": {
            "type": "integer",
            "description": "Ongkir dari kurir. Rupiah, bilangan bulat."
          },
          "insurancePrice": {
            "type": "integer",
            "description": "Biaya asuransi (0 jika tidak diasuransikan). Rupiah, bilangan bulat."
          },
          "serviceFee": {
            "type": "integer",
            "description": "Biaya layanan OngkirHub (saat ini Rp0). Rupiah, bilangan bulat."
          },
          "total": {
            "type": "integer",
            "description": "Total yang dipotong dari saldo. Rupiah, bilangan bulat."
          }
        },
        "required": [
          "courierPrice",
          "insurancePrice",
          "serviceFee",
          "total"
        ],
        "additionalProperties": false
      },
      "PickupSlot": {
        "type": "object",
        "properties": {
          "startAt": {
            "type": "string",
            "format": "date-time",
            "description": "Awal jadwal jemput (WIB, +07:00). Kirim sebagai pickupTime."
          },
          "endAt": {
            "type": "string",
            "format": "date-time",
            "description": "Akhir jadwal jemput."
          },
          "label": {
            "type": "string",
            "description": "Label jadwal, contoh 08.00 - 22.00."
          }
        },
        "required": [
          "startAt",
          "endAt",
          "label"
        ],
        "additionalProperties": false
      },
      "Rate": {
        "type": "object",
        "properties": {
          "rateCode": {
            "type": "string",
            "description": "Kode layanan, dipakai saat membuat pengiriman."
          },
          "courierCode": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "jne",
                  "jnt",
                  "jtc",
                  "sicepat",
                  "spx",
                  "lion",
                  "pos",
                  "paxel",
                  "gosend",
                  "anteraja"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Kode kurir."
          },
          "courierName": {
            "type": "string",
            "description": "Nama kurir."
          },
          "serviceName": {
            "type": "string",
            "description": "Nama layanan."
          },
          "etd": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Etd"
              },
              {
                "type": "null"
              }
            ]
          },
          "price": {
            "$ref": "#/components/schemas/PriceBreakdown"
          },
          "chargeableWeightKg": {
            "type": [
              "number",
              "null"
            ],
            "description": "Berat yang ditagihkan kurir (berat asli atau volumetrik)."
          },
          "pickupToday": {
            "type": "boolean",
            "description": "Kurir masih bisa menjemput hari ini."
          },
          "pickupSlots": {
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PickupSlot"
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "Untuk penawaran PICKUP: jadwal jemput dua hari ke depan. Null untuk DROP atau jika jadwal sedang tidak tersedia."
          }
        },
        "required": [
          "rateCode",
          "courierCode",
          "courierName",
          "serviceName",
          "etd",
          "price",
          "chargeableWeightKg",
          "pickupToday",
          "pickupSlots"
        ],
        "additionalProperties": false
      },
      "Quote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID penawaran (quoteId). Berlaku 30 menit."
          },
          "livemode": {
            "type": "boolean",
            "description": "False untuk kunci test."
          },
          "pickupType": {
            "type": "string",
            "enum": [
              "PICKUP",
              "DROP"
            ]
          },
          "insured": {
            "type": "boolean",
            "description": "Penawaran dengan asuransi."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Batas penawaran bisa dipakai."
          },
          "rates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rate"
            },
            "description": "Layanan yang tersedia, termurah dulu."
          }
        },
        "required": [
          "id",
          "livemode",
          "pickupType",
          "insured",
          "expiresAt",
          "rates"
        ],
        "additionalProperties": false
      },
      "Party": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nama."
          },
          "phone": {
            "type": "string",
            "description": "Nomor HP (62...)."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email."
          },
          "address": {
            "type": "string",
            "description": "Alamat."
          },
          "location": {
            "$ref": "#/components/schemas/Location"
          },
          "latlng": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LatLng"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "name",
          "phone",
          "email",
          "address",
          "location",
          "latlng"
        ],
        "additionalProperties": false
      },
      "ShipmentItem": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nama barang."
          },
          "sku": {
            "type": [
              "string",
              "null"
            ],
            "description": "SKU."
          },
          "qty": {
            "type": "integer",
            "description": "Jumlah."
          },
          "weightKg": {
            "type": "number",
            "description": "Berat satu unit dalam kg."
          },
          "value": {
            "type": "integer",
            "description": "Nilai satu unit. Rupiah, bilangan bulat."
          },
          "dimensions": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Dimensions"
              },
              {
                "type": "null"
              }
            ]
          },
          "itemType": {
            "type": "string",
            "enum": [
              "REGULAR",
              "ORGANIC",
              "LIQUID",
              "ELECTRONIC"
            ]
          }
        },
        "required": [
          "name",
          "sku",
          "qty",
          "weightKg",
          "value",
          "dimensions",
          "itemType"
        ],
        "additionalProperties": false
      },
      "TrackingEvent": {
        "type": "object",
        "properties": {
          "at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Waktu kejadian (UTC). Null jika tanggal dari kurir tidak terbaca."
          },
          "rawDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Tanggal seperti yang dikirim kurir."
          },
          "description": {
            "type": "string",
            "description": "Keterangan dari kurir."
          }
        },
        "required": [
          "at",
          "rawDate",
          "description"
        ],
        "additionalProperties": false
      },
      "Shipment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID pengiriman OngkirHub (OH + 12 karakter).",
            "pattern": "^OH[0-9A-HJKMNP-TV-Z]{12}$",
            "examples": [
              "OH0HZ3K7Q2M4XB"
            ]
          },
          "livemode": {
            "type": "boolean",
            "description": "False untuk pengiriman dari kunci test (simulasi, tanpa kurir sungguhan)."
          },
          "status": {
            "type": "string",
            "enum": [
              "quoted",
              "submitting",
              "booked",
              "awb_ready",
              "picked_up",
              "in_transit",
              "out_for_delivery",
              "delivered",
              "failed",
              "unknown",
              "problem",
              "returned",
              "cancel_requested",
              "cancelled",
              "expired"
            ],
            "description": "Status pengiriman di OngkirHub. quoted hanya dipakai untuk bayar per resi dari aplikasi (menunggu pembayaran), yang saat ini tidak tersedia."
          },
          "statusLabel": {
            "type": "string",
            "description": "Label status dalam Bahasa Indonesia."
          },
          "stage": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "manifested",
                  "picked_up",
                  "in_transit",
                  "out_for_delivery",
                  "delivered",
                  "problem",
                  "returned",
                  "cancelled",
                  "unknown"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Tahap perjalanan dari data pelacakan."
          },
          "courier": {
            "type": "object",
            "properties": {
              "code": {
                "oneOf": [
                  {
                    "type": "string",
                    "enum": [
                      "jne",
                      "jnt",
                      "jtc",
                      "sicepat",
                      "spx",
                      "lion",
                      "pos",
                      "paxel",
                      "gosend",
                      "anteraja"
                    ]
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "name": {
                "type": "string",
                "description": "Nama kurir."
              }
            },
            "required": [
              "code",
              "name"
            ],
            "additionalProperties": false
          },
          "service": {
            "type": "object",
            "properties": {
              "rateCode": {
                "type": "string",
                "description": "Kode layanan."
              },
              "name": {
                "type": "string",
                "description": "Nama layanan."
              },
              "etd": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Etd"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "rateCode",
              "name",
              "etd"
            ],
            "additionalProperties": false
          },
          "awb": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nomor resi. Null sampai resi terbit (status awb_ready)."
          },
          "shipper": {
            "$ref": "#/components/schemas/Party"
          },
          "receiver": {
            "$ref": "#/components/schemas/Party"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ShipmentItem"
            }
          },
          "parcel": {
            "type": "object",
            "properties": {
              "weightKg": {
                "type": "number",
                "description": "Total berat."
              },
              "chargeableWeightKg": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "declaredValue": {
                "type": "integer",
                "description": "Total nilai barang. Rupiah, bilangan bulat."
              }
            },
            "required": [
              "weightKg",
              "chargeableWeightKg",
              "declaredValue"
            ],
            "additionalProperties": false
          },
          "insurance": {
            "type": "object",
            "properties": {
              "insured": {
                "type": "boolean",
                "description": "Diasuransikan."
              },
              "price": {
                "type": "integer",
                "description": "Biaya asuransi. Rupiah, bilangan bulat."
              }
            },
            "required": [
              "insured",
              "price"
            ],
            "additionalProperties": false
          },
          "pricing": {
            "$ref": "#/components/schemas/PriceBreakdown"
          },
          "pickup": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "PICKUP",
                  "DROP"
                ]
              },
              "time": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Jadwal jemput."
              }
            },
            "required": [
              "type",
              "time"
            ],
            "additionalProperties": false
          },
          "tracking": {
            "type": "object",
            "properties": {
              "lastCheckedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Terakhir data pelacakan diperbarui."
              },
              "origin": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Asal menurut kurir."
              },
              "destination": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Tujuan menurut kurir."
              },
              "courierStatus": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Status menurut kurir."
              },
              "events": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TrackingEvent"
                },
                "description": "Riwayat dari kurir, terbaru dulu."
              }
            },
            "required": [
              "lastCheckedAt",
              "origin",
              "destination",
              "courierStatus",
              "events"
            ],
            "additionalProperties": false
          },
          "timeline": {
            "type": "array",
            "description": "Riwayat status OngkirHub, terlama dulu.",
            "items": {
              "type": "object",
              "properties": {
                "status": {
                  "type": "string",
                  "enum": [
                    "quoted",
                    "submitting",
                    "booked",
                    "awb_ready",
                    "picked_up",
                    "in_transit",
                    "out_for_delivery",
                    "delivered",
                    "failed",
                    "unknown",
                    "problem",
                    "returned",
                    "cancel_requested",
                    "cancelled",
                    "expired"
                  ]
                },
                "label": {
                  "type": "string",
                  "description": "Label status."
                },
                "at": {
                  "type": "string",
                  "format": "date-time",
                  "description": "Waktu perubahan."
                },
                "note": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Catatan."
                }
              },
              "required": [
                "status",
                "label",
                "at",
                "note"
              ],
              "additionalProperties": false
            }
          },
          "error": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "enum": [
                      "invalid_request",
                      "method_not_allowed",
                      "payload_too_large",
                      "unsupported_media_type",
                      "idempotency_key_reused",
                      "validation_error",
                      "unauthenticated",
                      "forbidden",
                      "not_found",
                      "rate_limited",
                      "idempotency_conflict",
                      "quote_expired",
                      "no_rates_available",
                      "pickup_time_required",
                      "coordinates_required",
                      "wallet_insufficient_funds",
                      "daily_limit_reached",
                      "amount_limit_exceeded",
                      "booking_disabled",
                      "courier_rejected",
                      "courier_service_unavailable",
                      "order_reconciling",
                      "service_unavailable",
                      "service_timeout",
                      "tracking_not_found",
                      "tracking_courier_required",
                      "tracking_verification_required",
                      "tracking_unsupported_courier",
                      "topup_below_minimum",
                      "topup_above_limit",
                      "payment_method_unavailable",
                      "payment_expired",
                      "shipment_payment_expired",
                      "shipment_needs_rebooking",
                      "claim_not_allowed",
                      "claim_already_open",
                      "upload_invalid",
                      "cancellation_not_allowed",
                      "mail_unavailable",
                      "network_error",
                      "internal_error"
                    ]
                  },
                  "message": {
                    "type": "string",
                    "description": "Penjelasan."
                  }
                },
                "required": [
                  "code",
                  "message"
                ],
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ]
          },
          "externalRef": {
            "type": [
              "string",
              "null"
            ],
            "description": "Referensi milikmu."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Catatan untuk kurir."
          },
          "payment": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "method": {
                    "type": "string",
                    "enum": [
                      "wallet",
                      "gateway"
                    ]
                  },
                  "topupCode": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Kode pembayaran (TU...) untuk bayar per resi."
                  },
                  "paymentMethod": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Metode pembayaran."
                  },
                  "expiresAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "Batas bayar."
                  },
                  "paidAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "Dibayar."
                  }
                },
                "required": [
                  "method",
                  "topupCode",
                  "paymentMethod",
                  "expiresAt",
                  "paidAt"
                ],
                "additionalProperties": false,
                "description": "Bayar per resi dari aplikasi. Saat ini tidak tersedia: pengiriman dibayar dari saldo, jadi bernilai null kecuali pada pengiriman lama. Pengiriman dari API selalu dibayar dari saldo."
              },
              {
                "type": "null"
              }
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "app",
              "api"
            ],
            "description": "Dibuat dari aplikasi atau API."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dibuat."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Terakhir berubah."
          },
          "bookedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Diterima kurir untuk diproses."
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Diterima penerima."
          }
        },
        "required": [
          "id",
          "livemode",
          "status",
          "statusLabel",
          "stage",
          "courier",
          "service",
          "awb",
          "shipper",
          "receiver",
          "items",
          "parcel",
          "insurance",
          "pricing",
          "pickup",
          "tracking",
          "timeline",
          "error",
          "externalRef",
          "notes",
          "payment",
          "source",
          "createdAt",
          "updatedAt",
          "bookedAt",
          "deliveredAt"
        ],
        "additionalProperties": false
      },
      "ShipmentList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Shipment"
            }
          },
          "hasMore": {
            "type": "boolean",
            "description": "Masih ada data setelah halaman ini."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kirim sebagai parameter `cursor` untuk halaman berikutnya. Null di halaman terakhir."
          }
        },
        "required": [
          "data",
          "hasMore",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "Daftar pengiriman, terbaru dulu."
      },
      "Cancellation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID permintaan pembatalan."
          },
          "shipmentId": {
            "type": "string",
            "description": "ID pengiriman (OH...)."
          },
          "status": {
            "type": "string",
            "enum": [
              "requested",
              "approved",
              "rejected",
              "refunded"
            ],
            "description": "requested: menunggu tinjauan tim OngkirHub. refunded: disetujui dan saldo dikembalikan (mode test: langsung, saldo uji). rejected: ditolak."
          },
          "reason": {
            "type": "string",
            "description": "Alasan pembatalan."
          },
          "reviewNote": {
            "type": [
              "string",
              "null"
            ],
            "description": "Catatan dari tim OngkirHub."
          },
          "refundAmount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Saldo yang dikembalikan setelah disetujui."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Diajukan."
          },
          "reviewedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Ditinjau."
          }
        },
        "required": [
          "id",
          "shipmentId",
          "status",
          "reason",
          "reviewNote",
          "refundAmount",
          "createdAt",
          "reviewedAt"
        ],
        "additionalProperties": false
      },
      "Tracking": {
        "type": "object",
        "properties": {
          "awb": {
            "type": "string",
            "description": "Nomor resi."
          },
          "courier": {
            "type": "object",
            "properties": {
              "code": {
                "oneOf": [
                  {
                    "type": "string",
                    "enum": [
                      "jne",
                      "jnt",
                      "jtc",
                      "sicepat",
                      "spx",
                      "lion",
                      "pos",
                      "paxel",
                      "gosend",
                      "anteraja"
                    ]
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Nama kurir."
              }
            },
            "required": [
              "code",
              "name"
            ],
            "additionalProperties": false
          },
          "shipmentId": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID pengiriman OngkirHub jika resi dibuat lewat OngkirHub."
          },
          "shipmentStatus": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "quoted",
                  "submitting",
                  "booked",
                  "awb_ready",
                  "picked_up",
                  "in_transit",
                  "out_for_delivery",
                  "delivered",
                  "failed",
                  "unknown",
                  "problem",
                  "returned",
                  "cancel_requested",
                  "cancelled",
                  "expired"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "shipmentStatusLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Label status OngkirHub."
          },
          "stage": {
            "type": "string",
            "enum": [
              "manifested",
              "picked_up",
              "in_transit",
              "out_for_delivery",
              "delivered",
              "problem",
              "returned",
              "cancelled",
              "unknown"
            ],
            "description": "Tahap perjalanan."
          },
          "stageLabel": {
            "type": "string",
            "description": "Label tahap."
          },
          "delivered": {
            "type": "boolean",
            "description": "Paket sudah diterima."
          },
          "origin": {
            "type": [
              "string",
              "null"
            ],
            "description": "Asal."
          },
          "destination": {
            "type": [
              "string",
              "null"
            ],
            "description": "Tujuan."
          },
          "courierStatus": {
            "type": [
              "string",
              "null"
            ],
            "description": "Status menurut kurir."
          },
          "shippedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Tanggal kirim."
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Tanggal diterima."
          },
          "lastCheckedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Terakhir dicek ke kurir."
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TrackingEvent"
            },
            "description": "Riwayat, terbaru dulu."
          }
        },
        "required": [
          "awb",
          "courier",
          "shipmentId",
          "shipmentStatus",
          "shipmentStatusLabel",
          "stage",
          "stageLabel",
          "delivered",
          "origin",
          "destination",
          "courierStatus",
          "shippedAt",
          "deliveredAt",
          "lastCheckedAt",
          "events"
        ],
        "additionalProperties": false
      },
      "Wallet": {
        "type": "object",
        "properties": {
          "livemode": {
            "type": "boolean",
            "description": "False untuk saldo uji (kunci test)."
          },
          "currency": {
            "type": "string",
            "enum": [
              "IDR"
            ]
          },
          "available": {
            "type": "integer",
            "description": "Saldo yang bisa dipakai. Rupiah, bilangan bulat."
          },
          "held": {
            "type": "integer",
            "description": "Saldo yang ditahan untuk pengiriman yang sedang diproses. Rupiah, bilangan bulat."
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Terakhir berubah."
          }
        },
        "required": [
          "livemode",
          "currency",
          "available",
          "held",
          "updatedAt"
        ],
        "additionalProperties": false
      },
      "WalletTransaction": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID mutasi."
          },
          "type": {
            "type": "string",
            "enum": [
              "topup",
              "hold",
              "release",
              "capture",
              "refund",
              "adjustment",
              "bonus"
            ],
            "description": "topup: isi saldo. hold: saldo ditahan saat pengiriman dibuat. capture: tahanan dipakai setelah kurir menerima pesanan. release: tahanan dikembalikan karena pesanan gagal. refund: pengembalian setelah pembatalan disetujui. adjustment dan bonus: penyesuaian oleh tim OngkirHub."
          },
          "amount": {
            "type": "integer",
            "description": "Perubahan saldo tersedia dalam rupiah (negatif berarti berkurang). Untuk capture: perubahan saldo ditahan."
          },
          "availableAfter": {
            "type": "integer",
            "description": "Saldo tersedia setelah mutasi. Rupiah, bilangan bulat."
          },
          "heldAfter": {
            "type": "integer",
            "description": "Saldo ditahan setelah mutasi. Rupiah, bilangan bulat."
          },
          "ref": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "shipment",
                      "topup",
                      "cancellation",
                      "claim",
                      "manual"
                    ]
                  },
                  "id": {
                    "type": "string",
                    "description": "ID terkait, contoh OH... atau TU..."
                  }
                },
                "required": [
                  "type",
                  "id"
                ],
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "Catatan."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Waktu mutasi."
          }
        },
        "required": [
          "id",
          "type",
          "amount",
          "availableAfter",
          "heldAfter",
          "ref",
          "note",
          "createdAt"
        ],
        "additionalProperties": false
      },
      "WalletTransactionList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WalletTransaction"
            }
          },
          "hasMore": {
            "type": "boolean",
            "description": "Masih ada data setelah halaman ini."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kirim sebagai parameter `cursor` untuk halaman berikutnya. Null di halaman terakhir."
          }
        },
        "required": [
          "data",
          "hasMore",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "Mutasi saldo, terbaru dulu."
      },
      "CourierService": {
        "type": "object",
        "properties": {
          "rateCode": {
            "type": "string",
            "description": "Kode layanan."
          },
          "name": {
            "type": "string",
            "description": "Nama layanan."
          },
          "category": {
            "type": "string",
            "enum": [
              "regular",
              "express",
              "same_day",
              "instant",
              "cargo"
            ]
          },
          "handover": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "PICKUP",
                "DROP"
              ]
            },
            "description": "Cara serah terima yang didukung."
          },
          "requiresCoordinates": {
            "type": "boolean",
            "description": "Butuh latlng pengirim dan penerima."
          },
          "enabled": {
            "type": "boolean",
            "description": "Bisa dipesan saat ini."
          }
        },
        "required": [
          "rateCode",
          "name",
          "category",
          "handover",
          "requiresCoordinates",
          "enabled"
        ],
        "additionalProperties": false
      },
      "Courier": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "jne",
              "jnt",
              "jtc",
              "sicepat",
              "spx",
              "lion",
              "pos",
              "paxel",
              "gosend",
              "anteraja"
            ]
          },
          "slug": {
            "type": "string",
            "description": "Slug untuk URL."
          },
          "name": {
            "type": "string",
            "description": "Nama kurir."
          },
          "logoUrl": {
            "type": "string",
            "description": "Logo kurir (PNG).",
            "format": "uri"
          },
          "bookable": {
            "type": "boolean",
            "description": "Ada layanan yang bisa dipesan."
          },
          "tracking": {
            "type": "object",
            "properties": {
              "ongkirhubShipments": {
                "type": "boolean",
                "description": "Resi yang dibuat lewat OngkirHub bisa dilacak."
              },
              "anyResi": {
                "type": "boolean",
                "description": "Resi dari luar OngkirHub bisa dilacak dengan parameter courier."
              },
              "requiresVerification": {
                "type": "boolean",
                "description": "Pelacakan resi dari luar OngkirHub butuh 5 digit terakhir nomor HP penerima."
              }
            },
            "required": [
              "ongkirhubShipments",
              "anyResi",
              "requiresVerification"
            ],
            "additionalProperties": false
          },
          "services": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CourierService"
            }
          }
        },
        "required": [
          "code",
          "slug",
          "name",
          "logoUrl",
          "bookable",
          "tracking",
          "services"
        ],
        "additionalProperties": false
      },
      "CourierList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Courier"
            }
          },
          "hasMore": {
            "type": "boolean",
            "description": "Masih ada data setelah halaman ini."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kirim sebagai parameter `cursor` untuk halaman berikutnya. Null di halaman terakhir."
          }
        },
        "required": [
          "data",
          "hasMore",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "Kurir di OngkirHub."
      },
      "LocationList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Location"
            }
          },
          "hasMore": {
            "type": "boolean",
            "description": "Masih ada data setelah halaman ini."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kirim sebagai parameter `cursor` untuk halaman berikutnya. Null di halaman terakhir."
          }
        },
        "required": [
          "data",
          "hasMore",
          "nextCursor"
        ],
        "additionalProperties": false,
        "description": "Hasil pencarian wilayah."
      },
      "Topup": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Kode isi saldo (TU...)."
          },
          "amount": {
            "type": "integer",
            "description": "Nominal yang masuk ke saldo. Rupiah, bilangan bulat."
          },
          "fee": {
            "type": "integer",
            "description": "Untuk method manual_transfer: kode unik 50 sampai 999 yang ditambahkan ke nominal transfer supaya transfer mudah dikenali. Kode unik tidak masuk ke saldo dan sama dengan manual.uniqueCode (0 pada transfer lama tanpa kode unik). Untuk metode lain: biaya metode pembayaran. Rupiah, bilangan bulat."
          },
          "total": {
            "type": "integer",
            "description": "Total yang dibayar, amount ditambah fee. Untuk manual_transfer: nominal persis yang ditransfer. Rupiah, bilangan bulat."
          },
          "method": {
            "type": "string",
            "description": "Metode pembayaran. manual_transfer untuk transfer bank yang dicek admin OngkirHub."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "paid",
              "expired",
              "failed",
              "cancelled"
            ]
          },
          "statusLabel": {
            "type": "string",
            "description": "Label status."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Batas bayar."
          },
          "paidAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Dibayar. Untuk manual_transfer: waktu admin OngkirHub menyetujui transfer."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dibuat."
          },
          "purpose": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "shipment"
                    ]
                  },
                  "orderId": {
                    "type": "string",
                    "description": "ID pengiriman yang dibayar."
                  }
                },
                "required": [
                  "kind",
                  "orderId"
                ],
                "additionalProperties": false,
                "description": "Bayar per resi dari aplikasi, hanya pada pembayaran lama karena saat ini tidak tersedia; null untuk isi saldo biasa."
              },
              {
                "type": "null"
              }
            ]
          },
          "payment": {
            "type": [
              "object",
              "null"
            ],
            "description": "Detail pembayaran. Null untuk manual_transfer."
          },
          "manual": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "bankAccounts": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "bank": {
                          "type": "string",
                          "description": "Nama bank."
                        },
                        "accountNumber": {
                          "type": "string",
                          "description": "Nomor rekening."
                        },
                        "accountName": {
                          "type": "string",
                          "description": "Nama pemilik rekening."
                        }
                      },
                      "required": [
                        "bank",
                        "accountNumber",
                        "accountName"
                      ],
                      "additionalProperties": false
                    },
                    "description": "Rekening tujuan transfer. Selalu kosong di event webhook."
                  },
                  "senderName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Nama pengirim, bila diisi."
                  },
                  "senderBank": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Bank asal, bila diisi."
                  },
                  "uniqueCode": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 50,
                    "maximum": 999,
                    "description": "Kode unik dalam rupiah, sama dengan fee. Null pada transfer lama tanpa kode unik."
                  },
                  "submittedAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "Waktu pelanggan menekan Saya sudah transfer."
                  },
                  "rejectionReason": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Alasan admin OngkirHub bila transfer tidak dikonfirmasi."
                  },
                  "proofUploadId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "ID bukti transfer, bila diunggah."
                  }
                },
                "required": [
                  "bankAccounts",
                  "senderName",
                  "senderBank",
                  "uniqueCode",
                  "submittedAt",
                  "rejectionReason",
                  "proofUploadId"
                ],
                "additionalProperties": false,
                "description": "Detail transfer bank (method manual_transfer); null untuk metode lain."
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "code",
          "amount",
          "fee",
          "total",
          "method",
          "status",
          "statusLabel",
          "expiresAt",
          "paidAt",
          "createdAt",
          "purpose",
          "payment",
          "manual"
        ],
        "additionalProperties": false
      },
      "ShipmentEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID event (evt_...). Simpan untuk mengabaikan event yang sama jika terkirim lebih dari sekali.",
            "pattern": "^evt_[0-9a-f]{24}$"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Waktu event."
          },
          "livemode": {
            "type": "boolean",
            "description": "False untuk event dari pengiriman uji (kunci test) dan dari Kirim tes."
          },
          "type": {
            "type": "string",
            "enum": [
              "shipment.booked",
              "shipment.awb_ready",
              "shipment.status_updated",
              "shipment.delivered",
              "shipment.failed",
              "shipment.cancelled"
            ]
          },
          "data": {
            "type": "object",
            "properties": {
              "shipment": {
                "$ref": "#/components/schemas/Shipment"
              },
              "previousStatus": {
                "type": "string",
                "enum": [
                  "quoted",
                  "submitting",
                  "booked",
                  "awb_ready",
                  "picked_up",
                  "in_transit",
                  "out_for_delivery",
                  "delivered",
                  "failed",
                  "unknown",
                  "problem",
                  "returned",
                  "cancel_requested",
                  "cancelled",
                  "expired"
                ],
                "description": "Status sebelum perubahan ini."
              }
            },
            "required": [
              "shipment",
              "previousStatus"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "createdAt",
          "livemode",
          "type",
          "data"
        ],
        "additionalProperties": false,
        "description": "Event perubahan status pengiriman."
      },
      "TopupPaidEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID event (evt_...). Simpan untuk mengabaikan event yang sama jika terkirim lebih dari sekali.",
            "pattern": "^evt_[0-9a-f]{24}$"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Waktu event."
          },
          "livemode": {
            "type": "boolean",
            "description": "False untuk event dari pengiriman uji (kunci test) dan dari Kirim tes."
          },
          "type": {
            "type": "string",
            "enum": [
              "topup.paid"
            ]
          },
          "data": {
            "type": "object",
            "properties": {
              "topup": {
                "$ref": "#/components/schemas/Topup"
              },
              "balance": {
                "type": "object",
                "properties": {
                  "available": {
                    "type": "integer",
                    "description": "Saldo tersedia setelah isi saldo. Rupiah, bilangan bulat."
                  }
                },
                "required": [
                  "available"
                ],
                "additionalProperties": false
              }
            },
            "required": [
              "topup",
              "balance"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "createdAt",
          "livemode",
          "type",
          "data"
        ],
        "additionalProperties": false,
        "description": "Isi saldo sudah dibayar dan masuk ke saldo."
      },
      "TestEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ID event (evt_...). Simpan untuk mengabaikan event yang sama jika terkirim lebih dari sekali.",
            "pattern": "^evt_[0-9a-f]{24}$"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Waktu event."
          },
          "livemode": {
            "type": "boolean",
            "description": "False untuk event dari pengiriman uji (kunci test) dan dari Kirim tes."
          },
          "type": {
            "type": "string",
            "enum": [
              "webhook.test"
            ]
          },
          "data": {
            "type": "object",
            "properties": {
              "message": {
                "type": "string",
                "description": "Pesan tes."
              }
            },
            "required": [
              "message"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "createdAt",
          "livemode",
          "type",
          "data"
        ],
        "additionalProperties": false,
        "description": "Dikirim oleh tombol Kirim tes di menu Developer."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Kunci unik dari sistem kamu (misalnya ID pesanan) agar permintaan yang diulang tidak diproses dua kali. Berlaku 24 jam.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._:-]{1,100}$"
        },
        "example": "order-INV-2026-10-0412"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Jumlah data per halaman.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "nextCursor dari halaman sebelumnya.",
        "schema": {
          "type": "string"
        }
      },
      "CreatedFrom": {
        "name": "createdFrom",
        "in": "query",
        "required": false,
        "description": "Dibuat sejak (inklusif), ISO 8601.",
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "example": "2026-10-01T00:00:00+07:00"
      },
      "CreatedTo": {
        "name": "createdTo",
        "in": "query",
        "required": false,
        "description": "Dibuat sebelum (eksklusif), ISO 8601.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "ShipmentId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "ID pengiriman (OH...).",
        "schema": {
          "type": "string"
        },
        "example": "OH0HZ3K7Q2M4XB"
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "Permintaan tidak bisa dibaca: JSON rusak, cursor atau Idempotency-Key tidak valid.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "invalid_request": {
                "summary": "Permintaan tidak bisa dibaca",
                "value": {
                  "error": {
                    "code": "invalid_request",
                    "message": "Body bukan JSON yang valid.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Kunci API tidak dikirim, tidak valid, atau sudah dicabut.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "unauthenticated": {
                "summary": "Kunci API tidak valid",
                "value": {
                  "error": {
                    "code": "unauthenticated",
                    "message": "Kunci API tidak valid, sudah dicabut, atau tidak dikirim. Kirim header Authorization: Bearer <kunci API>.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Kunci API tidak punya akses (scope) untuk endpoint ini.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "forbidden": {
                "summary": "Akses kunci kurang",
                "value": {
                  "error": {
                    "code": "forbidden",
                    "message": "Kunci API ini tidak punya akses ke endpoint ini. Tambahkan akses yang dibutuhkan di menu Developer.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                    "details": {
                      "requiredScope": "shipments:write"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Data tidak ditemukan untuk kunci API ini.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "not_found": {
                "summary": "Tidak ditemukan",
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "Pengiriman tidak ditemukan untuk kunci API ini.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Body lebih dari 64 KB.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "payload_too_large": {
                "summary": "Body terlalu besar",
                "value": {
                  "error": {
                    "code": "payload_too_large",
                    "message": "Body permintaan terlalu besar. Maksimal 64 KB.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                    "details": {
                      "maxBytes": 65536
                    }
                  }
                }
              }
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "Body bukan application/json.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "unsupported_media_type": {
                "summary": "Bukan JSON",
                "value": {
                  "error": {
                    "code": "unsupported_media_type",
                    "message": "Kirim body sebagai JSON dengan header Content-Type: application/json.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "Isi permintaan belum valid.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "validation_error": {
                "summary": "Isian belum valid",
                "value": {
                  "error": {
                    "code": "validation_error",
                    "message": "Ada data yang belum lengkap atau belum sesuai. Periksa lagi isianmu.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                    "details": {
                      "fields": [
                        {
                          "path": "receiver.location.zipcode",
                          "message": "Kode pos harus 5 digit"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Batas permintaan terlampaui.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "rate_limited": {
                "summary": "Batas permintaan terlampaui",
                "value": {
                  "error": {
                    "code": "rate_limited",
                    "message": "Terlalu banyak permintaan. Tunggu sebentar, lalu coba lagi.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12",
                    "details": {
                      "retryAfterSec": 21
                    }
                  }
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Kesalahan di sistem OngkirHub. Aman dicoba ulang dengan Idempotency-Key yang sama.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "internal_error": {
                "summary": "Kesalahan sistem",
                "value": {
                  "error": {
                    "code": "internal_error",
                    "message": "Terjadi kesalahan di sistem kami. Coba lagi sebentar lagi.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              }
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Layanan pengiriman sedang gangguan atau pembuatan resi sedang dijeda. Coba lagi nanti.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "service_unavailable": {
                "summary": "Layanan pengiriman gangguan",
                "value": {
                  "error": {
                    "code": "service_unavailable",
                    "message": "Layanan pengiriman sedang gangguan. Coba lagi beberapa saat lagi.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              },
              "booking_disabled": {
                "summary": "Pembuatan resi dijeda",
                "value": {
                  "error": {
                    "code": "booking_disabled",
                    "message": "Pembuatan resi sedang dijeda sementara. Cek ongkir dan cek resi tetap bisa dipakai.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              }
            }
          }
        }
      },
      "GatewayTimeout": {
        "description": "Layanan pengiriman lambat merespons. Coba lagi.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "service_timeout": {
                "summary": "Layanan pengiriman lambat",
                "value": {
                  "error": {
                    "code": "service_timeout",
                    "message": "Layanan pengiriman sedang lambat. Coba lagi sebentar lagi.",
                    "request_id": "req_5f0c2a9e7b1d4c3a8e6f0b12"
                  }
                }
              }
            }
          }
        }
      }
    },
    "headers": {
      "RequestId": {
        "description": "ID permintaan untuk dukungan.",
        "schema": {
          "type": "string",
          "pattern": "^req_[0-9a-f]{24}$"
        }
      },
      "RateLimitLimit": {
        "description": "Batas permintaan per jendela waktu.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Sisa permintaan di jendela waktu ini.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Detik sampai jendela waktu berikutnya.",
        "schema": {
          "type": "integer"
        }
      },
      "RetryAfter": {
        "description": "Detik sampai boleh mencoba lagi.",
        "schema": {
          "type": "integer"
        }
      },
      "IdempotentReplayed": {
        "description": "true jika respons ini adalah ulangan respons yang tersimpan untuk Idempotency-Key yang sama.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    }
  }
}