Happy Pony API Get an API key →

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.

Endpoint What it does
POST /api/v1/agent/runs/uploads Upload a local file; returns a URL to use in a run
POST /api/v1/agent/runs Start a run
GET /api/v1/agent/runs/:id/status Poll for status
GET /api/v1/agent/runs/:id Get the result: videos, credits, parameters, log
PUT /api/v1/agent/runs/:id/cancel Cancel

Quick start

The example below is a complete run: two reference images, one prompt, two independent takes (variants: 2), each reviewed and redone once (feedback_loops: 1). You need curl and jq; the full examples at the end of this section do the same in Python, TypeScript and Node.js.

1. Get an API key

Create a key at www.happypony.io → API Keys and export it. The full key is shown once.

export HP_KEY=hp_…

2. Your images

Reference images are passed by URL. Either:

3. Start the run

RUN=$(curl -s -X POST https://api.happypony.io/api/v1/agent/runs \
  -H "Authorization: Bearer $HP_KEY" -H "Content-Type: application/json" \
  -d '{
    "prompt": "The two girls have a bar fight",
    "reference_image_urls": [
      "https://resource.pipelet.net/images/test/selfie3.png",
      "https://resource.pipelet.net/images/test/selfie5.png"
    ],
    "variants": 2,
    "feedback_loops": 1
  }')
echo "$RUN"
STATUS_URL=$(jq -r .status_url <<<"$RUN"); RESPONSE_URL=$(jq -r .response_url <<<"$RUN")

The 202 response carries request_id, status_url, response_url and cancel_url. Use the last three as they are.

4. Poll the status

Every 15 seconds or so, until status is COMPLETED, FAILED or CANCELLED (a run takes several minutes):

curl -s -H "Authorization: Bearer $HP_KEY" "$STATUS_URL" | jq '{status, phase, credits_used}'

5. Get the result and download the videos

curl -s -H "Authorization: Bearer $HP_KEY" "$RESPONSE_URL" > result.json
jq -r '.videos[] | select(.final) | "\(.variant) \(.url)"' result.json |
  while read -r v url; do curl -s -o "video-$v.mp4" "$url"; done

videos[].final marks the render delivered for each variant; total_credits is what the run cost. The video urls are presigned for 2 hours: to download later, fetch response_url again for fresh links.

Full examples

Each one reads the key from HP_KEY, starts the run above, polls every 15 seconds, prints the credits and saves every video. upload() is only needed for a local file: pass its result in reference_image_urls instead of a sample URL.

curl (bash)

#!/usr/bin/env bash
# Needs curl and jq. Usage: HP_KEY=hp_… ./run.sh
set -euo pipefail
API=https://api.happypony.io/api/v1/agent/runs
AUTH="Authorization: Bearer $HP_KEY"

call() {  # prints the body; on an HTTP error prints it to stderr and fails
  local out code body
  out=$(curl -sS -w '\n%{http_code}' -H "$AUTH" "$@")
  code=${out##*$'\n'}; body=${out%$'\n'*}
  [[ $code == 2* ]] || { echo "HTTP $code: $body" >&2; return 1; }
  echo "$body"
}
upload() { call -X POST "$API/uploads" -F "file=@$1" | jq -r .url; }  # optional: local file → URL

RUN=$(call -X POST "$API" -H 'Content-Type: application/json' -d '{
  "prompt": "The two girls have a bar fight",
  "reference_image_urls": [
    "https://resource.pipelet.net/images/test/selfie3.png",
    "https://resource.pipelet.net/images/test/selfie5.png"],
  "variants": 2, "feedback_loops": 1}')
STATUS_URL=$(jq -r .status_url <<<"$RUN"); RESPONSE_URL=$(jq -r .response_url <<<"$RUN")

while :; do
  STATUS=$(call "$STATUS_URL" | jq -r .status); echo "status: $STATUS"
  case $STATUS in COMPLETED|FAILED|CANCELLED) break ;; esac
  sleep 15
done

call "$RESPONSE_URL" > result.json
echo "total credits: $(jq .total_credits result.json)"
jq -r '.videos[] | "\(.variant) \(.iteration) \(.final) \(.url)"' result.json |
  while read -r v i final url; do
    curl -sS --fail -o "video-v$v-i$i.mp4" "$url" && echo "saved video-v$v-i$i.mp4 (final=$final)"
  done

