Image-to-video
Generate video from text, first-frame images, or reference images with persistent jobs, status polling, and credit reservations.
Use x-ai/grok-video-1.5 or bytedance/seedance-1.0-pro-fast to animate a
source image. Grok has the lower minimum clip cost; Seedance has the lower
480p per-second rate. Video generation runs as
a persistent job: submit once, keep the returned job ID, and poll until the
job finishes. These requests use the action API:
POST https://api.portmodels.com/runThere is no OpenAI-compatible /openai/v1/videos endpoint for this flow.
Authentication#
Use a Connect access token (pmc_) with models.run consent when your app
runs video for a connected user. Existing valid models.run grants already
cover video creation, polling, listing and cancellation; a separate video
scope is not required. Jobs belong to the connected user and verified app.
A PortModels API key (pm_) with video permission or the short-lived
account token returned by auth.login is also supported. Send the
credential as Authorization: Bearer <token>. The account needs video access
(ai_video) and enough credits for the reservation. API-key spending limits
apply to reservations; unused credits are returned after settlement or
cancellation.
To sign in, send this JSON to POST /run from your trusted client:
{
"action": "auth.login",
"email": "[email protected]",
"password": "YOUR_PASSWORD"
}Keep the returned token private and use it as PORTMODELS_ACCOUNT_TOKEN in
the examples below. Refresh an expired account token with auth.refresh;
see Your account. Do not embed account
passwords or tokens in shared code.
Animate an image#
Replace the example image URL with a publicly reachable HTTPS image you have permission to use. This request uses exactly one source image, a one-second duration, and 480p output:
curl https://api.portmodels.com/run \
-H "Authorization: Bearer $PORTMODELS_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action": "ai.media.create",
"kind": "video",
"model": "x-ai/grok-video-1.5",
"prompt": "Slow camera push-in with gentle movement. Preserve the scene. No text.",
"inputs": {
"image": "https://your-host.example/source.jpg"
},
"parameters": {
"video_generation_mode": "first_frame_image_to_video",
"duration": 1,
"resolution": "480p",
"aspect_ratio": "16:9"
},
"wallet_kind": "standard",
"idempotency_key": "image-animation-001",
"max_cost_credits": 0.05
}'| Field | Meaning |
|---|---|
kind | Use video |
model | The public model key, here x-ai/grok-video-1.5 |
prompt | Required description of the motion or camera change |
inputs.image | One publicly reachable source-image URL; not a local path or HTML sharing page |
parameters.video_generation_mode | Use first_frame_image_to_video for this single-image flow |
parameters.duration | Integer seconds; start with the tested one-second request |
parameters.resolution | Start with the tested 480p setting |
parameters.aspect_ratio | Requested aspect ratio; image mode can retain the source image's proportions |
idempotency_key | Stable identifier for this exact request |
max_cost_credits | Positive maximum reservation, not a price quote |
An accepted response includes accepted: true and a job object with an
id, status, and billing. Admission reserves the maximum credits before
generation starts. Acceptance alone does not mean the video has succeeded.
Text and reference-image modes#
The single-image example above is one configuration, not a restriction on
video capabilities. Send model-specific media fields in inputs and generation
options in parameters. These fields are forwarded to the selected model;
PortModels does not restrict requests to the options shown in this guide or
silently discard additional input roles and parameters. Supported combinations
are determined by the selected model.
For x-ai/grok-video-1.5, select the mode explicitly:
parameters.video_generation_mode | Inputs |
|---|---|
text_to_video | inputs: {}; describe the scene in prompt |
first_frame_image_to_video | One first-frame URL in inputs.image or inputs.input_reference |
reference_images_to_video | 1–7 reference-image URLs in inputs.reference_images |
Reference images can be URL strings or objects containing url. Use reference
mode explicitly even for one reference image. A first-frame image and a
reference image have different roles; choose the mode matching your input
instead of combining fields from different examples.
curl https://api.portmodels.com/run \
-H "Authorization: Bearer $PORTMODELS_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action": "ai.media.create",
"kind": "video",
"model": "x-ai/grok-video-1.5",
"prompt": "Keep the subject and clothing consistent while the subject turns toward the camera.",
"inputs": {
"reference_images": [
{"url": "https://your-host.example/front.jpg"},
{"url": "https://your-host.example/side.jpg"}
]
},
"parameters": {
"video_generation_mode": "reference_images_to_video",
"duration": 1,
"resolution": "720p",
"aspect_ratio": "16:9"
},
"wallet_kind": "standard",
"idempotency_key": "reference-video-001",
"max_cost_credits": 0.10
}'For text-only generation, use the same envelope with inputs: {} and
parameters.video_generation_mode: "text_to_video". Saved video PortModels,
including abrans/video, accept the same input fields and options supported
by their selected base model. Replace model with the saved PortModel ID.
The reference example illustrates the model's request contract; it is not a claim that a paid reference-image generation was verified with these settings. Support for first/last-frame control, additional references, audio, duration, and resolution varies by model. Send the fields that model supports; forwarding a field does not establish that every model implements it.
For a new Grok job, PortModels checks published pricing when it matches the
requested settings. If a matching quote exceeds max_cost_credits, the error
gives the required minimum. Approve a higher ceiling or reduce the settings;
the API never raises the budget automatically. Settings without a matching
price entry still use the explicit reservation ceiling. An unavailable pricing
service returns HTTP 503 without creating a job. Retrieving an existing job
with the same idempotency key does not require another quote.
Resolution and aspect-ratio parameters are model settings, not a resize or
crop guarantee. The verified 480p request produced a 672 × 448 video from a
3:2 source image, despite requesting 16:9. Check the downloaded file's actual
dimensions if your application needs an exact canvas; do not assume the API
crops an input image to the requested ratio.
If the submission response is lost, retry the identical request with the same idempotency key to retrieve the original job without a second reservation. Use a new key for a different image, prompt, or parameter set.
Lower per-second cost with Seedance#
For clips of two seconds or longer, consider
bytedance/seedance-1.0-pro-fast. Its 480p configuration has a lower
per-second cost than Grok's. The source-image field differs: use
inputs.image_urls as an array, not inputs.image. Do not carry over Grok's
video_generation_mode field.
curl https://api.portmodels.com/run \
-H "Authorization: Bearer $PORTMODELS_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action": "ai.media.create",
"kind": "video",
"model": "bytedance/seedance-1.0-pro-fast",
"prompt": "Slow camera push-in with gentle movement. Preserve the scene. No text.",
"inputs": {
"image_urls": ["https://your-host.example/source.jpg"]
},
"parameters": {
"duration": 2,
"resolution": "480p",
"aspect_ratio": "16:9"
},
"wallet_kind": "standard",
"idempotency_key": "seedance-image-animation-001",
"max_cost_credits": 0.05
}'Use the same status polling and billing checks below for either model. Change the idempotency key whenever you change the model or request contents.
The two-second image-to-video request was verified on 2026-10-08 at
0.0176 credits ($0.0176), equivalent to 0.0088 credits per requested
second. The downloaded sample was approximately two seconds long at
1920 × 1088 despite the 480p setting. As with Grok, inspect the result's
dimensions rather than treating the resolution parameter as an exact canvas.
Poll and download#
Use the job ID returned by ai.media.create:
curl https://api.portmodels.com/run \
-H "Authorization: Bearer $PORTMODELS_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"action":"ai.media.get","job_id":"YOUR_JOB_ID"}'A video can be ready before its final cost is confirmed. In that case the
job stays running at progress 100 while the API confirms billing; keep
polling the same job. If cost confirmation remains unavailable, the completed
video can carry reservation_state: "reconciliation_required" and no final
charge. Keep the job ID and contact support rather than resubmitting it.
Poll at a modest interval, such as every five seconds. Keep polling through
queued, submitting, submitted, and running. On succeeded, read the
download URL from job.result[0].url. A result entry can also include
format: "mp4".
On failed or cancelled, inspect job.error and job.billing before
creating another job. Requests to /run can return HTTP 200 with a JSON
error; check the response body as well as the HTTP status.
Download finished media promptly. Hosted URLs may expire; store the actual file if you need to keep it. See File storage.
Cost and billing#
The account balance uses credits, where one credit equals one USD. Cost depends on the selected model, duration, resolution, and inputs. A lower per-second rate does not necessarily mean a lower total job cost.
The single-image, one-second, 480p Grok request above was verified on 2026-10-08 at 0.0102 credits ($0.0102). It was the lowest listed total video-generation cost in the price comparison used for this setup. This is a tested configuration and historical charge, not a promise that every video costs that amount or that prices will stay unchanged.
max_cost_credits: 0.05 reserves 0.05 credits; it does not mean the job
costs 0.05 credits. Read the actual charge from
job.billing.settled_credits once reservation_state is settled. Unused
reserved credits are refunded when settlement completes.
A video can become available before its final cost. While billing is pending,
the job can remain running at 100% with a held reservation. If the cost is
still unavailable after the bounded wait, the job can succeed with
reservation_state: "reconciliation_required" and no confirmed settled
amount. Do not interpret the reserved maximum as a final charge or submit a
duplicate job to resolve billing; contact support with the job ID.
Other video models#
google/veo-3.1-lite is also available through ai.media.create. The verified
text-to-video request uses inputs: {} and
parameters: {"duration":8,"resolution":"720p","size":"16:9"}.
That configuration was verified at 0.07 credits per generation. This is a
tested cost, not a guarantee of future pricing, and does not establish
image-to-video support for that model.
Troubleshooting#
- “Video service not connected” / “result polling is not implemented yet”:
if the message also says
ai.media.create acceptedand includes a job ID with statusqueued, PortModels accepted the request. The app needs to implement result polling; “no request was sent” is misleading in this case. Useai.media.getwith the returned job ID and the same credentials used to create it. Poll every five seconds throughqueued,submitting,submitted, andrunning. Onsucceeded, readjob.result[0].url; onfailedorcancelled, inspectjob.errorandjob.billing. Retrieve the existing job before submitting another request: it may still finish, and retrying creation can generate a second video and reserve more credits. - Access denied: use a Connect token with
models.run, an API key with video permission, or a valid account token. The account must have video access. - Image cannot be read: use a public HTTPS URL that returns image bytes, not a login page or an HTML sharing page. Do not send a local file path.
- Request parameters rejected: check the selected model's input format and
supported combinations. For Grok reference images, explicitly send
reference_images_to_video; for text-only generation, sendtext_to_video. - Insufficient credits: the available balance must cover the reservation, even when the expected final cost is smaller.
- Same key, changed request: restore the original request or choose a new idempotency key for a genuinely new generation.
- Generation or billing failed: retain the job ID for support. Do not retry a paid generation automatically under a new key.
Uncertain submission#
If a job reports error.code: "submission_unknown", its submission could not
be confirmed. Keep its job ID and contact support; do not automatically
create another generation. Its reservation remains marked
reconciliation_required until the outcome is checked. Temporary polling
failures are handled with increasing delays on the existing job.
Send X-Client-Request-ID with a bounded identifier when available. Known
input errors include an actionable error.message, error.code, and
correlation log_id. Include those identifiers and the job ID in a complaint;
exclude credentials, full prompts and original images.