{
  "openapi": "3.1.0",
  "info": {
    "title": "API Kareeb: CRM",
    "version": "2026-11-04",
    "summary": "Tarik data CRM, kirim pesan WhatsApp, dan terima webhook dengan token API Kareeb.",
    "description": "Referensi endpoint yang menerima token API Kareeb (awalan kt_). Endpoint layar operator tidak termasuk dan bisa berubah kapan saja; bangun integrasi hanya di atas jalur di halaman ini.\n\nSemua permintaan memakai HTTPS ke https://kareeb.id, badan dan jawaban dalam JSON. Token dibuat oleh admin organisasi di Kareeb (menu Token API) dan hanya ditampilkan sekali. Setiap endpoint menyebut satu izin; token tanpa izin itu dijawab 403 yang menyebut nama izinnya.\n\nPenolakan dengan kode mengirim detail berupa objek: bercabanglah pada code, bukan pada kalimat message. Kolom baru bisa ditambahkan kapan saja, jadi abaikan kolom yang belum Anda kenal.",
    "contact": {
      "name": "Kareeb",
      "url": "https://developers.kareeb.id"
    }
  },
  "servers": [
    {
      "url": "https://kareeb.id",
      "description": "Produksi. Tidak ada sandbox; untuk mencoba, kirim ke nomor Anda sendiri."
    }
  ],
  "security": [
    {
      "tokenApi": []
    }
  ],
  "tags": [
    {
      "name": "Token",
      "description": "Memeriksa token yang sedang dipakai."
    },
    {
      "name": "Template",
      "description": "Template WhatsApp yang boleh disebut saat mengirim."
    },
    {
      "name": "Kirim",
      "description": "Mengirim pesan WhatsApp ke satu atau banyak pelanggan."
    },
    {
      "name": "Tarik data",
      "description": "Membaca data CRM dengan kursor id."
    },
    {
      "name": "Pemantauan",
      "description": "Mengirim sebutan publik ke halaman Pemantauan."
    },
    {
      "name": "Panggilan suara",
      "description": "Token panggilan Agen AI suara untuk aplikasi Anda."
    },
    {
      "name": "Webhook",
      "description": "Semua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan."
    }
  ],
  "paths": {
    "/api/export/contacts": {
      "get": {
        "operationId": "exportContacts",
        "tags": [
          "Tarik data"
        ],
        "summary": "Daftar kontak",
        "description": "Kontak beserta status berhenti berlangganan dan persetujuan terbaru per tujuan. Token API membaca data cabang Utama organisasi Anda. Kontak bisa berubah (label, nama), jadi jalankan jalan penuh dari since_id=0 secara berkala. Kursor id hanya melihat baris baru, tidak melihat perubahan pada baris lama. Lihat panduan Tarik data untuk aturan jalan penuh.\n\nIzin: `read:contacts`.",
        "security": [
          {
            "tokenApi": [
              "read:contacts"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanKontak"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/conversations": {
      "get": {
        "operationId": "exportConversations",
        "tags": [
          "Tarik data"
        ],
        "summary": "Daftar percakapan",
        "description": "Percakapan per kontak dan saluran. Token API membaca data cabang Utama organisasi Anda. Status percakapan bisa berubah, jadi jalankan jalan penuh secara berkala. Kursor id hanya melihat baris baru, tidak melihat perubahan pada baris lama. Lihat panduan Tarik data untuk aturan jalan penuh.\n\nIzin: `read:conversations`.",
        "security": [
          {
            "tokenApi": [
              "read:conversations"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanPercakapan"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/messages": {
      "get": {
        "operationId": "exportMessages",
        "tags": [
          "Tarik data"
        ],
        "summary": "Daftar pesan",
        "description": "Pesan masuk dan keluar beserta status kirim terbaiknya. Mencakup semua cabang organisasi. Isi mentah dari Meta tidak ikut. Tabel ini hanya bertambah, jadi jalan bertahap dengan since_id sudah cukup.\n\nIzin: `read:messages`.",
        "security": [
          {
            "tokenApi": [
              "read:messages"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanPesan"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/campaigns": {
      "get": {
        "operationId": "exportCampaigns",
        "tags": [
          "Tarik data"
        ],
        "summary": "Daftar kampanye",
        "description": "Kampanye dari layar Kampanye dan dari POST /api/broadcasts. Token API membaca data cabang Utama organisasi Anda. Status kampanye berubah selama pengiriman, jadi jalankan jalan penuh secara berkala. Kursor id hanya melihat baris baru, tidak melihat perubahan pada baris lama. Lihat panduan Tarik data untuk aturan jalan penuh.\n\nIzin: `read:campaigns`.",
        "security": [
          {
            "tokenApi": [
              "read:campaigns"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanKampanye"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/campaign-stats": {
      "get": {
        "operationId": "exportCampaignStats",
        "tags": [
          "Tarik data"
        ],
        "summary": "Corong per kampanye",
        "description": "Satu baris per kampanye dengan angka corong yang sama dengan halaman Laporan. Angkanya berubah setiap kali penerima bereaksi, jadi endpoint ini selalu dibaca dengan jalan penuh dari since_id=0. Token API membaca data cabang Utama organisasi Anda.\n\nIzin: `read:campaigns`.",
        "security": [
          {
            "tokenApi": [
              "read:campaigns"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanStatistikKampanye"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/campaigns/recipients": {
      "get": {
        "operationId": "exportCampaignRecipients",
        "tags": [
          "Tarik data"
        ],
        "summary": "Penerima kampanye",
        "description": "Corong per penerima: terkirim, sampai, dibaca, dibalas, diklik, menghasilkan pesanan. Kursornya sendiri, terpisah dari daftar kampanye. Token API membaca data cabang Utama organisasi Anda.\n\nIzin: `read:campaigns`.",
        "security": [
          {
            "tokenApi": [
              "read:campaigns"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "campaign_id",
            "in": "query",
            "required": false,
            "description": "Batasi ke satu kampanye, misalnya id dari jawaban POST /api/broadcasts.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanPenerimaKampanye"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/orders": {
      "get": {
        "operationId": "exportOrders",
        "tags": [
          "Tarik data"
        ],
        "summary": "Daftar pesanan",
        "description": "Pesanan yang dibuat di percakapan. Tautan pembayaran tidak ikut. Token API membaca data cabang Utama organisasi Anda. Status pesanan berubah (misalnya menjadi paid), jadi jalankan jalan penuh secara berkala. Kursor id hanya melihat baris baru, tidak melihat perubahan pada baris lama. Lihat panduan Tarik data untuk aturan jalan penuh.\n\nIzin: `read:orders`.",
        "security": [
          {
            "tokenApi": [
              "read:orders"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanPesanan"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/komentar": {
      "get": {
        "operationId": "exportKomentar",
        "tags": [
          "Tarik data"
        ],
        "summary": "Komentar di unggahan sendiri",
        "description": "Komentar masuk di unggahan Instagram dan Halaman Facebook milik organisasi. Amplopnya sendiri: next_since_id berisi id terakhir hanya bila halaman penuh, dan null bila halaman tidak penuh atau kosong. Berhenti saat null, lalu simpan id terbesar yang Anda terima sebagai since_id berikutnya. Saluran yang sakelar komentar sebagai lead-nya mati tidak mengekspor apa pun, termasuk komentar yang tersimpan saat sakelar masih menyala. Token API membaca data cabang Utama organisasi Anda.\n\nIzin: `read:comments`.",
        "security": [
          {
            "tokenApi": [
              "read:comments"
            ]
          }
        ],
        "parameters": [
          {
            "name": "since_id",
            "in": "query",
            "required": false,
            "description": "Kursor eksklusif: hanya baris dengan id lebih besar dari angka ini. Mulai dari 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jumlah baris per halaman, 1 sampai 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Satu halaman komentar, urut id naik.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HalamanKomentar"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/templates": {
      "get": {
        "operationId": "exportTemplates",
        "tags": [
          "Template"
        ],
        "summary": "Daftar template WhatsApp",
        "description": "Template yang boleh disebut saat mengirim, dari salinan template Meta di Kareeb. Semua status ikut; hanya APPROVED yang bisa dikirim. Tidak berhalaman, paling banyak 500 baris. Template di akun WhatsApp yang tidak lagi dipakai organisasi, dan template di akun yang tidak dipakai cabang mana pun, tidak ditampilkan. Komponen template, id Meta, dan id akun WhatsApp tidak pernah ikut. Daftar ini sesegar sinkronisasi template terakhir; token tidak bisa memulai sinkronisasi, admin melakukannya dari menu Template. Jawaban dikirim dengan Cache-Control: no-store.\n\nIzin: `read:templates`.",
        "security": [
          {
            "tokenApi": [
              "read:templates"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar template.",
            "headers": {
              "Cache-Control": {
                "description": "Selalu no-store.",
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DaftarTemplate"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/export/token": {
      "get": {
        "operationId": "exportToken",
        "tags": [
          "Token"
        ],
        "summary": "Identitas token ini",
        "description": "Organisasi dan izin token yang dikirim di header, untuk diperiksa saat sistem Anda pertama kali tersambung. Hanya menerima token API; sesi operator ditolak dengan 403. Daftar scopes mengikuti urutan tetap daftar izin, apa pun urutan saat token dibuat. Token, hash, dan awalannya tidak pernah dikembalikan. Panggilan ini tercatat sebagai pemakaian token.\n\nIzin: tidak perlu izin khusus, cukup token yang masih aktif.",
        "security": [
          {
            "tokenApi": []
          }
        ],
        "responses": {
          "200": {
            "description": "Identitas token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdentitasToken"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/contacts/{contact_id}": {
      "get": {
        "operationId": "getContact",
        "tags": [
          "Tarik data"
        ],
        "summary": "Satu kontak",
        "description": "Satu kontak menurut id, tanpa kursor dan tanpa halaman. Berguna untuk mencocokkan satu baris tanpa menelusuri seluruh daftar. Kolom persetujuan tidak ada di sini; bacalah dari GET /api/export/contacts. Token API membaca data cabang Utama organisasi Anda.\n\nIzin: `read:contacts`.",
        "security": [
          {
            "tokenApi": [
              "read:contacts"
            ]
          }
        ],
        "parameters": [
          {
            "name": "contact_id",
            "in": "path",
            "required": true,
            "description": "Id kontak, sama dengan id di GET /api/export/contacts.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Kontak.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KontakSatu"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "404": {
            "description": "Kontak tidak ada di cabang ini, atau sudah digabung.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GalatKalimat"
                },
                "example": {
                  "detail": "Kontak tidak ditemukan. Mungkin sudah dihapus atau digabung."
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/broadcasts": {
      "post": {
        "operationId": "createBroadcast",
        "tags": [
          "Kirim"
        ],
        "summary": "Kirim satu template ke banyak nomor",
        "description": "Membuat kampanye bertanda source=api di antrean yang sama dengan kampanye dari layar, sehingga muncul di halaman Kampanye lengkap dengan corongnya. Hanya WhatsApp. Penerima yang belum ada menjadi kontak baru. Pengiriman berangkat dari cabang Utama. Dengan scheduled_at, kampanye menunggu sampai waktu itu lalu dijalankan oleh penjadwal Kareeb. Kunci idempotency_key yang sama dijawab 200 dengan kampanye pertama dan tidak mengirim apa pun lagi. Permintaan yang ditolak 422 tidak membuat apa pun, sehingga kuncinya tetap bebas dipakai ulang.\n\nIzin: `send:broadcast`.",
        "security": [
          {
            "tokenApi": [
              "send:broadcast"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BroadcastBaru"
              },
              "example": {
                "template_name": "pengingat_kontrol",
                "template_language": "id",
                "recipients": [
                  {
                    "wa_id": "6281234567890",
                    "params": {
                      "1": "Budi",
                      "2": "Selasa 10.00"
                    }
                  },
                  {
                    "wa_id": "6281234567891",
                    "params": {
                      "1": "Sari",
                      "2": "Rabu 09.00"
                    }
                  }
                ],
                "idempotency_key": "kontrol-2026-11-05",
                "name": "Pengingat kontrol besok"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Kampanye baru. Antrean sudah ditulis; pengirimannya berjalan setelah jawaban ini.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastJawaban"
                },
                "example": {
                  "id": 42,
                  "replay": false,
                  "idempotency_key": "kontrol-2026-11-05",
                  "template_name": "pengingat_kontrol",
                  "template_language": "id",
                  "scheduled_at": null,
                  "accepted": [
                    {
                      "wa_id": "6281234567890",
                      "contact_id": 8,
                      "recipient_id": 901
                    }
                  ],
                  "refused": [
                    {
                      "wa_id": "6281234567891",
                      "reason": "param 1 must be a non-empty string"
                    }
                  ],
                  "status": {
                    "state": "sending",
                    "targeted": 1,
                    "queued": 1,
                    "sent": 0,
                    "failed": 0
                  }
                }
              }
            }
          },
          "200": {
            "description": "Kunci yang sama sudah pernah dipakai. Tidak ada yang dibuat atau dikirim; status kampanye pertama dibaca langsung dari antrean.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BroadcastJawaban"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "402": {
            "$ref": "#/components/responses/BacaSaja"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "409": {
            "description": "Kunci ini dipegang kampanye yang tidak bisa dibaca lagi. Kirim ulang dengan kunci baru.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GalatKalimat"
                }
              }
            }
          },
          "422": {
            "description": "Tidak ada yang dibuat. Bila detail berupa objek dengan code, bercabanglah pada code, bukan pada kalimatnya: template_not_synced, template_not_approved, template_other_account, template_other_cabang, campaign_quality_red, campaign_quality_yellow, campaign_tier_budget. Bila semua penerima ditolak, detail berisi error dan daftar refused. Waktu scheduled_at yang tidak valid dan badan yang salah bentuk dijawab dengan kalimat atau daftar galat FastAPI.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GalatApaSaja"
                },
                "examples": {
                  "template": {
                    "summary": "Template belum disetujui",
                    "value": {
                      "detail": {
                        "code": "template_not_approved",
                        "template_name": "pengingat_kontrol",
                        "template_language": "id",
                        "message": "Template “pengingat_kontrol” (id) berstatus PENDING di Kareeb, belum APPROVED. WhatsApp akan menolak setiap pengiriman sampai Meta menyetujuinya."
                      }
                    }
                  },
                  "penerima": {
                    "summary": "Semua penerima ditolak",
                    "value": {
                      "detail": {
                        "error": "no usable recipients in this request",
                        "refused": [
                          {
                            "wa_id": "6281234567890",
                            "reason": "param 1 must be a non-empty string"
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/messages": {
      "post": {
        "operationId": "sendMessage",
        "tags": [
          "Kirim"
        ],
        "summary": "Kirim satu pesan ke satu pelanggan",
        "description": "Satu teks (hanya di dalam jendela 24 jam) atau satu template yang sudah disetujui, ke satu pelanggan WhatsApp. Isi tepat satu dari to atau contact_id, dan tepat satu dari text atau template. Pemeriksaan berjalan berurutan: batas permintaan per token, kunci, cabang, tujuan, saluran, berhenti berlangganan, lalu jendela 24 jam untuk teks atau syarat template. Pesan tercatat tanpa nama pengirim, ditandai sebagai kiriman API, dan tidak mengambil alih percakapan dari Agen AI. Status kirim selanjutnya datang lewat webhook message.status.\n\nIzin: `write:messages`.",
        "security": [
          {
            "tokenApi": [
              "write:messages"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PesanBaru"
              },
              "examples": {
                "template": {
                  "summary": "Template",
                  "value": {
                    "to": "6281234567890",
                    "template": {
                      "name": "pengingat_kontrol",
                      "language": "id",
                      "body_params": {
                        "1": "Budi",
                        "2": "Rumah Sakit Sentra",
                        "3": "Selasa 10.00"
                      }
                    },
                    "idempotency_key": "janji-88123-h1"
                  }
                },
                "teks": {
                  "summary": "Teks dalam 24 jam",
                  "value": {
                    "contact_id": 88,
                    "text": "Halo Budi, hasil laboratorium Anda sudah bisa diambil.",
                    "idempotency_key": "lab-88-2026-11-04"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pesan dikirim ke WhatsApp.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PesanJawaban"
                },
                "example": {
                  "message_id": 9137,
                  "conversation_id": 412,
                  "wa_message_id": "wamid.HBgM...",
                  "contact_id": 88,
                  "replay": false
                }
              }
            }
          },
          "200": {
            "description": "Kunci yang sama sudah pernah dikirim. Jawaban berisi id pesan pertama dengan replay true; tidak ada yang dikirim lagi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PesanJawaban"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "402": {
            "$ref": "#/components/responses/BacaSaja"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "404": {
            "description": "Kode cabang_not_found (workspace_id bukan cabang organisasi ini) atau contact_not_found (contact_id bukan kontak cabang ini).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Penolakan"
                },
                "example": {
                  "detail": {
                    "code": "contact_not_found",
                    "message": "Kontak tidak ditemukan di cabang ini."
                  }
                }
              }
            }
          },
          "409": {
            "description": "Kode window_closed: teks di luar jendela 24 jam. Kirim template.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Penolakan"
                }
              }
            }
          },
          "422": {
            "description": "Bercabanglah pada code: invalid_phone, channel_unsupported, opted_out, consent_required, template_not_synced, template_other_account, template_other_cabang, template_not_approved, invalid_params, template_needs_cta. Badan yang tidak sesuai bentuk dijawab FastAPI dengan daftar galat per kolom.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GalatApaSaja"
                },
                "example": {
                  "detail": {
                    "code": "template_needs_cta",
                    "message": "Template ini punya tombol tautan per pelanggan. Isi template.cta_param dengan akhiran tautannya, lalu kirim lagi."
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TerlaluBanyak"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/pemantauan/masuk": {
      "post": {
        "operationId": "pemantauanMasuk",
        "tags": [
          "Pemantauan"
        ],
        "summary": "Kirim satu sebutan publik ke halaman Pemantauan",
        "description": "Sistem pemantauan Anda mengirim satu sebutan publik tentang merek (komentar YouTube, unggahan Threads, ulasan Google). Sebutan muncul di halaman Pemantauan cabang Utama, tempat agen CS menanganinya. Ini bukan percakapan, bukan kontak, dan tidak bisa dipakai untuk broadcast. Kunci idempotensinya adalah pasangan sumber dan external_id: kiriman ulang dijawab 200 tanpa menimpa apa pun. Hanya menerima token API; sesi operator ditolak dengan 403. Badan paling besar 16 KB.\n\nIzin: `write:pemantauan`.",
        "security": [
          {
            "tokenApi": [
              "write:pemantauan"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PemantauanMasuk"
              },
              "example": {
                "sumber": "youtube",
                "external_id": "Ugx-contoh-komentar",
                "referensi": "sebutan-4821",
                "url": "https://www.youtube.com/watch?v=contoh&lc=Ugx-contoh-komentar",
                "teks": "Pengirimannya lama sekali.",
                "penulis": "Pemirsa Contoh",
                "penulis_ref": "a1b2c3",
                "diterbitkan_pada": "2026-10-24T09:00:00+07:00",
                "sentimen": "negatif",
                "topik": "pengiriman",
                "kedaluwarsa_pada": "2026-11-23T09:00:00+07:00"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sebutan baru.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PemantauanJawaban"
                },
                "example": {
                  "id": 7,
                  "status": "baru",
                  "replay": false,
                  "url_crm": "/pemantauan?id=7"
                }
              }
            }
          },
          "200": {
            "description": "Pasangan sumber dan external_id yang sama sudah ada. Tidak ada yang ditimpa; status berisi status saat ini.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PemantauanJawaban"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "402": {
            "$ref": "#/components/responses/BacaSaja"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "413": {
            "$ref": "#/components/responses/TerlaluBesar"
          },
          "422": {
            "description": "Satu kalimat bahasa Indonesia yang menyebut kolomnya: kolom wajib kosong, sumber tidak dikenal, skema URL salah, atau kolom lain di luar daftar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GalatKalimat"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TerlaluBanyak"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/pemantauan/hapus": {
      "post": {
        "operationId": "pemantauanHapus",
        "tags": [
          "Pemantauan"
        ],
        "summary": "Hapus isi satu sebutan",
        "description": "Menghapus teks, penulis, penulis_ref, dan url satu sebutan; barisnya, id, dan statusnya tetap. Pakai saat platform menghapus komentar itu atau masa simpan sistem Anda habis. Aman diulang: sebutan yang tidak dikenal dijawab 200 dengan ditemukan false. Tetap berjalan untuk organisasi yang dinonaktifkan atau berstatus baca saja, karena penghapusan adalah kewajiban. Penghitung permintaan dipakai bersama dengan /api/pemantauan/masuk.\n\nIzin: `write:pemantauan`.",
        "security": [
          {
            "tokenApi": [
              "write:pemantauan"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PemantauanHapus"
              },
              "example": {
                "sumber": "youtube",
                "external_id": "Ugx-contoh-komentar"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hasil penghapusan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HapusJawaban"
                },
                "example": {
                  "ditemukan": true,
                  "dihapus": true,
                  "id": 7
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "413": {
            "$ref": "#/components/responses/TerlaluBesar"
          },
          "422": {
            "description": "Satu kalimat bahasa Indonesia yang menyebut kolomnya.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GalatKalimat"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TerlaluBanyak"
          },
          "503": {
            "$ref": "#/components/responses/BelumSiap"
          }
        }
      }
    },
    "/api/suara/token": {
      "post": {
        "operationId": "suaraToken",
        "tags": [
          "Panggilan suara"
        ],
        "summary": "Minta token panggilan suara untuk satu sesi aplikasi",
        "description": "Backend aplikasi Anda meminta token berumur pendek, lalu aplikasi membuka panggilan langsung ke mesin suara dengan token itu. Kunci API mesin suara tetap di Kareeb. Berikan user_id apa adanya sebagai userId di SDK browser mesin suara, karena dari situlah transkrip panggilan menemukan cabang dan sesi ini. Minta token tepat sebelum setiap panggilan. Jangan menaruh nama, nomor rekam medis, atau nomor telepon di session_ref. Selesainya panggilan dikabarkan lewat webhook call.finished.\n\nIzin: `write:suara`.",
        "security": [
          {
            "tokenApi": [
              "write:suara"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SuaraTokenBaru"
              },
              "example": {
                "session_ref": "sesi-7f3a9c",
                "workspace_id": 4
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token untuk satu panggilan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuaraTokenJawaban"
                },
                "example": {
                  "token": "...",
                  "agent_id": "...",
                  "conversation_id": "...",
                  "user_id": "4:sesi-7f3a9c",
                  "expires_at": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/TidakDikenal"
          },
          "402": {
            "description": "Kode kredit_habis: Kredit AI organisasi sudah habis. Bisa juga langganan_baca_saja bila organisasi berstatus baca saja.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Penolakan"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Ditolak"
          },
          "404": {
            "description": "Kode cabang_not_found: workspace_id bukan cabang organisasi token ini.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Penolakan"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/TidakValid"
          },
          "502": {
            "description": "Kode suara_gagal: mesin suara tidak bisa dihubungi. Coba lagi beberapa saat lagi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Penolakan"
                }
              }
            }
          },
          "503": {
            "description": "Kode suara_mati (panggilan suara dimatikan untuk organisasi ini) atau suara_belum_diatur (agen atau kunci belum diisi di Pengaturan). Tanpa basis data, jawabannya kalimat biasa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GalatApaSaja"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "lead.created": {
      "post": {
        "operationId": "webhook_lead_created",
        "tags": [
          "Webhook"
        ],
        "summary": "Lead baru dibuka",
        "description": "Kartu baru muncul di Pipeline, dari percakapan atau dibuat operator. data berisi baris lead.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "lead.created"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer",
                        "description": "Id lead."
                      },
                      "contact_id": {
                        "type": "integer",
                        "description": "Id kontak."
                      },
                      "conversation_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Id percakapan."
                      },
                      "stage": {
                        "type": "string",
                        "description": "Tahap di Pipeline."
                      },
                      "value_idr": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Nilai lead dalam rupiah."
                      },
                      "note": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Catatan."
                      },
                      "created_at": {
                        "type": "string",
                        "description": "Waktu dibuat.",
                        "format": "date-time"
                      },
                      "updated_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu terakhir diubah.",
                        "format": "date-time"
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "lead.created",
                "data": {
                  "id": 31,
                  "contact_id": 88,
                  "conversation_id": 412,
                  "stage": "incoming",
                  "value_idr": null,
                  "note": null,
                  "created_at": "2026-11-04T03:00:00+00:00",
                  "updated_at": "2026-11-04T03:00:00+00:00"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "lead.stage_changed": {
      "post": {
        "operationId": "webhook_lead_stage_changed",
        "tags": [
          "Webhook"
        ],
        "summary": "Lead pindah tahap",
        "description": "Kartu dipindah ke tahap lain. data berisi baris lead beserta from_stage. Hanya dikirim bila tahapnya benar berubah.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "lead.stage_changed"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer",
                        "description": "Id lead."
                      },
                      "contact_id": {
                        "type": "integer",
                        "description": "Id kontak."
                      },
                      "conversation_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Id percakapan."
                      },
                      "stage": {
                        "type": "string",
                        "description": "Tahap di Pipeline."
                      },
                      "value_idr": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Nilai lead dalam rupiah."
                      },
                      "note": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Catatan."
                      },
                      "created_at": {
                        "type": "string",
                        "description": "Waktu dibuat.",
                        "format": "date-time"
                      },
                      "updated_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu terakhir diubah.",
                        "format": "date-time"
                      },
                      "from_stage": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Tahap sebelum dipindah."
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "lead.stage_changed",
                "data": {
                  "id": 31,
                  "contact_id": 88,
                  "conversation_id": 412,
                  "stage": "qualified",
                  "value_idr": 1500000,
                  "note": null,
                  "created_at": "2026-11-04T03:00:00+00:00",
                  "updated_at": "2026-11-04T05:10:00+00:00",
                  "from_stage": "incoming"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "message.inbound": {
      "post": {
        "operationId": "webhook_message_inbound",
        "tags": [
          "Webhook"
        ],
        "summary": "Pesan masuk dari pelanggan",
        "description": "Pelanggan mengirim pesan. data memakai nama kolom yang sama dengan GET /api/export/messages, ditambah wa_id, contact_name, dan channel kontak. Webhook ini memuat isi pesan dan nomor pelanggan.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "message.inbound"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer",
                        "description": "Id pesan."
                      },
                      "conversation_id": {
                        "type": "integer",
                        "description": "Id percakapan."
                      },
                      "contact_id": {
                        "type": "integer",
                        "description": "Id kontak."
                      },
                      "direction": {
                        "type": "string",
                        "description": "Selalu in.",
                        "const": "in"
                      },
                      "wa_message_id": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Id pesan dari Meta."
                      },
                      "msg_type": {
                        "type": "string",
                        "description": "Jenis pesan."
                      },
                      "body": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Isi teks."
                      },
                      "media_id": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Id lampiran."
                      },
                      "media_mime": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Jenis berkas lampiran."
                      },
                      "created_at": {
                        "type": "string",
                        "description": "Waktu tercatat.",
                        "format": "date-time"
                      },
                      "wa_id": {
                        "type": "string",
                        "description": "Id kontak di salurannya."
                      },
                      "contact_name": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Nama kontak."
                      },
                      "channel": {
                        "type": "string",
                        "description": "Saluran kontak."
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "message.inbound",
                "data": {
                  "id": 9140,
                  "conversation_id": 412,
                  "contact_id": 88,
                  "direction": "in",
                  "wa_message_id": "wamid.HBgN...",
                  "msg_type": "text",
                  "body": "Baik, saya datang hari Selasa.",
                  "media_id": null,
                  "media_mime": null,
                  "created_at": "2026-11-04T03:20:00+00:00",
                  "wa_id": "6281234567890",
                  "contact_name": "Budi",
                  "channel": "wa"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "campaign.finished": {
      "post": {
        "operationId": "webhook_campaign_finished",
        "tags": [
          "Webhook"
        ],
        "summary": "Kampanye selesai",
        "description": "Antrean kampanye habis. data berisi baris kampanye yang dibaca ulang setelah selesai, sehingga state sudah done.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "campaign.finished"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer",
                        "description": "Id kampanye."
                      },
                      "name": {
                        "type": "string",
                        "description": "Nama kampanye."
                      },
                      "template_name": {
                        "type": "string",
                        "description": "Nama template."
                      },
                      "template_language": {
                        "type": "string",
                        "description": "Kode bahasa."
                      },
                      "segment_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Segmen, null untuk kampanye dari API."
                      },
                      "cta_target_url": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Alamat tujuan tautan."
                      },
                      "ignore_duplicates": {
                        "type": "boolean",
                        "description": "Pilihan lama."
                      },
                      "state": {
                        "type": "string",
                        "description": "Tahap kampanye."
                      },
                      "pause_reason": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Alasan jeda, bila pernah dijeda."
                      },
                      "source": {
                        "type": "string",
                        "description": "ui atau api."
                      },
                      "cost": {
                        "type": [
                          "number",
                          "null"
                        ],
                        "description": "Biaya yang diketik manusia."
                      },
                      "scheduled_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu kirim terjadwal.",
                        "format": "date-time"
                      },
                      "variabel": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        },
                        "description": "Pemetaan variabel dari layar Kampanye."
                      },
                      "jeda_otomatis": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Jeda yang dilepas Kareeb sendiri."
                      },
                      "lanjut_pada": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Kapan jeda otomatis dilepas.",
                        "format": "date-time"
                      },
                      "created_at": {
                        "type": "string",
                        "description": "Waktu dibuat.",
                        "format": "date-time"
                      },
                      "started_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu mulai.",
                        "format": "date-time"
                      },
                      "finished_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu selesai.",
                        "format": "date-time"
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "campaign.finished",
                "data": {
                  "id": 42,
                  "name": "Pengingat kontrol besok",
                  "template_name": "pengingat_kontrol",
                  "template_language": "id",
                  "segment_id": null,
                  "cta_target_url": null,
                  "ignore_duplicates": false,
                  "state": "done",
                  "pause_reason": null,
                  "source": "api",
                  "cost": null,
                  "scheduled_at": null,
                  "variabel": [],
                  "jeda_otomatis": null,
                  "lanjut_pada": null,
                  "created_at": "2026-11-04T01:00:00+00:00",
                  "started_at": "2026-11-04T01:00:02+00:00",
                  "finished_at": "2026-11-04T01:04:40+00:00"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "conversation.status_changed": {
      "post": {
        "operationId": "webhook_conversation_status_changed",
        "tags": [
          "Webhook"
        ],
        "summary": "Status percakapan berubah",
        "description": "Percakapan ditutup, dibuka lagi, atau dikembalikan ke antrean. Hanya id dan status, tanpa isi pesan. Hanya dikirim bila statusnya benar berubah.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "conversation.status_changed"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "conversation_id": {
                        "type": "integer",
                        "description": "Id percakapan."
                      },
                      "from_status": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Status sebelumnya."
                      },
                      "status": {
                        "type": "string",
                        "description": "Status baru."
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "conversation.status_changed",
                "data": {
                  "conversation_id": 412,
                  "from_status": "open",
                  "status": "closed"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "pemantauan.status_changed": {
      "post": {
        "operationId": "webhook_pemantauan_status_changed",
        "tags": [
          "Webhook"
        ],
        "summary": "Status sebutan Pemantauan berubah",
        "description": "Operator mengubah status sebutan yang dikirim lewat POST /api/pemantauan/masuk. Hanya id dan status, tanpa teks, penulis, atau nama operator. dibalas adalah keterangan operator; Kareeb tidak bisa memeriksa balasan di platform lain.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "pemantauan.status_changed"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "pemantauan_id": {
                        "type": "integer",
                        "description": "Id sebutan di Kareeb."
                      },
                      "referensi": {
                        "type": "string",
                        "description": "referensi dari sistem Anda."
                      },
                      "sumber": {
                        "type": "string",
                        "description": "Asal sebutan."
                      },
                      "external_id": {
                        "type": "string",
                        "description": "Id di platform."
                      },
                      "from_status": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Status sebelumnya."
                      },
                      "status": {
                        "type": "string",
                        "description": "Status baru: baru, ditangani, dibalas, atau diabaikan."
                      },
                      "at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu perubahan.",
                        "format": "date-time"
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "pemantauan.status_changed",
                "data": {
                  "pemantauan_id": 7,
                  "referensi": "sebutan-4821",
                  "sumber": "youtube",
                  "external_id": "Ugx-contoh-komentar",
                  "from_status": "baru",
                  "status": "ditangani",
                  "at": "2026-11-04T04:00:00+00:00"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "message.status": {
      "post": {
        "operationId": "webhook_message_status",
        "tags": [
          "Webhook"
        ],
        "summary": "Status kirim pesan",
        "description": "Meta melaporkan sent, delivered, read, atau failed untuk pesan yang dikirim Kareeb, dari API, broadcast, maupun kotak masuk. Hanya id dan status, tanpa teks atau nomor telepon. Status yang diulang Meta diantrekan lagi, jadi buang duplikat dengan pasangan wa_message_id dan status.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "message.status"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "message_id": {
                        "type": "integer",
                        "description": "Id pesan di Kareeb."
                      },
                      "conversation_id": {
                        "type": "integer",
                        "description": "Id percakapan."
                      },
                      "wa_message_id": {
                        "type": "string",
                        "description": "Id pesan dari Meta."
                      },
                      "status": {
                        "type": "string",
                        "description": "Status kirim.",
                        "enum": [
                          "sent",
                          "delivered",
                          "read",
                          "failed"
                        ]
                      },
                      "occurred_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu menurut Meta.",
                        "format": "date-time"
                      },
                      "failure_reason": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Hanya pada failed: judul kegagalan dari Meta, dalam bahasa Inggris."
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "message.status",
                "data": {
                  "message_id": 9137,
                  "conversation_id": 412,
                  "wa_message_id": "wamid.HBgM...",
                  "status": "failed",
                  "occurred_at": "2026-11-04T03:15:00+00:00",
                  "failure_reason": "Message undeliverable"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "call.finished": {
      "post": {
        "operationId": "webhook_call_finished",
        "tags": [
          "Webhook"
        ],
        "summary": "Panggilan suara selesai",
        "description": "Panggilan Agen AI suara di aplikasi Anda berakhir. Tanpa transkrip dan tanpa ringkasan.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "call.finished"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "call_id": {
                        "type": "integer",
                        "description": "Id panggilan di Kareeb."
                      },
                      "channel": {
                        "type": "string",
                        "description": "Selalu app untuk panggilan dari aplikasi."
                      },
                      "conversation_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Id percakapan."
                      },
                      "contact_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Id kontak."
                      },
                      "session_ref": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "session_ref dari sistem Anda."
                      },
                      "duration_s": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Lama panggilan dalam detik."
                      },
                      "outcome": {
                        "type": "string",
                        "description": "Hasil panggilan.",
                        "enum": [
                          "completed",
                          "handoff",
                          "failed"
                        ]
                      },
                      "handoff": {
                        "type": "boolean",
                        "description": "Agen menyerahkan penelepon ke petugas."
                      },
                      "ended_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Waktu berakhir.",
                        "format": "date-time"
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "call.finished",
                "data": {
                  "call_id": 55,
                  "channel": "app",
                  "conversation_id": 420,
                  "contact_id": 91,
                  "session_ref": "sesi-7f3a9c",
                  "duration_s": 184,
                  "outcome": "completed",
                  "handoff": false,
                  "ended_at": "2026-11-04T06:03:04+00:00"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    },
    "agen_ai.sambungan": {
      "post": {
        "operationId": "webhook_agen_ai_sambungan",
        "tags": [
          "Webhook"
        ],
        "summary": "Sambungan model Agen AI berubah",
        "description": "Sambungan ke model Agen AI berganti antara menjawab dan gagal. Dikirim hanya saat berganti, tanpa kunci dan tanpa isi jawaban.\n\nSemua webhook memakai amplop yang sama: id, timestamp, event, data. Kareeb menandatangani byte badan persis seperti dikirim; hitung HMAC-SHA256 dari byte mentah itu dengan secret webhook (hex huruf kecil) lalu bandingkan dengan X-Kareeb-Signature memakai perbandingan waktu tetap. Jawab 2xx dalam 10 detik. Kiriman yang gagal dicoba lagi pada putaran penjadwal berikutnya, sekitar tiap 5 menit, paling banyak 5 kali. Setelah 10 kegagalan berturut-turut, langganan dinonaktifkan dan admin perlu mengaktifkannya lagi. Pengiriman bersifat at least once: buang duplikat dengan id di badan, yang sama untuk satu kejadian di semua endpoint dan semua percobaan.",
        "parameters": [
          {
            "name": "X-Kareeb-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "HMAC-SHA256 dari byte badan mentah dengan secret webhook, hex huruf kecil, tanpa awalan sha256=."
          },
          {
            "name": "X-Kareeb-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Nama peristiwa, sama dengan event di badan."
          },
          {
            "name": "X-Kareeb-Delivery",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Id kiriman ke endpoint Anda. Sama pada setiap percobaan ulang kiriman yang sama, berbeda untuk setiap endpoint."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "timestamp",
                  "event",
                  "data"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Id kejadian (UUID). Sama di semua percobaan ulang; pakai untuk membuang duplikat.",
                    "format": "uuid"
                  },
                  "timestamp": {
                    "type": "string",
                    "description": "Waktu kejadian diantrekan.",
                    "format": "date-time"
                  },
                  "event": {
                    "type": "string",
                    "description": "Nama peristiwa.",
                    "const": "agen_ai.sambungan"
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "status": {
                        "type": "string",
                        "description": "Keadaan sambungan.",
                        "enum": [
                          "ok",
                          "gagal"
                        ]
                      },
                      "kode": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Kode HTTP terakhir, 0 bila tidak ada jawaban."
                      },
                      "host": {
                        "type": "string",
                        "description": "Host model; kosong pada instalasi yang menyembunyikannya."
                      },
                      "sejak": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Sejak kapan keadaan ini.",
                        "format": "date-time"
                      }
                    }
                  }
                }
              },
              "example": {
                "id": "0b6f2c1e-6d0a-4f7b-9a51-3c2d8e4f1a20",
                "timestamp": "2026-11-04T03:15:02+00:00",
                "event": "agen_ai.sambungan",
                "data": {
                  "status": "gagal",
                  "kode": 0,
                  "host": "",
                  "sejak": "2026-11-04T07:00:00+00:00"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jawab 2xx secepatnya, lalu proses isinya. Selain 2xx dianggap gagal dan dicoba lagi."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "tokenApi": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "kt_",
        "description": "Token API dengan awalan kt_, dikirim di header Authorization: Bearer kt_.... Token di URL (?token=) ditolak dengan 403. Token yang dicabut langsung mati pada permintaan berikutnya (401). Daftar di security setiap operasi adalah izin yang harus dimiliki token."
      }
    },
    "schemas": {
      "Kontak": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id kontak, juga kursor."
          },
          "wa_id": {
            "type": "string",
            "description": "Id kontak di salurannya. Di WhatsApp berisi nomor telepon, atau id pengguna khusus bisnis dari Meta (BSUID, diawali ID.) bila pelanggan menyembunyikan nomornya. Tidak pernah null."
          },
          "channel": {
            "type": "string",
            "description": "Saluran kontak.",
            "enum": [
              "wa",
              "ig",
              "messenger"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nomor telepon. Null untuk kontak WhatsApp yang nomornya disembunyikan Meta."
          },
          "bsuid": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id pengguna khusus bisnis dari Meta. Tidak pernah sama antarorganisasi."
          },
          "username": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama pengguna WhatsApp pelanggan, bila ada."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama profil WhatsApp, atau nama yang diisi operator."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Label kontak. Tidak pernah null."
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Data tambahan kontak, kunci dan nilai teks. Tidak pernah null."
          },
          "created_at": {
            "type": "string",
            "description": "Waktu kontak tercatat.",
            "format": "date-time"
          },
          "opted_out_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Terisi berarti pelanggan meminta berhenti dikirimi pesan. Kareeb menahan semua pengiriman proaktif kepadanya.",
            "format": "date-time"
          },
          "consent_reminders": {
            "type": [
              "string",
              "null"
            ],
            "description": "Persetujuan terbaru untuk pengingat janji. Null berarti belum pernah ditanya, bukan menolak.",
            "enum": [
              "granted",
              "revoked",
              null
            ]
          },
          "consent_reminders_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu persetujuan pengingat terbaru dicatat.",
            "format": "date-time"
          },
          "consent_marketing": {
            "type": [
              "string",
              "null"
            ],
            "description": "Persetujuan terbaru untuk pesan promosi. Null berarti belum pernah ditanya, bukan menolak.",
            "enum": [
              "granted",
              "revoked",
              null
            ]
          },
          "consent_marketing_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu persetujuan promosi terbaru dicatat.",
            "format": "date-time"
          }
        },
        "description": "Identitas kontak adalah pasangan channel dan wa_id, bukan wa_id saja. Persetujuan berlaku per tujuan: persetujuan pengingat tidak berarti persetujuan promosi."
      },
      "KontakSatu": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id kontak."
          },
          "wa_id": {
            "type": "string",
            "description": "Sama dengan di daftar kontak."
          },
          "channel": {
            "type": "string",
            "description": "Saluran kontak.",
            "enum": [
              "wa",
              "ig",
              "messenger"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nomor telepon, bila ada."
          },
          "bsuid": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id pengguna khusus bisnis dari Meta."
          },
          "username": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama pengguna WhatsApp, bila ada."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama kontak."
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Data tambahan kontak. Tidak pernah null."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Label kontak. Tidak pernah null."
          },
          "auto_tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Bagian dari tags yang diusulkan pelabel otomatis dan belum dibatalkan operator. Tidak pernah null."
          },
          "opted_out_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Terisi berarti pelanggan meminta berhenti dikirimi pesan.",
            "format": "date-time"
          },
          "blocked_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Terisi bila WhatsApp melaporkan nomor ini tidak bisa menerima pesan dari bisnis Anda.",
            "format": "date-time"
          },
          "asal_iklan": {
            "type": [
              "object",
              "null"
            ],
            "description": "Iklan pertama yang membawa kontak ini, bila ada. Null berarti tidak pernah datang dari iklan.",
            "properties": {
              "headline": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Judul iklan."
              },
              "source_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Jenis sumber iklan dari Meta."
              },
              "source_url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Tautan sumber iklan."
              }
            }
          },
          "created_at": {
            "type": "string",
            "description": "Waktu kontak tercatat.",
            "format": "date-time"
          }
        }
      },
      "Percakapan": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id percakapan, juga kursor."
          },
          "contact_id": {
            "type": "integer",
            "description": "Id kontak."
          },
          "channel": {
            "type": "string",
            "description": "Saluran percakapan.",
            "enum": [
              "wa",
              "ig",
              "messenger"
            ]
          },
          "opened_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu percakapan dibuka.",
            "format": "date-time"
          },
          "last_inbound_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pesan terakhir dari pelanggan. Inilah yang menentukan jendela 24 jam WhatsApp.",
            "format": "date-time"
          },
          "last_activity_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Aktivitas terakhir ke dua arah.",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "description": "Status percakapan.",
            "enum": [
              "open",
              "pending",
              "closed"
            ]
          },
          "handled_by": {
            "type": "string",
            "description": "Siapa yang sedang menangani.",
            "enum": [
              "bot",
              "human"
            ]
          },
          "assigned_to": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id operator yang ditugaskan."
          },
          "unread_count": {
            "type": "integer",
            "description": "Pesan yang belum dibaca operator."
          },
          "created_at": {
            "type": "string",
            "description": "Waktu baris dibuat.",
            "format": "date-time"
          }
        }
      },
      "Pesan": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id pesan, juga kursor."
          },
          "conversation_id": {
            "type": "integer",
            "description": "Id percakapan."
          },
          "contact_id": {
            "type": "integer",
            "description": "Id kontak."
          },
          "direction": {
            "type": "string",
            "description": "Arah pesan.",
            "enum": [
              "in",
              "out"
            ]
          },
          "wa_message_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id pesan dari Meta."
          },
          "msg_type": {
            "type": "string",
            "description": "Jenis pesan, misalnya text, template, image."
          },
          "body": {
            "type": [
              "string",
              "null"
            ],
            "description": "Isi teks pesan."
          },
          "template_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama template untuk pesan template keluar."
          },
          "media_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id lampiran."
          },
          "media_mime": {
            "type": [
              "string",
              "null"
            ],
            "description": "Jenis berkas lampiran."
          },
          "sent_by": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id operator pengirim. Null untuk pesan masuk dan kiriman sistem atau API."
          },
          "created_at": {
            "type": "string",
            "description": "Waktu pesan tercatat.",
            "format": "date-time"
          },
          "media_dihapus_pada": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu lampiran dihapus oleh masa simpan lampiran organisasi. Baris pesannya sendiri tidak berubah.",
            "format": "date-time"
          },
          "delivery_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Status kirim untuk pesan keluar: peristiwa dengan urutan tertinggi, bukan yang terbaru.",
            "enum": [
              "sent",
              "delivered",
              "read",
              "failed",
              null
            ]
          },
          "failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alasan dari Meta, hanya bila delivery_status failed."
          },
          "jalur_kirim": {
            "type": [
              "string",
              "null"
            ],
            "description": "Jalur kirim yang diminta Kareeb untuk pesan WhatsApp keluar (cloud atau mm). Null untuk pesan masuk dan pesan lama."
          }
        }
      },
      "Kampanye": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id kampanye, juga kursor."
          },
          "name": {
            "type": "string",
            "description": "Nama di halaman Kampanye."
          },
          "template_name": {
            "type": "string",
            "description": "Nama template."
          },
          "template_language": {
            "type": "string",
            "description": "Kode bahasa template."
          },
          "segment_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Segmen penerima. Null untuk kampanye dari API."
          },
          "cta_target_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alamat tujuan tautan per pelanggan."
          },
          "ignore_duplicates": {
            "type": "boolean",
            "description": "Pilihan lama yang tidak dipakai lagi; setiap kampanye kini membuang penerima ganda."
          },
          "source": {
            "type": "string",
            "description": "Asal kampanye.",
            "enum": [
              "ui",
              "api"
            ]
          },
          "idempotency_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kunci dari POST /api/broadcasts. Null untuk kampanye dari layar."
          },
          "state": {
            "type": "string",
            "description": "Tahap kampanye.",
            "enum": [
              "draft",
              "sending",
              "done",
              "cancelled",
              "paused"
            ]
          },
          "cost": {
            "type": [
              "number",
              "null"
            ],
            "description": "Biaya yang diketik manusia, atau null. Kareeb tidak menghitung biaya."
          },
          "variabel": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Cara variabel template diisi untuk kampanye dari layar. Daftar kosong bila tidak ada."
          },
          "created_at": {
            "type": "string",
            "description": "Waktu dibuat.",
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu mulai mengirim.",
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu selesai.",
            "format": "date-time"
          }
        }
      },
      "StatistikKampanye": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id kampanye, juga kursor."
          },
          "name": {
            "type": "string",
            "description": "Nama kampanye."
          },
          "state": {
            "type": "string",
            "description": "Tahap kampanye."
          },
          "source": {
            "type": "string",
            "description": "Asal kampanye.",
            "enum": [
              "ui",
              "api"
            ]
          },
          "template_name": {
            "type": "string",
            "description": "Nama template."
          },
          "template_language": {
            "type": "string",
            "description": "Kode bahasa template."
          },
          "created_at": {
            "type": "string",
            "description": "Waktu dibuat.",
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu mulai.",
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu selesai.",
            "format": "date-time"
          },
          "targeted": {
            "type": "integer",
            "description": "Penerima di antrean, tanpa penerima ganda yang dibuang saat peluncuran."
          },
          "sent": {
            "type": "integer",
            "description": "Penerima yang terkirim."
          },
          "delivered": {
            "type": "integer",
            "description": "Sampai atau dibaca. Corongnya menurun, jadi angka ini sudah mencakup yang dibaca."
          },
          "read": {
            "type": "integer",
            "description": "Dibaca."
          },
          "replied": {
            "type": "integer",
            "description": "Penerima yang mengirim pesan setelah kiriman kampanye."
          },
          "clicked": {
            "type": "integer",
            "description": "Penerima yang membuka tautan pribadinya."
          },
          "converted": {
            "type": "integer",
            "description": "Penerima dengan pesanan terbayar yang diatribusikan."
          },
          "converted_amount": {
            "type": "number",
            "description": "Jumlah pesanan tersebut, dalam mata uang pesanannya."
          },
          "failed": {
            "type": "integer",
            "description": "Gagal saat dikirim ke WhatsApp, atau dilaporkan gagal oleh Meta sesudahnya."
          },
          "cost": {
            "type": [
              "number",
              "null"
            ],
            "description": "Biaya yang diketik manusia, atau null."
          },
          "roi": {
            "type": [
              "number",
              "null"
            ],
            "description": "(converted_amount dikurangi cost) dibagi cost, sebagai rasio (0,5 berarti naik 50%). Null kecuali cost terisi dan lebih dari nol."
          }
        }
      },
      "PenerimaKampanye": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id baris penerima, juga kursor."
          },
          "campaign_id": {
            "type": "integer",
            "description": "Id kampanye."
          },
          "contact_id": {
            "type": "integer",
            "description": "Id kontak."
          },
          "send_state": {
            "type": "string",
            "description": "Tahap kirim. skipped berarti penerima ganda yang dibuang saat peluncuran.",
            "enum": [
              "queued",
              "sent",
              "failed",
              "skipped"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alasan penolakan saat dikirim, bila ada."
          },
          "sent_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu terkirim menurut jam basis data.",
            "format": "date-time"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kode tautan pribadi penerima, bila kampanye punya tautan."
          },
          "wa_message_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id pesan dari Meta."
          },
          "delivery_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Status kirim dengan urutan tertinggi.",
            "enum": [
              "sent",
              "delivered",
              "read",
              "failed",
              null
            ]
          },
          "failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alasan dari Meta, hanya bila gagal."
          },
          "clicked": {
            "type": "boolean",
            "description": "Tautan pribadi penerima pernah dibuka. WhatsApp tidak melaporkan ketukan tombol, jadi hanya ini buktinya."
          },
          "replied": {
            "type": "boolean",
            "description": "Kontak mengirim pesan setelah sent_at."
          },
          "converted_order_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pesanan terbayar yang diatribusikan ke kiriman ini."
          },
          "converted_amount": {
            "type": [
              "number",
              "null"
            ],
            "description": "Jumlah pesanan itu."
          },
          "converted_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu atribusi.",
            "format": "date-time"
          },
          "converted_source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Asal atribusi: xendit_callback (gerbang pembayaran) atau manual (operator). Daftarnya bisa bertambah."
          }
        }
      },
      "Pesanan": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id pesanan, juga kursor."
          },
          "conversation_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id percakapan."
          },
          "contact_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id kontak."
          },
          "items": {
            "type": "array",
            "description": "Rincian barang, hanya keterangan.",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Nama barang."
                },
                "qty": {
                  "type": "number",
                  "description": "Jumlah."
                },
                "price": {
                  "type": "number",
                  "description": "Harga satuan."
                }
              }
            }
          },
          "amount": {
            "type": "number",
            "description": "Jumlah yang ditagihkan. Inilah angka yang berlaku."
          },
          "currency": {
            "type": "string",
            "description": "Mata uang."
          },
          "status": {
            "type": "string",
            "description": "Status pesanan.",
            "enum": [
              "draft",
              "sent",
              "paid",
              "expired",
              "cancelled"
            ]
          },
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "Gerbang pembayaran."
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id pesanan di gerbang pembayaran."
          },
          "created_at": {
            "type": "string",
            "description": "Waktu dibuat.",
            "format": "date-time"
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu dibayar.",
            "format": "date-time"
          }
        }
      },
      "Komentar": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id komentar, juga kursor."
          },
          "channel": {
            "type": "string",
            "description": "ig untuk Instagram, messenger untuk Halaman Facebook.",
            "enum": [
              "ig",
              "messenger"
            ]
          },
          "post_id": {
            "type": "string",
            "description": "Id unggahan dari Meta."
          },
          "comment_id": {
            "type": "string",
            "description": "Id komentar dari Meta."
          },
          "thread_id": {
            "type": "integer",
            "description": "Id utas komentar di Kareeb."
          },
          "body": {
            "type": "string",
            "description": "Isi komentar."
          },
          "author_name": {
            "type": "string",
            "description": "Nama penulis komentar."
          },
          "commented_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu komentar, UTC.",
            "format": "date-time"
          },
          "replied": {
            "type": "boolean",
            "description": "Kareeb sudah membalas komentar ini secara publik atau pribadi, dinilai saat halaman dibaca. Komentar yang dibalas setelah Anda tarik tidak muncul lagi."
          }
        }
      },
      "HalamanKomentar": {
        "type": "object",
        "required": [
          "komentar",
          "next_since_id"
        ],
        "properties": {
          "komentar": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Komentar"
            }
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id terakhir bila halaman penuh; null bila tidak penuh atau kosong."
          }
        }
      },
      "Template": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nama yang diisi di template.name atau template_name."
          },
          "language": {
            "type": "string",
            "description": "Kode bahasa yang diisi di template.language atau template_language."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kategori dari Meta: MARKETING, UTILITY, atau AUTHENTICATION."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Status dari Meta saat sinkronisasi terakhir, misalnya APPROVED, PENDING, REJECTED, PAUSED. Hanya APPROVED yang bisa dikirim."
          },
          "parameter_count": {
            "type": "integer",
            "description": "Jumlah variabel badan ({{1}}, {{2}}, dan seterusnya) yang harus diisi di body_params atau params. 0 bila tidak ada."
          },
          "needs_cta": {
            "type": "boolean",
            "description": "true bila template punya tombol tautan dengan variabel, sehingga cta_param wajib diisi."
          },
          "workspace_id": {
            "type": "integer",
            "description": "Cabang yang akun WhatsApp-nya memegang template. Sebut sebagai workspace_id di POST /api/messages. POST /api/broadcasts selalu mengirim dari cabang Utama."
          },
          "synced_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu Kareeb terakhir membaca template ini dari Meta.",
            "format": "date-time"
          }
        }
      },
      "DaftarTemplate": {
        "type": "object",
        "required": [
          "templates"
        ],
        "properties": {
          "templates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Template"
            }
          }
        },
        "example": {
          "templates": [
            {
              "name": "pengingat_kontrol",
              "language": "id",
              "category": "UTILITY",
              "status": "APPROVED",
              "parameter_count": 3,
              "needs_cta": false,
              "workspace_id": 1,
              "synced_at": "2026-11-04T04:12:00+00:00"
            }
          ]
        }
      },
      "IdentitasToken": {
        "type": "object",
        "required": [
          "org_id",
          "org_name",
          "token_id",
          "scopes",
          "created_at"
        ],
        "properties": {
          "org_id": {
            "type": "integer",
            "description": "Organisasi tempat token bertindak."
          },
          "org_name": {
            "type": "string",
            "description": "Nama organisasi."
          },
          "token_id": {
            "type": "integer",
            "description": "Id token."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Izin token, dalam urutan tetap daftar izin."
          },
          "created_at": {
            "type": "string",
            "description": "Waktu token dibuat.",
            "format": "date-time"
          }
        },
        "example": {
          "org_id": 1,
          "org_name": "Rumah Sakit Sentra",
          "token_id": 12,
          "scopes": [
            "write:messages",
            "read:templates"
          ],
          "created_at": "2026-11-04T02:00:00+00:00"
        }
      },
      "BroadcastBaru": {
        "type": "object",
        "required": [
          "template_name",
          "recipients",
          "idempotency_key"
        ],
        "properties": {
          "template_name": {
            "type": "string",
            "description": "Harus ada di Kareeb, di akun WhatsApp cabang Utama, dan berstatus APPROVED.",
            "minLength": 1
          },
          "template_language": {
            "type": "string",
            "description": "Kode bahasa template.",
            "default": "id",
            "minLength": 1
          },
          "recipients": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5000,
            "description": "1 sampai 5.000 penerima per permintaan.",
            "items": {
              "type": "object",
              "required": [
                "wa_id"
              ],
              "properties": {
                "wa_id": {
                  "type": "string",
                  "description": "Nomor WhatsApp, 7 sampai 20 digit format internasional. Tanda plus di depan dan spasi di tepi dibuang; selain itu ditolak. Selalu nomor telepon, bukan BSUID."
                },
                "params": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  },
                  "default": {},
                  "description": "Variabel badan berurutan: kunci \"1\", \"2\", dan seterusnya tanpa celah. Nilai tidak boleh kosong dan tidak boleh berisi baris baru, tab, atau lebih dari empat spasi berurutan."
                }
              }
            }
          },
          "idempotency_key": {
            "type": "string",
            "description": "1 sampai 128 karakter, unik di semua broadcast organisasi.",
            "minLength": 1,
            "maxLength": 128
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama di halaman Kampanye. Bawaan: \"API: \" diikuti nama template.",
            "maxLength": 120
          },
          "scheduled_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu kirim, wajib dengan zona waktu, paling jauh 30 hari ke depan. Kosongkan untuk langsung masuk antrean.",
            "format": "date-time"
          }
        }
      },
      "BroadcastJawaban": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id kampanye."
          },
          "replay": {
            "type": "boolean",
            "description": "true bila kunci ini sudah pernah dipakai."
          },
          "idempotency_key": {
            "type": "string",
            "description": "Kunci kampanye."
          },
          "template_name": {
            "type": "string",
            "description": "Nama template."
          },
          "template_language": {
            "type": "string",
            "description": "Kode bahasa template."
          },
          "scheduled_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Waktu kirim yang dijadwalkan, atau null.",
            "format": "date-time"
          },
          "accepted": {
            "type": "array",
            "description": "Penerima yang diterima oleh permintaan ini. Kosong pada replay.",
            "items": {
              "type": "object",
              "properties": {
                "wa_id": {
                  "type": "string",
                  "description": "Nomor yang sudah dirapikan."
                },
                "contact_id": {
                  "type": "integer",
                  "description": "Id kontak."
                },
                "recipient_id": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Id baris penerima, sama dengan id di GET /api/export/campaigns/recipients."
                }
              }
            }
          },
          "refused": {
            "type": "array",
            "description": "Penerima yang ditolak beserta alasannya. Kiriman tetap berjalan untuk penerima lain. Kosong pada replay.",
            "items": {
              "type": "object",
              "properties": {
                "wa_id": {
                  "type": "string",
                  "description": "Nomor seperti yang dikirim."
                },
                "reason": {
                  "type": "string",
                  "description": "Alasan, untuk dibaca orang."
                }
              }
            }
          },
          "status": {
            "type": "object",
            "description": "Keadaan antrean saat ini, dibaca langsung.",
            "properties": {
              "state": {
                "type": "string",
                "description": "Tahap kampanye."
              },
              "targeted": {
                "type": "integer",
                "description": "Penerima di antrean."
              },
              "queued": {
                "type": "integer",
                "description": "Menunggu dikirim."
              },
              "sent": {
                "type": "integer",
                "description": "Terkirim."
              },
              "failed": {
                "type": "integer",
                "description": "Gagal."
              }
            }
          }
        }
      },
      "PesanBaru": {
        "type": "object",
        "required": [
          "idempotency_key"
        ],
        "properties": {
          "to": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nomor WhatsApp tujuan, 7 sampai 20 digit format internasional. Tanda plus dan spasi di tepi dibuang. Nomor yang belum dikenal menjadi kontak WhatsApp baru di cabang itu."
          },
          "contact_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Kontak di cabang tempat panggilan ini berjalan. Isi to atau contact_id, salah satu saja."
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "description": "Teks bebas, 1 sampai 4096 karakter, hanya di dalam jendela 24 jam. Isi text atau template, salah satu saja.",
            "minLength": 1,
            "maxLength": 4096
          },
          "template": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "Harus ada di Kareeb, di akun WhatsApp cabang ini, dan berstatus APPROVED.",
                "minLength": 1
              },
              "language": {
                "type": "string",
                "description": "Kode bahasa template.",
                "default": "id",
                "minLength": 1
              },
              "body_params": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "default": {},
                "description": "Variabel badan, aturannya sama dengan params di POST /api/broadcasts."
              },
              "cta_param": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Akhiran yang ditempel ke URL tetap tombol. Wajib bila template punya tombol tautan per pelanggan."
              }
            }
          },
          "idempotency_key": {
            "type": "string",
            "description": "1 sampai 128 karakter, unik per organisasi.",
            "minLength": 1,
            "maxLength": 128
          },
          "workspace_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Cabang pengirim, harus milik organisasi token. Bawaan: cabang Utama."
          }
        }
      },
      "PesanJawaban": {
        "type": "object",
        "properties": {
          "message_id": {
            "type": "integer",
            "description": "Id pesan di Kareeb."
          },
          "conversation_id": {
            "type": "integer",
            "description": "Id percakapan."
          },
          "wa_message_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id pesan dari Meta. Status kirim di webhook message.status memakai id ini."
          },
          "contact_id": {
            "type": "integer",
            "description": "Id kontak."
          },
          "replay": {
            "type": "boolean",
            "description": "true bila kunci ini sudah pernah dikirim."
          }
        }
      },
      "PemantauanMasuk": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "sumber",
          "external_id",
          "referensi",
          "url",
          "teks"
        ],
        "properties": {
          "sumber": {
            "type": "string",
            "description": "Asal sebutan. instagram dan facebook ditolak karena komentar di unggahan sendiri sudah masuk lewat Meta.",
            "enum": [
              "youtube",
              "threads",
              "google_reviews",
              "lainnya"
            ]
          },
          "external_id": {
            "type": "string",
            "description": "Id di platform, 1 sampai 200 karakter. Bersama sumber menjadi kunci idempotensi.",
            "minLength": 1,
            "maxLength": 200
          },
          "referensi": {
            "type": "string",
            "description": "Id sebutan di sistem Anda, 1 sampai 100 karakter. Dikembalikan di webhook pemantauan.status_changed.",
            "minLength": 1,
            "maxLength": 100
          },
          "url": {
            "type": "string",
            "description": "Diawali https:// atau http://, paling panjang 2000 karakter. Kareeb tidak membukanya; halaman menampilkannya sebagai tautan.",
            "maxLength": 2000
          },
          "teks": {
            "type": "string",
            "description": "1 sampai 4000 karakter.",
            "minLength": 1,
            "maxLength": 4000
          },
          "penulis": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nama tampilan, paling panjang 120 karakter.",
            "maxLength": 120
          },
          "penulis_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nilai pengelompokan dari sistem Anda, paling panjang 64 karakter. Tidak ditampilkan.",
            "maxLength": 64
          },
          "diterbitkan_pada": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601 dengan zona waktu. Lebih dari 5 menit di masa depan ditolak.",
            "format": "date-time"
          },
          "sentimen": {
            "type": [
              "string",
              "null"
            ],
            "description": "Label sentimen. Kosong atau null berarti tanpa label.",
            "enum": [
              "positif",
              "netral",
              "negatif",
              "perlu ditinjau",
              null
            ]
          },
          "topik": {
            "type": [
              "string",
              "null"
            ],
            "description": "Paling panjang 60 karakter.",
            "maxLength": 60
          },
          "kedaluwarsa_pada": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kapan teks harus dihapus. Dibatasi oleh masa simpan Pemantauan di Kareeb; kosong berarti batas itu.",
            "format": "date-time"
          }
        },
        "description": "Kolom lain di luar daftar ini (misalnya org_id atau workspace_id) ditolak 422: organisasi dan cabang selalu diambil dari token."
      },
      "PemantauanJawaban": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id sebutan di Kareeb."
          },
          "status": {
            "type": "string",
            "description": "Status saat ini.",
            "enum": [
              "baru",
              "ditangani",
              "dibalas",
              "diabaikan"
            ]
          },
          "replay": {
            "type": "boolean",
            "description": "true bila pasangan sumber dan external_id sudah ada."
          },
          "url_crm": {
            "type": "string",
            "description": "Jalur halaman Pemantauan untuk sebutan ini."
          }
        }
      },
      "PemantauanHapus": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "sumber",
          "external_id"
        ],
        "properties": {
          "sumber": {
            "type": "string",
            "description": "Sama dengan saat dikirim.",
            "enum": [
              "youtube",
              "threads",
              "google_reviews",
              "lainnya"
            ]
          },
          "external_id": {
            "type": "string",
            "description": "Sama dengan saat dikirim.",
            "minLength": 1,
            "maxLength": 200
          }
        }
      },
      "HapusJawaban": {
        "type": "object",
        "properties": {
          "ditemukan": {
            "type": "boolean",
            "description": "Sebutan ada."
          },
          "dihapus": {
            "type": "boolean",
            "description": "Isinya dihapus oleh permintaan ini. false bila sudah terhapus sebelumnya atau tidak ditemukan."
          },
          "id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id sebutan, atau null bila tidak ditemukan."
          }
        }
      },
      "SuaraTokenBaru": {
        "type": "object",
        "required": [
          "session_ref"
        ],
        "properties": {
          "session_ref": {
            "type": "string",
            "description": "Id sesi buram dari sistem Anda, 1 sampai 120 karakter yang dapat dicetak.",
            "minLength": 1,
            "maxLength": 120
          },
          "workspace_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Cabang, harus milik organisasi token. Bawaan: cabang Utama."
          }
        }
      },
      "SuaraTokenJawaban": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "Token untuk SDK browser mesin suara."
          },
          "agent_id": {
            "type": "string",
            "description": "Agen suara yang menjawab."
          },
          "conversation_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id dari mesin suara, bila ada."
          },
          "user_id": {
            "type": "string",
            "description": "Berikan apa adanya sebagai userId di SDK."
          },
          "expires_at": {
            "type": "null",
            "description": "Selalu null karena mesin suara tidak menyebut umur token. Minta token baru tepat sebelum setiap panggilan."
          }
        }
      },
      "Penolakan": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Kode untuk dibaca program. Inilah kontraknya."
              },
              "message": {
                "type": "string",
                "description": "Kalimat bahasa Indonesia untuk dibaca orang. Isinya bisa berubah kapan saja."
              },
              "template_name": {
                "type": "string",
                "description": "Ikut pada penolakan template."
              },
              "template_language": {
                "type": "string",
                "description": "Ikut pada penolakan template."
              }
            }
          }
        },
        "description": "Penolakan dengan kode. Bercabanglah pada code, jangan pada kalimatnya."
      },
      "GalatKalimat": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "string",
            "description": "Satu kalimat bahasa Indonesia untuk dibaca orang."
          }
        }
      },
      "GalatApaSaja": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "description": "Objek dengan code, kalimat, atau daftar galat per kolom dari FastAPI.",
            "oneOf": [
              {
                "type": "object"
              },
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "object"
                }
              }
            ]
          }
        }
      },
      "HalamanKontak": {
        "type": "object",
        "required": [
          "items",
          "count",
          "next_since_id",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Kontak"
            }
          },
          "count": {
            "type": "integer",
            "description": "Jumlah baris di halaman ini."
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id baris terakhir di halaman ini, untuk since_id berikutnya. Null pada halaman kosong."
          },
          "has_more": {
            "type": "boolean",
            "description": "Petunjuk saja: true bila halaman ini penuh. Tanda selesai yang pasti adalah items kosong."
          }
        }
      },
      "HalamanPercakapan": {
        "type": "object",
        "required": [
          "items",
          "count",
          "next_since_id",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Percakapan"
            }
          },
          "count": {
            "type": "integer",
            "description": "Jumlah baris di halaman ini."
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id baris terakhir di halaman ini, untuk since_id berikutnya. Null pada halaman kosong."
          },
          "has_more": {
            "type": "boolean",
            "description": "Petunjuk saja: true bila halaman ini penuh. Tanda selesai yang pasti adalah items kosong."
          }
        }
      },
      "HalamanPesan": {
        "type": "object",
        "required": [
          "items",
          "count",
          "next_since_id",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pesan"
            }
          },
          "count": {
            "type": "integer",
            "description": "Jumlah baris di halaman ini."
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id baris terakhir di halaman ini, untuk since_id berikutnya. Null pada halaman kosong."
          },
          "has_more": {
            "type": "boolean",
            "description": "Petunjuk saja: true bila halaman ini penuh. Tanda selesai yang pasti adalah items kosong."
          }
        }
      },
      "HalamanKampanye": {
        "type": "object",
        "required": [
          "items",
          "count",
          "next_since_id",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Kampanye"
            }
          },
          "count": {
            "type": "integer",
            "description": "Jumlah baris di halaman ini."
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id baris terakhir di halaman ini, untuk since_id berikutnya. Null pada halaman kosong."
          },
          "has_more": {
            "type": "boolean",
            "description": "Petunjuk saja: true bila halaman ini penuh. Tanda selesai yang pasti adalah items kosong."
          }
        }
      },
      "HalamanStatistikKampanye": {
        "type": "object",
        "required": [
          "items",
          "count",
          "next_since_id",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatistikKampanye"
            }
          },
          "count": {
            "type": "integer",
            "description": "Jumlah baris di halaman ini."
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id baris terakhir di halaman ini, untuk since_id berikutnya. Null pada halaman kosong."
          },
          "has_more": {
            "type": "boolean",
            "description": "Petunjuk saja: true bila halaman ini penuh. Tanda selesai yang pasti adalah items kosong."
          }
        }
      },
      "HalamanPenerimaKampanye": {
        "type": "object",
        "required": [
          "items",
          "count",
          "next_since_id",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PenerimaKampanye"
            }
          },
          "count": {
            "type": "integer",
            "description": "Jumlah baris di halaman ini."
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id baris terakhir di halaman ini, untuk since_id berikutnya. Null pada halaman kosong."
          },
          "has_more": {
            "type": "boolean",
            "description": "Petunjuk saja: true bila halaman ini penuh. Tanda selesai yang pasti adalah items kosong."
          }
        }
      },
      "HalamanPesanan": {
        "type": "object",
        "required": [
          "items",
          "count",
          "next_since_id",
          "has_more"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pesanan"
            }
          },
          "count": {
            "type": "integer",
            "description": "Jumlah baris di halaman ini."
          },
          "next_since_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id baris terakhir di halaman ini, untuk since_id berikutnya. Null pada halaman kosong."
          },
          "has_more": {
            "type": "boolean",
            "description": "Petunjuk saja: true bila halaman ini penuh. Tanda selesai yang pasti adalah items kosong."
          }
        }
      }
    },
    "responses": {
      "TidakDikenal": {
        "description": "tidak ada kredensial, atau token tidak dikenal atau sudah dicabut. Token yang tidak dikenal dan yang dicabut dijawab sama.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/GalatKalimat"
            },
            "example": {
              "detail": "Token API tidak dikenal atau sudah dicabut. Minta admin membuat token baru di Pengaturan."
            }
          }
        }
      },
      "Ditolak": {
        "description": "token tidak punya izin yang dibutuhkan (nama izinnya disebut), token dikirim di URL atau ke alamat di luar API ini, sesi operator dikirim ke alamat yang hanya menerima token, atau organisasi sedang dinonaktifkan.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/GalatKalimat"
            },
            "example": {
              "detail": "Token API ini tidak punya izin read:contacts. Minta admin membuat token baru yang mencakup izin itu."
            }
          }
        }
      },
      "BacaSaja": {
        "description": "organisasi sedang berstatus baca saja, jadi tidak bisa mengirim atau menulis. Berhenti dan jangan langsung mengulang; admin organisasi Anda perlu memulihkannya di Kareeb.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Penolakan"
            },
            "example": {
              "detail": {
                "code": "langganan_baca_saja",
                "message": "(kalimat untuk dibaca orang)"
              }
            }
          }
        }
      },
      "TidakValid": {
        "description": "permintaan tidak valid. Parameter atau badan yang salah bentuk dijawab FastAPI dengan daftar galat per kolom.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/GalatApaSaja"
            }
          }
        }
      },
      "TerlaluBesar": {
        "description": "badan kiriman Pemantauan lebih dari 16 KB. Potong teksnya lalu kirim lagi.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/GalatKalimat"
            }
          }
        }
      },
      "TerlaluBanyak": {
        "description": "terlalu banyak permintaan, tunggu Retry-After. Ulangi setelah itu dengan kunci yang sama.",
        "headers": {
          "Retry-After": {
            "description": "Berapa detik harus menunggu.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Penolakan"
            },
            "example": {
              "detail": {
                "code": "rate_limited",
                "message": "(kalimat untuk dibaca orang)"
              }
            }
          }
        }
      },
      "BelumSiap": {
        "description": "Kareeb belum bisa menjawab karena basis data tidak tersedia. Tidak pernah 200 dengan data kosong. Ulangi beberapa saat lagi.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/GalatKalimat"
            },
            "example": {
              "detail": "Basis data Kareeb belum disiapkan di server ini. Hubungi tim Kareeb."
            }
          }
        }
      }
    }
  }
}