Python

# pip install requests   |   HP_KEY=hp_… python run.py
import os, sys, time, requests

API = "https://api.happypony.io/api/v1/agent/runs"
HEADERS = {"Authorization": f"Bearer {os.environ['HP_KEY']}"}

def check(r: requests.Response) -> dict:
    if not r.ok:
        sys.exit(f"HTTP {r.status_code}: {r.text}")
    return r.json()

def upload(path: str) -> str:  # optional: local file -> presigned URL (valid 2 h)
    with open(path, "rb") as f:
        return check(requests.post(f"{API}/uploads", headers=HEADERS, files={"file": f}))["url"]

run = check(requests.post(API, headers=HEADERS, json={
    "prompt": "The two girls have a bar fight",
    "reference_image_urls": [
        "https://resource.pipelet.net/images/test/selfie3.png",
        "https://resource.pipelet.net/images/test/selfie5.png",
    ],
    "variants": 2,
    "feedback_loops": 1,
}))

while True:
    status = check(requests.get(run["status_url"], headers=HEADERS))["status"]
    print("status:", status)
    if status in ("COMPLETED", "FAILED", "CANCELLED"):
        break
    time.sleep(15)

result = check(requests.get(run["response_url"], headers=HEADERS))
print("total credits:", result["total_credits"])
for v in result["videos"]:
    name = f"video-v{v['variant']}-i{v['iteration']}.mp4"
    r = requests.get(v["url"])  # presigned: no auth header
    r.raise_for_status()
    open(name, "wb").write(r.content)
    print("saved", name, "final" if v["final"] else "")

TypeScript

// Node 18+ (or Deno/Bun). Run with: HP_KEY=hp_… npx tsx run.ts
import { readFile, writeFile } from "node:fs/promises";
import { basename } from "node:path";

const API = "https://api.happypony.io/api/v1/agent/runs";
const AUTH = { Authorization: `Bearer ${process.env.HP_KEY}` };

interface Created { request_id: string; status_url: string; response_url: string; cancel_url: string }
type Status = "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | "CANCELLED";
interface Video { variant: number; iteration: number; final: boolean; url: string; credits: number; prompt_used: string }
interface Result { status: Status; videos: Video[]; total_credits: number }

async function call<T>(url: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(url, { ...init, headers: { ...AUTH, ...init.headers } });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return (await res.json()) as T;
}

// Optional: upload a local file, get a presigned URL (valid 2 h) for reference_image_urls.
async function upload(path: string): Promise<string> {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)]), basename(path));
  return (await call<{ url: string }>(`${API}/uploads`, { method: "POST", body: form })).url;
}

const run = await call<Created>(API, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "The two girls have a bar fight",
    reference_image_urls: [
      "https://resource.pipelet.net/images/test/selfie3.png",
      "https://resource.pipelet.net/images/test/selfie5.png",
    ],
    variants: 2,
    feedback_loops: 1,
  }),
});

let status: Status;
do {
  status = (await call<{ status: Status }>(run.status_url)).status;
  console.log("status:", status);
  if (["COMPLETED", "FAILED", "CANCELLED"].includes(status)) break;
  await new Promise((r) => setTimeout(r, 15_000));
} while (true);

const result = await call<Result>(run.response_url);
console.log("total credits:", result.total_credits);
for (const v of result.videos) {
  const name = `video-v${v.variant}-i${v.iteration}.mp4`;
  const res = await fetch(v.url); // presigned: no auth header
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
  await writeFile(name, Buffer.from(await res.arrayBuffer()));
  console.log("saved", name, v.final ? "(final)" : "");
}

Node.js

// Node 18+ (built-in fetch / FormData / Blob). Save as run.mjs: HP_KEY=hp_… node run.mjs
import { readFileSync, writeFileSync } from "node:fs";
import { basename } from "node:path";

const API = "https://api.happypony.io/api/v1/agent/runs";
const AUTH = { Authorization: `Bearer ${process.env.HP_KEY}` };

async function call(url, init = {}) {
  const res = await fetch(url, { ...init, headers: { ...AUTH, ...init.headers } });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}

