Overview
PND AI Public API is a RESTful service for developers — upscale photos to 4K + face restoration. Async pipeline: POST base64 image → receive job_id → poll status until done → fetch the image from result_url.
https://pndai.ai/api/Auth: Header
x-key: pndai.ai:xxxxxxxx (get it from /member after signing in with Google)Format: Both request & response are JSON. Upload images as base64 string.
• Max image file size: 1 MB (after base64 decode)
• Max longest edge: 1024px — resize before sending if larger
• Format:
jpg, jpeg, png, webp• Output size: HD / FHD / 2K / 4K (8K not supported via public API)
4 endpoints:
| Method + URL | Purpose |
|---|---|
POST /api/upload/ | Upload V1 (no face) — fast, fewer credits |
POST /api/uploadv2/ | Upload V2 (face restoration) — recommended |
GET /api/status/?job_id=... | Check status + get result URL |
GET /api/result/?job_id=... | Download result image (binary, can be embedded directly via <img>) |
Getting Started
4 steps to run the PND AI API for the first time:
- Sign in with Google at pndai.ai — the system auto-generates an API Key in the format
pndai.ai:<32 chars> - Get your key at /member (under "API Key" — click to copy)
- Resize the image if longest edge > 1024px or size > 1MB, then encode to base64
- POST JSON body
{"image_base64":"...","size":"2K"}tohttps://pndai.ai/api/uploadv2/with thex-keyheader → poll status → download the result
API Key authentication
The API Key is auto-generated when you sign in with Google for the first time at pndai.ai. Format: pndai.ai:<32 random chars>. Each user has one unique, permanent key.
Every request (upload, status, result) must include the x-key header:
x-key: pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8
The /api/result/ endpoint additionally supports a ?key= query param (so you can embed <img src=...> directly in HTML — browsers cannot send custom headers):
<img src="https://pndai.ai/api/result/?job_id=vn_api.xxx&key=pndai.ai:FUy0..." />
If the key is wrong/missing, the server returns HTTP 401:
{
"ok": false,
"error": "invalid_key",
"detail": "Invalid API key or account is locked"
}
Processing workflow
A 3-step async pipeline; everything goes through pndai.ai — you never call the GPU backend directly:
┌─────────────┐ POST /api/uploadv2/ ┌──────────────────────┐
│ Your app │ ──────► │ pndai.ai (gateway) │
│ │ ◄─── ─────── │ • validate x-key │
└─────────────┘ │ • check credit │
│ │ • forward to backend │
│ └──────────┬───────────┘
│ │ admin key
│ ▼
│ ┌──────────────────────┐
│ │ AI GPU cluster │
│ │ (15-60s processing) │
│ └──────────┬───────────┘
│ │
│ GET /api/status/?job_id=... │
└──────────► poll mỗi 3s ──────────────────┤
◄── { status, result_url } ───┤
│
▼ status=done │
GET /api/result/?job_id=... │
◄── binary image (jpg/png/webp) ───────────┘
Upload photo
PND AI PRO — upscale photo + face restoration (recommended). Returns job_id immediately (async pipeline).
Headers
| Name | Required | Description |
|---|---|---|
x-key | ★ | API key (pndai.ai:xxxxx) |
Content-Type | ★ | application/json |
Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
image_base64 | string | ★ | Base64 of the image (with or without data:image/jpeg;base64, prefix). Max 1 MB after decode, longest edge max 1024px. |
size | string | opt | Output: HD, FHD, 2K, 4K (default FHD) |
model | int | opt | Model ID 1-5 (see Models), default 1 |
wb | 0/1 | opt | Auto white balance, default 0 |
color | int | opt | -10 (cool) → +10 (warm), default 0 |
Response (200 OK)
{
"ok": true,
"job_id": "vn_api.1777812129.36d2e2e10d31ca33",
"status": "queued",
"endpoint": "v2",
"size": "2K",
"cost": 2,
"credit_source": "sub",
"remaining": 198
}
PND AI 1.0 — basic upscale, no face restoration. Faster, fewer credits. Headers/body are identical to /api/uploadv2/.
Response (same as V2 but with endpoint:"v1")
{
"ok": true,
"job_id": "vn_api.1777812129.xxxxxxxxxxxxxxxx",
"status": "queued",
"endpoint": "v1",
"size": "FHD",
"cost": 1,
"credit_source": "free",
"remaining": 2
}
Check status
Poll every 3 seconds. The server auto-detects V1/V2 from job_id — no separate endpoints needed. The first time status=done, the credit is deducted and the response includes result_url.
Headers
| Name | Required |
|---|---|
x-key | ★ |
Query params
| Name | Required | Description |
|---|---|---|
job_id | ★ | Job ID returned from /api/upload/ or /api/uploadv2/ |
Status values
| Status | Description |
|---|---|
waiting / queued | Waiting in the Redis queue |
processing | GPU worker is processing (15-60s depending on size) |
done | ✅ Completed — fetch the image at result_url |
error / not_found | ❌ Failed — credit is NOT deducted |
Response (status=done)
{
"ok": true,
"job_id": "vn_api.xxx",
"status": "done",
"endpoint": "v2",
"size": "2K",
"cost": 2,
"deducted_this_call": true,
"result_url": "https://pndai.ai/api/result/?job_id=vn_api.xxx",
"face_urls": [
"https://pndai.ai/api/result/?job_id=vn_api.xxx&face=0",
"https://pndai.ai/api/result/?job_id=vn_api.xxx&face=1"
],
"countfaces": 2,
"remaining": 196
}
processing after 90s, treat it as failed. Credits are only deducted when status=done — calling status multiple times will not double-charge.
Fetch result
Download the result image as a binary stream (Content-Type: image/jpeg). Can be embedded directly in HTML or saved as a file.
Auth
This endpoint supports 2 ways to pass the key (useful for both backend and browser):
| Method | When to use |
|---|---|
Header x-key: pndai.ai:xxx | Backend (cURL, requests, axios...) |
Query ?key=pndai.ai:xxx | Embedding <img src> on the web (browsers cannot send custom headers) |
Query params
| Name | Required | Description |
|---|---|---|
job_id | ★ | Job ID with status=done |
face | opt | Face thumbnail index (V2 only): 0, 1, ... |
key | opt | Fallback if you cannot send the header (see above) |
Response
200+ binary image (cached for 1 day)425 Too Earlyif the job is notdoneyet403 not_your_jobif the key is not the owner404 job_not_foundif invalid/expired
Example
# Backend cURL — header curl -H "x-key: pndai.ai:xxxxx" \ -o result.jpg \ "https://pndai.ai/api/result/?job_id=vn_api.xxx" <!-- HTML — query param (browser embed) --> <img src="https://pndai.ai/api/result/?job_id=vn_api.xxx&key=pndai.ai:xxxxx" />
<img> on a public page, the key will leak via DOM/console. Better to proxy through your backend (signed URL with expiry) instead of putting the raw key on the frontend.
Headers + Body fields Reference
The Public API uses just 1 required header (x-key). Other parameters go in the JSON body.
Headers (every endpoint)
| Header | Required | Description |
|---|---|---|
x-key | ★ | pndai.ai:xxxxx — get it at /member |
Content-Type | Upload only | application/json |
JSON body (upload endpoints)
| Field | Default | Range/Values |
|---|---|---|
image_base64 | — | Base64 string (with/without data URI prefix). Required. |
size | FHD | HD / FHD / 2K / 4K (also accepts pixels: 1280/1920/2560/3840) |
model | 1 | 1, 2, 3, 4, 5 (see AI Models) |
wb | 0 | 0 (off) / 1 (auto white balance) |
color | 0 | -10 (cool) → +10 (warm) |
lang | ai | 2-letter locale — used in the job_id prefix |
Sizes (x-size)
The Public API supports 4 output levels. The header value is the name (case-insensitive) or the pixel number:
| x-size | Longest edge (px) | Common resolution | Credit cost |
|---|---|---|---|
HD / 1280 | 1280px | 1280×720 (~1MP) | 1 |
FHD / 1920 | 1920px | 1920×1080 (~2MP) | 1 |
2K / 2560 | 2560px | 2560×1440 (~4MP) | 2 |
4K / 3840 | 3840px | 3840×2160 (~8MP) | 4 |
AI Models (x-model)
| ID | Name | Description |
|---|---|---|
1 | Realistic | Preserves natural detail close to the original (recommended for portraits) |
2 | Smooth Beauty | Smooth, evenly-lit skin — great for selfie portraits |
3 | Vivid Colors | Bright, vivid colors with high contrast |
4 | Wedding/Soft | Warm, soft tone — great for wedding/event photos |
5 | Old Photo Restore | Restore old photos and black & white pictures |
Error codes (HTTP status + error code)
Every error response has the form:
{ "ok": false, "error": "<code>", "detail": "<message>" }
| HTTP | error | When |
|---|---|---|
400 | invalid_json | Body is not valid JSON |
400 | invalid_size | size is not in the whitelist |
400 | invalid_base64 | Base64 decode failed |
400 | no_image | Missing image_base64 |
400 | corrupt_image | Decoded fine but cannot read dimensions |
400 | invalid_job_id | job_id has invalid format |
401 | missing_key | Missing x-key header |
401 | invalid_key_format | Key does not start with pndai.ai: |
401 | invalid_key | Key not in DB or account is locked |
402 | out_of_credits | Not enough credits for the chosen size |
403 | not_your_job | job_id does not belong to this key’s user |
404 | job_not_found | job_id is wrong or has expired |
413 | image_too_large | File > 1 MB after decode |
413 | pixel_too_large | Longest edge > 1024px |
415 | invalid_format | Not a jpg/png/webp file |
425 | not_ready | Calling /api/result/ while the job is not done |
502 | upstream_* | GPU backend error (curl fail / 5xx / bad JSON) |
out_of_credits, the response also returns remaining and cost so you know how much more to top up.
GET /api/credits/
Check remaining credits + tier info — does NOT consume an upload, no credit cost.
{
"ok": true,
"user_type": "free", // hoặc "vip"
"vip_active": false,
"vip_expires": null, // "2026-12-31" nếu VIP
"credits": {
"sub": 0, // VIP credits còn (tính khi vip_active=true)
"extra": 0, // IAP +100 vĩnh viễn
"free_today": 3, // free còn dùng hôm nay (max 3)
"total": 3 // sub + extra + free_today
},
"today": { "used": 0, "limit": 3 }
}
POST /api/rotate/
Generate a new API key + invalidate the old one IMMEDIATELY. Use this when the key is leaked. All requests with the old key after that will return 401.
{
"ok": true,
"old_key_prefix": "pndai.ai:Fa80...XQH8",
"new_key": "pndai.ai:<32 chars>",
"rotated_at": "2026-05-04 10:35:12"
}
Or simpler: go to /member and click the 🔄 Rotate button next to the API Key.
POST /api/webhook/ — Setup Webhook Callback
Register a webhook URL so the PND AI server POSTs the result back to you when a job is done — no polling required.
Set up webhook
curl -X POST https://pndai.ai/api/webhook/ \ -H "x-key: pndai.ai:xxxxx" \ -H "Content-Type: application/json" \ -d '{"url":"https://yoursite.com/pndai/callback","secret":"my_random_string"}'
What the server will POST to your URL
POST https://yoursite.com/pndai/callback Headers: Content-Type: application/json X-PNDAI-Signature: sha256=<HMAC-SHA256 của body với secret> User-Agent: PNDAI-Webhook/1.0 Body: { "event": "job.done", "job_id": "vn_api.xxx", "endpoint": "v2", "size": "FHD", "cost": 1, "result_url": "https://pndai.ai/api/result/?job_id=vn_api.xxx", "countfaces": 1, "timestamp": 1777812129 }
Verify the signature in your code (PHP)
<?php $secret = 'my_random_string'; $body = file_get_contents('php://input'); $received = $_SERVER['HTTP_X_PNDAI_SIGNATURE'] ?? ''; $expected = 'sha256='.hash_hmac('sha256', $body, $secret); if (!hash_equals($expected, $received)) { http_response_code(401); die('invalid signature'); } $payload = json_decode($body, true); // xử lý $payload['result_url']...
To disable the webhook, send {"url":""}.
GET /api/stats/?days=N
API usage stats for the last N days (max 90). Use this to build your own dashboard for your users.
{
"ok": true,
"period_days": 30,
"totals": { "all":42, "done":38, "error":2, "queued":2, "credits_used":52 },
"by_endpoint": { "v1":10, "v2":32 },
"by_size": { "HD":5, "FHD":25, "2K":8, "4K":4 },
"daily": [
{ "date":"2026-04-30", "count":12, "credits":18 },
...
],
"recent": [ // 20 jobs gần nhất
{ "job_id":..., "endpoint":..., "status":..., "created_at":... },
...
]
}
Sample: cURL + bash
#!/bin/bash # B1: encode ảnh sang base64 (đảm bảo <= 1024px / 1MB trước) B64=$(base64 -w0 photo.jpg) # B2: upload (V2 = face restoration) RES=$(curl -s -X POST "https://pndai.ai/api/uploadv2/" \ -H "x-key: pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8" \ -H "Content-Type: application/json" \ -d "{\"image_base64\":\"$B64\",\"size\":\"2K\",\"model\":1}") JOB=$(echo $RES | jq -r .job_id) echo "Job: $JOB" # B3: poll status (mỗi 3s, max 30 lần) for i in ; do sleep 3 ST=$(curl -s "https://pndai.ai/api/status/?job_id=$JOB" \ -H "x-key: pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8") S=$(echo $ST | jq -r .status) echo "[$i] $S" [ "$S" = "done" ] && break done # B4: tải ảnh kết quả curl -H "x-key: pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8" \ -o result.jpg \ "https://pndai.ai/api/result/?job_id=$JOB"
Sample: PHP
<?php define('API_KEY', 'pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8'); define('BASE', 'https://pndai.ai/api/'); function apiCall($path, $body = null) { $ch = curl_init(BASE.$path); $opts = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['x-key: '.API_KEY, 'Content-Type: application/json'], ]; if ($body) { $opts[CURLOPT_POST] = true; $opts[CURLOPT_POSTFIELDS] = json_encode($body); } curl_setopt_array($ch, $opts); return json_decode(curl_exec($ch), true); } // 1. Upload — encode ảnh sang base64 (giả sử đã <= 1024px / 1MB) $b64 = base64_encode(file_get_contents('photo.jpg')); $res = apiCall('uploadv2/', [ 'image_base64' => $b64, 'size' => '2K', 'model' => 1, ]); $jobId = $res['job_id']; echo "Job: $jobId\n"; // 2. Poll status mỗi 3s, max 90s $resultUrl = null; for ($i=0; $i<30; $i++) { sleep(3); $st = apiCall('status/?job_id='.$jobId); if (($st['status'] ?? '') === 'done') { $resultUrl = $st['result_url']; break; } } // 3. Tải ảnh kết quả (binary) $ch = curl_init($resultUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['x-key: '.API_KEY], ]); file_put_contents('result.jpg', curl_exec($ch));
Sample: Python
import base64, requests, time API_KEY = 'pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8' BASE = 'https://pndai.ai/api/' HEADERS = {'x-key': API_KEY} # 1. Encode ảnh sang base64 + upload V2 (face restoration) with open('photo.jpg', 'rb') as f: b64 = base64.b64encode(f.read()).decode() r = requests.post(BASE + 'uploadv2/', headers=HEADERS, json={ 'image_base64': b64, 'size': '2K', 'model': 1, }) job_id = r.json()['job_id'] print(f'Job: ') # 2. Poll mỗi 3s, max 30 lần (~90s) result_url = None for _ in range(30): time.sleep(3) st = requests.get(BASE + 'status/', headers=HEADERS, params={'job_id': job_id}).json() print(st['status']) if st.get('status') == 'done': result_url = st['result_url'] break # 3. Tải binary img = requests.get(result_url, headers=HEADERS) with open('result.jpg', 'wb') as f: f.write(img.content)
Sample: Node.js
const axios = require('axios'); const fs = require('fs'); const API_KEY = 'pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8'; const BASE = 'https://pndai.ai/api/'; const H = { 'x-key': API_KEY }; async function enhance(filePath) { // 1. Encode + upload V2 const b64 = fs.readFileSync(filePath).toString('base64'); const { data: up } = await axios.post(BASE + 'uploadv2/', { image_base64: b64, size: '2K', model: 1 }, { headers: H }); console.log('Job:', up.job_id); // 2. Poll status (3s × 30 lần) let resultUrl; for (let i = 0; i < 30; i++) { await new Promise(r => setTimeout(r, 3000)); const { data: st } = await axios.get(BASE + 'status/', { headers: H, params: { job_id: up.job_id } }); if (st.status === 'done') { resultUrl = st.result_url; break; } } // 3. Tải binary const img = await axios.get(resultUrl, { headers: H, responseType: 'arraybuffer' }); fs.writeFileSync('result.jpg', img.data); } enhance('photo.jpg');
Sample: Android (Kotlin + OkHttp)
Dependency required: com.squareup.okhttp3:okhttp:4.12.0
val client = OkHttpClient() val apiKey = "pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8" val BASE = "https://pndai.ai/api/" val JSON = "application/json".toMediaType() // 1. Encode bitmap → base64 + POST /uploadv2/ val bytes = File("photo.jpg").readBytes() val b64 = Base64.encodeToString(bytes, Base64.NO_WRAP) val body = JSONObject().apply { put("image_base64", b64) put("size", "2K") put("model", 1) }.toString().toRequestBody(JSON) val req = Request.Builder() .url(BASE + "uploadv2/") .post(body) .addHeader("x-key", apiKey) .build() val json = JSONObject(client.newCall(req).execute().body!!.string()) val jobId = json.getString("job_id") // 2. Poll: GET BASE + "status/?job_id=$jobId" với header x-key (3s × 30) // 3. Tải kết quả: GET BASE + "result/?job_id=$jobId" → body!!.bytes() // Có thể nhúng trực tiếp: ImageView.load("$BASE/result/?job_id=$jobId&key=$apiKey")
Sample: Android (Java + OkHttp)
Dependency required: com.squareup.okhttp3:okhttp:4.12.0
import okhttp3.*; import org.json.JSONObject; import android.util.Base64; import java.io.File; import java.nio.file.Files; public class PndaiClient { private static final String API_KEY = "pndai.ai:FUy0Xmb0Z7JIwrSpysE9C9t3VCbZXQH8"; private static final String BASE = "https://pndai.ai/api/"; private static final MediaType JSON = MediaType.get("application/json"); private final OkHttpClient client = new OkHttpClient(); // 1. Encode ảnh + POST /uploadv2/ → trả về job_id public String upload(File photo) throws Exception { byte[] bytes = Files.readAllBytes(photo.toPath()); String b64 = Base64.encodeToString(bytes, Base64.NO_WRAP); JSONObject body = new JSONObject() .put("image_base64", b64) .put("size", "2K") .put("model", 1); Request req = new Request.Builder() .url(BASE + "uploadv2/") .post(RequestBody.create(body.toString(), JSON)) .addHeader("x-key", API_KEY) .build(); try (Response r = client.newCall(req).execute()) { return new JSONObject(r.body().string()).getString("job_id"); } } // 2. Poll status mỗi 3s, max 30 lần (~90s) → trả về result_url public String pollStatus(String jobId) throws Exception { for (int i = 0; i < 30; i++) { Thread.sleep(3000); Request req = new Request.Builder() .url(BASE + "status/?job_id=" + jobId) .addHeader("x-key", API_KEY) .build(); try (Response r = client.newCall(req).execute()) { JSONObject json = new JSONObject(r.body().string()); if ("done".equals(json.optString("status"))) { return json.getString("result_url"); } } } throw new RuntimeException("Timeout"); } // 3. Tải binary kết quả với header x-key public byte[] download(String resultUrl) throws Exception { Request req = new Request.Builder() .url(resultUrl) .addHeader("x-key", API_KEY) .build(); try (Response r = client.newCall(req).execute()) { return r.body().bytes(); } } } // Usage trong Activity / ViewModel (background thread): // String jobId = client.upload(photoFile); // String url = client.pollStatus(jobId); // byte[] result = client.download(url); // Bitmap bmp = BitmapFactory.decodeByteArray(result, 0, result.length); // imageView.setImageBitmap(bmp); // → Hoặc nhúng trực tiếp ImageView qua Glide/Coil: // Glide.with(ctx).load(GlideUrl(url, LazyHeaders.Builder().addHeader("x-key", API_KEY).build())).into(iv)
Sample: iOS (Swift 5)
import Foundation import UIKit let API_KEY = "pndai.ai:Fa80Xmb0Z7JIwrSpysE9C9t3VCbZXQH8" let BASE = "https://pndai.ai/api/" struct UploadResp: Codable { let ok: Bool; let job_id: String } struct StatusResp: Codable { let ok: Bool; let status: String; let result_url: String? } func enhance(image: UIImage, completion: @escaping (Data?) -> Void) { // 1. Resize ≤ 1024px + JPEG → base64 guard let jpeg = image.jpegData(compressionQuality: 0.85) else { return completion(nil) } let b64 = jpeg.base64EncodedString() // 2. POST /uploadv2/ var req = URLRequest(url: URL(string: BASE + "uploadv2/")!) req.httpMethod = "POST" req.setValue(API_KEY, forHTTPHeaderField: "x-key") req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try? JSONSerialization.data(withJSONObject: [ "image_base64": b64, "size": "2K", "model": 1 ]) URLSession.shared.dataTask(with: req) { data, _, _ in guard let data = data, let up = try? JSONDecoder().decode(UploadResp.self, from: data), up.ok else { return completion(nil) } pollStatus(jobId: up.job_id, attempt: 0, completion: completion) }.resume() } func pollStatus(jobId: String, attempt: Int, completion: @escaping (Data?) -> Void) { if attempt > 30 { return completion(nil) } var req = URLRequest(url: URL(string: BASE + "status/?job_id=" + jobId)!) req.setValue(API_KEY, forHTTPHeaderField: "x-key") URLSession.shared.dataTask(with: req) { data, _, _ in guard let data = data, let st = try? JSONDecoder().decode(StatusResp.self, from: data) else { DispatchQueue.main.asyncAfter(deadline: .now() + 3) { pollStatus(jobId: jobId, attempt: attempt + 1, completion: completion) }; return } if st.status == "done", let url = st.result_url { // 3. Download binary var r = URLRequest(url: URL(string: url)!) r.setValue(API_KEY, forHTTPHeaderField: "x-key") URLSession.shared.dataTask(with: r) { d, _, _ in completion(d) }.resume() } else { DispatchQueue.main.asyncAfter(deadline: .now() + 3) { pollStatus(jobId: jobId, attempt: attempt + 1, completion: completion) } } }.resume() } // Usage: enhance(image: myUIImage) { data in if let d = data { let img = UIImage(data: d) } }
Sample: iOS (Objective-C)
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> static NSString *const API_KEY = @"pndai.ai:Fa80Xmb0Z7JIwrSpysE9C9t3VCbZXQH8"; static NSString *const BASE = @"https://pndai.ai/api/"; @interface PndaiAI : NSObject + (void)enhanceImage:(UIImage *)image completion:(void (^)(NSData *))cb; @end @implementation PndaiAI + (void)enhanceImage:(UIImage *)image completion:(void (^)(NSData *))cb { // 1. JPEG → base64 NSData *jpeg = UIImageJPEGRepresentation(image, 0.85); NSString *b64 = [jpeg base64EncodedStringWithOptions:0]; // 2. POST /uploadv2/ NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL: [NSURL URLWithString:[BASE stringByAppendingString:@"uploadv2/"]]]; req.HTTPMethod = @"POST"; [req setValue:API_KEY forHTTPHeaderField:@"x-key"]; [req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; NSDictionary *body = @{ @"image_base64": b64, @"size": @"2K", @"model": @1 }; req.HTTPBody = [NSJSONSerialization dataWithJSONObject:body options:0 error:nil]; [[NSURLSession.sharedSession dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *_, NSError *_) { NSDictionary *up = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSString *jobId = up[@"job_id"]; if (jobId) [self pollStatus:jobId attempt:0 completion:cb]; else cb(nil); }] resume]; } + (void)pollStatus:(NSString *)jobId attempt:(NSInteger)attempt completion:(void (^)(NSData *))cb { if (attempt > 30) { cb(nil); return; } NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL: [NSURL URLWithString:[BASE stringByAppendingFormat:@"status/?job_id=%@", jobId]]]; [req setValue:API_KEY forHTTPHeaderField:@"x-key"]; [[NSURLSession.sharedSession dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *_, NSError *_) { NSDictionary *st = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSString *status = st[@"status"]; NSString *url = st[@"result_url"]; if ([status isEqualToString:@"done"] && url) { // 3. Download binary với x-key NSMutableURLRequest *r = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; [r setValue:API_KEY forHTTPHeaderField:@"x-key"]; [[NSURLSession.sharedSession dataTaskWithRequest:r completionHandler:^(NSData *d, NSURLResponse *_, NSError *_) { cb(d); }] resume]; } else { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [self pollStatus:jobId attempt:attempt + 1 completion:cb]; }); } }] resume]; } @end // Usage: [PndaiAI enhanceImage:myImage completion:^(NSData *data) { // if (data) { UIImage *result = [UIImage imageWithData:data]; } // }];
Pricing
Every free user automatically gets 3 photos / day + an API key as soon as they sign in with Google. Buy more credits at the /pricing page:
| Tier | Price | Quota | Best for |
|---|---|---|---|
| FREE | $0 | 3 photos/day | Test, prototype |
| +100 | $2.99 | +100 forever | Occasional use |
| API Monthly | $4.99/month | +300 credits / 30 days | Production app, small dev |
| API Yearly | $29.99/year (–50%) | +4000 credits / 365 days | SaaS, steady traffic |
| Enterprise | Contact us | Unlimited + 8K | High volume, SLA |
API Yearly saves ~50% compared to buying 12 months separately ($59.88 → $29.99). The credit pool is separate (api_credits) — it is not shared with the web/mobile VIP plan, ideal for devs integrating their own apps.
Rate Limits
Applied per user + endpoint, sliding 60s window. When exceeded → HTTP 429 with the Retry-After header.
| Tier | upload uploadv2 |
status | result | credits stats |
rotate webhook |
|---|---|---|---|---|---|
| Free | 10/min | 60/min | 120/min | 30/min | 3/min |
| VIP / Pro | 60/min | 300/min | 600/min | 60/min | 5/min |
| Enterprise | Custom (contact us) | ||||
Every response includes 3 headers for tracking quota:
X-RateLimit-Limit: 10 X-RateLimit-Remaining: 7 X-RateLimit-Reset: 1777812180
When exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 23
{
"ok": false,
"error": "rate_limit_exceeded",
"detail": "Tier 'free' limit cho 'upload': 10/min. Reset sau 23s",
"limit": 10, "reset_at": 1777812180, "tier": "free"
}
Support
📧 Email: support@pndai.ai
🌐 Website: pndai.ai