Agent API
Send reference material and instructions; the video agent decides everything (story, shots, prompt, canvas), renders, optionally reviews the result against your instructions and tries again, and hands back one or more videos. No questions are asked back and nothing needs confirming.
Every call is authenticated with an API key. Create one at www.happypony.io → API Keys; the full key is shown once, so store it somewhere safe. Revoke a leaked key there and create a new one.
Authorization: Bearer hp_…
Session cookies are not accepted on these endpoints. Runs belong to the key's
user: another user's run id answers 404.
Base URL below: https://api.happypony.io.
Start a run
POST /api/v1/agent/runs
curl -X POST https://api.happypony.io/api/v1/agent/runs \
-H "Authorization: Bearer $HP_KEY" -H "Content-Type: application/json" \
-d '{
"prompt": "A 15 second ad for this trail shoe: a runner at dawn, energetic, ends on the logo.",
"reference_image_urls": ["https://cdn.example.com/shoe-front.png", "https://cdn.example.com/shoe-side.png"],
"reference_audio_urls": ["https://cdn.example.com/voice.mp3"],
"hints": { "duration": 15, "aspect_ratio": "9:16", "short_edge": 768, "tier": "quality", "language": "en" },
"variants": 2,
"feedback_loops": 1
}'
{
"request_id": "6f1c…",
"status": "IN_QUEUE",
"status_url": "https://api.happypony.io/api/v1/agent/runs/6f1c…/status",
"response_url": "https://api.happypony.io/api/v1/agent/runs/6f1c…",
"cancel_url": "https://api.happypony.io/api/v1/agent/runs/6f1c…/cancel"
}
The response is 202. Unknown fields are rejected (400), so a typo never
silently changes what is rendered.
| Field | Type | Notes |
|---|---|---|
prompt |
string, required | Your instructions, up to 8000 characters. |
reference_image_urls |
string[] ≤ 8 | Subjects, products, style. More than 4 are collated into reference sheets by the agent. |
reference_video_urls |
string[] ≤ 1 | Motion / identity reference. The render model takes one. |
reference_audio_urls |
string[] ≤ 2 | Voice / sound references. |
first_frame_url, last_frame_url |
string | Pin the opening / closing frame. |
hints.duration |
int 5–30 | Seconds. Must fit the canvas (short_edge, aspect_ratio). |
hints.aspect_ratio |
auto 21:9 16:9 4:3 1:1 3:4 9:16 |
|
hints.short_edge |
480 768 1080 |
|
hints.tier |
turbo | quality |
Default: the agent prefers quality. |
hints.language |
string | Language of speech and on-screen text. |
variants |
int 1–4, default 1 | Independent takes on the brief, each its own agent. |
feedback_loops |
int 0–3, default 0 | How many times a finished render may be reviewed and redone. |
A hint you give is binding: the agent cannot override it. Anything you leave out, the agent chooses.
Media URLs
Each URL is downloaded when the run is created and stored as a private
attachment, so a bad URL fails the request (400 invalid_media, naming the
field) instead of a run later. The rules:
https://only, default port, no credentials in the URL.- The host must resolve to public addresses only (loopback, private, link-local, carrier-NAT and multicast ranges are refused, including after redirects). Up to 3 redirects, each re-checked.
- Content type must match the field: images
jpg/png/webp, videomp4/mov/webm/mkv, audiowav/mp3/m4a/aac/ogg/flac.application/octet-streamis accepted when the file extension says so. - Size caps: image 20 MB, audio 30 MB, video 200 MB. Timeouts: 30 s / 45 s / 120 s.
Credits and limits
- Quote and pre-check. The run's worst case is
variants × (1 + feedback_loops) × the price of one video, where one video is priced from your hints; without hints, atquality, 768 short edge and 15 s. If your balance is below it, the request is402:{"error":"insufficient_credits","required":600,"available":420}. The agent is held to that per-video price: it will not propose a dearer video. Givehints.duration/short_edgeif you want longer or larger videos budgeted for. - What is actually charged is only the renders that were submitted, at the
moment each is submitted. A render that fails is refunded automatically. A
run that stops early (satisfied review, cancellation, failure) costs less than
the quote.
credits_used/total_creditsreport the actual amount. - Concurrency. At most 3 active (
IN_QUEUE/IN_PROGRESS) runs per user; the fourth is429 too_many_runs.
Poll for status
GET /api/v1/agent/runs/:id/status
{
"request_id": "6f1c…",
"status": "IN_PROGRESS",
"phase": "rendering",
"created_at": "2026-09-28T10:00:00.000Z",
"started_at": "2026-09-28T10:00:03.000Z",
"completed_at": null,
"variants": [
{ "variant": 1, "state": "running", "phase": "rendering", "iteration": 1, "max_iterations": 2,
"renders_finished": 0, "render_status": "pending", "queue_position": 3, "eta_seconds": 240 },
{ "variant": 2, "state": "running", "phase": "analysing", "iteration": 0, "max_iterations": 2, "renders_finished": 0 }
],
"credits_used": 60,
"quoted_credits": 240,
"queue_position": 3,
"eta_seconds": 240,
"status_url": "…", "response_url": "…", "cancel_url": "…"
}
status uses the batch queue's names:
| Status | Meaning |
|---|---|
IN_QUEUE |
Accepted, not yet picked up by the agent runner. |
IN_PROGRESS |
The agent is working (see phase). |
COMPLETED |
Every variant that could finish has a final video. A variant that failed is listed in variant_errors; the run is still COMPLETED if at least one variant has a video. |
FAILED |
No variant produced a video. error says why. |
CANCELLED |
Cancelled by you. |
phase (run level: the most advanced running variant; null once finished):
| Phase | Meaning |
|---|---|
analysing |
The agent reads your material and writes a plan. |
planning |
The plan is being priced and submitted. |
rendering |
A render is queued or running. queue_position and eta_seconds come from the render queue. |
reviewing |
Frames of a finished render are being checked against your instructions. |
revising |
The agent is rewriting the plan after a review (or a content-check refusal). |
Per variant, iteration counts renders started and max_iterations is
feedback_loops + 1. state is running, done or failed.
Polling every 10–30 s is plenty; there are no webhooks.
Get the result
GET /api/v1/agent/runs/:id
{
"request_id": "6f1c…",
"status": "COMPLETED",
"videos": [
{ "variant": 1, "iteration": 1, "final": false, "url": "https://…signed…", "activity_id": 812,
"credits": 120, "prompt_used": "…",
"review": { "satisfied": false, "summary": "The shoe is not visible in the last shot.", "issues": ["logo missing"], "revision": "end on a close-up of the logo" } },
{ "variant": 1, "iteration": 2, "final": true, "url": "https://…signed…", "activity_id": 815,
"credits": 120, "prompt_used": "…" }
],
"total_credits": 240,
"variant_errors": [{ "variant": 2, "error": "the content check refused every plan (minor): …" }],
"log": [
{ "at": "2026-09-28T10:00:03.100Z", "kind": "run", "message": "Run started", "data": { "variants": 2, "feedback_loops": 1 } },
{ "at": "2026-09-28T10:00:20.400Z", "variant": 1, "kind": "plan", "message": "Proposed \"Trail shoe dawn run\" (120 credits)", "data": { "credits": 120, "settings": { }, "prompt": "…" } }
]
}
videoshas one entry per finished render.final: truemarks the render the run delivers for that variant: the one that satisfied the review, or the last one when the loops ran out (or when a later render failed). Usefinal, not the order.urlis a presigned download link, valid for 2 hours. Fetch the run again for a fresh one.creditsis what that render cost;total_creditsis the run's total after refunds.reviewis present when that render was reviewed. The review sees sampled frames, not the audio.logis every step in order, each{at, variant?, iteration?, kind, message, data?}. Kinds:run,phase,input,agent,plan,job,review,variant,cancel,error. It never contains signed URLs, the agent's system prompt or its private reasoning.
While a run is in progress the same endpoint returns the videos finished so far.
Cancel
PUT /api/v1/agent/runs/:id/cancel
{ "request_id": "6f1c…", "status": "CANCELLED", "renders_cancelled": 1, "renders_still_running": 0 }
The run stops within seconds. Renders still waiting in the render queue are
cancelled there and refunded. A render a worker has already started cannot be
stopped: it finishes, its credits stay spent, and it does not appear in the
run's videos. Cancelling a run that already finished is 409 not_cancellable.
Errors
| Status | error |
When |
|---|---|---|
| 400 | invalid_request |
Bad JSON, unknown field, out-of-range value. details lists every problem. |
| 400 | invalid_media |
A URL was refused or could not be downloaded. field names it, code is blocked, invalid_url, unreachable, too_large, bad_type or http_error. |
| 401 | unauthorized |
Missing, wrong or revoked API key. |
| 402 | insufficient_credits |
Balance below the worst case. Has required and available. |
| 404 | not_found |
No such run, or it is not yours. |
| 409 | not_cancellable |
The run is already finished. |
| 429 | too_many_runs |
Three runs are already active. |
How a run works
- The request is validated, quoted and its media stored. Each variant gets its own private agent conversation (never shown in the chat history) starting from your instructions.
- A runner process claims the run (with a lease it keeps renewing, so a restart or another replica picks it up where it stopped, without submitting a render twice), and drives every variant in parallel: the agent analyses the material and proposes a video; the proposal is confirmed automatically at the quoted price and submitted to the render queue.
- When a render finishes and loops remain, sampled frames plus your instructions and the prompt go to the vision model for a structured critique. If it is satisfied the variant is done; otherwise the agent revises and proposes again.
- A content-check refusal is fed back to the agent, which revises the plan (up to 3 times per variant). Any other hard failure ends that variant only.