// Optional: upload a local file, get a presigned URL (valid 2 h) for reference_image_urls.
async function upload(path) {
  const form = new FormData();
  form.append("file", new Blob([readFileSync(path)]), basename(path));
  return (await call(`${API}/uploads`, { method: "POST", body: form })).url;
}

const run = await call(API, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "The two girls have a bar fight",
    reference_image_urls: [
      "https://resource.pipelet.net/images/test/selfie3.png",
      "https://resource.pipelet.net/images/test/selfie5.png",
    ],
    variants: 2,
    feedback_loops: 1,
  }),
});

let status;
while (true) {
  ({ status } = await call(run.status_url));
  console.log("status:", status);
  if (["COMPLETED", "FAILED", "CANCELLED"].includes(status)) break;
  await new Promise((r) => setTimeout(r, 15_000));
}

const result = await call(run.response_url);
console.log("total credits:", result.total_credits);
for (const v of result.videos) {
  const name = `video-v${v.variant}-i${v.iteration}.mp4`;
  const res = await fetch(v.url); // presigned: no auth header
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
  writeFileSync(name, Buffer.from(await res.arrayBuffer()));
  console.log("saved", name, v.final ? "(final)" : "");
}

Upload a file

POST /api/v1/agent/runs/uploads (multipart/form-data)

For media that isn't at a public URL: upload it first, then pass the returned url in any *_url field of Start a run — reference_image_urls, reference_video_urls, reference_audio_urls, first_frame_url or last_frame_url. Upload one file per request.

Form field Required Notes
file yes The image, video or audio file.
kind no image, video or audio. Inferred from the file's content type (or its extension, when the type is application/octet-stream) if left out.
curl -X POST https://api.happypony.io/api/v1/agent/runs/uploads \
  -H "Authorization: Bearer $HP_KEY" \
  -F file=@selfie.png
{ "url": "https://…signed…", "kind": "image", "expires_in": 7200 }

The response is 201.

Status error When
400 invalid_request No multipart file field.
400 invalid_media code is bad_type (unsupported type, or not the kind you named) or too_large; field is file.
401 unauthorized Missing, wrong or revoked API key.
502 media_failed The file could not be stored. Retry.

Two local images into one run:

up() { curl -s -X POST https://api.happypony.io/api/v1/agent/runs/uploads \
         -H "Authorization: Bearer $HP_KEY" -F "file=@$1" | jq -r .url; }
A=$(up selfie3.png); B=$(up selfie4.png)
curl -X POST https://api.happypony.io/api/v1/agent/runs \
  -H "Authorization: Bearer $HP_KEY" -H "Content-Type: application/json" \
  -d "{\"prompt\": \"The two girls have a bar fight\", \"reference_image_urls\": [\"$A\", \"$B\"], \"variants\": 2, \"feedback_loops\": 1}"

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 quality. Every render in the run uses this tier; the agent never switches it.
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

Files you uploaded are passed the same way, by their URL. 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:

Credits and limits

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,
      "parameters": { "title": "Trail shoe dawn run", "mode": "from references", "tier": "quality", "duration": 15,
                      "short_edge": 768, "aspect_ratio": "9:16", "inputs": "reference images img1, img2", "seed": 1804289383,
                      "brief": "A runner laces the shoe at dawn, then…" },
      "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, "parameters": { "…": "…" }, "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": "…" } }
  ]
}

While a run is in progress the same endpoint returns the videos finished so far.

Runs also appear in the video agent's chat history on happypony.io, marked API: one read-only chat per run, with your instructions and each video's plan, render and review in order.

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.
502 media_failed The media could not be stored. Retry.

How a run works

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Agent API

发送参考素材和指令,视频 agent 会自行决定一切(故事、分镜、提示词、画布),渲染视频,并可按你的指令审查结果、重新渲染,最后交付一个或多个视频。它不会向你反问,也无需任何确认。

每次调用都需要 API key 鉴权。在 www.happypony.io → API Keys 创建;完整的 key 只显示一次,请妥善保存。key 泄露后请在同一页面吊销并重新创建。

Authorization: Bearer hp_…

