Lompat ke konten utama
Dokumentasi AdForm
llms.txt Pusat Bantuan

Pencarian Pelanggan

Referensi endpoint POST /api/customer_lookup.php — cari pelanggan lewat satu nomor telepon, dapatkan riwayat pesanan singkat dan pesan pembuka siap kirim. Body, respons, kode galat, dan batas laju.

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

ItemNilai
URL produksihttps://adform.id/api/customer_lookup.php
URL staginghttps://staging.adform.id/api/customer_lookup.php
MethodPOST saja. Method lain dibalas 405.
AutentikasiHeader X-API-Key: adk_…
Scopecustomers:read
Content-Type permintaanapplication/json
Ukuran body maksimum4 KB
Batas laju30 / menit, 600 / jam, 4.000 / hari — per API key
Waktu jawab minimum150 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. 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

FieldTipeWajibKeterangan
phonestringwajibNomor telepon pelanggan. Format bebas: 08…, 62…, +62…, boleh berisi spasi atau tanda hubung. Kami normalkan sendiri dan mencocokkan semua variannya.
ordersintegeropsionalBerapa 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

Terminal
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:

Terminal
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

FieldTipeKeterangan
foundbooleantrue kalau ada minimal satu pesanan dengan nomor itu.
customerobject | nullnull saat found bernilai false.
customer.namestring | nullNama dari pesanan terbaru.
customer.phonestringNomor ternormalisasi, format 62, tanpa + dan tanpa spasi.
customer.citystring | nullKota dari pesanan terbaru.
customer.provincestring | nullProvinsi dari pesanan terbaru.
customer.orders_totalintegerJumlah pesanan, dihitung dari maksimal 100 pesanan terbaru.
customer.orders_paidintegerJumlah pesanan yang uangnya sudah masuk.
customer.lifetime_valueintegerRupiah dari pesanan yang uangnya sudah masuk. Bukan total keranjang.
customer.first_order_atstring | nullPesanan terlama di antara 100 pesanan terbaru, bukan pesanan pertama seumur hidup.
customer.last_order_atstring | nullWaktu pesanan terbaru.
recent_ordersarraySebanyak-banyaknya nilai orders yang diminta, urut dari terbaru. Kosong saat found bernilai false.
recent_orders[].idintegerID pesanan di AdForm. Unik per tenant, bukan lintas tenant.
recent_orders[].productstring | nullNama produk.
recent_orders[].variantstring | nullVarian produk.
recent_orders[].quantityintegerMinimal 1.
recent_orders[].totalintegerTotal tagihan rupiah penuh, sudah termasuk ongkir.
recent_orders[].shipping_feeintegerOngkir. 0 kalau tidak ada.
recent_orders[].payment_methodstring | nullContoh: COD, QRIS, Transfer Bank.
recent_orders[].statusstring | nullNilai 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[].courierstring | nullNama kurir + layanannya digabung, mis. "JNE REG".
recent_orders[].tracking_numberstring | nullNomor resi. null saat pesanan belum dikirim.
recent_orders[].created_atstring | nullWaktu pesanan dibuat.
conversation.existsbooleanAda tidaknya riwayat percakapan CS di AdForm untuk nomor itu. Selalu false saat found bernilai false.
conversation.statusstring | nullHanya ada saat exists bernilai true.
conversation.last_atstring | nullHanya ada saat exists bernilai true.
welcome.messagestring | nullPesan pembuka siap kirim. Lihat bagian berikutnya.
welcome.templatestring | nullPenanda asal template, bentuknya "<lapisan>:<kunci>", mis. "settings:fu_welcome_cod". Lapisan menjawab "diedit di mana", kunci menjawab "yang mana". Sertakan saat melapor keluhan "isi pesannya salah".
welcome.reasonstring | nullAlasan 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.warningstring | nullKebalikan 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_idstring8 karakter heksadesimal. Sertakan saat melapor masalah ke kami.
meta.matched_variantsintegerBerapa varian format nomor yang cocok di basis data.
meta.degradedbooleanHanya 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.

Kondisifoundwelcome.reasonYang harus dilakukan konektor
Nomor tidak ditemukanfalsekunci tidak adaTidak ada pesan pembuka. Tangani sebagai pelanggan baru.
Pesanan terbarunya lebih tua dari 48 jamtrueterlalu_lamaJangan 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 dibatalkantruesudah_bayarJangan 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 kalimatnyatruesudah_dikirimJangan 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 kamitruenullData 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: 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, 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

KodeBodySebabTindakan 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.

JendelaBatasMelekat pada
60 detik30 permintaanAPI key
1 jam600 permintaanAPI key
24 jam4.000 permintaanAPI key
60 detik45 permintaanAPI key (rem kedua, dihitung di basis data — menangkap ledakan paralel)
60 detik120 permintaanKombinasi API key + alamat IP (rem sekunder)
60 detik1.200 permintaanAlamat 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.

#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 *.

Ketik minimal dua huruf.

↑↓ pindah Enter buka Esc tutup