Semua endpoint API AdForm yang bisa dipanggil pihak ketiga memakai **API key** yang dikirim di header `X-API-Key`. Tidak ada OAuth, tidak ada token yang perlu di-refresh, tidak ada login berjangka. Satu key terikat ke satu tenant. Konteks tenant ditentukan sepenuhnya oleh key — Anda tidak perlu (dan tidak bisa) mengirim ID tenant sendiri. ## Format key ``` adk_0123456789abcdef0123456789abcdef0123456789abcdef ``` - Prefix tetap `adk_`. - Diikuti **48 karakter heksadesimal huruf kecil** (`a`–`f`, `0`–`9`). - Total panjang 52 karakter. - Server memvalidasi bentuk ini sebelum menyentuh database. Key yang bentuknya tidak cocok langsung ditolak `401 Malformed API key`. Key disimpan di sisi AdForm hanya sebagai hash SHA-256. **Nilai aslinya ditampilkan satu kali saat dibuat dan tidak bisa dilihat lagi.** Yang tersisa di dashboard cuma 12 karakter pertama (`adk_0123456`) sebagai penanda. Kalau key hilang, cabut key lama dan minta yang baru. ## Mendapatkan API key Saat ini **belum ada halaman self-service di dashboard** untuk membuat key sendiri — itu masih dalam pengerjaan. Untuk sekarang, key diterbitkan atas permintaan: 1. Login ke dashboard AdForm dengan akun pemilik (owner) tenant Anda. 2. Ajukan permintaan lewat [Pusat Bantuan](https://adform.id/pusat-bantuan.html), sebutkan: nama tenant, nama integrasi (mis. "Sinkronisasi katalog CRM X"), scope yang dibutuhkan, dan apakah key perlu tanggal kedaluwarsa. 3. Anda menerima key satu kali. Simpan di secret manager atau environment variable server Anda, bukan di kode. Key bisa diterbitkan dengan atau tanpa tanggal kedaluwarsa. Tanpa tanggal kedaluwarsa, key berlaku sampai dicabut. Untuk mencabut key (misalnya bocor, atau integrasi dihentikan), ajukan lewat jalur yang sama. Pencabutan berlaku seketika pada request berikutnya. ## Mengirim key Header, bukan query string: ```http GET /api/products.php?action=embed&id=12 HTTP/1.1 Host: adform.id X-API-Key: adk_0123456789abcdef0123456789abcdef0123456789abcdef ``` Jangan pernah menaruh key di URL, di kode frontend, di aplikasi mobile, atau di repositori publik. Key tidak dibatasi per-domain maupun per-IP: siapa pun yang memegangnya bisa memakainya dari mana saja atas nama tenant Anda. ## Scope Scope menentukan operasi apa yang boleh dipanggil key. Yang **benar-benar aktif dan diperiksa server** hari ini ada tiga: | Scope | Membuka | Catatan | |---|---|---| | `products:read` | `GET /api/products.php?action=embed` | Scope default kalau tidak diminta scope lain. | | `products:write` | `POST /api/products.php?action=duplicate` | Membuat produk baru di tenant. Berikan hanya kalau memang perlu. | | `*` | Semua operasi API key | Wildcard, melewati semua pengecekan scope. Hindari kecuali benar-benar butuh. | Aturan pencocokan scope adalah **kecocokan string persis**. Tidak ada pola glob: `products:*` bukan scope yang valid dan tidak akan cocok dengan apa pun. > **Scope lain bersifat cadangan (reserved), belum aktif.** Sistem penerbitan key masih menerima beberapa nama scope lain yang direncanakan untuk endpoint yang belum ada (mis. area pesanan atau tim). Tidak ada endpoint yang memeriksanya, jadi memilikinya tidak memberi akses apa pun. Jangan bangun logika di atas scope selain tiga di tabel atas. Satu key bisa memegang beberapa scope sekaligus, misalnya `products:read` + `products:write`. ## Response error Semua error autentikasi berupa JSON dengan satu field `error`. | Status | Body | Penyebab | |---|---|---| | `401` | `{"error":"API key required (X-API-Key header)"}` | Header `X-API-Key` tidak dikirim atau kosong. | | `401` | `{"error":"Malformed API key"}` | Bentuk key tidak cocok `adk_` + 48 hex huruf kecil. | | `401` | `{"error":"Invalid API key"}` | Bentuknya benar tapi tidak dikenal (salah ketik, key sudah dihapus). | | `401` | `{"error":"API key revoked"}` | Key sudah dinonaktifkan. | | `401` | `{"error":"API key expired"}` | Key punya tanggal kedaluwarsa dan sudah lewat. | | `403` | `{"error":"Tenant inactive"}` | Akun tenant pemilik key sedang nonaktif. | | `403` | `{"error":"Missing scope: products:write"}` | Key valid, tapi tidak punya scope yang dibutuhkan endpoint. | Contoh scope ditolak: ```json { "error": "Missing scope: products:write" } ``` Pesan hanya menyebut **satu** scope pertama yang kurang, bukan daftar lengkap. Kalau endpoint butuh beberapa scope dan key Anda kurang dua, perbaiki satu lalu coba lagi. Perlakukan `401` dan `403` sebagai **kesalahan konfigurasi, bukan error sementara** — jangan retry otomatis. Yang layak di-retry cuma `429` dan `5xx`. ## Rate limit Endpoint `/api/products.php` dibatasi **60 request per 60 detik per alamat IP**. Beberapa hal yang perlu dipahami supaya tidak salah rancang: - Hitungannya **per IP, bukan per key**. Dua key berbeda dari satu server berbagi jatah yang sama. - Jatah itu juga dipakai bersama trafik lain ke endpoint yang sama dari IP tersebut. - Kalau ambang terlampaui, IP diblokir sampai jendela berikutnya. Response: ```http HTTP/1.1 429 Too Many Requests Retry-After: 60 Content-Type: application/json {"error":"Terlalu banyak permintaan. Coba lagi nanti."} ``` - Hormati header `Retry-After`. Pola yang aman: batasi sendiri ke sekitar 1 request per detik, dan pakai exponential backoff saat kena `429`. Belum ada kuota harian atau kuota per-key. Kalau integrasi Anda butuh throughput lebih tinggi, hubungi kami dulu sebelum menaikkan beban. ## Contoh ### curl ```bash curl -sS \ -H "X-API-Key: adk_0123456789abcdef0123456789abcdef0123456789abcdef" \ "https://adform.id/api/products.php?action=embed&id=12" ``` Dengan body JSON (duplikat produk): ```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" ``` ### PHP ```php true, CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => [ 'X-API-Key: ' . $apiKey, 'Accept: application/json', ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($body, true); if ($status !== 200) { // $data['error'] berisi pesan dari AdForm, mis. "Missing scope: products:read" throw new RuntimeException("AdForm API {$status}: " . ($data['error'] ?? $body)); } echo $data['embed']['form_url']; // https://adform.id/t/12/kaos-polos-hitam ``` ### JavaScript (Node 18+) ```js const API_KEY = process.env.ADFORM_API_KEY; // jangan pernah dipakai di browser async function getEmbed(productId) { const res = await fetch( `https://adform.id/api/products.php?action=embed&id=${productId}`, { headers: { 'X-API-Key': API_KEY, Accept: 'application/json' } } ); const data = await res.json(); if (!res.ok) { // 401/403 = perbaiki konfigurasi. 429 = tunggu Retry-After lalu ulangi. const err = new Error(`AdForm API ${res.status}: ${data.error ?? 'unknown error'}`); err.status = res.status; err.retryAfter = Number(res.headers.get('Retry-After')) || null; throw err; } return data; } const produk = await getEmbed(12); console.log(produk.embed.iframe_with_pixel); ``` ## Praktik yang kami sarankan - Satu key per integrasi, diberi nama jelas. Kalau satu bocor, cabut yang itu saja. - Minta scope seminimal mungkin. Butuh baca saja? `products:read` cukup. - Simpan key di environment variable atau secret manager, bukan di file konfigurasi yang ikut ter-commit. - Log status dan pesan `error` dari AdForm saat gagal — itu yang kami butuhkan kalau Anda melapor. - Rotasi key secara berkala, dan segera setelah ada pergantian orang di tim yang memegangnya.