这些接口不接受 session cookie。run 属于该 key 所属的用户:使用他人的 run id 会返回 404。

下文的 Base URL 为 https://api.happypony.io。

接口 作用
POST /api/v1/agent/runs/uploads 上传本地文件;返回可在 run 中使用的 URL
POST /api/v1/agent/runs 发起 run
GET /api/v1/agent/runs/:id/status 轮询状态
GET /api/v1/agent/runs/:id 获取结果:视频、积分、参数、日志
PUT /api/v1/agent/runs/:id/cancel 取消

快速开始

下面的示例是一次完整的 run:两张参考图、一条提示词、两个相互独立的版本(variants: 2),每个版本审查后最多重做一次(feedback_loops: 1)。你需要 curl 和 jq;本节末尾的完整示例用 Python、TypeScript 和 Node.js 做了同样的事。

1. 获取 API key

在 www.happypony.io → API Keys 创建 key 并导出为环境变量。完整的 key 只显示一次。

export HP_KEY=hp_…

2. 准备图片

参考图通过 URL 传入。两种方式任选:

3. 发起 run

RUN=$(curl -s -X POST https://api.happypony.io/api/v1/agent/runs \
  -H "Authorization: Bearer $HP_KEY" -H "Content-Type: application/json" \
  -d '{
    "prompt": "The two girls have a bar fight",
    "reference_image_urls": [
      "https://resource.pipelet.net/images/test/selfie3.png",
      "https://resource.pipelet.net/images/test/selfie5.png"
    ],
    "variants": 2,
    "feedback_loops": 1
  }')
echo "$RUN"
STATUS_URL=$(jq -r .status_url <<<"$RUN"); RESPONSE_URL=$(jq -r .response_url <<<"$RUN")

202 响应包含 request_id、status_url、response_url 和 cancel_url。后三个 URL 直接原样使用即可。

4. 轮询状态

大约每 15 秒一次,直到 status 变为 COMPLETED、FAILED 或 CANCELLED(一次 run 需要几分钟):

curl -s -H "Authorization: Bearer $HP_KEY" "$STATUS_URL" | jq '{status, phase, credits_used}'

5. 获取结果并下载视频

curl -s -H "Authorization: Bearer $HP_KEY" "$RESPONSE_URL" > result.json
jq -r '.videos[] | select(.final) | "\(.variant) \(.url)"' result.json |
  while read -r v url; do curl -s -o "video-$v.mp4" "$url"; done

videos[].final 标记每个版本最终交付的那次渲染;total_credits 是这次 run 的花费。视频 url 的预签名有效期为 2 小时:如需稍后下载,请重新请求 response_url 获取新的链接。

完整示例

每个示例都从 HP_KEY 读取 key,发起上面的 run,每 15 秒轮询一次,打印积分并保存所有视频。upload() 仅在使用本地文件时需要:把它的返回值放进 reference_image_urls,代替示例 URL 即可。

curl (bash)

#!/usr/bin/env bash
# Needs curl and jq. Usage: HP_KEY=hp_… ./run.sh
set -euo pipefail
API=https://api.happypony.io/api/v1/agent/runs
AUTH="Authorization: Bearer $HP_KEY"

call() {  # prints the body; on an HTTP error prints it to stderr and fails
  local out code body
  out=$(curl -sS -w '\n%{http_code}' -H "$AUTH" "$@")
  code=${out##*$'\n'}; body=${out%$'\n'*}
  [[ $code == 2* ]] || { echo "HTTP $code: $body" >&2; return 1; }
  echo "$body"
}
upload() { call -X POST "$API/uploads" -F "file=@$1" | jq -r .url; }  # optional: local file → URL

RUN=$(call -X POST "$API" -H 'Content-Type: application/json' -d '{
  "prompt": "The two girls have a bar fight",
  "reference_image_urls": [
    "https://resource.pipelet.net/images/test/selfie3.png",
    "https://resource.pipelet.net/images/test/selfie5.png"],
  "variants": 2, "feedback_loops": 1}')
STATUS_URL=$(jq -r .status_url <<<"$RUN"); RESPONSE_URL=$(jq -r .response_url <<<"$RUN")

while :; do
  STATUS=$(call "$STATUS_URL" | jq -r .status); echo "status: $STATUS"
  case $STATUS in COMPLETED|FAILED|CANCELLED) break ;; esac
  sleep 15
