curl -X POST "https://api.hooked.so/v1/media/generate/image" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets", "model": "gpt_image_2", "aspectRatio": "ratio_1_1", "name": "Serum hero shot" }'
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const response = await fetch("https://api.hooked.so/v1/media/generate/image", {
method: "POST",
headers,
body: JSON.stringify({
prompt: "A glass serum bottle on wet black stone, soft studio light, water droplets",
model: "gpt_image_2",
aspectRatio: "ratio_1_1",
}),
});
const { data } = await response.json();
// Poll until the image is there
let media;
do {
await new Promise((resolve) => setTimeout(resolve, 3000));
media = (await (await fetch(`https://api.hooked.so/v1/media/${data.mediaId}`, { headers })).json()).data;
} while (media.status === "PROCESSING" || media.status === "PENDING");
console.log(media.status, media.url);
import os
import time
import requests
headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}
data = requests.post(
"https://api.hooked.so/v1/media/generate/image",
headers=headers,
json={
"prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets",
"model": "gpt_image_2",
"aspectRatio": "ratio_1_1",
},
).json()["data"]
while True:
time.sleep(3)
media = requests.get(f"https://api.hooked.so/v1/media/{data['mediaId']}", headers=headers).json()["data"]
if media["status"] in ("COMPLETED", "FAILED"):
break
print(media["status"], media["url"])
{
"success": true,
"message": "Image generation started",
"data": {
"status": "PROCESSING",
"mediaId": "cm4x9r2t50007ab12gh34ij78",
"type": "image",
"model": "gpt_image_2",
"aspectRatio": "ratio_1_1",
"usedCredits": 4
}
}
{
"success": false,
"message": "referenceMediaIds: Nano Banana takes at most 3 reference images"
}
Generate Image
Generate an AI image from a prompt into your media library
curl -X POST "https://api.hooked.so/v1/media/generate/image" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets", "model": "gpt_image_2", "aspectRatio": "ratio_1_1", "name": "Serum hero shot" }'
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const response = await fetch("https://api.hooked.so/v1/media/generate/image", {
method: "POST",
headers,
body: JSON.stringify({
prompt: "A glass serum bottle on wet black stone, soft studio light, water droplets",
model: "gpt_image_2",
aspectRatio: "ratio_1_1",
}),
});
const { data } = await response.json();
// Poll until the image is there
let media;
do {
await new Promise((resolve) => setTimeout(resolve, 3000));
media = (await (await fetch(`https://api.hooked.so/v1/media/${data.mediaId}`, { headers })).json()).data;
} while (media.status === "PROCESSING" || media.status === "PENDING");
console.log(media.status, media.url);
import os
import time
import requests
headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}
data = requests.post(
"https://api.hooked.so/v1/media/generate/image",
headers=headers,
json={
"prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets",
"model": "gpt_image_2",
"aspectRatio": "ratio_1_1",
},
).json()["data"]
while True:
time.sleep(3)
media = requests.get(f"https://api.hooked.so/v1/media/{data['mediaId']}", headers=headers).json()["data"]
if media["status"] in ("COMPLETED", "FAILED"):
break
print(media["status"], media["url"])
{
"success": true,
"message": "Image generation started",
"data": {
"status": "PROCESSING",
"mediaId": "cm4x9r2t50007ab12gh34ij78",
"type": "image",
"model": "gpt_image_2",
"aspectRatio": "ratio_1_1",
"usedCredits": 4
}
}
{
"success": false,
"message": "referenceMediaIds: Nano Banana takes at most 3 reference images"
}
Overview
The dashboard’s Media Generator, from the API: one image from a prompt, made with the same models, styles and references, at the same price, and saved to your media library. The call answers202 right away with the new mediaId; the image is generated after the response. Poll Get Media until status is COMPLETED (its url is the image) or FAILED, then use the id in any create endpoint.
model: anidofGET /v1/catalog/image-models. Defaults togpt_image_2.style: anidofGET /v1/catalog/visual-styles(your owncustom-<id>styles included). Its look is added to the prompt; with noreferenceMediaIds, a library style also sends its sample images as references, as in the dashboard.referenceMediaIds: finished images of your library for the model to keep (a person, a product, a look), up to the model’smaxReferenceImages.
usedCredits in the answer); teams on their own provider keys (BYOK) are not charged and need their OpenRouter key. If the generation fails the credits come back, and one still running after 60 minutes is given up on, reads as FAILED and is refunded too.webhook (with optional metadata) to be told once it is COMPLETED or FAILED: see media webhooks. An image usually takes a few seconds to a minute. A FAILED media says why in its error.
curl -X POST "https://api.hooked.so/v1/media/generate/image" \
-H "x-api-key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets", "model": "gpt_image_2", "aspectRatio": "ratio_1_1", "name": "Serum hero shot" }'
const headers = { "x-api-key": process.env.HOOKED_API_KEY, "Content-Type": "application/json" };
const response = await fetch("https://api.hooked.so/v1/media/generate/image", {
method: "POST",
headers,
body: JSON.stringify({
prompt: "A glass serum bottle on wet black stone, soft studio light, water droplets",
model: "gpt_image_2",
aspectRatio: "ratio_1_1",
}),
});
const { data } = await response.json();
// Poll until the image is there
let media;
do {
await new Promise((resolve) => setTimeout(resolve, 3000));
media = (await (await fetch(`https://api.hooked.so/v1/media/${data.mediaId}`, { headers })).json()).data;
} while (media.status === "PROCESSING" || media.status === "PENDING");
console.log(media.status, media.url);
import os
import time
import requests
headers = {"x-api-key": os.environ["HOOKED_API_KEY"]}
data = requests.post(
"https://api.hooked.so/v1/media/generate/image",
headers=headers,
json={
"prompt": "A glass serum bottle on wet black stone, soft studio light, water droplets",
"model": "gpt_image_2",
"aspectRatio": "ratio_1_1",
},
).json()["data"]
while True:
time.sleep(3)
media = requests.get(f"https://api.hooked.so/v1/media/{data['mediaId']}", headers=headers).json()["data"]
if media["status"] in ("COMPLETED", "FAILED"):
break
print(media["status"], media["url"])
{
"success": true,
"message": "Image generation started",
"data": {
"status": "PROCESSING",
"mediaId": "cm4x9r2t50007ab12gh34ij78",
"type": "image",
"model": "gpt_image_2",
"aspectRatio": "ratio_1_1",
"usedCredits": 4
}
}
{
"success": false,
"message": "referenceMediaIds: Nano Banana takes at most 3 reference images"
}
Errors
Every refusal below happens before anything is charged or created.| Status | Message | Cause |
|---|---|---|
| 400 | Invalid JSON body | The body is not JSON. |
| 400 | prompt: Required | No prompt, or an empty one (at most 5000 characters). |
| 400 | model: Invalid model "…" | Not an id of GET /v1/catalog/image-models. |
| 400 | style: … | Not a visual style, or a custom style of another team. |
| 400 | referenceMediaIds: … | An id that is not a finished image of your library, one listed twice, or more than the model takes. |
| 401 | code: "not_authenticated" | Missing or invalid x-api-key. |
| 402 | INSUFFICIENT_CREDITS / subscription_required / missing_credentials | Not enough credits, no live plan, or a BYOK team without its OpenRouter key. |
| 403 | code: "entitlement_required" | Your team does not have the Hooked app product. |
mediaId as imageMediaId to Generate Video.Authorizations
Headers
Makes the request safe to retry. 1-255 printable ASCII characters, one per operation (your job id, or a UUID you store), reused on every retry. Within 24 hours the same key with the same body answers with the first response and the header Idempotent-Replayed: true, without running or charging again. Keys are scoped to your team and the endpoint. 5xx answers and refusals before anything ran (401, 402, 403, 409, 429) are not kept. See Idempotency.
1 - 255^[\x20-\x7E]+$Body
What to generate.
5000An id of GET /v1/catalog/image-models.
ratio_9_16, ratio_1_1, ratio_16_9 An id of GET /v1/catalog/visual-styles (your own custom-<id> included): its look is added to the prompt. Without referenceMediaIds, a library style also sends its sample images as references.
Ids of finished images of your media library for the model to keep. At most the model's maxReferenceImages (400 above it, or when the model takes none).
Name in your library. Defaults to Generated Image - <date> / Generated Video - <date>.
255Any JSON object of your own (max 5 KB). Sent back in the webhook payload.