Halaman ini mengumpulkan aturan yang berlaku sama di semua endpoint AdForm yang bisa dipanggil dengan [API key](/docs/authentication). Kalau satu halaman referensi endpoint tidak menyebut sesuatu, aturan di sini yang berlaku. Ruang lingkupnya sempit dan kami sebut apa adanya: hari ini ada **dua berkas endpoint** yang memeriksa API key — `/api/products.php` (dua operasi, dipilih lewat query `action`) dan `/api/customer_lookup.php` (satu operasi, `POST` saja). Konvensi di bawah ditulis dari perilaku kode yang berjalan di produksi, bukan dari rencana. Dua pengecualian yang berlaku khusus untuk `/api/customer_lookup.php`, dan disebutkan lagi di halaman [Pencarian Pelanggan](/docs/api-customer-lookup): - Operasinya **tidak** dipilih lewat `action`. Method HTTP-nya yang menentukan, dan hanya `POST` yang diterima. - Batas lajunya dihitung **per API key**, bukan per alamat IP. ## Base URL ``` https://adform.id ``` Semua endpoint berada di bawah `/api/` dan diakses sebagai berkas PHP, misalnya `/api/products.php`. Tidak ada prefix versi (`/v1/`, `/v2/`). Operasi dipilih lewat query parameter, bukan lewat segmen path. Hanya HTTPS. Tidak ada host terpisah untuk sandbox atau staging yang dibuka untuk pihak ketiga — semua panggilan mengenai data produksi tenant Anda. ## Bentuk request ### Autentikasi Satu header: ```http X-API-Key: adk_0123456789abcdef0123456789abcdef0123456789abcdef ``` Tidak ada skema `Authorization: Bearer`, tidak ada tanda tangan request, tidak ada nonce. Detail format key ada di [Autentikasi](/docs/authentication). ### Parameter | Letak | Dipakai untuk | |---|---| | Query string | Memilih operasi (`action`) dan menunjuk objek (`id`). | | Body JSON | Semua data yang dikirim (misalnya `name`, `price`, `variants`). | Server membaca body sebagai **JSON mentah dari `php://input`**. Konsekuensinya: - Body harus JSON yang sah. `application/x-www-form-urlencoded` dan `multipart/form-data` **tidak dibaca** — field-nya akan hilang tanpa pesan galat, dan Anda hanya melihat error "wajib diisi" untuk field yang sebenarnya sudah Anda kirim. - Body yang gagal di-parse diperlakukan sebagai objek kosong, bukan sebagai galat tersendiri. - Kirim `Content-Type: application/json`. Contoh minimum yang benar: ```bash curl -sS -X POST \ -H "X-API-Key: adk_0123456789abcdef0123456789abcdef0123456789abcdef" \ -H "Content-Type: application/json" \ -d '{"name":"Kaos Polos Hitam - Promo Juli"}' \ "https://adform.id/api/products.php?action=duplicate&id=12" ``` ### Method HTTP Gunakan method yang tercantum di halaman referensi tiap operasi (`GET` untuk baca, `POST` untuk tulis). Perilakunya berbeda di dua endpoint, dan bedanya penting: - **`/api/products.php`: method tidak diperiksa.** Yang menentukan operasi adalah nilai `action`. Memanggil `action=duplicate` dengan `GET` tidak dibalas `405`; request itu tetap masuk lalu gagal di validasi body dengan `400 name required`. Jangan bergantung pada perilaku ini; ia bisa diperketat kapan saja. - **`/api/customer_lookup.php`: method diperiksa keras.** Selain `POST`, semuanya dibalas `405 {"error":"Method not allowed"}`. Ini bukan kelonggaran yang bisa berubah — alasannya menjaga nomor telepon supaya tidak pernah masuk query string, dan karena itu tidak akan dilonggarkan. ## Bentuk response ### Sukses Selalu JSON, selalu objek datar. Tidak ada envelope `data`, tidak ada `meta`, tidak ada pagination. ```json { "success": true, "id": 87, "slug": "kaos-polos-hitam-promo-juli" } ``` - Field `success` bernilai `true` ada di setiap response sukses operasi API key. - Status `200` untuk baca, `201` untuk pembuatan objek baru. - Field baru bisa ditambahkan sewaktu-waktu. Parser Anda harus mengabaikan field yang tidak dikenal, bukan menolaknya. ### Galat Selalu JSON dengan **satu** field: ```json { "error": "Missing scope: products:write" } ``` Yang perlu Anda ketahui sejak awal: - Tidak ada kode galat mesin (`code`), tidak ada daftar galat per-field, tidak ada `request_id`. Yang membedakan jenis galat adalah **status HTTP** ditambah teks `error`. - Teks `error` bercampur bahasa: sebagian Inggris (berasal dari lapisan autentikasi), sebagian Indonesia (berasal dari validasi). Ini kondisi nyata hari ini. - Jangan mencocokkan teks `error` secara persis untuk logika program. Cabangkan pada status HTTP; pakai teks hanya untuk log dan pesan ke manusia. - Field `success` **tidak** ikut dikirim pada response galat. Jangan menguji `data.success === false`; uji status HTTP. Daftar lengkap ada di [Kode Error](/docs/kode-error). ### Header response Yang dikirim di setiap response: | Header | Nilai | |---|---| | `Content-Type` | `application/json; charset=utf-8` | | `X-Content-Type-Options` | `nosniff` | | `Cache-Control` | `no-store, no-cache, must-revalidate, private` | | `Pragma` | `no-cache` | Tidak ada `ETag`, tidak ada `Last-Modified`, tidak ada dukungan permintaan bersyarat (`If-None-Match`). Response tidak boleh di-cache oleh perantara. Header `Retry-After` hanya muncul pada `429`. Tidak ada header sisa kuota — lihat [Batas Penggunaan](/docs/batas-penggunaan). ### Encoding Response di-encode dengan `JSON_UNESCAPED_UNICODE`, jadi karakter non-ASCII muncul apa adanya (`Ukuran M`, bukan `U...`). Baca sebagai UTF-8. ## CORS — API key tidak bisa dipanggil dari browser Ini pembatasan yang paling sering menyita waktu orang, jadi kami tulis eksplisit. Server mengizinkan header berikut pada request lintas-origin: ``` Access-Control-Allow-Headers: Content-Type, X-Admin-Password, Authorization ``` `X-API-Key` **tidak ada di daftar itu**. Artinya browser akan menggagalkan preflight `OPTIONS` untuk setiap `fetch()` lintas-origin yang membawa `X-API-Key`, dan request aslinya tidak pernah terkirim. Kesimpulannya: **panggil API key hanya dari server Anda.** Itu memang yang benar dari sisi keamanan — key tidak dibatasi per-domain maupun per-IP, jadi key yang masuk ke kode frontend sama saja dengan key yang dibagikan ke publik. Untuk konteks, origin yang diperlakukan sebagai tepercaya oleh lapisan CORS adalah domain AdForm sendiri (`adform.id`, `www.adform.id`, `staging.adform.id`, `form.adstack.id`, subdomain `.adform.id` dan `.adstack.id`) serta dua origin pengembangan lokal. Origin lain tetap dilayani, tetapi tanpa kredensial. Request `OPTIONS` dibalas `200` kosong. ## Tanggal dan zona waktu Keadaan hari ini, tanpa dibagus-baguskan: **dua operasi API key yang tersedia tidak mengembalikan satu pun field tanggal.** Jadi belum ada format tanggal response yang perlu Anda parse. Yang tetap perlu Anda pahami karena memengaruhi perilaku sistem: - Waktu disimpan dalam **UTC**, dan zona waktu default proses PHP adalah UTC. - Batas yang berbasis "hari" atau "bulan" dihitung menurut **WIB (Asia/Jakarta, UTC+7)** lalu dikonversi ke UTC. Misalnya kuota harian paket gratis di-reset pukul 00:00 WIB, yang setara 17:00 UTC hari sebelumnya. - Kedaluwarsa API key dievaluasi dengan jam server, bukan jam pemanggil. Kalau Anda mengirim tanggal (saat ini hanya relevan ketika meminta key dengan masa berlaku), pakai bentuk yang tidak ambigu seperti `2027-01-31 23:59:59` atau ISO 8601 lengkap dengan offset. ## Nomor telepon AdForm menyimpan nomor Indonesia dalam **bentuk kanonik berawalan `62`**, tanpa `+`, tanpa spasi, tanpa tanda hubung. Normalisasi yang dijalankan server: 1. Semua karakter selain angka dibuang (`+`, spasi, `-`, `.`, kurung). 2. Kalau hasilnya diawali `0`, angka `0` itu diganti `62`. 3. Kalau hasilnya belum diawali `62`, `62` ditambahkan di depan. | Masukan | Tersimpan | |---|---| | `0812-3456-7890` | `6281234567890` | | `+62 812 3456 7890` | `6281234567890` | | `812 3456 7890` | `6281234567890` | Dua catatan penting supaya tidak salah asumsi: - Normalisasi ini **tidak memvalidasi panjang**. Masukan `1` menghasilkan `621`. Kalau Anda punya sumber data sendiri, validasi panjang di sisi Anda sebelum mengirim. - Dua operasi API key yang ada sekarang tidak menerima maupun mengembalikan nomor telepon. Konvensi ini berlaku untuk nomor yang masuk lewat form order AdForm dan yang Anda lihat di dashboard. ## Uang dan mata uang - Mata uang selalu **rupiah (IDR)**. Tidak ada dukungan multi-mata-uang. - Semua nilai uang adalah **bilangan bulat rupiah**, dikirim dan diterima sebagai angka JSON: `150000` berarti Rp150.000. - Tidak ada desimal, tidak ada satuan sen, tidak ada pemisah ribuan, tidak ada simbol mata uang di dalam nilai. `"Rp150.000"` dan `150000.00` bukan bentuk yang dipakai. - Nilai harga yang Anda kirim di-cast ke integer, dan nilai negatif dijadikan `0`. `-5000` menjadi `0`, bukan galat. - Harga coret bernilai `0` berarti "tanpa harga coret", bukan "gratis". ## Idempotensi dan pengulangan Belum ada dukungan idempotency key. Operasi tulis **tidak** idempoten: memanggil duplikat produk dua kali menghasilkan dua produk. Karena itu: - Jangan mengulang otomatis operasi tulis yang gagal dengan `5xx` atau timeout tanpa memeriksa dulu apakah objeknya sudah terbentuk. - Yang aman diulang otomatis hanya `429` (setelah menunggu sesuai `Retry-After`) dan operasi baca. - Simpan `id` dari response sukses di sisi Anda sebagai penanda bahwa operasi sudah selesai. ## Isolasi tenant Satu API key terikat ke satu tenant. Konteks tenant sepenuhnya ditentukan oleh key — tidak ada parameter tenant yang bisa (atau perlu) Anda kirim pada jalur API key. ID objek bersifat lokal per tenant: `id=12` di tenant Anda tidak ada hubungannya dengan `id=12` di tenant lain. Objek yang bukan milik tenant pemilik key dibalas `404`, bukan `403`. ## Bagaimana perubahan kontrak diumumkan Karena API ini belum diversikan, kami memakai aturan berikut: 1. **Perubahan aditif bisa terjadi kapan saja tanpa pengumuman.** Field baru di response, operasi baru, parameter opsional baru. Integrasi Anda harus tahan terhadap field yang tidak dikenal. 2. **Perubahan yang merusak diumumkan lebih dulu** di halaman [Changelog](/docs/2026-07-rilis-awal) situs ini, sebelum berlaku. Termasuk: menghapus atau mengubah nama field, mengubah tipe data, mengubah status HTTP untuk kasus yang sama, memperketat validasi, atau menurunkan batas laju. 3. **Kalau ada di dokumentasi ini, artinya sudah berjalan di produksi hari ini.** Kami sengaja tidak menerbitkan kontrak yang belum final — halaman [Webhook](/docs/webhooks) contohnya: statusnya ditulis "belum ada" alih-alih dikarang. 4. **Belum ada kebijakan deprecation formal** dengan tenggat tetap. Kalau integrasi Anda kritis, beri tahu kami lewat [Pusat Bantuan](https://adform.id/pusat-bantuan.html) supaya Anda masuk daftar pemberitahuan langsung. Halaman ini sendiri mencantumkan tanggal `updated` di bagian atas. Kalau berubah, ada yang berubah.