done

call "$RESPONSE_URL" > result.json
echo "total credits: $(jq .total_credits result.json)"
jq -r '.videos[] | "\(.variant) \(.iteration) \(.final) \(.url)"' result.json |
  while read -r v i final url; do
    curl -sS --fail -o "video-v$v-i$i.mp4" "$url" && echo "saved video-v$v-i$i.mp4 (final=$final)"
  done

Python

# pip install requests   |   HP_KEY=hp_… python run.py
import os, sys, time, requests

API = "https://api.happypony.io/api/v1/agent/runs"
HEADERS = {"Authorization": f"Bearer {os.environ['HP_KEY']}"}

def check(r: requests.Response) -> dict:
    if not r.ok:
        sys.exit(f"HTTP {r.status_code}: {r.text}")
    return r.json()

def upload(path: str) -> str:  # optional: local file -> presigned URL (valid 2 h)
    with open(path, "rb") as f:
        return check(requests.post(f"{API}/uploads", headers=HEADERS, files={"file": f}))["url"]

run = check(requests.post(API, headers=HEADERS, json={
    "prompt": "The two girls have a bar fight",
    "reference_image_urls": [
        "https://resource.pipelet.net/images/test/selfie3.png",
        "https://resource.pipelet.net/images/test/selfie5.png",
    ],
    "variants": 2,
    "feedback_loops": 1,
}))

while True:
    status = check(requests.get(run["status_url"], headers=HEADERS))["status"]
    print("status:", status)
    if status in ("COMPLETED", "FAILED", "CANCELLED"):
        break
    time.sleep(15)

result = check(requests.get(run["response_url"], headers=HEADERS))
print("total credits:", result["total_credits"])
for v in result["videos"]:
    name = f"video-v{v['variant']}-i{v['iteration']}.mp4"
    r = requests.get(v["url"])  # presigned: no auth header
    r.raise_for_status()
    open(name, "wb").write(r.content)
    print("saved", name, "final" if v["final"] else "")

TypeScript

// Node 18+ (or Deno/Bun). Run with: HP_KEY=hp_… npx tsx run.ts
import { readFile, writeFile } from "node:fs/promises";
import { basename } from "node:path";

const API = "https://api.happypony.io/api/v1/agent/runs";
const AUTH = { Authorization: `Bearer ${process.env.HP_KEY}` };

interface Created { request_id: string; status_url: string; response_url: string; cancel_url: string }
type Status = "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | "CANCELLED";
interface Video { variant: number; iteration: number; final: boolean; url: string; credits: number; prompt_used: string }
interface Result { status: Status; videos: Video[]; total_credits: number }

async function call<T>(url: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(url, { ...init, headers: { ...AUTH, ...init.headers } });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return (await res.json()) as T;
}

// Optional: upload a local file, get a presigned URL (valid 2 h) for reference_image_urls.
async function upload(path: string): Promise<string> {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)]), basename(path));
  return (await call<{ url: string }>(`${API}/uploads`, { method: "POST", body: form })).url;
}

const run = await call<Created>(API, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "The two girls have a bar fight",
    reference_image_urls: [
      "https://resource.pipelet.net/images/test/selfie3.png",
      "https://resource.pipelet.net/images/test/selfie5.png",
    ],
    variants: 2,
    feedback_loops: 1,
  }),
});

let status: Status;
do {
  status = (await call<{ status: Status }>(run.status_url)).status;
  console.log("status:", status);
  if (["COMPLETED", "FAILED", "CANCELLED"].includes(status)) break;
  await new Promise((r) => setTimeout(r, 15_000));
} while (true);

const result = await call<Result>(run.response_url);
console.log("total credits:", result.total_credits);
for (const v of result.videos) {
  const name = `video-v${v.variant}-i${v.iteration}.mp4`;
  const res = await fetch(v.url); // presigned: no auth header
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
  await writeFile(name, Buffer.from(await res.arrayBuffer()));
  console.log("saved", name, v.final ? "(final)" : "");
}

Node.js

