Skin Analysis

Dokumentasi API

Integrasikan analisis kulit ke backend-mu. Kirim foto wajah, terima skor kulit, masalah, dan estimasi usia kulit — server-to-server, dilindungi API key.

curl -X POST https://api.contoh.com/analyze \ -H "X-API-Key: sk_...." \ -F "file=@wajah.jpg"

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

  1. Daftar akun, lalu verifikasi email (kode 6 digit).
  2. Login untuk mendapatkan sesi.
  3. Buat API key — lewat halaman API Keys di web, atau POST /keys dengan header Authorization: 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.

Catatan. /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:

FieldTipeKeterangan
filefile gambarFoto 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:

MetrikSkor tinggi berarti
hydrationTerhidrasi baik
oilinessMinyak terkontrol
textureTekstur halus
brightnessCerah & merata
poresPori minim
wrinklesMinim 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, butuh X-API-Key seperti 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).

Butuh kuota lebih besar? Upgrade ke Pro (300 analisis/bulan) lewat halaman Upgrade.

Error

Semua error kustom berbentuk {"error": "<code>"}.

HTTPcodeSituasi
401invalid_api_key / unauthorizedAPI key/kredensial salah atau kosong
422invalid_imageBukan gambar / format tak didukung / byte rusak
422no_face_detectedTak ada wajah terdeteksi di foto
422image_quality_failedFoto perlu diulang; respons berisi alasan retake
413file_too_largeFile > 10 MB
429quota_exceededKuota bulanan habis
429rate_limitedTerlalu banyak percobaan (endpoint auth); ada header Retry-After
502analysis_failed / storage_failedProvider AI atau upload penyimpanan gagal
502storage_delete_failedFoto R2 gagal dihapus; record analisis dipertahankan
500internal_errorError 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).