Endpoint ini menjawab satu pertanyaan: **"nomor yang barusan chat ini siapa, dan pesanan apa yang sedang berjalan?"** Dipakai konektor CRM dan aplikasi CS. Satu permintaan mencari **satu nomor telepon secara persis**. Tidak ada pencarian sebagian, tidak ada daftar, tidak ada halaman berikutnya. ## Ringkasan | Item | Nilai | |---|---| | URL produksi | `https://adform.id/api/customer_lookup.php` | | URL staging | `https://staging.adform.id/api/customer_lookup.php` | | Method | `POST` saja. Method lain dibalas `405`. | | Autentikasi | Header `X-API-Key: adk_…` | | Scope | `customers:read` | | Content-Type permintaan | `application/json` | | Ukuran body maksimum | 4 KB | | Batas laju | 30 / menit, 600 / jam, 4.000 / hari — **per API key** | | Waktu jawab minimum | 150 milidetik, disengaja | ## Kenapa `POST`, bukan `GET` Nomor telepon pelanggan tidak boleh masuk query string. Query string tercatat di log akses server pada **setiap** panggilan, dan log itu berputar serta ikut tersalin ke cadangan. Dalam sebulan, log berisi ribuan nomor pelanggan dalam bentuk terbaca, di tempat yang tidak diaudit sebagai penyimpanan data pribadi. Body `POST` tidak dicatat. Karena itu `GET` dibalas `405` dan tidak akan pernah dilonggarkan. Ini bukan preferensi gaya REST. Konsekuensi untuk Anda: endpoint ini tidak bisa di-cache oleh proxy, dan tidak bisa dipanggil dari browser. Panggil dari server Anda. ## Autentikasi Satu header: ```http X-API-Key: adk_0123456789abcdef0123456789abcdef0123456789abcdef ``` Key harus memuat scope **`customers:read`**. Key ber-scope `*` juga lolos, tetapi jangan meminta tenant membuat key seperti itu untuk konektor CS — mintalah `customers:read` saja. Cara tenant memperoleh key ada di [Autentikasi](/docs/authentication#mendapatkan-api-key). Satu key terikat ke satu tenant; konteks tenant ditentukan sepenuhnya oleh key dan tidak bisa Anda kirim sendiri. Endpoint ini menolak pemanggilan lintas-asal dari browser. Header `Access-Control-Allow-Origin` yang dikirimnya bernilai `null`, dan `X-API-Key` tidak ada di daftar header yang diizinkan preflight. Ini server-ke-server. ## Body permintaan | Field | Tipe | Wajib | Keterangan | |---|---|---|---| | `phone` | string | wajib | Nomor telepon pelanggan. Format bebas: `08…`, `62…`, `+62…`, boleh berisi spasi atau tanda hubung. Kami normalkan sendiri dan mencocokkan semua variannya. | | `orders` | integer | opsional | Berapa pesanan terakhir yang ikut dikembalikan. Rentang **1–5**, bawaan **3**. Nilai di luar rentang dipangkas ke batas terdekat, bukan ditolak. | Field lain **diabaikan diam-diam**, bukan ditolak. Tidak ada `q`, `limit`, `page`, atau `offset`. ## Contoh permintaan ```bash curl -sS -X POST 'https://adform.id/api/customer_lookup.php' \ -H 'X-API-Key: adk_0123456789abcdef0123456789abcdef0123456789abcdef' \ -H 'Content-Type: application/json' \ -d '{"phone":"081234567890","orders":3}' ``` Contoh minimum, tanpa `orders`: ```bash curl -sS -X POST 'https://adform.id/api/customer_lookup.php' \ -H 'X-API-Key: adk_0123456789abcdef0123456789abcdef0123456789abcdef' \ -H 'Content-Type: application/json' \ -d '{"phone":"+62 812-3456-7890"}' ``` ## Respons — pelanggan ditemukan (`200`) ```json { "found": true, "customer": { "name": "Budi Santoso", "phone": "6281234567890", "city": "Bandung", "province": "Jawa Barat", "orders_total": 4, "orders_paid": 3, "lifetime_value": 742000, "first_order_at": "2026-03-11 08:22:41", "last_order_at": "2026-07-27 09:41:11" }, "recent_orders": [ { "id": 48211, "product": "Kemeja Linen Pria Lengan Panjang", "variant": "Navy / L", "quantity": 2, "total": 189000, "shipping_fee": 24000, "payment_method": "COD", "status": "pending", "courier": "JNE REG", "tracking_number": null, "created_at": "2026-07-27 09:41:11" }, { "id": 47903, "product": "Celana Chino Slim Fit", "variant": "Khaki / 32", "quantity": 1, "total": 235000, "shipping_fee": 20000, "payment_method": "QRIS", "status": "completed", "courier": "SiCepat BEST", "tracking_number": "005412998877", "created_at": "2026-06-30 19:05:12" } ], "conversation": { "exists": true, "status": "open", "last_at": "2026-07-27 10:03:55" }, "welcome": { "message": "Halo Kak Budi Santoso 😊\n\nTerima kasih sudah order di toko kami.\nBerikut rincian pesanan Kakak:\n\nProduk: Kemeja Linen Pria Lengan Panjang Navy / L\nOngkir: JNE Rp 24.000\nTotal: *Rp 189.000* - COD (Bayar di Tempat)\n\nMohon pastikan alamat di atas sudah lengkap dan benar.\nAlamat yang lengkap akan mempermudah kurir dan mempercepat pengiriman paket.\n\nPesanan akan segera kami proses.\nTerima kasih 🙏", "template": "default:fu_welcome_cod" }, "meta": { "request_id": "9f3c1a7b", "matched_variants": 2 } } ``` ## Respons — tidak ditemukan (`200`) Bentuknya sama dan **kode statusnya juga `200`**. Kode status sengaja tidak dibedakan supaya tidak bisa dipakai sebagai penanda murah "nomor ini terdaftar atau tidak". ```json { "found": false, "customer": null, "recent_orders": [], "conversation": { "exists": false }, "welcome": { "message": null, "template": null }, "meta": { "request_id": "2b8d0e14", "matched_variants": 0 } } ``` Perhatikan tiga hal: 1. `conversation` **diratakan** jadi `{"exists": false}` saja. Nomor yang pernah berkirim chat tetapi belum pernah memesan tidak bisa dibedakan dari nomor yang sama sekali asing. 2. Blok `welcome` diratakan jadi dua `null`. **`reason` tidak pernah terisi** saat `found` bernilai `false` — kalau ia terisi di situ, ia jadi penanda keanggotaan yang persis dihindari. 3. Waktu jawabnya sama dengan jalur "ketemu". Jangan mencoba menyimpulkan apa pun dari lamanya respons. ## Tabel field respons | Field | Tipe | Keterangan | |---|---|---| | `found` | boolean | `true` kalau ada minimal satu pesanan dengan nomor itu. | | `customer` | object \| null | `null` saat `found` bernilai `false`. | | `customer.name` | string \| null | Nama dari pesanan **terbaru**. | | `customer.phone` | string | Nomor ternormalisasi, format 62, tanpa `+` dan tanpa spasi. | | `customer.city` | string \| null | Kota dari pesanan terbaru. | | `customer.province` | string \| null | Provinsi dari pesanan terbaru. | | `customer.orders_total` | integer | Jumlah pesanan, dihitung dari maksimal **100 pesanan terbaru**. | | `customer.orders_paid` | integer | Jumlah pesanan yang uangnya sudah masuk. | | `customer.lifetime_value` | integer | Rupiah dari pesanan yang uangnya sudah masuk. Bukan total keranjang. | | `customer.first_order_at` | string \| null | Pesanan terlama **di antara 100 pesanan terbaru**, bukan pesanan pertama seumur hidup. | | `customer.last_order_at` | string \| null | Waktu pesanan terbaru. | | `recent_orders` | array | Sebanyak-banyaknya nilai `orders` yang diminta, urut dari terbaru. Kosong saat `found` bernilai `false`. | | `recent_orders[].id` | integer | ID pesanan di AdForm. Unik per tenant, bukan lintas tenant. | | `recent_orders[].product` | string \| null | Nama produk. | | `recent_orders[].variant` | string \| null | Varian produk. | | `recent_orders[].quantity` | integer | Minimal 1. | | `recent_orders[].total` | integer | Total tagihan rupiah penuh, sudah termasuk ongkir. | | `recent_orders[].shipping_fee` | integer | Ongkir. `0` kalau tidak ada. | | `recent_orders[].payment_method` | string \| null | Contoh: `COD`, `QRIS`, `Transfer Bank`. | | `recent_orders[].status` | string \| null | Nilai status **mentah** AdForm. Satu kolom ini mencampur status bayar dan tahap kirim; tidak ada field status pengiriman terpisah. Perlakukan sebagai string bebas, jangan enum keras. | | `recent_orders[].courier` | string \| null | Nama kurir + layanannya digabung, mis. `"JNE REG"`. | | `recent_orders[].tracking_number` | string \| null | Nomor resi. `null` saat pesanan belum dikirim. | | `recent_orders[].created_at` | string \| null | Waktu pesanan dibuat. | | `conversation.exists` | boolean | Ada tidaknya riwayat percakapan CS di AdForm untuk nomor itu. Selalu `false` saat `found` bernilai `false`. | | `conversation.status` | string \| null | Hanya ada saat `exists` bernilai `true`. | | `conversation.last_at` | string \| null | Hanya ada saat `exists` bernilai `true`. | | `welcome.message` | string \| null | Pesan pembuka **siap kirim**. Lihat bagian berikutnya. | | `welcome.template` | string \| null | Penanda asal template, bentuknya `":"`, mis. `"settings:fu_welcome_cod"`. Lapisan menjawab "diedit di mana", kunci menjawab "yang mana". Sertakan saat melapor keluhan "isi pesannya salah". | | `welcome.reason` | string \| null | Alasan pesan tidak diberikan. **Terisi hanya saat `found` bernilai `true` dan `welcome.message` bernilai `null`.** Tidak pernah terisi saat `found` bernilai `false` — kalau terisi di situ, ia jadi penanda keanggotaan. Lihat tabel di bawah. | | `welcome.warning` | string \| null | Kebalikan dari `reason`: hanya ada saat pesannya **diserahkan**, sebagai catatan setelan penjual yang belum lengkap. Satu nilai hari ini: `rekening_belum_diatur`. Pesannya tetap harus dikirim apa adanya. | | `meta.request_id` | string | 8 karakter heksadesimal. Sertakan saat melapor masalah ke kami. | | `meta.matched_variants` | integer | Berapa varian format nomor yang cocok di basis data. | | `meta.degraded` | boolean | **Hanya muncul** kalau pencarian pesanan gagal di sisi kami. Kalau ada, jangan simpulkan pelanggan tidak terdaftar — coba lagi nanti. | ## Blok `welcome` — pesan pembuka siap kirim `welcome.message` bukan data, melainkan **kalimat jadi**. Kirim apa adanya sebagai balasan pertama ke pelanggan. Isinya dikarang penjual di dashboard AdForm (Pengaturan → Follow-Up), bukan ditulis di sistem Anda. Tanpa blok ini, konektor tidak punya kalimat apa pun untuk dikirim. Tiga hal yang mudah salah: 1. **`\n` adalah baris baru sungguhan** setelah JSON di-parse. Kirim sebagai baris baru, bukan sebagai dua karakter `\` dan `n`. 2. **Ada spasi tanpa-putus (U+00A0)** di antara `Rp` dan angkanya, hasil pemformatan rupiah. Jangan menormalkan spasi di dalam pesan — kalau diganti spasi biasa, teks yang diterima pelanggan berbeda dari yang dilihat penjual di dashboard. 3. **Jangan mem-parse nomor telepon dari dalam teks pesan.** Pakai `customer.phone`. ### Kapan `welcome.message` bernilai `null` Ini yang paling sering disalahartikan sebagai kerusakan. Ia bukan kerusakan — ia gerbang yang disengaja. | Kondisi | `found` | `welcome.reason` | Yang harus dilakukan konektor | |---|---|---|---| | Nomor tidak ditemukan | `false` | **kunci tidak ada** | Tidak ada pesan pembuka. Tangani sebagai pelanggan baru. | | Pesanan terbarunya lebih tua dari 48 jam | `true` | `terlalu_lama` | **Jangan** mengirim pesan pembuka. Pelanggan yang memesan berbulan-bulan lalu lalu menghubungi untuk urusan lain akan tersinggung menerima "terima kasih sudah order, silakan bayar". | | Pesanan terbarunya sudah lunas, atau dibatalkan | `true` | `sudah_bayar` | **Jangan** mengirim. Pesan pembuka bawaan menyuruh transfer. | | Pesan untuk pesanan itu sudah pernah diserahkan — lewat endpoint ini, lewat panggilan lain yang datang bersamaan, atau lewat kiriman webhook `order.created` yang benar-benar membawa kalimatnya | `true` | `sudah_dikirim` | **Jangan** mengirim. Sekali serah, tidak diulang. Ini yang mencegah pelanggan menerima sapaan yang sama dua kali — termasuk saat ia mengirim beberapa pesan beruntun dan Anda memanggil endpoint ini beberapa kali dalam sedetik. | | Perender pesan gagal di sisi kami | `true` | `null` | Data pelanggannya tetap benar dan tetap berguna. Balas dengan kalimat Anda sendiri. | `welcome.template` boleh terisi walau `welcome.message` bernilai `null` — itu berarti template-nya ketemu tetapi hasil rendernya kosong. **Nomor rekening penjual yang belum diisi tidak ada di tabel ini lagi.** Dulu keadaan itu membuat `welcome.message` bernilai `null` dengan `reason: "rekening_belum_diatur"`. Sekarang pesannya **tetap diserahkan**: di tempat nomor rekening ada kalimat *"(Nomor rekeningnya kami kirimkan menyusul lewat chat ini ya Kak.)"*, dan jawabannya membawa key baru `welcome.warning` bernilai `rekening_belum_diatur`. Alasannya: metode bayar bawaan di AdForm adalah transfer, jadi aturan lama ikut membungkam toko COD dan toko produk digital yang memang tidak butuh nomor rekening — pembelinya tidak disapa sama sekali, untuk setiap pesanan. **`warning` bukan `reason`.** Kirim pesannya apa adanya. Key itu untuk panel operator Anda, supaya setelan penjual yang belum lengkap kelihatan — bukan perintah menahan pesan. Nilainya bisa bertambah; nilai yang tidak dikenal diperlakukan sama: tampilkan, jangan bercabang. ```json { "found": true, "welcome": { "message": "Halo Kak Budi Santoso 😊\n…", "template": "default:fu_welcome_transfer", "warning": "rekening_belum_diatur" } } ``` Contoh respons yang pelanggannya ketemu tetapi pesan pembukanya digerbangi — pesanan terakhirnya tiga bulan lalu: ```json { "found": true, "customer": { "name": "Siti Nurhaliza", "phone": "6285712345678", "city": "Malang", "province": "Jawa Timur", "orders_total": 2, "orders_paid": 2, "lifetime_value": 510000, "first_order_at": "2026-02-14 11:07:20", "last_order_at": "2026-04-02 16:40:55" }, "recent_orders": [ { "id": 41880, "product": "Paket Kopi Gayo 500gr", "variant": "Halus", "quantity": 1, "total": 275000, "shipping_fee": 20000, "payment_method": "QRIS", "status": "completed", "courier": "SiCepat BEST", "tracking_number": "005412998877", "created_at": "2026-04-02 16:40:55" } ], "conversation": { "exists": true, "status": "open", "last_at": "2026-07-27 10:11:02" }, "welcome": { "message": null, "template": "default:fu_welcome_transfer", "reason": "terlalu_lama" }, "meta": { "request_id": "c41e77a0", "matched_variants": 1 } } ``` Yang benar dilakukan konektor di sini: **pakai data pelanggannya, jangan kirim pesan pembukanya.** Balas dengan kalimat CS biasa. ### Alamat pelanggan tidak ada di sini `welcome.message` yang dikembalikan endpoint ini **sudah dibersihkan dari alamat**. Template bawaan memuat penanda alamat; pada jalur ini penanda itu dirender jadi kosong, lalu **seluruh blok alamatnya dibuang** — bukan disisakan sebagai baris kosong. Bandingkan contoh di atas dengan contoh `order.created` di [Webhook Keluar](/docs/webhooks#order-created): pesanan yang sama, template yang sama, dan satu-satunya beda adalah blok alamat yang hilang di sini. Yang ikut hilang bersama baris alamatnya, pada template bawaan: judul blok `Alamat Pengiriman:` beserta nama dan nomor di bawahnya. Aturannya: baris yang isinya tinggal penanda alamat dibuang, judul blok di **paragraf yang sama** dengan penanda itu ikut dibuang, dan paragraf yang kehilangan baris judulnya dibuang seluruhnya. Baris di paragraf LAIN tidak pernah tersentuh, meski ia menyebut kata "alamat". > **Berubah 2026-07-28 — dua kalimat bawaan sekarang BERTAHAN.** Kalimat "Mohon pastikan alamat di atas sudah lengkap dan benar." dan "Alamat yang lengkap akan mempermudah kurir dan mempercepat pengiriman paket." ada di paragraf tersendiri, terpisah dari blok alamat oleh satu baris kosong. Dulu keduanya ikut dibuang karena menyebut kata "alamat". Aturan itu terlalu lebar: ia juga membuang kalimat **penjual** yang cuma kebetulan memakai kata yang sama — mis. "File-nya kami kirim ke alamat email yang Kakak isi di form." pada toko produk digital, yang hilangnya membuat pembeli tidak pernah diberi tahu ke mana filenya dikirim. Penyapu sekarang hanya bekerja di dalam paragraf yang memuat penandanya. **Akibatnya:** pesan di jalur ini bisa memuat kalimat "Mohon pastikan alamat di atas sudah lengkap dan benar." **tanpa ada alamat di atasnya**. Itu disengaja dan bukan tanda pesan terpotong — kirim apa adanya, jangan disaring. Sisa pesannya utuh: sapaan, produk, varian, ongkir, total, metode bayar, dan — pada cabang transfer — nomor rekening beserta perintah mengirim bukti pembayaran. Template buatan penjual diperlakukan dengan aturan yang sama. Konsekuensi yang perlu disadari konektor: pada jalur ini alamatnya sendiri **tidak ditampilkan**. Kalau penjual keberatan, sarankan ia mengubah template Welcome di dashboard AdForm — jangan menambahkan alamat dari sisi konektor. Alasannya sama dengan alasan blok `customer` hanya memuat kota dan provinsi: > **Yang sengaja tidak dikirim: alamat lengkap, kelurahan, kecamatan, kode pos, dan email.** Field itu tidak menjawab satu pun pertanyaan CS, sementara gabungan nama + nomor + alamat rumah adalah paket data yang berbahaya kalau API key bocor. Alamat lengkap **tetap dikirim** lewat [Webhook Keluar](/docs/webhooks), di field `data.welcome_message` dan `data.customer.address_full`. Bedanya di sana: tenant memilih sendiri alamat tujuannya per endpoint, tujuan itu harus lolos verifikasi kepemilikan, dan datanya **didorong** hanya untuk pesanan yang memang baru terjadi — bukan bisa **ditarik** per nomor sesuka penarik. Kalau produk Anda benar-benar butuh alamat lewat jalur tarik, hubungi kami. Kami akan menambah scope terpisah, bukan melonggarkan `customers:read`. ## Tabel kode galat | Kode | Body | Sebab | Tindakan konektor | |---|---|---|---| | `400` | `{"error":"phone wajib diisi"}` | Field `phone` tidak ada, bukan nilai skalar, atau hanya spasi. | Perbaiki permintaan. Jangan ulangi. | | `400` | `{"error":"phone tidak valid"}` | Setelah dibersihkan, nomornya tidak menghasilkan bentuk yang bisa dicocokkan. | Jangan ulangi. | | `401` | `{"error":"API key required (X-API-Key header)"}` | Header tidak dikirim atau kosong. | Jangan ulangi. Periksa apakah proxy Anda membuang header kustom. | | `401` | `{"error":"Malformed API key"}` | Bentuk bukan `adk_` + 48 heksadesimal huruf kecil. | Jangan ulangi. | | `401` | `{"error":"Invalid API key"}` | Bentuknya benar tetapi tidak dikenal. | Jangan ulangi. Pastikan Anda memakai key untuk lingkungan yang benar. | | `401` | `{"error":"API key revoked"}` | Key sudah dicabut. | Minta tenant membuat key baru. | | `401` | `{"error":"API key expired"}` | Key punya tanggal kedaluwarsa dan sudah lewat. | Minta tenant memperpanjang. | | `403` | `{"error":"Missing scope: customers:read"}` | Key valid tetapi tidak memuat scope ini. | Minta tenant membuat key dengan scope `customers:read`. | | `403` | `{"error":"Tenant inactive"}` | Akun tenant pemilik key sedang nonaktif. | Hentikan pemanggilan sampai tenant aktif lagi. | | `405` | `{"error":"Method not allowed"}` | Method bukan `POST`. | Perbaiki permintaan. | | `413` | `{"error":"Payload too large"}` | Body lebih dari 4 KB. | Perbaiki permintaan. | | `429` | `{"error":"Terlalu banyak permintaan. Coba lagi nanti."}` | Batas laju terlampaui. | Tunggu selama `Retry-After` detik, lalu ulangi dengan jeda yang membesar. | Hanya `429` yang layak diulang otomatis. Sisanya kesalahan konfigurasi atau kesalahan permintaan. **Tidak ada `500` untuk kegagalan basis data.** Kalau pencarian pesanan gagal di sisi kami, responsnya tetap `200` dengan `meta.degraded` bernilai `true`, dan blok `customer` bisa kosong walau pelanggannya sebenarnya ada. Rincian kegagalan internal tidak pernah dibocorkan ke pemanggil. ## Batas laju Dihitung **per API key**, bukan per alamat IP. Menyebar panggilan ke banyak server tidak menambah jatah. | Jendela | Batas | Melekat pada | |---|---|---| | 60 detik | 30 permintaan | API key | | 1 jam | 600 permintaan | API key | | 24 jam | 4.000 permintaan | API key | | 60 detik | 45 permintaan | API key (rem kedua, dihitung di basis data — menangkap ledakan paralel) | | 60 detik | 120 permintaan | Kombinasi **API key + alamat IP** (rem sekunder) | | 60 detik | 1.200 permintaan | Alamat IP saja, diperiksa **sebelum** key dibaca | Ketiga jendela per-key berlaku bersamaan; yang pertama tersentuh yang menolak. Rata-rata aman jangka panjang adalah 4.000 per hari, bukan 30 per menit dikalikan 1.440. Rem 120 per menit melekat pada **pasangan key + IP**, bukan pada IP saja: dua tenant yang memanggil dari satu IP keluar yang sama punya jatah 120 masing-masing dan tidak bisa saling mengunci. Yang dipakai bersama hanya baris terakhir — 1.200 per menit per IP, yang berjalan sebelum key dibaca dan hanya ada untuk melindungi server dari banjir. Angka itu setara 40 key yang semuanya sedang berada di puncak jatah sahnya. Saat terlampaui: ```http HTTP/1.1 429 Too Many Requests Retry-After: 3600 Content-Type: application/json; charset=utf-8 {"error":"Terlalu banyak permintaan. Coba lagi nanti."} ``` `Retry-After` selalu ikut dan berisi **jumlah detik**. Nilainya lebar jendela yang tersentuh (`60`, `3600`, atau `86400`), atau sisa detik blokir kalau Anda mencoba lagi saat masih diblokir. Rincian lengkap ada di [Batas Penggunaan](/docs/batas-penggunaan). ## Yang kami catat Setiap pencarian dicatat, **termasuk yang tidak menemukan apa pun**, dengan nomor tersamar (`6281****7890`). Nomor lengkap tidak pernah ditulis ke catatan mana pun. Pola pemanggilan yang menyerupai penyisiran daftar nomor — rasio gagal tinggi ditambah nomor yang nyaris selalu berbeda — ditandai untuk ditinjau manual. Kami tidak mencabut key secara otomatis (mematikan CS tenant di tengah jam ramai lebih merugikan), tetapi pemilik akun bisa mencabutnya sendiri. ## Praktik yang kami sarankan - **Panggil saat ada chat masuk, bukan secara terjadwal.** Endpoint ini dirancang untuk satu nomor yang baru saja menghubungi, bukan untuk menyegarkan basis data Anda. - **Jangan memakai `Retry-After` sebagai satu-satunya rem.** Batasi sendiri di sisi Anda, jauh di bawah 30 per menit. - **Timeout klien minimal 2 detik.** Ada lantai waktu jawab 150 milidetik yang disengaja. - **Simpan `meta.request_id`** di log Anda. Itu yang kami minta pertama kali saat Anda melapor. - **Periksa `meta.degraded` sebelum menyimpulkan "pelanggan tidak terdaftar".** - **Minta key ber-scope `customers:read` saja.** Jangan meminta tenant membuat key ber-scope `*`.