📡 PND AI Public API

API for Developers

Integrate PND AI into your app/website — upscale photos up to 4K + face restoration. Upload via base64, RESTful, asynchronous. Sign in with Google to get an API Key instantly.

📡Overview

PND AI Public API is a RESTful service for developers — upscale photos to 4K + face restoration. Async pipeline: POST base64 image → receive job_idpoll status until done → fetch the image from result_url.

Base 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.
⚠️ Upload limits:
• 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 + URLPurpose
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:

  1. Sign in with Google at pndai.ai — the system auto-generates an API Key in the format pndai.ai:<32 chars>
  2. Get your key at /member (under "API Key" — click to copy)
  3. Resize the image if longest edge > 1024px or size > 1MB, then encode to base64
  4. POST JSON body {"image_base64":"...","size":"2K"} to https://pndai.ai/api/uploadv2/ with the x-key header → poll status → download the result
⚠️ Security note: The API Key is PRIVATE — do not commit it to a public repo, do not expose it on a customer-facing JavaScript frontend. Always call from server-side or a native app. Each credit is charged according to size (HD/FHD = 1, 2K = 2, 4K = 4 credits).

🔐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

POST https://pndai.ai/api/uploadv2/

PND AI PRO — upscale photo + face restoration (recommended). Returns job_id immediately (async pipeline).

Headers

NameRequiredDescription
x-keyAPI key (pndai.ai:xxxxx)
Content-Typeapplication/json

Body (JSON)

FieldTypeRequiredDescription
image_base64stringBase64 of the image (with or without data:image/jpeg;base64, prefix). Max 1 MB after decode, longest edge max 1024px.
sizestringoptOutput: HD, FHD, 2K, 4K (default FHD)
modelintoptModel ID 1-5 (see Models), default 1
wb0/1optAuto white balance, default 0
colorintopt-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
}
POST https://pndai.ai/api/upload/

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

GET https://pndai.ai/api/status/?job_id=

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

NameRequired
x-key

Query params

NameRequiredDescription
job_idJob ID returned from /api/upload/ or /api/uploadv2/

Status values

StatusDescription
waiting / queuedWaiting in the Redis queue
processingGPU 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
}
💡 30 polls (90 seconds total) is enough for any size. If the job is still processing after 90s, treat it as failed. Credits are only deducted when status=done — calling status multiple times will not double-charge.

🖼️Fetch result

GET https://pndai.ai/api/result/?job_id=[&face=N]

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):

MethodWhen to use
Header x-key: pndai.ai:xxxBackend (cURL, requests, axios...)
Query ?key=pndai.ai:xxxEmbedding <img src> on the web (browsers cannot send custom headers)

Query params

NameRequiredDescription
job_idJob ID with status=done
faceoptFace thumbnail index (V2 only): 0, 1, ...
keyoptFallback if you cannot send the header (see above)

Response

  • 200 + binary image (cached for 1 day)
  • 425 Too Early if the job is not done yet
  • 403 not_your_job if the key is not the owner
  • 404 job_not_found if 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" />
⚠️ With <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)

HeaderRequiredDescription
x-keypndai.ai:xxxxx — get it at /member
Content-TypeUpload onlyapplication/json

JSON body (upload endpoints)

FieldDefaultRange/Values
image_base64Base64 string (with/without data URI prefix). Required.
sizeFHDHD / FHD / 2K / 4K (also accepts pixels: 1280/1920/2560/3840)
model11, 2, 3, 4, 5 (see AI Models)
wb00 (off) / 1 (auto white balance)
color0-10 (cool) → +10 (warm)
langai2-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-sizeLongest edge (px)Common resolutionCredit cost
HD / 12801280px1280×720 (~1MP)1
FHD / 19201920px1920×1080 (~2MP)1
2K / 25602560px2560×1440 (~4MP)2
4K / 38403840px3840×2160 (~8MP)4
⚠️ 8K (7680px) is not available via the public API to avoid GPU overload. If you need 8K, contact us for an enterprise plan.

🤖AI Models (x-model)

IDNameDescription
1RealisticPreserves natural detail close to the original (recommended for portraits)
2Smooth BeautySmooth, evenly-lit skin — great for selfie portraits
3Vivid ColorsBright, vivid colors with high contrast
4Wedding/SoftWarm, soft tone — great for wedding/event photos
5Old Photo RestoreRestore old photos and black & white pictures

⚠️Error codes (HTTP status + error code)

Every error response has the form:

{ "ok": false, "error": "<code>", "detail": "<message>" }
HTTPerrorWhen
400invalid_jsonBody is not valid JSON
400invalid_sizesize is not in the whitelist
400invalid_base64Base64 decode failed
400no_imageMissing image_base64
400corrupt_imageDecoded fine but cannot read dimensions
400invalid_job_idjob_id has invalid format
401missing_keyMissing x-key header
401invalid_key_formatKey does not start with pndai.ai:
401invalid_keyKey not in DB or account is locked
402out_of_creditsNot enough credits for the chosen size
403not_your_jobjob_id does not belong to this key’s user
404job_not_foundjob_id is wrong or has expired
413image_too_largeFile > 1 MB after decode
413pixel_too_largeLongest edge > 1024px
415invalid_formatNot a jpg/png/webp file
425not_readyCalling /api/result/ while the job is not done
502upstream_*GPU backend error (curl fail / 5xx / bad JSON)
💡 On 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.

⚠️ No grace period — after rotating, every app/script using the old key must be updated immediately. new_key is shown only once in the response — it cannot be retrieved later (you will have to rotate again if lost).
{
  "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']...
💡 Webhook is fire-and-forget — server times out after 5s. Your endpoint just needs to respond HTTP 200 OK and must NOT do heavy synchronous work. If processing takes time, use a queue/cron of your own.

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:

TierPriceQuotaBest for
FREE$03 photos/dayTest, prototype
+100$2.99+100 foreverOccasional use
API Monthly$4.99/month+300 credits / 30 daysProduction app, small dev
API Yearly$29.99/year (–50%)+4000 credits / 365 daysSaaS, steady traffic
EnterpriseContact usUnlimited + 8KHigh 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/min30/min3/min
VIP / Pro60/min 300/min 600/min60/min5/min
EnterpriseCustom (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

📱 Mobile apps: iOS · Android