P-Image LoRA API
pruna-ai/p-image-lora/editÀ partir de 2 crédits par image· Modifier une imageRetouche par instruction sur P-Image-Edit de Pruna AI avec jusqu’à cinq images sources et un LoRA facultatif.
Appelez-le avec le nom de modèle pruna-ai/p-image-lora/edit sur POST /jobs/createTask, à partir de 2 crédits par image (0,02 $US). Les nouveaux comptes reçoivent 30 crédits gratuits pour l’essayer d’abord dans le Playground.
- Créer une image
- Modifier une image
- Jusqu’à 5 images de référence
- 8 formats d’image
- Charge vos propres LoRA
pruna-ai/p-image-lora/editPOST https://api.popnow.ai/api/v1/jobs/createTask# 1. Create the task. Keep the Idempotency-Key if you retry after a network error.
export IDEMPOTENCY_KEY="$(uuidgen)"
curl -X POST https://api.popnow.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"model": "pruna-ai/p-image-lora/edit",
"input": {
"prompt": "A ceramic teapot on a linen cloth, soft window light, muted palette",
"image_urls": [
"https://media.popnow.ai/influencer/examples/marco.webp"
],
"output_format": "jpeg"
}
}'
# 2. Copy data.taskId from the response, then poll until state is succeeded, failed, canceled or expired.
TASK_ID="TASK_ID_FROM_RESPONSE"
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"// Node 18+ (or any runtime with fetch). Keep the API key on your server.
const BASE = "https://api.popnow.ai/api/v1";
const headers = { Authorization: `Bearer ${process.env.POPNOW_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(path, init = {}) {
const res = await fetch(BASE + path, { ...init, headers: { ...headers, ...init.headers } });
const body = await res.json();
if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
return body.data;
}
const { taskId } = await call("/jobs/createTask", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
"model": "pruna-ai/p-image-lora/edit",
"input": {
"prompt": "A ceramic teapot on a linen cloth, soft window light, muted palette",
"image_urls": [
"https://media.popnow.ai/influencer/examples/marco.webp"
],
"output_format": "jpeg"
}
}),
});
// Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
let task;
for (let delay = 2000; ; delay = Math.min(delay * 1.5, 15000)) {
await sleep(delay);
task = await call(`/jobs/recordInfo?taskId=${taskId}`);
const copying = (task.output?.assets ?? []).some((a) => a.pending && !a.unavailable);
if (["succeeded", "failed", "canceled", "expired"].includes(task.state) && !copying) break;
}
if (task.state !== "succeeded") throw new Error(`${task.state}: ${task.errorMessage ?? "no detail"}`);
// Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for (const asset of task.output.assets) console.log(asset.url);import os, time, uuid, requests
BASE = "https://api.popnow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['POPNOW_API_KEY']}"}
def call(method, path, **kwargs):
body = requests.request(method, BASE + path, headers={**HEADERS, **kwargs.pop("headers", {})}, timeout=30, **kwargs).json()
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]}')
return body["data"]
task_id = call(
"POST",
"/jobs/createTask",
headers={"Idempotency-Key": str(uuid.uuid4())}, # reuse it if you retry
json={
"model": "pruna-ai/p-image-lora/edit",
"input": {
"prompt": "A ceramic teapot on a linen cloth, soft window light, muted palette",
"image_urls": [
"https://media.popnow.ai/influencer/examples/marco.webp"
],
"output_format": "jpeg"
}
},
)["taskId"]
# Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
delay = 2.0
while True:
time.sleep(delay)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
assets = (task.get("output") or {}).get("assets") or []
copying = any(a.get("pending") and not a.get("unavailable") for a in assets)
if task["state"] in ("succeeded", "failed", "canceled", "expired") and not copying:
break
delay = min(delay * 1.5, 15.0)
if task["state"] != "succeeded":
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
# Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for asset in assets:
print(asset["url"])L’input est construit à partir du schéma ci-dessous : il passe la validation tel quel. Remplacez le prompt et les liens par les vôtres.
Prix
| Option | Crédits par image | ≈ USD |
|---|---|---|
| Standard | 1,8 | 0,018 $US |
1 crédit = 0,01 $ · Toutes les options et leur prix : GET /models/pruna-ai/p-image-lora/edit
Chaque tâche est arrondie au crédit entier supérieur : une exécution au palier le plus bas coûte donc 2 crédits par image (0,02 $US).
Limites
- Formats d’image
- 1:1 · 16:9 · 9:16 · 4:3 · 3:4 · 3:2 · 2:3
- Images de référence
- 5
- Longueur du prompt (caractères)
- 5,000
- Taille maximale d’envoi
- 10 MB
Paramètres
Ces champs viennent du schéma avec lequel nous vérifions chaque requête. Les champs qui n’y figurent pas sont refusés.
promptstringObligatoireSay what to change and what to keep. With several images, refer to them as image 1, image 2 and so on.
image_urlsarray<string>ObligatoireLes photos à retoucher ou combiner. Dites dans le prompt ce que chacune apporte.
aspect_ratiostringFacultatifLa forme du cadre : large, carrée ou verticale.
lorasarray<object>FacultatifChargez vos propres fichiers LoRA pour fixer un style ou un personnage.
Champs internes
loras[].pathstringObligatoireDirect URL of a LoRA weights file.
loras[].scalenumberObligatoireWeight strength from -1 to 3.
output_formatstringFacultatifLe type de fichier du résultat.
seedintegerFacultatifLe même nombre donne un résultat reproductible.
Schéma JSON brut
{
"additionalProperties": false,
"properties": {
"aspect_ratio": {
"description": "Output aspect ratio. Leave it empty to follow the first source image.",
"enum": [
"1:1",
"16:9",
"9:16",
"4:3",
"3:4",
"3:2",
"2:3"
],
"type": "string"
},
"image_urls": {
"description": "One to five source images. Without aspect_ratio the result follows the first one.",
"items": {
"contentMediaType": [
"image/jpeg",
"image/png",
"image/webp",
"image/gif"
],
"format": "uri",
"minLength": 1,
"type": "string"
},
"maxItems": 5,
"minItems": 1,
"type": "array"
},
"loras": {
"default": [],
"description": "One LoRA weight file trained for P-Image editing, with its strength.",
"items": {
"additionalProperties": false,
"properties": {
"path": {
"contentMediaType": [
"application/octet-stream"
],
"description": "Direct URL of a LoRA weights file.",
"format": "uri",
"minLength": 1,
"type": "string"
},
"scale": {
"default": 1,
"description": "Weight strength from -1 to 3.",
"maximum": 3,
"minimum": -1,
"type": "number"
}
},
"required": [
"path",
"scale"
],
"type": "object"
},
"maxItems": 1,
"type": "array"
},
"output_format": {
"default": "jpeg",
"description": "Container of the returned image.",
"enum": [
"jpeg",
"png",
"webp"
],
"type": "string"
},
"prompt": {
"description": "Say what to change and what to keep. With several images, refer to them as image 1, image 2 and so on.",
"maxLength": 5000,
"minLength": 1,
"type": "string"
},
"seed": {
"description": "Random seed for controlled comparisons.",
"maximum": 2147483647,
"minimum": 0,
"type": "integer"
}
},
"required": [
"prompt",
"image_urls"
],
"type": "object"
}Réponses, interrogation et callbacks
Tous les modèles fonctionnent de la même façon : créez une tâche, puis interrogez-la ou attendez le callback.
Connaître le prix d’abord
curl -X POST https://api.popnow.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "pruna-ai/p-image-lora/edit",
"input": {
"prompt": "A ceramic teapot on a linen cloth, soft window light, muted palette",
"image_urls": [
"https://media.popnow.ai/influencer/examples/marco.webp"
],
"output_format": "jpeg"
}
}'Même corps que createTask. Renvoie le débit attendu et le débit maximal possible, sans rien lancer.
Acceptée
HTTP/1.1 202 Accepted
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued"
},
"request_id": "req_example_create"
}HTTP 202 signifie que la tâche est acceptée et que les crédits sont réservés, pas encore débités. Le débit final ne dépasse jamais la réservation.
Vérifier l’état
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"Interrogez toutes les quelques secondes en espaçant progressivement, et arrêtez dès que state vaut succeeded, failed, canceled ou expired.
Tâche terminée
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "pruna-ai/p-image-lora/edit",
"state": "succeeded",
"output": {
"assets": [
{
"url": "https://cdn.popnow.ai/tasks/task_01k3m8x9q2z4v7n5p6r8s0t1w3/output.png?signature=FROM_RESPONSE",
"expiresAt": "2026-10-17T12:20:00Z",
"mime": "image/png",
"width": 1024,
"height": 1024
}
]
},
"settled": true
},
"request_id": "req_example_record"
}Téléchargez depuis l’url avant expiresAt. Besoin d’un nouveau lien plus tard ? Demandez-le à POST /common/download-url.
Vérifier un callback
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// express.raw keeps the exact bytes that were signed; express.json would not match.
app.post("/hooks/popnow", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.header("X-Webhook-Timestamp");
const signature = req.header("X-Webhook-Signature") ?? "";
const body = JSON.parse(req.body.toString("utf8"));
const taskId = body.data?.taskId ?? body.task_id;
const digest = createHash("sha256").update(req.body).digest("hex");
const expected = createHmac("sha256", process.env.POPNOW_WEBHOOK_SECRET)
.update(`${taskId}.${timestamp}.${digest}`)
.digest("base64");
const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
// Check the signature first, then reject anything older than five minutes.
if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
res.status(200).end(); // answer fast, then fetch the task and store its files
});Avec une callBackUrl, nous vous envoyons la tâche terminée. Vérifiez la signature sur le corps brut avant de lui faire confiance.
Envoyer un fichier local
curl -X POST https://api.popnow.ai/api/v1/common/upload-url \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contentType": "image/png", "bytes": 482113}'
# PUT the file bytes to data.uploadUrl with every header in data.headers, then commit it:
curl -X POST https://api.popnow.ai/api/v1/files/$FILE_ID/commit \
-H "Authorization: Bearer $POPNOW_API_KEY"
# The commit returns a popnow://f/... reference. Put it in any image, video or
# audio field of the task input instead of a public https URL.Les liens https publics fonctionnent tels quels. Les fichiers locaux passent d’abord par upload-url et commit.


