Ikhtisar
API analisis kulit wajah. Dirancang untuk dipanggil server-to-server dari backend aplikasimu, dilindungi API key. Kirim foto wajah → dapatkan skor kulit (6 metrik), daftar masalah, dan estimasi usia kulit.
Base URL: https://<domain-kamu> (mis. domain tempat API ini di-deploy).
Semua contoh di bawah memakai https://api.contoh.com sebagai placeholder.
Autentikasi
Endpoint produk dilindungi API key. Sertakan di header tiap request:
X-API-Key: sk_xxxxxxxx_....
Cara mendapatkan API key
- Daftar akun, lalu verifikasi email (kode 6 digit).
- Login untuk mendapatkan sesi.
- Buat API key — lewat halaman API Keys di web, atau
POST /keysdengan headerAuthorization: Bearer <access_token>.
Key ditampilkan sekali saat dibuat (disimpan ter-hash di server). Simpan dengan aman. Maksimal 10 key aktif per akun; key bisa dicabut kapan saja.
/analyze, /history, dan /quota
juga menerima sesi login (Authorization: Bearer) untuk pemakaian first-party dari
browser. Untuk integrasi server-to-server, pakai X-API-Key.POST/analyze
Analisis satu foto wajah.
Request
multipart/form-data dengan satu field:
| Field | Tipe | Keterangan |
|---|---|---|
file | file gambar | Foto wajah. Format: JPEG, PNG, WebP, GIF. Maks 10 MB. |
Response 200
{
"id": "3f2a9c1e-8b4d-4c2a-9e77-0a1b2c3d4e5f",
"image_key": "analyses/3f2a....png",
"image_url": "https://<r2>/analyses/...&X-Amz-Expires=3600&...",
"image_path": "/history/3f2a..../image",
"skin_age": 27,
"skin_age_range": { "min": 24, "max": 30 },
"schema_version": "2.3",
"methodology_version": "2.4",
"display_image_url": "https://<r2>/analyses/..._display.jpg&X-Amz-Expires=3600&...",
"display_image_path": "/history/3f2a..../image?display=true",
"annotated_image_url": "https://<r2>/analyses/..._annotated.jpg&X-Amz-Expires=3600&...",
"annotated_image_path": "/history/3f2a..../image?display=true&annotated=true",
"face_zones": {
"forehead": [ [ [0.31, 0.14], [0.52, 0.11], "..." ] ],
"nose": [ [ [0.46, 0.33], "..." ] ]
},
"face_landmarks": [ [0.42, 0.19], [0.55, 0.21], "..." ],
"image_quality": {
"acceptable": true, "score": 88, "lighting": "good", "sharpness": "good",
"face_box": [0.12, 0.18, 0.68, 0.62]
},
"scores": {
"hydration": 62, "oiliness": 48, "texture": 70,
"brightness": 65, "pores": 55, "wrinkles": 80
},
"problems": [
{ "type": "visible_blemishes", "severity": "moderate",
"areas": ["forehead", "chin"], "observation": "Noda terlihat pada area T." }
],
"summary": "Kulit kombinasi, hidrasi sedang, jerawat ringan di area T.",
"report_summary": {
"overview": "Profil visual kulit tampak cukup seimbang.",
"strengths": ["Garis halus tampak minimal."],
"focus_areas": ["Kilap permukaan pada area T."]
},
"metric_details": [
{ "metric": "hydration", "score": 62, "confidence": 0.82,
"observation": "Indikator kekeringan ringan terlihat pada pipi.", "areas": ["left_cheek", "right_cheek"] }
],
"priority_findings": [
{ "type": "visible_blemishes", "severity": "moderate", "confidence": 0.84,
"areas": ["forehead", "chin"], "observation": "Noda terlihat pada area T.",
"impact": "Menjadi perhatian visual utama pada foto ini." }
],
"zone_findings": [
{ "area": "forehead", "concerns": ["visible_blemishes"], "severity": "moderate",
"confidence": 0.84, "observation": "Beberapa noda terlihat." }
],
"routine": {
"morning": [
{ "step": 1, "category": "sunscreen", "active_ingredient": "none",
"frequency": "daily", "reason": "Melindungi dari paparan UV.",
"usage_notes": "Gunakan sebagai langkah terakhir pagi hari." }
],
"evening": [], "weekly": [],
"avoid": ["Hindari menambah beberapa bahan aktif baru sekaligus."],
"review_after_days": 28
},
"active_guardrails": [],
"safety_guidance": {
"patch_test_required": true,
"stop_conditions": ["Hentikan produk baru jika muncul iritasi berat atau menetap."],
"professional_help_conditions": ["Konsultasikan jika keluhan berat atau memburuk."]
},
"limitations": ["Hasil merupakan observasi visual, bukan diagnosis medis."],
"disclaimer": "Hasil ini merupakan observasi visual berbasis foto, bukan diagnosis medis atau pengukuran klinis.",
"created_at": "2026-07-15T09:30:00+00:00"
}
image_url adalah presigned URL, kedaluwarsa 1 jam — bukan URL publik permanen.
Jangan simpan di database dan jangan diteruskan ke pihak lain: siapa pun yang
memegangnya bisa membuka foto itu selama satu jam tanpa autentikasi.
Yang disimpan adalah image_path (dan display_image_path):
path permanen di API ini yang mengirimkan byte fotonya langsung. Butuh
X-API-Key tiap akses dan difilter per akun, jadi tak ada URL tanpa
auth yang beredar. Karena butuh header, path ini tak bisa dipasang langsung di
<img src> lintas domain — ambil lewat server kamu, lalu teruskan ke browser.
Arti skor
Semua metrik 0–100 dengan konvensi seragam makin tinggi = makin sehat:
| Metrik | Skor tinggi berarti |
|---|---|
hydration | Terhidrasi baik |
oiliness | Minyak terkontrol |
texture | Tekstur halus |
brightness | Cerah & merata |
pores | Pori minim |
wrinkles | Minim kerutan |
Jadi wrinkles: 80 = kulit minim kerut (bagus), bukan banyak kerut. Angka ini
heuristik model vision — cocok untuk MVP, bukan diagnosis medis.
Zona wajah
display_image_url adalah foto yang sudah diproses: latar diputihkan dan
dipotong ke wajah. face_zones dan face_landmarks memakai koordinat
fraksi 0–1 terhadap gambar itu, bukan terhadap image_url. Gambar aslinya
tetap utuh di image_url.
Tiap zona adalah daftar cincin: cincin pertama tepi luar, sisanya lubang
(mis. alis di dalam zona dahi). Gambar dengan aturan isian even-odd. Ketiga field ini
null bila wajah tak terpetakan atau untuk analisa yang dibuat sebelum fitur ini
ada — perlakukan sebagai opsional.
Tidak mau menggambar sendiri? Foto tampilan yang sudah diberi zona berwarna sesuai keparahan, titik mesh, dan kartu label per keluhan — versi server dari yang digambar web UI — tersedia dalam dua bentuk:
annotated_image_url: URL presigned, kedaluwarsa 1 jam, langsung bisa dipasang di<img src>tanpa header. Jangan disimpan di database.annotated_image_path(GET /history/{id}/image?display=true&annotated=true): permanen, butuhX-API-Keyseperti path gambar lain. Ini yang disimpan.
annotated_image_url null bila gambarnya belum pernah disimpan —
analisa lama, atau analisa tanpa zona/keluhan yang tidak ada bedanya dengan
display_image_url. Path-nya tetap jalan: digambar saat diminta.
GET/history, GET/history/{id}, GET/history/{id}/image & DELETE/history/{id}
Tarik kembali analisis yang pernah dibuat akun (via API key yang sama).
GET /history?limit=20&offset=0 — berpaginasi, terbaru dulu. limit
1–100, offset ≥ 0.
{
"total": 42,
"limit": 20,
"offset": 0,
"items": [ { /* objek analisis, bentuk identik /analyze */ } ]
}
total = jumlah keseluruhan (bukan jumlah di halaman ini). GET /history/{id}
mengembalikan satu objek analisis. Terisolasi per akun — id milik akun lain balas
404 not_found (sama dengan id yang tak ada, sengaja, agar tak membocorkan keberadaan id).
GET /history/{id}/image mengirimkan byte foto aslinya
(?display=true untuk foto tampilan; 404 bila analisis itu
tak punya). Ini alamat permanen foto — simpan path ini, bukan
image_url yang presigned dan kedaluwarsa 1 jam.
DELETE /history/{id} menghapus hasil analisis beserta foto asli dan foto
tampilannya dari Cloudflare R2 secara permanen. Response sukses: {"id":"...","deleted":true}. Jika penghapusan R2
gagal, API mengembalikan 502 storage_delete_failed dan record analisis dipertahankan.
GET/quota
Cek sisa kuota analisis akun.
{ "used": 12, "limit": 30, "remaining": 18,
"plan": "free", "pro_until": null,
"resets_at": "2026-08-01T00:00:00+00:00",
"pro_price": 49000, "pro_quota": 300 }
Kuota dihitung per bulan kalender. Saat habis, POST /analyze balas
429 quota_exceeded (dicek sebelum pemrosesan, jadi nol biaya).
Error
Semua error kustom berbentuk {"error": "<code>"}.
| HTTP | code | Situasi |
|---|---|---|
| 401 | invalid_api_key / unauthorized | API key/kredensial salah atau kosong |
| 422 | invalid_image | Bukan gambar / format tak didukung / byte rusak |
| 422 | no_face_detected | Tak ada wajah terdeteksi di foto |
| 422 | image_quality_failed | Foto perlu diulang; respons berisi alasan retake |
| 413 | file_too_large | File > 10 MB |
| 429 | quota_exceeded | Kuota bulanan habis |
| 429 | rate_limited | Terlalu banyak percobaan (endpoint auth); ada header Retry-After |
| 502 | analysis_failed / storage_failed | Provider AI atau upload penyimpanan gagal |
| 502 | storage_delete_failed | Foto R2 gagal dihapus; record analisis dipertahankan |
| 500 | internal_error | Error tak terduga |
Contoh kode
cURL
curl -X POST https://api.contoh.com/analyze \
-H "X-API-Key: sk_xxxxxxxx_...." \
-F "file=@/path/ke/wajah.jpg"
Python
import requests
resp = requests.post(
"https://api.contoh.com/analyze",
headers={"X-API-Key": "sk_xxxxxxxx_...."},
files={"file": open("wajah.jpg", "rb")},
timeout=60, # panggilan AI bisa beberapa detik
)
resp.raise_for_status()
hasil = resp.json()
print(hasil["skin_age"], hasil["scores"])
Node.js
import fs from "node:fs";
const form = new FormData();
form.append("file", new Blob([fs.readFileSync("wajah.jpg")]), "wajah.jpg");
const resp = await fetch("https://api.contoh.com/analyze", {
method: "POST",
headers: { "X-API-Key": process.env.SKIN_API_KEY },
body: form,
});
const hasil = await resp.json();
console.log(hasil.skin_age, hasil.scores);
Referensi OpenAPI
Unduh spesifikasi OpenAPI publik. Endpoint admin dan webhook sengaja tidak dimasukkan. Dokumentasi interaktif internal hanya tersedia melalui jalur administrasi yang dilindungi.
Keamanan
- API key adalah kredensial penuh: bisa membuat analisis dan membaca seluruh arsip analisis akunnya. Perlakukan seperti password.
- Jangan taruh key di kode sisi klien (browser/mobile) — hanya di backend tepercaya.
- Kalau dicurigai bocor, rotasi: cabut key lama lewat API Keys lalu buat yang baru.
- Foto wajah = data sensitif. Jaga kerahasiaan presigned URL (kedaluwarsa 1 jam).