// Node 18+ (built-in fetch / FormData / Blob). Save as run.mjs: HP_KEY=hp_… node run.mjs
import { readFileSync, writeFileSync } from "node:fs";
import { basename } from "node:path";

const API = "https://api.happypony.io/api/v1/agent/runs";
const AUTH = { Authorization: `Bearer ${process.env.HP_KEY}` };

async function call(url, init = {}) {
  const res = await fetch(url, { ...init, headers: { ...AUTH, ...init.headers } });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}

// Optional: upload a local file, get a presigned URL (valid 2 h) for reference_image_urls.
async function upload(path) {
  const form = new FormData();
  form.append("file", new Blob([readFileSync(path)]), basename(path));
  return (await call(`${API}/uploads`, { method: "POST", body: form })).url;
}

const run = await call(API, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    prompt: "The two girls have a bar fight",
    reference_image_urls: [
      "https://resource.pipelet.net/images/test/selfie3.png",
      "https://resource.pipelet.net/images/test/selfie5.png",
    ],
    variants: 2,
    feedback_loops: 1,
  }),
});

let status;
while (true) {
  ({ status } = await call(run.status_url));
  console.log("status:", status);
  if (["COMPLETED", "FAILED", "CANCELLED"].includes(status)) break;
  await new Promise((r) => setTimeout(r, 15_000));
}

const result = await call(run.response_url);
console.log("total credits:", result.total_credits);
for (const v of result.videos) {
  const name = `video-v${v.variant}-i${v.iteration}.mp4`;
  const res = await fetch(v.url); // presigned: no auth header
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
  writeFileSync(name, Buffer.from(await res.arrayBuffer()));
  console.log("saved", name, v.final ? "(final)" : "");
}

上传文件

POST /api/v1/agent/runs/uploads(multipart/form-data)

适用于没有公开 URL 的媒体:先上传,再把返回的 url 填入发起 run 的任一 *_url 字段:reference_image_urls、reference_video_urls、reference_audio_urls、first_frame_url 或 last_frame_url。每次请求上传一个文件。

表单字段 是否必填 说明
file 是 图片、视频或音频文件。
kind 否 image、video 或 audio。省略时根据文件的 content type 推断(content type 为 application/octet-stream 时根据扩展名推断)。
curl -X POST https://api.happypony.io/api/v1/agent/runs/uploads \
  -H "Authorization: Bearer $HP_KEY" \
  -F file=@selfie.png
{ "url": "https://…signed…", "kind": "image", "expires_in": 7200 }

响应状态码为 201。

状态码 error 触发条件
400 invalid_request 没有 multipart file 字段。
400 invalid_media code 为 bad_type(类型不支持,或与你指定的 kind 不符)或 too_large;field 为 file。
401 unauthorized 缺少、错误或已吊销的 API key。
502 media_failed 文件无法保存。请重试。

把两张本地图片放进同一次 run:

up() { curl -s -X POST https://api.happypony.io/api/v1/agent/runs/uploads \
         -H "Authorization: Bearer $HP_KEY" -F "file=@$1" | jq -r .url; }
A=$(up selfie3.png); B=$(up selfie4.png)
curl -X POST https://api.happypony.io/api/v1/agent/runs \
  -H "Authorization: Bearer $HP_KEY" -H "Content-Type: application/json" \
  -d "{\"prompt\": \"The two girls have a bar fight\", \"reference_image_urls\": [\"$A\", \"$B\"], \"variants\": 2, \"feedback_loops\": 1}"

发起 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"
}

响应状态码为 202。未知字段会被拒绝(400),因此拼写错误不会悄悄改变渲染内容。

