{"openapi":"3.0.3","info":{"title":"Face Recognition API","description":"Multi-app face verification API for register/login.\n\n## Authentication\nAll `/api/v1/faces/*` endpoints require HMAC headers:\n\n- `X-App-Id`\n- `X-Api-Key`\n- `X-Timestamp` (unix seconds, ±5 min)\n- `X-Nonce` (unique 16–64 chars)\n- `X-Signature` = hex(hmac_sha256(api_secret, canonical))\n\n**Canonical string:**\n```\nMETHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(raw_body)\n```\n\nPath example: `/api/v1/faces/verify`\n\nKeep `API_SECRET` on your **backend only**. Never embed it in browsers.\n\n## Matching\nClients send a normalized face feature vector (128 numbers). The API compares it securely against stored encrypted templates and returns a match decision with confidence.\n\n**Runtime base URL:** `https://api-face.alfanrizkiw.my.id`\n\nTry the interactive demo: `https://api-face.alfanrizkiw.my.id/demo`","version":"1.0.0","contact":{"name":"Face API Support"}},"servers":[{"url":"https://api-face.alfanrizkiw.my.id","description":"Configured baseURL (production)"}],"tags":[{"name":"Health","description":"Service status"},{"name":"Liveness","description":"Active liveness challenge / token"},{"name":"Faces","description":"Enroll, verify, compare, manage templates"}],"paths":{"/api/v1/health":{"get":{"tags":["Health"],"summary":"Health check","operationId":"healthCheck","security":[],"responses":{"200":{"description":"Service healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"Database unavailable"}}}},"/api/v1/liveness/challenge":{"post":{"tags":["Liveness"],"summary":"Create liveness challenge","operationId":"livenessChallenge","security":[{"HmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/XAppId"},{"$ref":"#/components/parameters/XApiKey"},{"$ref":"#/components/parameters/XTimestamp"},{"$ref":"#/components/parameters/XNonce"},{"$ref":"#/components/parameters/XSignature"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["blink","turn_left","turn_right","mouth_open"]},"purpose":{"type":"string","enum":["register","verify","general"]},"user_ref":{"type":"string"}}}}}},"responses":{"201":{"description":"Challenge issued"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/liveness/verify":{"post":{"tags":["Liveness"],"summary":"Verify liveness evidence and get token","operationId":"livenessVerify","security":[{"HmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/XAppId"},{"$ref":"#/components/parameters/XApiKey"},{"$ref":"#/components/parameters/XTimestamp"},{"$ref":"#/components/parameters/XNonce"},{"$ref":"#/components/parameters/XSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["challenge_id","evidence"],"properties":{"challenge_id":{"type":"string","format":"uuid"},"evidence":{"type":"object","required":["samples"],"properties":{"samples":{"type":"array","items":{"type":"object","required":["t","ear_left","ear_right","yaw","pitch","mouth"],"properties":{"t":{"type":"number","description":"ms from start"},"ear_left":{"type":"number"},"ear_right":{"type":"number"},"yaw":{"type":"number"},"pitch":{"type":"number"},"mouth":{"type":"number"}}}}}}}}}}},"responses":{"200":{"description":"Returns liveness_token"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/faces/register":{"post":{"tags":["Faces"],"summary":"Register / enroll face","description":"Store encrypted face template for `user_ref`. One template is enough; additional templates (append) are optional for better accuracy.","operationId":"registerFace","security":[{"HmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/XAppId"},{"$ref":"#/components/parameters/XApiKey"},{"$ref":"#/components/parameters/XTimestamp"},{"$ref":"#/components/parameters/XNonce"},{"$ref":"#/components/parameters/XSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterFaceRequest"},"example":{"user_ref":"user-1001","embedding":[0.01,-0.02],"replace":false,"quality_score":0.82,"model":"default"}}}},"responses":{"201":{"description":"Registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/faces/verify":{"post":{"tags":["Faces"],"summary":"Verify face (1:1 login)","description":"Compare probe embedding against enrolled templates for `user_ref`.","operationId":"verifyFace","security":[{"HmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/XAppId"},{"$ref":"#/components/parameters/XApiKey"},{"$ref":"#/components/parameters/XTimestamp"},{"$ref":"#/components/parameters/XNonce"},{"$ref":"#/components/parameters/XSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyFaceRequest"}}}},"responses":{"200":{"description":"Verification result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyFaceResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/faces/compare":{"post":{"tags":["Faces"],"summary":"Compare two embeddings","description":"Compare without persistence (preview / QA).","operationId":"compareFaces","security":[{"HmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/XAppId"},{"$ref":"#/components/parameters/XApiKey"},{"$ref":"#/components/parameters/XTimestamp"},{"$ref":"#/components/parameters/XNonce"},{"$ref":"#/components/parameters/XSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompareFaceRequest"}}}},"responses":{"200":{"description":"Compare result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"}}}},"/api/v1/faces/{user_ref}":{"get":{"tags":["Faces"],"summary":"Enrollment status","operationId":"faceStatus","security":[{"HmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/XAppId"},{"$ref":"#/components/parameters/XApiKey"},{"$ref":"#/components/parameters/XTimestamp"},{"$ref":"#/components/parameters/XNonce"},{"$ref":"#/components/parameters/XSignature"},{"name":"user_ref","in":"path","required":true,"schema":{"type":"string","maxLength":128}}],"responses":{"200":{"description":"Status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"delete":{"tags":["Faces"],"summary":"Delete face templates","operationId":"deleteFace","security":[{"HmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/XAppId"},{"$ref":"#/components/parameters/XApiKey"},{"$ref":"#/components/parameters/XTimestamp"},{"$ref":"#/components/parameters/XNonce"},{"$ref":"#/components/parameters/XSignature"},{"name":"user_ref","in":"path","required":true,"schema":{"type":"string","maxLength":128}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccess"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}},"components":{"securitySchemes":{"HmacAuth":{"type":"apiKey","in":"header","name":"X-Signature","description":"HMAC-SHA256 signature. Also send X-App-Id, X-Api-Key, X-Timestamp, X-Nonce."}},"parameters":{"XAppId":{"name":"X-App-Id","in":"header","required":true,"schema":{"type":"string","format":"uuid"}},"XApiKey":{"name":"X-Api-Key","in":"header","required":true,"schema":{"type":"string"}},"XTimestamp":{"name":"X-Timestamp","in":"header","required":true,"schema":{"type":"string","example":"1710000000"}},"XNonce":{"name":"X-Nonce","in":"header","required":true,"schema":{"type":"string","minLength":16,"maxLength":64}},"XSignature":{"name":"X-Signature","in":"header","required":true,"schema":{"type":"string"}}},"schemas":{"Embedding":{"type":"array","minItems":128,"maxItems":128,"items":{"type":"number","format":"float"},"description":"Face feature vector (128 numbers, L2-normalized by the API)"},"RegisterFaceRequest":{"type":"object","required":["user_ref","embedding"],"properties":{"user_ref":{"type":"string","maxLength":128},"embedding":{"$ref":"#/components/schemas/Embedding"},"liveness_token":{"type":"string","description":"Required when require_liveness=1"},"replace":{"type":"boolean","default":false,"description":"false = append another template (min 1 is enough); true = replace all existing templates"},"replace_oldest":{"type":"boolean","default":false,"description":"When at max templates and not replacing all, soft-delete the oldest then append"},"quality_score":{"type":"number","minimum":0,"maximum":1},"model":{"type":"string","maxLength":64},"meta":{"type":"object","additionalProperties":true}}},"VerifyFaceRequest":{"type":"object","required":["user_ref","embedding"],"properties":{"user_ref":{"type":"string"},"embedding":{"$ref":"#/components/schemas/Embedding"},"liveness_token":{"type":"string","description":"Required when require_liveness=1"},"quality_score":{"type":"number","minimum":0,"maximum":1}}},"CompareFaceRequest":{"type":"object","required":["embedding_a","embedding_b"],"properties":{"embedding_a":{"$ref":"#/components/schemas/Embedding"},"embedding_b":{"$ref":"#/components/schemas/Embedding"}}},"ApiSuccess":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object"},"request_id":{"type":"string"}}},"VerifyFaceResponse":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"verified":{"type":"boolean"},"reason":{"type":"string","enum":["matched","distance_above_threshold","not_enrolled"]},"distance":{"type":"number","nullable":true},"threshold":{"type":"number"},"confidence":{"type":"number","nullable":true},"cosine":{"type":"number","nullable":true},"template_hits":{"type":"integer"},"template_count":{"type":"integer"},"user_ref":{"type":"string"}}},"request_id":{"type":"string"}}},"HealthResponse":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"service":{"type":"string"},"version":{"type":"string"},"status":{"type":"string","enum":["ok","degraded"]},"database":{"type":"string"},"timestamp":{"type":"string"}}}}},"ApiError":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}},"request_id":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Missing/invalid HMAC auth","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"ValidationError":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"Conflict":{"description":"Conflict (max templates / duplicate)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"RateLimited":{"description":"Too many requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}}}}