Customer API
Create videos, upload references, and poll generation status with the HappyHorse API.
Availability
The HappyHorse Customer API is available at /api/v1 for customer
integrations when API keys are enabled for your workspace. If you cannot see
the API Keys page, contact your HappyHorse administrator or support team.
When access is enabled, signed-in users can manage keys from Dashboard / Settings / API Keys. Create a key, copy the full secret shown once, store it securely, and delete keys that should no longer be used.
Use every API key as a Bearer token:
Authorization: Bearer hh_live_your_api_keyBase URL
Use your production HappyHorse domain as the base URL:
https://<your-happyhorse-domain>All examples below assume:
export HAPPYHORSE_API_KEY="hh_live_your_api_key"
export HAPPYHORSE_BASE_URL="https://<your-happyhorse-domain>"Authentication Check
Every Customer API endpoint requires the Authorization header. Missing,
invalid, or rate-limited keys return the stable error shape documented below.
curl "$HAPPYHORSE_BASE_URL/api/v1/videos/example-id" \
-H "Authorization: Bearer $HAPPYHORSE_API_KEY"Upload Reference Media
POST /api/v1/uploads/presign creates a short-lived upload URL for reference
assets that you want to use in a video request. The API returns a storage
key; pass that key into POST /api/v1/videos.
HappyHorse currently supports direct customer uploads for:
| Kind | Content types | Max size |
|---|---|---|
image | image/png, image/jpeg, image/webp | 10 MB |
video | video/mp4, video/quicktime, video/webm | 50 MB |
audio | audio/mpeg, audio/mp3, audio/wav, audio/webm | 15 MB |
If your recorder exports audio as audio/mp4, convert it to one of the
accepted audio MIME types before uploading. The current API rejects unsupported
types with upload_type_not_supported.
Request
{
"kind": "image",
"contentType": "image/png",
"sizeBytes": 5242880
}kind is optional and defaults to image. sizeBytes must be the exact file
size you will upload.
Response
{
"uploadUrl": "https://storage.example.com/presigned-url",
"key": "videos/inputs/user-id/asset-id.png",
"publicUrl": "https://cdn.example.com/videos/inputs/user-id/asset-id.png",
"kind": "image"
}You can create the presign, save the response, extract uploadUrl and key,
then upload the file with the same content type and size:
curl -X POST "$HAPPYHORSE_BASE_URL/api/v1/uploads/presign" \
-H "Authorization: Bearer $HAPPYHORSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "image",
"contentType": "image/png",
"sizeBytes": 5242880
}' > presign.json
UPLOAD_URL="$(jq -r '.uploadUrl' presign.json)"
SOURCE_IMAGE_KEY="$(jq -r '.key' presign.json)"
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/png" \
--data-binary "@reference.png"Use SOURCE_IMAGE_KEY as sourceImageKey in the video creation request. For
reference videos or audio, pass the extracted key as referenceVideoKey or
referenceAudioKey.
Common errors are invalid_input, upload_file_too_large,
upload_type_not_supported, missing_api_key, invalid_api_key,
rate_limited, and internal_error.
Create a Video
POST /api/v1/videos starts an asynchronous video generation job. It charges
credits immediately, queues the job, and returns the video ID. Poll
GET /api/v1/videos/{id} until the status is ready or failed.
The model is selected by the customer's plan. Do not send a model field in
the current API.
Request
{
"prompt": "A cinematic shot of a chestnut horse running through a sunlit meadow",
"style": "cinematic",
"durationSec": 6,
"aspectRatio": "16:9",
"resolution": "720p",
"sourceImageKey": "videos/inputs/user-id/asset-id.png",
"referenceVideoKey": "videos/reference-video/user-id/clip-id.mp4",
"referenceAudioKey": "videos/reference-audio/user-id/audio-id.mp3",
"cameraMotion": "pan-right",
"motionIntensity": 3,
"cfgScale": 0.5
}Supported fields:
| Field | Type | Notes |
|---|---|---|
prompt | string | Required, 3 to 2000 characters. |
style | string | Optional, up to 50 characters. |
durationSec | integer | Required, 3 to 12 seconds. Plan limits apply. |
aspectRatio | enum | Optional, default 16:9. Values: 16:9, 9:16, 1:1. |
resolution | enum | Optional, default 720p. Values: 720p, 1080p. |
sourceImageKey | string | Optional first-frame/reference image key returned by upload presign. |
lastFrameKey | string | Optional last-frame image key. Cannot be combined with sourceImageKey. |
referenceVideoKey | string | Optional reference video key returned by upload presign. |
referenceAudioKey | string | Optional reference audio key returned by upload presign. |
cameraMotion | enum | Optional, default none. Values: none, static, pan-left, pan-right, tilt-up, tilt-down, zoom-in, zoom-out, orbit. |
motionIntensity | integer | Optional, 1 to 5. |
cfgScale | number | Optional, 0 to 1. |
The current API uses HappyHorse storage keys for input media. It does not
accept arbitrary inputImageUrl, referenceVideoUrl, or audioUrl fields.
Use POST /api/v1/uploads/presign, upload the asset, then pass the returned
key.
Response
{
"id": "018f3f1d-8c2e-7b4d-9db1-8e8f3a0c8f21",
"status": "queued"
}Example:
curl -X POST "$HAPPYHORSE_BASE_URL/api/v1/videos" \
-H "Authorization: Bearer $HAPPYHORSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A cinematic shot of a chestnut horse running through a sunlit meadow",
"style": "cinematic",
"durationSec": 6,
"aspectRatio": "16:9",
"resolution": "720p"
}'Credit and plan errors:
insufficient_creditswith HTTP 402 means the account does not have enough credits for the requested duration and resolution.plan_gate_exceededwith HTTP 403 means the current plan does not allow a requested setting, such as duration, reference audio, last-frame input, or another gated feature. The response includesfield,requiredPlan, andcurrentPlan.
Get Video Status
GET /api/v1/videos/{id} returns the current state of one video. A key can
only read videos owned by the user who created that API key. Videos owned by
another account return not_found.
curl "$HAPPYHORSE_BASE_URL/api/v1/videos/018f3f1d-8c2e-7b4d-9db1-8e8f3a0c8f21" \
-H "Authorization: Bearer $HAPPYHORSE_API_KEY"Response:
{
"id": "018f3f1d-8c2e-7b4d-9db1-8e8f3a0c8f21",
"status": "ready",
"prompt": "A cinematic shot of a chestnut horse running through a sunlit meadow",
"style": "cinematic",
"durationSec": 6,
"aspectRatio": "16:9",
"resolution": "720p",
"videoUrl": "https://cdn.example.com/videos/outputs/018f3f1d.mp4",
"thumbnailUrl": null,
"errorMessage": null,
"readyAt": "2026-04-26T12:30:00.000Z"
}Typical statuses are queued, processing, ready, and failed.
videoUrl and thumbnailUrl are null until the asset exists. If generation
fails, status becomes failed and errorMessage describes the failure.
Error Format
Customer API errors use a stable JSON envelope:
{
"error": {
"code": "invalid_input",
"message": "Request body is invalid."
}
}Some errors include extra details:
{
"error": {
"code": "plan_gate_exceeded",
"message": "Your plan does not allow this video setting.",
"field": "referenceAudio",
"requiredPlan": "pro",
"currentPlan": "basic"
}
}Main error codes:
| Code | HTTP | Meaning |
|---|---|---|
missing_api_key | 401 | No Bearer token was provided. |
invalid_api_key | 401 | The token is missing, deleted, malformed, or not valid. |
rate_limited | 429 | The API key limit was exceeded. |
invalid_input | 400 | The JSON body or route input failed validation. |
insufficient_credits | 402 | The account does not have enough credits. |
plan_gate_exceeded | 403 | The current plan does not allow the requested setting. |
upload_file_too_large | 413 | The upload exceeds the kind-specific max size. |
upload_type_not_supported | 400 | The MIME type is not accepted for that upload kind. |
not_found | 404 | The video does not exist or belongs to another account. |
internal_error | 500 | HappyHorse could not complete the request. |
Webhooks
HappyHorse does not yet expose a customer webhook endpoint for video status
callbacks. Integrations should poll GET /api/v1/videos/{id} until ready or
failed. A 5 to 10 second polling interval is a good default for most jobs.
HappyHorse Docs