字段 类型 说明
prompt string,必填 你的指令,最多 8000 个字符。
reference_image_urls string[] ≤ 8 主体、产品、风格。超过 4 张时,agent 会把它们拼成参考图板。
reference_video_urls string[] ≤ 1 动作 / 身份参考。渲染模型只接受一个。
reference_audio_urls string[] ≤ 2 声音 / 音效参考。
first_frame_url, last_frame_url string 固定开场 / 结尾画面。
hints.duration int 5–30 秒。必须与画布(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 默认 quality。run 中的所有渲染都使用该档位,agent 不会切换。
hints.language string 语音和画面文字的语言。
variants int 1–4,默认 1 对同一需求相互独立的多个版本,每个版本由各自的 agent 负责。
feedback_loops int 0–3,默认 0 一次完成的渲染最多可以被审查并重做几次。

你给出的 hint 具有约束力:agent 无法覆盖。没有给出的部分由 agent 自行决定。

媒体 URL

通过上传得到的文件也以 URL 的方式传入。每个 URL 都会在 run 创建时下载,并保存为私有附件,因此错误的 URL 会让请求当场失败(400 invalid_media,并指出字段),而不是等到 run 之后才出错。规则如下:

积分与限制

轮询状态

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 沿用批处理队列的命名:

状态 含义
IN_QUEUE 已接受,尚未被 agent runner 领取。
IN_PROGRESS agent 正在工作(见 phase)。
COMPLETED 每个能够完成的版本都有了最终视频。失败的版本会列在 variant_errors 中;只要至少有一个版本产出了视频,run 仍然是 COMPLETED。
FAILED 没有任何版本产出视频。error 说明原因。
CANCELLED 已被你取消。

phase(run 级别:取进度最靠前的运行中版本;结束后为 null):

阶段 含义
analysing agent 阅读你的素材并撰写方案。
planning 方案正在定价并提交。
rendering 渲染已排队或正在运行。queue_position 和 eta_seconds 来自渲染队列。
reviewing 正在对照你的指令检查已完成渲染的画面。
revising agent 在审查(或内容检查被拒)后重写方案。

对每个版本而言,iteration 是已开始的渲染次数,max_iterations 为 feedback_loops + 1。state 为 running、done 或 failed。建议每 10–30 秒轮询一次;不提供 webhook。

获取结果

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,
      "parameters": { "title": "Trail shoe dawn run", "mode": "from references", "tier": "quality", "duration": 15,
                      "short_edge": 768, "aspect_ratio": "9:16", "inputs": "reference images img1, img2", "seed": 1804289383,
                      "brief": "A runner laces the shoe at dawn, then…" },
      "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, "parameters": { "…": "…" }, "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": "…" } }
  ]
}

run 进行中时,同一接口返回目前已完成的视频。

run 也会出现在 happypony.io 视频 agent 的聊天历史中,标记为 API:每个 run 对应一个只读聊天,按顺序展示你的指令以及每个视频的方案、渲染和审查。

取消

PUT /api/v1/agent/runs/:id/cancel

{ "request_id": "6f1c…", "status": "CANCELLED", "renders_cancelled": 1, "renders_still_running": 0 }

run 会在几秒内停止。仍在渲染队列中等待的渲染会在队列中取消并退款。已被 worker 开始的渲染无法停止:它会跑完,积分不退,也不会出现在 run 的视频中。取消已经结束的 run 会返回 409 not_cancellable。

错误

状态码 error 触发条件
400 invalid_request JSON 格式错误、未知字段、取值越界。details 列出所有问题。
400 invalid_media URL 被拒绝或无法下载。field 指出字段,code 为 blocked、invalid_url、unreachable、too_large、bad_type 或 http_error。
401 unauthorized 缺少、错误或已吊销的 API key。
402 insufficient_credits 余额低于最坏花费。包含 required 和 available。
404 not_found 没有这个 run,或它不属于你。
409 not_cancellable run 已经结束。
429 too_many_runs 已有三个 run 处于活跃状态。
502 media_failed 媒体无法保存。请重试。

run 的工作原理

  1. 请求先经过校验、报价,媒体被保存。每个版本都有自己的私有 agent 对话(不会出现在聊天历史中),从你的指令开始。
  2. runner 进程领取 run(持有会不断续期的租约,因此重启或由其他副本接手时会从中断处继续,且不会重复提交渲染),并并行驱动每个版本:agent 分析素材并提出一个视频方案;方案按报价自动确认并提交到渲染队列。
  3. 渲染完成且还有剩余循环次数时,抽样画面连同你的指令和提示词会交给视觉模型做结构化评审。如果满意,该版本完成;否则 agent 修改后重新提出方案。
  4. 内容检查被拒的反馈会回传给 agent,由它修改方案(每个版本最多 3 次)。其他任何严重失败只会终止该版本。