PortModels
Log in

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:

text
POST https://api.portmodels.com/run

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

json
{
  "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:

bash
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
  }'
FieldMeaning
kindUse video
modelThe public model key, here x-ai/grok-video-1.5
promptRequired description of the motion or camera change
inputs.imageOne publicly reachable source-image URL; not a local path or HTML sharing page
parameters.video_generation_modeUse first_frame_image_to_video for this single-image flow
parameters.durationInteger seconds; start with the tested one-second request
parameters.resolutionStart with the tested 480p setting
parameters.aspect_ratioRequested aspect ratio; image mode can retain the source image's proportions
idempotency_keyStable identifier for this exact request
max_cost_creditsPositive 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_modeInputs
text_to_videoinputs: {}; describe the scene in prompt
first_frame_image_to_videoOne first-frame URL in inputs.image or inputs.input_reference
reference_images_to_video1–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.

bash
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.

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

bash
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 accepted and includes a job ID with status queued, PortModels accepted the request. The app needs to implement result polling; “no request was sent” is misleading in this case. Use ai.media.get with the returned job ID and the same credentials used to create it. Poll every five seconds through queued, submitting, submitted, and running. On succeeded, read job.result[0].url; on failed or cancelled, inspect job.error and job.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, send text_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.