ilikeyacut API Contract
Overview
This document defines the API contract between the iOS frontend and AWS serverless backend for the ilikeyacut app. All endpoints use HTTPS with JSON payloads. The backend is hosted on AWS API Gateway with Lambda functions.
Base URL
Production: https://api.ilikeyacut.app
Development: https://dev-api.ilikeyacut.app
Authentication
- Guest Mode: Device-based tracking for 1 lifetime credit
- OAuth Login: Google or X authentication for 4 lifetime credits
- Premium Users: Subscription or bundle purchasers with extended credits
- Admin Access: Development-only unlimited credits (via backend script)
Common Headers
Content-Type: application/json
X-API-Version: 1.0
Authorization: Bearer <token> (optional)
Error Response Format
{
"error": {
"code": "ERROR_CODE",
"message": "Human readable error message",
"details": {} // Optional additional context
}
}
API Endpoints
1. POST /api/gemini-edit
Description: Proxy endpoint for Gemini 2.5 Flash Image API to process hairstyle transformations with credit validation and deduction.
Credit Requirements:
- Single-image generation: 1 credit
- Multi-angle generation (4 images): 4 credits
Request Body:
{
"contents": [
{
"inlineData": {
"mimeType": "image/jpeg",
"data": "base64_encoded_user_selfie"
}
},
{
"inlineData": {
"mimeType": "image/jpeg",
"data": "base64_encoded_reference_image" // Optional
}
},
{
"text": "Hairstyle transformation prompt with face preservation instructions"
}
],
"options": {
"model": "gemini-2.5-flash-image-preview",
"variations": 1, // 1-4 variations
"temperature": 0.8, // 0.0-1.0
"maxOutputTokens": 32768
}
}
Response (200 OK):
{
"generatedImages": [
"base64_encoded_result_image_1",
"base64_encoded_result_image_2" // If variations > 1
],
"model": "gemini-2.5-flash-image-preview",
"usage": {
"inputTokens": 2048,
"outputTokens": 1290, // Per image
"cost": 0.039 // Per image in USD
},
"processingTime": 1850, // Milliseconds
"creditsUsed": 1,
"creditsRemaining": 167
}
Response Headers:
X-Credits-Limit: 168
X-Credits-Remaining: 165
X-Credits-Reset: 1704067200 // Unix timestamp (subscribers only)
Error Codes:
400 BAD_REQUEST: Invalid request format or unsupported image type402 PAYMENT_REQUIRED: Insufficient credits (see error format below)413 PAYLOAD_TOO_LARGE: Image exceeds 20MB limit429 RATE_LIMIT_EXCEEDED: Too many requests (global rate limit)500 INTERNAL_ERROR: Gemini API or Lambda error503 SERVICE_UNAVAILABLE: Service temporarily unavailable
402 Insufficient Credits Response:
{
"error": {
"code": "insufficient_credits",
"message": "You need 4 credits for multi-angle generation. You have 2 credits."
},
"credits": {
"required": 4,
"available": 2,
"userType": "free" // guest, free, premium, or admin
},
"upgrade_options": {
"subscription": {
"credits_per_month": 168,
"price": "$9.99/month"
},
"bundles": [
{ "credits": 8, "price": "$0.99" },
{ "credits": 48, "price": "$4.99" }
]
}
}
2. GET /api/hairstyles
Description: Fetch hairstyle template library from DynamoDB with S3-hosted reference images. Templates are cached locally with 1-hour TTL.
Query Parameters:
limit(number): Max templates to return (default: 50, max: 100)category(string): Filter by category (e.g., "short", "long", "trendy")offset(number): Pagination offset
Response (200 OK):
{
"templates": [
{
"id": "classic-bob-001",
"name": "Classic Bob Cut",
"category": "short",
"prompt": "Transform to a classic chin-length bob cut with subtle layering while preserving facial features completely",
"thumbnailUrl": "https://s3.amazonaws.com/bucket/signed-url...", // 1-hour expiry
"referenceImageUrl": "https://s3.amazonaws.com/bucket/signed-url...", // Optional, 1-hour expiry
"popularity": 85, // Usage score for sorting
"tags": ["professional", "classic", "short"]
}
],
"totalCount": 75,
"nextOffset": 50
}
3. GET /api/user/credits
Description: Fetch user's current credit balance and subscription status.
Response (200 OK):
{
"userId": "google_12345",
"credits": {
"available": 42,
"monthlyLimit": 168, // Only for subscribers
"resetDate": "2025-02-01T00:00:00Z" // Only for subscribers
},
"subscription": {
"tier": "free", // guest, free, premium, or admin
"status": "active", // Only for premium
"expiresAt": "2025-02-01T00:00:00Z" // Only for premium
},
"bundles": {
"purchased": 48, // Total bundle credits purchased
"remaining": 12 // Bundle credits available
}
}
4. POST /api/purchase
Description: Verify in-app purchase and allocate credits.
Request Body:
{
"receipt": "base64_encoded_receipt",
"productId": "com.ilikeyacut.subscription.monthly" | "com.ilikeyacut.bundle.small" | "com.ilikeyacut.bundle.large",
"platform": "ios" | "android"
}
Response (200 OK):
{
"success": true,
"creditsAdded": 168, // Or 8/48 for bundles
"newBalance": 180,
"purchaseType": "subscription" | "bundle",
"expiresAt": "2025-02-01T00:00:00Z" // Only for subscriptions
}
5. GET /api/usage-history
Description: Retrieve user's generation history with credit costs.
Query Parameters:
limit(number): Max items to return (default: 50)offset(number): Pagination offset
Response (200 OK):
{
"history": [
{
"id": "gen_12345",
"timestamp": "2025-01-15T10:30:00Z",
"prompt": "Classic bob cut with highlights",
"creditCost": 4,
"type": "multi-angle" | "single",
"thumbnailUrl": "https://s3.amazonaws.com/bucket/signed-url...",
"balanceAfter": 164
}
],
"totalCreditsUsed": 127,
"currentPeriod": "2025-01"
}
6. POST /api/feedback
Description: Submit user feedback on generated results.
Request Body:
{
"sessionId": "uuid-v4",
"rating": 4,
"prompt": "Original prompt used for generation",
"feedback": "Optional text feedback from user"
}
Response (200 OK):
{
"success": true,
"feedbackId": "feedback-uuid"
}
7. POST /api/auth/login
Description: Authenticate user with OAuth provider. Allocates 4 lifetime credits for new users.
Request Body:
{
"provider": "google" | "x",
"authCode": "oauth_authorization_code"
}
Response (200 OK):
{
"accessToken": "jwt-access-token",
"refreshToken": "refresh-token",
"expiresIn": 3600,
"user": {
"id": "google_12345" | "x_12345",
"email": "user@example.com",
"name": "User Name",
"tier": "free", // guest, free, premium, or admin
"credits": {
"available": 4, // Or current balance
"isNewUser": true // If first-time login
}
}
}
8. POST /api/auth/guest
Description: Create guest session with 1 lifetime credit. Tracked by device ID.
Request Body:
{
"deviceId": "device-identifier-for-vendor", // iOS: identifierForVendor
"platform": "ios" | "android"
}
Response (200 OK):
{
"guestToken": "guest-jwt-token",
"expiresIn": 86400,
"credits": {
"available": 1, // Or 0 if already used
"lifetime": 1,
"used": 0
},
"limitations": {
"creditsOnly": true,
"featuresDisabled": ["save_history", "multi_angle"]
}
}
Rate Limiting
Credit-Based Limits
- Guest Users: 1 lifetime credit (tracked by device ID)
- Free Users: 4 lifetime credits (OAuth sign-in required)
- Premium Subscribers: 168 credits/month (resets on billing date)
- Bundle Purchasers: Credits added to balance (no expiration)
- Admin Users: Unlimited (development only)
Global API Limits
- 60 requests/second burst rate
- 100,000 requests/day quota
- Enforced via AWS API Gateway Usage Plans
Image Requirements
- Format: JPEG or PNG
- Max Size: 20MB (after base64 encoding)
- Recommended Resolution: 1024x1024 for optimal processing (1-3 second latency)
- Aspect Ratio: Square preferred, will be auto-cropped if needed
- Multiple Images: Supports up to 3 images as input (user photo + reference + style guide)
Caching Strategy
- Template endpoints:
Cache-Control: public, max-age=3600 - ETags implemented for conditional requests
- S3 signed URLs valid for 1 hour
Security Headers
All responses include:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Strict-Transport-Security: max-age=31536000; includeSubDomains
iOS Swift Integration Example
// Using URLSession with async/await and credit handling
func processHairstyle(image: Data, referenceImage: Data? = nil, prompt: String, multiAngle: Bool = false) async throws -> [Data] {
let url = URL(string: "\(baseURL)/api/gemini-edit")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
var contents: [[String: Any]] = [
["inlineData": ["mimeType": "image/jpeg", "data": image.base64EncodedString()]]
]
// Add reference image if provided
if let refImage = referenceImage {
contents.append(["inlineData": ["mimeType": "image/jpeg", "data": refImage.base64EncodedString()]])
}
// Add prompt with face preservation
let fullPrompt = multiAngle ?
"\(prompt). Generate four views: front facing, left profile, right profile, and back view. Preserve facial features completely." :
"\(prompt). Preserve facial features and identity completely unchanged."
contents.append(["text": fullPrompt])
let body = [
"contents": contents,
"options": [
"model": "gemini-2.5-flash-image-preview",
"variations": multiAngle ? 4 : 1
]
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
do {
let (data, response) = try await URLSession.shared.data(for: request)
// Check for insufficient credits
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 402 {
let errorResponse = try JSONDecoder().decode(CreditError.self, from: data)
throw InsufficientCreditsError(errorResponse)
}
let geminiResponse = try JSONDecoder().decode(GeminiResponse.self, from: data)
return geminiResponse.generatedImages.compactMap { Data(base64Encoded: $0) }
} catch {
throw error
}
}
Backend Go Lambda Example
func handleGeminiEdit(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// Parse request
var editRequest EditRequest
json.Unmarshal([]byte(request.Body), &editRequest)
// Extract user ID from JWT or device ID for guests
userID := getUserID(request.Headers["Authorization"])
// Check and deduct credits
creditsNeeded := editRequest.Options.Variations
if creditsNeeded == 0 {
creditsNeeded = 1
}
remaining, err := deductCredits(ctx, userID, creditsNeeded)
if err != nil {
if err == ErrInsufficientCredits {
return events.APIGatewayProxyResponse{
StatusCode: 402,
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: buildInsufficientCreditsResponse(userID, creditsNeeded),
}, nil
}
return events.APIGatewayProxyResponse{StatusCode: 500}, err
}
// Get API key from Secrets Manager
apiKey := getSecretValue("gemini-api-key")
// Initialize Gemini client and process
client, _ := genai.NewClient(ctx, option.WithAPIKey(apiKey))
model := client.GenerativeModel("gemini-2.5-flash-image-preview")
// Generate content
resp, _ := model.GenerateContent(ctx, editRequest.Contents...)
// Extract images and build response
var generatedImages []string
for _, candidate := range resp.Candidates {
for _, part := range candidate.Content.Parts {
if part.InlineData != nil {
generatedImages = append(generatedImages,
base64.StdEncoding.EncodeToString(part.InlineData.Data))
}
}
}
// Log usage for analytics
logUsage(ctx, userID, creditsNeeded, editRequest.Contents[len(editRequest.Contents)-1].Text)
// Return response with credit info
return events.APIGatewayProxyResponse{
StatusCode: 200,
Headers: map[string]string{
"Content-Type": "application/json",
"X-Credits-Remaining": strconv.Itoa(remaining),
},
Body: json.Marshal(map[string]interface{}{
"generatedImages": generatedImages,
"model": "gemini-2.5-flash-image-preview",
"creditsUsed": creditsNeeded,
"creditsRemaining": remaining,
}),
}, nil
}
Product IDs for In-App Purchases
iOS (App Store)
com.ilikeyacut.subscription.monthly- $9.99/month (168 credits)com.ilikeyacut.bundle.small- $0.99 (8 credits)com.ilikeyacut.bundle.large- $4.99 (48 credits)
Android (Google Play)
com.ilikeyacut.subscription.monthly- $9.99/month (168 credits)com.ilikeyacut.bundle.small- $0.99 (8 credits)com.ilikeyacut.bundle.large- $4.99 (48 credits)
Versioning
API version specified in X-API-Version header. Current version: 1.0
Breaking changes will increment major version with deprecation notices.
Admin Tools (Development Only)
Setting Admin Access
cd backend/scripts
# Update by email
go run update-user-tier.go -email admin@example.com -tier admin
# Update by user ID
go run update-user-tier.go -user google_12345 -tier admin
Admin users have unlimited credits for testing and development purposes.
