Batch
Submit many videos in one request and track them as a unit.
A batch runs the same capabilities on many videos and tracks them as one. It can also answer aggregate questions across the whole set. Batch sizes per plan are on Limits.
POST /v1/batch
Request body
Send urls, uploads or both.
| Prop | Type | Description |
|---|---|---|
| urls | string[] | Video links. Up to 50, within your plan's batch size across urls and uploads together. |
| uploads | UploadRef[] | Uploaded files as { key, filename }, with keys from POST /v1/uploads/presign. Up to 50. |
| capabilities | string[] | At least one of: transcription, summary, brands, metadata, custom_query, objects. Applied to every video. |
| queries | string[] | Questions asked of every video. Up to 10, 500 characters each, within your plan's questions per job. |
| objects | string[] | Things to track in every video. Up to 5. |
| fps | integer (1-4) | Frames looked at per second in every video. Higher catches brief appearances and costs more. Default 1, clamped to your plan's max_fps. |
| aggregate | { enabled, queries } | Questions answered once across the whole set, for 5 credits. Send enabled: true and at least one question. |
| webhook_url | string | An https URL for batch.completed and batch.paused. Starter and above. |
| return_proof | boolean | Include visual evidence, as on single jobs. Defaults to true. |
| dry_run | boolean | Return the per-video and total estimate with HTTP 200 and create nothing. Defaults to false. |
| max_credits | integer | Refuse the batch with BUDGET_EXCEEDED (402) if its total estimate passes this. |
A batch takes no brands, surfaces, from_job or language; those fields are ignored, and brand detection finds
every brand.
curl -X POST https://api.netraflow.com/v1/batch \
-H "Content-Type: application/json" \
-H "X-Api-Key: sk_live_your_key_here" \
-d '{
"urls": [
"https://www.youtube.com/watch?v=jNQXAC9IVRw",
"https://www.tiktok.com/@nba/video/7672506408675314974"
],
"capabilities": ["brands"],
"aggregate": { "enabled": true, "queries": ["Which brand appears most often?"] }
}'{
"data": {
"batch_id": "batch_abcdefghij123456",
"status": "processing",
"total_credits_estimated": 105,
"balance": 400,
"would_exceed_balance": false,
"jobs": [
{
"job_id": "job_aaaaaaaaaa000001",
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"platform": "youtube",
"duration_seconds": null,
"credits_estimated": 50,
"status": "pending",
"note": "duration_unresolved"
},
{
"job_id": "job_bbbbbbbbbb000002",
"url": "https://www.tiktok.com/@nba/video/7672506408675314974",
"platform": "tiktok",
"duration_seconds": null,
"credits_estimated": 50,
"status": "pending",
"note": "duration_unresolved"
}
]
},
"meta": { "request_id": "req_xyz789" }
}total_credits_estimated is the jobs' estimates plus 5 credits for the aggregate answers. Those 5 are charged once the
answers are written (a usage transaction with job_id: null), and not at all if no video completed.
would_exceed_balanceis a warning: the batch is still created. A job you can't afford once its real length is known isheld, and the batchpaused.- Dry run. With
dry_run: truethe response holdsstatus,total_credits_estimated,balance,would_exceed_balanceandjobs, with nobatch_idand no per-jobjob_idorstatus. - Repeats. A video listed twice, even under two links to it, runs and is charged once, and has one entry in
jobs. - Concurrency. Videos past your plan's jobs-at-once limit wait in
pendingwithCONCURRENT_LIMIT_REACHEDand start by themselves. While they wait, a new job or batch is refused withCONCURRENT_LIMIT_REACHED(429). - Body size. 10 KB, counted in bytes. 50 links with 10 long questions fit; add 10 long aggregate questions and they don't. Split such a batch.
The 202 also carries total_jobs, completed_count, failed_count, credits_estimated, credits_charged,
webhook_url, created_at and completed_at, kept for older clients.
GET /v1/batch/:id
curl https://api.netraflow.com/v1/batch/batch_abcdefghij123456 \
-H "X-Api-Key: sk_live_your_key_here"{
"data": {
"batch_id": "batch_abcdefghij123456",
"status": "paused",
"total_jobs": 10,
"completed_count": 4,
"failed_count": 0,
"rollup": {
"total": 10,
"pending": 0,
"processing": 0,
"completed": 4,
"held": 6,
"failed": 0,
"cancelled": 0
},
"credits_estimated": 500,
"credits_charged": 112,
"jobs": [
{
"job_id": "job_aaaaaaaaaa000001",
"status": "held",
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"credits_estimated": 50,
"reason": "insufficient_credits"
}
],
"aggregate_results": null,
"webhook_url": null,
"created_at": "2026-04-03T12:00:00.000Z",
"completed_at": null
},
"meta": { "request_id": "req_abc456" }
}Use rollup: completed_count and failed_count are kept for older clients and can't show held.
Batch status values
| Prop | Type | Description |
|---|---|---|
| processing | string | At least one job is pending or processing. |
| paused | string | Not final. Nothing left to run and at least one job held. Add credits and it resumes by itself. |
| completed | string | Every job ended and at least one completed. aggregate_results and credits_charged are final. |
| failed | string | Every job ended and none completed. The webhook still sends batch.completed. |
| cancelled | string | Cancelled with POST /v1/batch/:id/cancel. A batch whose held jobs expired is failed, not cancelled. |
GET /v1/batch
Your batches, newest first. Takes status, limit (1-100, default 20) and cursor, and pages like GET /v1/jobs.
curl "https://api.netraflow.com/v1/batch?status=completed&limit=10" \
-H "X-Api-Key: sk_live_your_key_here"{
"data": [
{
"batch_id": "batch_abcdefghij123456",
"status": "completed",
"total_jobs": 2,
"completed_count": 2,
"failed_count": 0,
"rollup": { "total": 2, "pending": 0, "processing": 0, "completed": 2, "held": 0, "failed": 0, "cancelled": 0 },
"credits_estimated": 105,
"credits_charged": 30,
"created_at": "2026-04-03T12:00:00.000Z",
"completed_at": "2026-04-03T12:04:10.000Z"
}
],
"pagination": { "has_more": false, "next_cursor": null },
"meta": { "request_id": "req_xyz789" }
}Each item is a summary, with fields as on GET /v1/batch/:id, except that credits_charged stays null until the
batch completes or fails (and for good on a cancelled one). GET /v1/batch/:id shows the charges so far.
POST /v1/batch/:id/cancel
Cancel a processing or paused batch and every job in it that hasn't ended. Returns HTTP 200:
{
"data": { "batch_id": "batch_abcdefghij123456", "status": "cancelled", "cancelled_jobs": 3 },
"meta": { "request_id": "req_xyz789" }
}curl -X POST https://api.netraflow.com/v1/batch/batch_abcdefghij123456/cancel \
-H "X-Api-Key: sk_live_your_key_here"Completed jobs stay completed and charged. Cancelling again returns cancelled_jobs: 0; a batch that already
completed or failed returns BATCH_NOT_CANCELLABLE (409). No webhook is sent. This is the only way to cancel a batch's
jobs.
Error codes
| Code | HTTP | Description |
|---|---|---|
BATCH_NOT_ALLOWED | 403 | Batch is not available on your plan |
TOO_MANY_URLS | 422 | urls + uploads over your plan's max_batch_size |
TOO_MANY_AGGREGATE_QUERIES | 422 | Over your plan's aggregate questions per batch |
VALIDATION_FAILED | 422 | aggregate missing enabled: true or a question |
BATCH_NOT_FOUND | 404 | No such batch in your workspace |
BATCH_NOT_CANCELLABLE | 409 | Batch already ended completed or failed |
BODY_TOO_LARGE | 413 | Request body exceeds 10 KB |
Every job error code applies too.