UGC video analyser: API for agents
Send one UGC video, get back a score and a rule-by-rule report against the Ryze filming guide (https://ugc-filming-guide.vercel.app). Each video is checked by a lead agent plus seven check agents. The report counts 14 scored rules; good-to-have and alternative rules are reported but never change the score. Every report has a share link a person can open in the browser and a clean JSON file for you.
Base URL: https://analyzer-production-1bcb.up.railway.app
API key
Send your key in a header. $UGC_API_KEY below is a placeholder; ask the Ryze team for a key.
Authorization: Bearer $UGC_API_KEY
X-API-Key: $UGC_API_KEY works too. Without a key you get the public limit (30 videos per hour per IP).
1. Send a video
By file (multipart, field video):
curl -s https://analyzer-production-1bcb.up.railway.app/api/jobs \
-H "Authorization: Bearer $UGC_API_KEY" \
-F video=@file.mp4 \
-F ref=creator-042/take-3
By URL (JSON, video_url must be a public https link to the video file itself):
curl -s https://analyzer-production-1bcb.up.railway.app/api/jobs \
-H "Authorization: Bearer $UGC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"video_url": "https://example.com/videos/take-3.mp4", "ref": "creator-042/take-3", "callback_url": "https://example.com/hooks/ugc"}'
Optional fields (multipart field or JSON key):
| field | what it does |
|---|---|
ref |
Free text to match results on your side. Echoed in the report as ref. |
name |
Display name of the video. Defaults to the file name. |
callback_url |
https. When the check ends, the server POSTs the report JSON there (3 tries with backoff). |
Answer 202:
{
"id": "Qm7tX2c9LpZa",
"status": "queued",
"share_url": "https://ugc-filming-guide.vercel.app/r/Qm7tX2c9LpZa",
"json_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/report.json",
"status_url": "/api/jobs/Qm7tX2c9LpZa",
"poll_after_s": 15
}
Errors come as {"error": "plain message"}: 400 wrong type, too long, too big or a bad URL; 429 limit reached;
503 analyser offline.
2. Wait for the report
Poll GET /api/jobs/<id>/report.json every 15 seconds.
202while the check runs:{"id", "status": "queued" | "running", "progress": {"done": 3, "total": 7}, "poll_after_s": 10}200when it ends: the report below.statusis"done", or"error"with a plainerrormessage.404unknown id.
ID=Qm7tX2c9LpZa
while :; do
code=$(curl -s -o report.json -w '%{http_code}' \
https://analyzer-production-1bcb.up.railway.app/api/jobs/$ID/report.json)
[ "$code" = 200 ] && break
[ "$code" = 202 ] || { cat report.json; exit 1; }
sleep 15
done
jq '{status, score, verdict, to_fix}' report.json
If you passed callback_url you can skip polling, but keep a slow poll as a fallback.
A finished report never changes: opening it again does not re-run the check.
3. Report JSON (schema ugc-report/1)
| field | meaning |
|---|---|
schema |
"ugc-report/1" |
id, ref |
job id; your ref if you sent one |
status, error |
"done" or "error"; plain message or null |
created_at, finished_at, duration_s |
ISO times; how long the check took |
video |
{"name", "length_s"} |
format |
"switch" (cuts between face and filmed laptop), "split" (face and screen share the frame) or "other" |
share_url, json_url |
page for people; this file |
score |
{"hit", "total", "percent"}. total = scored rules that apply to this video (max 14) |
verdict, verdict_text |
"ready" (hit >= total - 1), "close" (hit >= total - 3), "work" |
to_fix |
scored rules that failed, guide order: {"number", "rule", "at", "note"} |
scored |
the 14 scored rules, guide order |
optional |
good-to-have and alternative rules. Never affect the score. |
Each item in scored and optional:
| field | meaning |
|---|---|
number, id |
guide number (1-21) and rule id |
section, group, rule |
guide section, group and rule text |
kind |
"required", "good_to_have" or "alternative" |
counts_toward_score |
true only for required |
status |
"pass", "fail", "na" (does not apply, left out of the total), "unknown" (check failed, left out) |
at_s, at |
moment in the video that shows it, seconds and "m:ss", or null |
note |
one or two plain sentences: what was seen and what to change |
snapshot_url |
jpeg frame at that moment, or null |
For optional items pass means the creator used it and fail means they did not. Do not treat them as mistakes.
Full example: 14 scored rules, optional block, to_fix
{
"schema": "ugc-report/1",
"id": "Qm7tX2c9LpZa",
"status": "done",
"error": null,
"ref": "creator-042/take-3",
"created_at": "2026-10-11T18:02:11Z",
"finished_at": "2026-10-11T18:03:55Z",
"duration_s": 104,
"video": {
"name": "ryze-demo-take3.mp4",
"length_s": 59.4
},
"format": "switch",
"share_url": "https://ugc-filming-guide.vercel.app/r/Qm7tX2c9LpZa",
"json_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/report.json",
"score": {
"hit": 12,
"total": 14,
"percent": 86
},
"verdict": "close",
"verdict_text": "Almost there. Fix the points below before you submit.",
"to_fix": [
{
"number": 14,
"rule": "Not too close, not too far. Show a vertical slice of the screen and part of the keyboard, with the backlight on.",
"at": "0:44",
"note": "Most screen shots are close-ups of one line. Step back so a vertical slice of the screen and part of the keyboard are in frame."
},
{
"number": 20,
"rule": "Start every new screen with a zoom in or a zoom out in the first two seconds.",
"at": "0:03",
"note": "Only 3 of 9 new screens open with a zoom. Start each new screen with a quick zoom in or out."
}
],
"scored": [
{
"number": 1,
"id": "open-emotion",
"section": "Opening",
"group": "You on camera",
"rule": "Say your first line with real emotion. Shock, anger or excitement is what stops the scroll.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 0.4,
"at": "0:00",
"note": "You open on your face with a shocked look and a raised hand on the first line.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/open-emotion.jpg"
},
{
"number": 2,
"id": "open-selfie",
"section": "Opening",
"group": "You on camera",
"rule": "Hold the phone yourself and film your face on the front camera. No tripod. A slightly low angle is fine.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 1.2,
"at": "0:01",
"note": "Handheld front camera, your face fills most of the frame.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/open-selfie.jpg"
},
{
"number": 4,
"id": "open-glow",
"section": "Opening",
"group": "You on camera",
"rule": "Let the glow of your laptop or phone light your face. The camera must be sharp.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 2.6,
"at": "0:02",
"note": "Soft light from the screen on your face, and the face is sharp.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/open-glow.jpg"
},
{
"number": 6,
"id": "open-title",
"section": "Opening",
"group": "On screen",
"rule": "Put the main title at the top of the frame, above your head.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 0.8,
"at": "0:00",
"note": "The main title sits at the top of the frame, above your head.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/open-title.jpg"
},
{
"number": 8,
"id": "open-return",
"section": "Opening",
"group": "On screen",
"rule": "When you are just talking on screen, keep your hands moving in the frame.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 31.5,
"at": "0:31",
"note": "Your hands move in the frame each time you talk to camera.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/open-return.jpg"
},
{
"number": 9,
"id": "open-captions",
"section": "Opening",
"group": "On screen",
"rule": "Captions go at the bottom: white text, black outline, one line at a time. Words fill the line instead of flashing one by one.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 4.1,
"at": "0:04",
"note": "White captions with a black outline in the lower half, words build up on one line.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/open-captions.jpg"
},
{
"number": 11,
"id": "screen-clean",
"section": "Filming the screen",
"group": "Setup",
"rule": "Keep the desk empty: laptop, second screen, cables. Nothing else.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 10.5,
"at": "0:10",
"note": "Only the laptop and a second screen are on the desk.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/screen-clean.jpg"
},
{
"number": 13,
"id": "screen-lens",
"section": "Filming the screen",
"group": "Setup",
"rule": "Wipe the camera lens before you film. Every frame must be sharp.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 28.3,
"at": "0:28",
"note": "Screen text is sharp and readable all the way through.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/screen-lens.jpg"
},
{
"number": 14,
"id": "screen-framing",
"section": "Filming the screen",
"group": "Framing",
"rule": "Not too close, not too far. Show a vertical slice of the screen and part of the keyboard, with the backlight on.",
"kind": "required",
"counts_toward_score": true,
"status": "fail",
"at_s": 44,
"at": "0:44",
"note": "Most screen shots are close-ups of one line. Step back so a vertical slice of the screen and part of the keyboard are in frame.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/screen-framing.jpg"
},
{
"number": 15,
"id": "move-hand",
"section": "Movement",
"group": "",
"rule": "Glide a relaxed hand over the part of the screen you are talking about.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 8.4,
"at": "0:08",
"note": "A relaxed hand glides over the dashboard while you explain it.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/move-hand.jpg"
},
{
"number": 17,
"id": "move-point",
"section": "Movement",
"group": "",
"rule": "When you need to show something specific, point right at it with the pen or your finger.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 20.4,
"at": "0:20",
"note": "The pen tip lands on the cost number and holds.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/move-point.jpg"
},
{
"number": 19,
"id": "screen-closeup",
"section": "Movement",
"group": "",
"rule": "For the key detail, go very close and point at it with a pen or anything else that catches the eye.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 39.6,
"at": "0:39",
"note": "A very close shot of the key number, with the pen on it.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/screen-closeup.jpg"
},
{
"number": 20,
"id": "move-zoom",
"section": "Movement",
"group": "",
"rule": "Start every new screen with a zoom in or a zoom out in the first two seconds.",
"kind": "required",
"counts_toward_score": true,
"status": "fail",
"at_s": 3,
"at": "0:03",
"note": "Only 3 of 9 new screens open with a zoom. Start each new screen with a quick zoom in or out.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/move-zoom.jpg"
},
{
"number": 21,
"id": "move-camera",
"section": "Movement",
"group": "",
"rule": "Hold the phone still only while the screen animates. Otherwise glide the phone to each part as you explain it.",
"kind": "required",
"counts_toward_score": true,
"status": "pass",
"at_s": 12.2,
"at": "0:12",
"note": "The camera glides between parts of the screen and stays still while the page animates.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/move-camera.jpg"
}
],
"optional": [
{
"number": 3,
"id": "open-dark",
"section": "Opening",
"group": "You on camera",
"rule": "Film in a dark or dim room.",
"kind": "good_to_have",
"counts_toward_score": false,
"status": "pass",
"at_s": 2,
"at": "0:02",
"note": "The room behind you is dim.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/open-dark.jpg"
},
{
"number": 5,
"id": "open-distance",
"section": "Opening",
"group": "You on camera",
"rule": "Film from further away, holding the laptop with the demo playing on it.",
"kind": "alternative",
"counts_toward_score": false,
"status": "fail",
"at_s": null,
"at": null,
"note": "You film close to your face, not from further away with the laptop.",
"snapshot_url": null
},
{
"number": 7,
"id": "open-gesture",
"section": "Opening",
"group": "On screen",
"rule": "Hook with a gesture in the first second: point up at the title, drop a pen, or turn your phone toward the laptop.",
"kind": "good_to_have",
"counts_toward_score": false,
"status": "fail",
"at_s": null,
"at": null,
"note": "No gesture in the first 2 seconds.",
"snapshot_url": null
},
{
"number": 10,
"id": "screen-dark",
"section": "Filming the screen",
"group": "Setup",
"rule": "Film the laptop in a dark room with a dim background.",
"kind": "good_to_have",
"counts_toward_score": false,
"status": "pass",
"at_s": 9.5,
"at": "0:09",
"note": "Screen shots are filmed in a dim room.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/screen-dark.jpg"
},
{
"number": 12,
"id": "screen-second",
"section": "Filming the screen",
"group": "Setup",
"rule": "Put a related image from another demo on the second screen behind the laptop.",
"kind": "good_to_have",
"counts_toward_score": false,
"status": "fail",
"at_s": 12,
"at": "0:12",
"note": "The second screen is blank.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/screen-second.jpg"
},
{
"number": 16,
"id": "move-pen",
"section": "Movement",
"group": "",
"rule": "Switch from your hand to a pen now and then, and hover over the screen with the pen.",
"kind": "good_to_have",
"counts_toward_score": false,
"status": "pass",
"at_s": 14.1,
"at": "0:14",
"note": "You switch to a pen and hover over the chart.",
"snapshot_url": "https://analyzer-production-1bcb.up.railway.app/api/jobs/Qm7tX2c9LpZa/snap/move-pen.jpg"
},
{
"number": 18,
"id": "move-object",
"section": "Movement",
"group": "",
"rule": "Swap the pen for something unexpected, like a watch.",
"kind": "good_to_have",
"counts_toward_score": false,
"status": "fail",
"at_s": null,
"at": null,
"note": "Only a hand and a pen are used.",
"snapshot_url": null
}
]
}
4. Share link
share_url (https://ugc-filming-guide.vercel.app/r/<id>) opens the same report as a page: score, verdict,
every rule with timestamps and snapshots. Send it to the creator or a reviewer. Anyone with the link can see
the report; the id is random and works as the access token. Reports are kept for 365 days. The uploaded video
is deleted as soon as the check ends.
5. Limits
- mp4, mov or webm, up to 300 MB and 180 seconds.
- About 2-3 minutes per video.
- 3 videos are checked at a time; the rest wait in a queue (
status: "queued"). - 60 videos per day per API key. Without a key: 30 per hour per IP.
video_url: https only, public hosts only, 5 minute download limit.
Other endpoints
GET /api/jobs/<id>the live job the web page uses (steps and raw result).GET /api/health{"ok", "auth", "running", "queued"}.GET /api/agentthis how-to as plain text.- Human readable page: https://ugc-filming-guide.vercel.app/agent.html
Errors
401 {"error": "Invalid API key."}: the key in the header is wrong.429: the daily limit for the key (60 videos) or the hourly limit without a key was reached.400: the file is not a video, is over 300 MB or longer than 3 minutes, or thevideo_urlcan't be downloaded.503: the analyser is not connected to Claude right now. Try again later.