Image and video API
This content is not available in your language yet.
The image and video API follows the shape of OpenAI’s image and video endpoints, so the request you already build for those works here with a different base URL and key. Every generation is a task: you can wait for it, poll it, and keep its results in your personal files.
It uses the same keys as the Model API. Base URL:
https://api.harperharbor.com/v1
Getting a key
Section titled “Getting a key”Open Personal API keys in the console, tick Call models directly through the API, and
add the image or video models to the key’s model list, or tick All models. A key can only
call the models on its list, exactly as with chat models; any other model returns 403.
GET /v1/models lists what the key may use. Each entry now carries a type: chat,
image or video.
Generating images
Section titled “Generating images”POST /v1/images/generations takes JSON:
| Field | Required | Meaning |
|---|---|---|
model |
yes | An id with type: "image" from /v1/models |
prompt |
yes | Up to 4,000 characters |
n |
no | Number of images, 1–4. Default 1 |
size |
no | One of the model’s capabilities.sizes, such as 1920x1080. Omit it, or send auto, for the model’s default |
images |
no | Reference images, up to the model’s capabilities.max_reference_images: https:// URLs of PNG, JPEG or WebP images in your own media library, 10 MB each. They keep a character’s face, hair and outfit the same across images and do not change the price |
from openai import OpenAI
client = OpenAI(api_key="hh_live_…", base_url="https://api.harperharbor.com/v1")
result = client.images.generate( model="gemini-3.1-flash-image", prompt="A lighthouse on a cliff at dawn, watercolour", size="1920x1080",)print(result.data[0].url)curl https://api.harperharbor.com/v1/images/generations \ -H "Authorization: Bearer hh_live_…" \ -H "Content-Type: application/json" \ -d '{"model":"gemini-3.1-flash-image","prompt":"A lighthouse on a cliff at dawn, watercolour","size":"1920x1080"}'The request waits up to 85 seconds. When the images are ready within that time you get
200 in the OpenAI shape, plus a harbor field holding the task:
{ "created": 1761700000, "data": [{ "url": "https://…/media-gen/…/0.png" }], "harbor": { "id": "0199…", "object": "image.generation.task", "status": "completed", "credits": 600, "…": "…" }}When the images take longer, you get 202 with the task object instead, and the
generation keeps running. Poll GET /v1/images/tasks/{id} until status is completed
or failed. Every image and video response carries the task id in the X-Harbor-Task-Id
header, failed generations included.
Disconnecting does not cancel a generation. The task still finishes, is charged if it succeeds, and stays readable at its task URL.
Editing images
Section titled “Editing images”POST /v1/images/edits takes multipart/form-data, the way the OpenAI SDK sends it:
| Field | Required | Meaning |
|---|---|---|
image |
yes | One to four image files. image[] is accepted too |
prompt |
yes | What to change |
model |
yes | A model whose capabilities.edits is true |
n, size |
no | As for generations |
result = client.images.edit( model="gemini-3.1-flash-image", image=[open("room.png", "rb")], prompt="Turn the lights on and add rain on the window",)curl https://api.harperharbor.com/v1/images/edits \ -H "Authorization: Bearer hh_live_…" \ -F model=gemini-3.1-flash-image \ -F prompt="Turn the lights on and add rain on the window" \ -F image=@room.pngResponses work exactly like generations: 200 with images, or 202 with a task to poll.
A request body, files included, can be up to 20 MiB.
Images already in your media library don’t need uploading again: pass their URLs as
images to POST /v1/images/generations instead. We read them from your library
ourselves; another site’s URL or someone else’s file returns 400 before anything is
charged.
Generating videos
Section titled “Generating videos”POST /v1/videos takes JSON and always answers 202 with a task, because videos take
minutes.
| Field | Required | Meaning |
|---|---|---|
model |
yes | An id with type: "video" from /v1/models |
prompt |
yes | Up to 4,000 characters |
seconds |
no | One of the model’s capabilities.durations, as "5" or 5. Default 5. duration is accepted too |
resolution |
no | One of the model’s capabilities.resolutions, such as 720p. size is accepted too. Default is the model’s own |
images |
no | Reference images as https:// URLs, up to capabilities.max_reference_images. A single image is accepted too |
aspect_ratio |
no | Such as 16:9 or 9:16 |
curl https://api.harperharbor.com/v1/videos \ -H "Authorization: Bearer hh_live_…" \ -H "Content-Type: application/json" \ -d '{"model":"seedance-2.0-fast","prompt":"Waves rolling onto a beach at sunset","seconds":5,"resolution":"720p"}'curl https://api.harperharbor.com/v1/videos/0199… -H "Authorization: Bearer hh_live_…"
curl -L -o wave.mp4 https://api.harperharbor.com/v1/videos/0199…/content \ -H "Authorization: Bearer hh_live_…"GET /v1/videos/{id} returns the task. GET /v1/videos/{id}/content redirects (302) to
the file once the video is ready, and returns 404 while it is still running, after it
failed, or after the result expired.
Reference image URLs are passed to the model provider as they are; we do not fetch them ourselves, so they must be reachable from the public internet.
The task object
Section titled “The task object”Image tasks and video tasks share one shape:
| Field | Meaning |
|---|---|
id |
Task id |
object |
image.generation.task or video |
model |
The model id you called |
status |
queued, in_progress, completed or failed |
harbor_status |
A finer state: submitting, submitted, storing, succeeded, failed, rejected (refused by the model provider) or submit_unknown (the submission was lost) |
created_at, completed_at |
Unix seconds; completed_at only once the task has finished |
expires_at |
When the results will be deleted, unless you save them |
units |
Images, or seconds of video, that the price applies to |
price_tier |
The size or resolution tier the price was taken from |
credits |
Credits charged. It stays 0 unless the task completed |
data |
[{ "url": … }] once results are stored |
error |
{ "code", "message" } when the task failed |
saved_asset_ids |
Personal file ids, once the results were saved |
Keeping results
Section titled “Keeping results”Result URLs point at our storage, not the model provider’s, and they are kept for 7 days. To keep them longer, copy them into your account’s personal files:
curl -X POST https://api.harperharbor.com/v1/media/tasks/0199…/save \ -H "Authorization: Bearer hh_live_…"{ "id": "0199…", "object": "media.saved", "asset_ids": ["0199…"] }Saved files count toward your 2 GiB personal storage and are not deleted after 7 days.
Saving again returns the same files without copying them twice. Saving a task that has
not completed returns 409; after the 7 days it returns 410, so download or save what
you want to keep before then.
Retrying safely
Section titled “Retrying safely”Send an Idempotency-Key header on generation requests. Repeating a request with the same
key from the same API key returns the original task instead of starting a new one, and
it is charged once. Use a new key when you deliberately want a second generation, for
example after submit_unknown.
Listing media models
Section titled “Listing media models”Media entries in GET /v1/models add pricing units, capabilities and a content policy:
{ "id": "gemini-3.1-flash-image", "object": "model", "owned_by": "harperharbor", "type": "image", "pricing": { "unit": "credits_per_image", "credits": { "1080p": 600, "1440p": 900, "2160p": 1200 } }, "capabilities": { "sizes": ["1920x1080", "1080x1920", "2560x1440", "…"], "edits": true, "max_images": 4, "max_reference_images": 4 }, "content_policy": { "nsfw": "suggestive", "real_person": true }}The prices above are illustrative; read the live ones from /v1/models or the console.
Video entries use "unit": "credits_per_second" with prices per resolution, and their
capabilities hold resolutions, durations and max_reference_images.
pricing.credits is keyed by tier: for images each tier covers a group of sizes, for
videos each tier is a resolution. A task’s price_tier tells you which one applied.
What a generation costs
Section titled “What a generation costs”Images are charged per image, videos per second, at the tier’s price times units.
Credits are held when the task starts and charged only when it succeeds. Refusals by the
model provider, failed generations and lost submissions (submit_unknown) cost nothing,
and neither do requests we refuse before submitting.
Spending counts toward the key’s monthly credit limit, the same limit chat calls use.
Content policy
Section titled “Content policy”Each model’s content_policy comes from our own tests against that model, not from what
the provider advertises.
| Field | Values |
|---|---|
nsfw |
none: adult content is refused. suggestive: adult, non-explicit content is accepted. explicit: explicit adult content is accepted |
real_person |
true when the model accepts photos of real people as reference images |
Prompts are screened before they reach the model provider. On a model with
nsfw: "none", an adult prompt is refused up front with 400 and is not charged. Models
that allow adult content pass such prompts on, and the model provider makes the final
decision. Sexual content involving minors is refused on every model.
Screening reads the prompt only; reference images are checked by the model provider.
Errors
Section titled “Errors”Errors use the OpenAI envelope {"error": {"type", "code", "message", "param"}}.
| Status | What happened | What to do |
|---|---|---|
| 400 | A field is missing or not supported by this model; the message names it | Check the model’s capabilities in /v1/models |
| 400 | code: content_policy_violation: screening refused the prompt on this model, or the model provider refused it. Not charged |
Change the prompt, or pick a model whose content_policy fits |
| 401 | The key is missing, revoked or expired | Create a new key in the console |
| 402 | Out of credits, or the key hit its monthly limit | Top up, or raise the limit |
| 403 | The model is not on this key’s list | Add the model to the key in the console |
| 404 | No such model, or no such task for your account | Take the id from /v1/models |
| 409 | Saving a task that has not completed | Wait for completed, then save |
| 410 | The result was deleted after 7 days | Generate it again |
| 413 | The result is larger than a personal file can be | Download it instead |
| 502 | The model provider failed, or the submission was lost (code: submit_unknown). Not charged |
Retry; after submit_unknown, use a new Idempotency-Key |
| 503 | Content screening is briefly unavailable (only models that allow adult content wait for it), or image and video generation is not available yet. Not charged | Retry shortly |
| 507 | Your personal storage is full | Delete files, or download the result instead |