Usage and limits
Every example uses https://video.pyler.ai and an API key in $VUF_API_KEY. See Authentication.
Check your quota
GET /api/v1/usage returns the current quota and remaining allowance of the organization that owns the API key.
- cURL
- Python
curl -s https://video.pyler.ai/api/v1/usage \
-H "Authorization: Bearer $VUF_API_KEY"
import os, requests
BASE = "https://video.pyler.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['VUF_API_KEY']}"}
usage = requests.get(f"{BASE}/usage", headers=HEADERS).json()
if usage["remaining"]["videos"] == 0:
print("video quota exhausted; new submissions will be refused")
{
"plan": "trial",
"videos": { "used": 12, "limit": 100 },
"minutes": { "used": 340, "limit": 1000 },
"ads": { "used": 2, "limit": 20 },
"summaries": { "used": 5, "limit": 30 },
"remaining": { "videos": 88, "minutes": 660, "ads": 18, "summaries": 25 }
}
planstringName of the organization's quota plan.
videosUsageVideos counted against the plan: used and limit.
minutesUsageVideo minutes counted against the plan: used and limit.
adsUsageAd creatives counted against the plan: used and limit.
summariesUsageCustom summary requests counted against the plan: used and limit. Counted separately from videos.
remainingRemainingvideos, minutes, ads and summaries still available.
| Status | Meaning |
|---|---|
404 | No quota plan has been assigned to this organization yet, for example while the account is still being provisioned. Contact support if it persists. |
503 | Quota datastore temporarily unavailable. Retry. |
What the spec documents about how quota is consumed:
- A custom summary request counts as one summary against
summaries, not againstvideos, including one that ends asrejected. A request that ends asfailed, or is stillqueuedwhen its video is deleted, is returned to the quota. When the summary quota is exhausted thePOSTanswers422withcode: quota_exceeded_summariesand nothing is created. - An MRSS feed whose plan no longer permits MRSS shows
plan_blocked: true; re-enabling it returns422withcode: mrss.disabled.
Request limits
Caps and defaults declared in the OpenAPI spec. Values outside a range fail schema validation with a 422.
Indexing
| Endpoint | Limit |
|---|---|
GET /api/v1/videos | limit 1–100, default 50. Cursor pagination. |
GET /api/v1/videos/{video_id} | scene_limit 1–200, default 50; scene_offset ≥ 0. |
GET /api/v1/videos/{video_id}/scenes | limit 1–200, default 50; offset ≥ 0. |
GET /api/v1/videos/{video_id}/scenes/stream | Server-side maximum stream duration; the stream ends with {"event": "timeout"} when reached. Reconnect for a fresh snapshot. |
POST /api/v1/videos/batch | items up to 500 rows; total 0–500; per row url ≤ 2048 characters, title ≤ 500. Over-length items returns the 422 list form. |
POST /api/v1/videos/batch/upload | Recommended about 10 files or fewer per request. Safety backstop: 413 above 500 files, counted per request and across a reused batch_id. |
POST /api/v1/videos/batch/upload-sheet | 413 above the 10 MB sheet size cap or above 500 valid URL rows. 422 above 10000 data rows, with too many columns, or when the sheet expands past the uncompressed-size cap. Up to 100 dropped rows are listed in rejected_rows. |
POST /api/v1/videos/{video_id}/custom-summaries | instructions ≤ 2000 characters; cast ≤ 100 members, each name 1–200 characters with ≤ 20 aliases; language ≤ 35 characters, language-tag pattern. |
GET /api/v1/videos/{video_id}/custom-summaries | limit 1–100, default 50. Cursor pagination. |
PATCH /api/v1/mrss-feeds/{feed_id} | poll_interval_s one of 3600, 21600, 86400; label ≤ 200 characters, no control characters. Empty body is 422. |
GET /api/v1/mrss-feeds/{feed_id}/items | limit 1–200, default 50; offset ≥ 0. |
Search
| Endpoint | Limit |
|---|---|
POST /api/v1/search, POST /api/v1/search/deep | k 1–100, default 20; scene_pool 10–200, default 60; min_relevance 0–10, default 3. video_id must be a UUID. No cursor: results are bounded by k per column. |
POST /api/v1/search/multimodal | k default 20; scene_pool default 60. |
Ads
| Endpoint | Limit |
|---|---|
POST /api/v1/ads/match | per_query_k 2–400, default 100; max_candidates 5–400, default 100; max_per_video 1–50, default 10. |
Live
| Endpoint | Limit |
|---|---|
POST /api/v1/live/streams, PATCH /api/v1/live/streams/{stream_id} | segment_n 1–60, default 8; poll_interval_s 1–3600 seconds, default 6. |
Rate limits
Throughput is bounded by the organization quota above, the per-request caps, and the size backstops (413) on batch and sheet uploads. One request that is too large is rejected whole; it is not throttled or partially processed.
Request-rate limits may also be applied per organization, and the analysis stages behind search, matching and screening share capacity across customers. Build clients that treat 429 Too Many Requests like any other transient error: back off (honouring a Retry-After header when present) and retry under an overall deadline. Keep polling intervals at a few seconds, prefer the streaming endpoints over tight polling loops, and batch submissions instead of sending one request per video.
Timeouts and retries
The following guidance is written for brand-safety screening and applies to every call that does work after the request is accepted.
A timeout is an unknown outcome, not a failure. Once the API has accepted a request it runs to completion and stores the result, whether or not your connection is still open. Only the response is lost. On a timeout, do not assume the work was not done: look the result up before resubmitting.
Recommended client timeout: 120 seconds. Most calls return in seconds; the ceiling is for the tail, not the typical case.
Raising it past 5 minutes has no effect. A request that has produced no data for 5 minutes is closed with 504. A streaming response that keeps producing lines can run longer, while a quiet request ends at the 5-minute mark. Streaming endpoints (/api/v1/ads/match, /api/v1/videos/{video_id}/scenes/stream, /api/v1/live/streams/{stream_id}/events, and /api/v1/search with judge: "text") return NDJSON; parse them line by line as data arrives.
Backoff. One or two retries under an overall deadline is enough. A retry occupies capacity that would otherwise serve your other requests and counts toward any rate limit, so keep retries deliberate rather than routine.
What is safe to retry
| Situation | Retry? | Why |
|---|---|---|
500 on any endpoint | Yes | Documented as "upstream service error, usually safe to retry". |
503 on GET /api/v1/usage | Yes | Quota datastore temporarily unavailable. |
503 on POST /api/v1/videos/{video_id}/custom-summaries | Yes | The request was not queued and nothing will be generated for it. Each retry creates a new request. |
Timeout on POST /api/v1/videos (URL) | Yes | Resubmitting a URL that is still being indexed for you returns the existing video_id / job_id with created: false instead of starting a second pipeline. |
Timeout on POST /api/v1/videos/batch (kind: "url") | Yes, with care | Rows whose URL is already being indexed are linked to the existing job (created: false). failed rows, including dispatch_failed, are meant to be resubmitted; confirm with GET /api/v1/videos/{video_id} first. |
Timeout on POST /api/v1/videos/upload or /batch/upload | Not blindly | Uploads have no natural dedup key. Poll GET /api/v1/videos/batch/{batch_id} first and resend only the files still missing. Skip items whose error is dispatch_failed: their video row is already committed and resending indexes the file twice. |
504 from the edge | Look up first | The work may have completed. Check the job, video, batch or screening result before sending again. |
409, 413, 422 | No | The request itself is wrong. Change it (see below). |
Oversized and invalid requests
413 Payload too large
| Endpoint | Trigger | Fix |
|---|---|---|
POST /api/v1/videos/batch/upload | More than 500 files, counted per request and across a reused batch_id. | Split into requests of about 10 files. |
POST /api/v1/videos/batch/upload-sheet | Sheet larger than 10 MB, or more valid URL rows than one batch accepts (currently 500). | Split into multiple sheets. |
422 Unprocessable
Two body shapes exist, so branch on the type of detail before reading it:
- Envelope form:
{"detail": "message", "code": "stable.code"}.codeis present when the failure has a stable machine-readable reason and absent otherwise. Examples in this guide:quota_exceeded_summaries,custom_summary_no_base_summary,custom_summary_no_org,mrss.bad_interval,mrss.bad_label,mrss.bad_status,mrss.bad_index_status,mrss.disabled. - Schema-validation list form:
{"detail": [{"loc": [...], "msg": "..."}]}, with nocode. Any endpoint that declares a body can return it; sending an over-lengthitemsarray toPOST /api/v1/videos/batchis the common case. APATCH /api/v1/mrss-feeds/{feed_id}with an empty object returns this form too.
Sheet uploads add one sibling key: when code is no_valid_urls, the 413 / 422 body also carries rejected, the number of rows dropped while parsing (never recorded in any batch).
{ "detail": "no valid url rows in sheet", "code": "no_valid_urls", "rejected": 17 }
Other documented 422 triggers:
GET /api/v1/videoswith a malformedreindex_run_idUUID or areindexed_sincewithout a UTC offset.POST /api/v1/live/streamsorPATCHwith a newurlwhose manifest is unreachable or not a valid playlist.POST /api/v1/videos/batch/upload-sheetwith a file that is not.csv/.xlsx, has nourlcolumn, is malformed or corrupted, has no valid URL rows, exceeds 10000 data rows or the column / uncompressed-size caps.
Oversized error bodies
One code is produced by the gateway itself: upstream_error_too_large means a downstream error body was too large to relay. The status code is preserved but the per-item reasons are gone. Retry with fewer items to get the detailed response.
See Errors for the full envelope and the common status table.