NetraFlow

Jobs

Create, retrieve, list and cancel video analysis jobs.

POST /v1/jobs

Queue a video for analysis. Returns 202 Accepted with the job.

Request body

Send exactly one of url or upload_key.

PropTypeDescription
urlstringA YouTube, TikTok or Instagram video link.
upload_keystringThe key from POST /v1/uploads/presign, to analyze your own file.
upload_filenamestringThe file name of an upload. Becomes the video title. Up to 255 characters.
capabilitiesstring[]At least one of: transcription, summary, brands, metadata, custom_query, objects.
queriesstring[]Your own questions about the video. Up to 10, 500 characters each, within your plan's limit.
brandsstring[]Brand names to report. Up to 50, 100 characters each. Leave out to get every brand.
surfacesstring[]Surface slugs to look on, such as "jersey". Up to 10.
objectsstring[]Things to track, in your own words, such as "water bottle". Up to 5. Required by objects.
from_jobstringAn earlier job of yours on the same video. Charged the same either way. An unknown id is JOB_NOT_FOUND; a job that failed, was cancelled or is on another platform is VALIDATION_FAILED.
languagestringLanguage hint for transcription, such as "en". Up to 10 characters.
webhook_urlstringAn https URL for job webhooks. Starter and above.
fpsinteger (1-4)Frames looked at per second. Higher catches brief appearances and costs more. Default 1, clamped to your plan's max_fps.
return_proofbooleanInclude the storyboard, and speech_context on brands heard in speech. Defaults to true.
dry_runbooleanReturn the estimate with HTTP 200 and create nothing. Defaults to false.
max_creditsintegerFail the job with BUDGET_EXCEEDED, free, if its estimate on the real video length passes this. Checked after the video is fetched.

custom_query needs at least one question and objects at least one thing: without them the job is refused with INVALID_CAPABILITIES (422). Every limit is on Limits.

curl -X POST https://api.netraflow.com/v1/jobs \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: sk_live_your_key_here" \
  -H "Idempotency-Key: my-unique-key-123" \
  -d '{
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "capabilities": ["transcription", "summary", "brands"],
    "brands": ["Nike"]
  }'
{
  "data": {
    "job_id": "job_abcdefghij123456",
    "status": "pending",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "platform": "youtube",
    "capabilities": ["transcription", "summary", "brands"],
    "brands": ["Nike"],
    "language": null,
    "video_title": null,
    "video_duration_seconds": null,
    "video_thumbnail_url": null,
    "credits_estimated": 66,
    "credits_charged": null,
    "processing_seconds": null,
    "return_proof": true,
    "follow_id": null,
    "created_at": "2026-04-03T12:00:00.000Z",
    "started_at": null,
    "completed_at": null
  },
  "meta": { "request_id": "req_xyz789" }
}

Headers: Location: /v1/jobs/job_abcdefghij123456, Retry-After: 30, X-Automated-Processing: true.

The job echoes back the queries, brands, surfaces, objects and from_job you sent. A job that starts at once can already be processing in the 202.

Idempotency

Send an Idempotency-Key header to avoid creating a job twice. The same key returns the original job with HTTP 200 for as long as that job exists. Keys are per workspace and matched on the key alone: a different body with the same key still returns the original job, once that body passes validation. dry_run requests are never deduplicated.

Dry run

With "dry_run": true the request is checked, priced and returned with HTTP 200. Nothing is created, so there is no job_id.

{
  "data": {
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "platform": "youtube",
    "duration_seconds": null,
    "credits_estimated": 7,
    "balance": 850,
    "would_exceed_balance": false,
    "note": "duration_unresolved"
  },
  "meta": { "request_id": "req_xyz789" }
}

The length isn't known before the video is fetched, so the estimate assumes your plan's longest video and note says duration_unresolved. would_exceed_balance is a warning, not a refusal. A dry run works on an empty balance too.


GET /v1/jobs/:id

A job, with results once it has completed.

curl https://api.netraflow.com/v1/jobs/job_abcdefghij123456 \
  -H "X-Api-Key: sk_live_your_key_here"
{
  "data": {
    "job_id": "job_abcdefghij123456",
    "status": "completed",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "platform": "youtube",
    "capabilities": ["transcription", "summary"],
    "language": null,
    "video_title": "Never Gonna Give You Up",
    "video_duration_seconds": 212,
    "video_thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
    "credits_estimated": 17,
    "credits_charged": 6,
    "processing_seconds": 43,
    "results": {
      "transcription": {
        "text": "...",
        "segments": [{ "start": 0.0, "end": 5.2, "text": "..." }],
        "language": "en",
        "duration_seconds": 212.0,
        "word_count": 1250
      },
      "summary": {
        "text": "...",
        "key_topics": ["topic1", "topic2"]
      }
    },
    "return_proof": true,
    "follow_id": null,
    "created_at": "2026-04-03T12:00:00.000Z",
    "started_at": "2026-04-03T12:00:01.000Z",
    "completed_at": "2026-04-03T12:00:45.000Z"
  },
  "meta": { "request_id": "req_abc456" }
}

A failed job has error: { code, message } instead of results. processing_seconds is how long the job took to process, null until it settles. It doesn't change what you pay.

Result keys

results has one key per capability: transcription, summary, brands, metadata, objects, and query_results for your questions. Each is described on its capability page. Also:

PropTypeDescription
confidence_summaryobjectBrand detections per confidence band: high, medium, low, total.
storyboardobjectFrames from a brands or objects job, tiled into sheets. See Frames.
metaobjectbrand_detection_degraded and object_detection_degraded when detection returned only part of its work; failed_capabilities when a capability could not run.

results can hold keys you didn't ask for: a job on a link can return the video's metadata, and a brands job returns the transcription it listened to. Neither adds to the cost.

When one capability cannot run

When brand detection or object tracking is unavailable, a job that asked only for it fails with a job failure code. A job that asked for more completes with the rest, and:

  • the missing capability's key is absent from results (an empty list would mean "none in the video");
  • results.meta.failed_capabilities lists it, with the code and message a failed job would carry;
  • nothing it did is charged.
"meta": {
  "failed_capabilities": [
    {
      "capability": "brands",
      "code": "BRAND_DETECTION_UNAVAILABLE",
      "message": "Brand detection is temporarily unavailable. Please try again."
    }
  ]
}

Submit the video again with just that capability to fill the gap. The job.completed webhook carries the same list.

What you are charged for

credits_charged is a flat $0.10 per video plus a usage charge for the analysis your job actually ran. 1 credit is $0.10, and the total is rounded up to a whole credit. Every amount in the API is in credits.

The usage part follows the work, not the length: brand detection on a 30-second clip can cost more than transcribing a long video. It is measured on every run, so the same request can cost a little differently twice.

JobCharged
CompletedFlat fee + usage
Completed, a capability failedEverything except that capability
Cancelled while runningThe work already done
Cancelled before it startedNothing
Failed or heldNothing

credits_charged is null until the job settles, and whenever nothing was billed.

credits_estimated is a guardrail, not a quote: the settled charge can land either side of it, usually below. max_credits is checked against the estimate once the real length is known. It does not cap the settled charge.

Job status values

PropTypeDescription
pendingstringQueued. With error.code CREDITS_SET_ASIDE or CONCURRENT_LIMIT_REACHED it is waiting for your other jobs, not failed, and starts by itself.
processingstringRunning. progress shows the stage.
completedstringDone. results is filled.
failedstringerror holds the code and message. Not charged.
cancelledstringCancelled with POST /v1/jobs/:id/cancel.
heldstringNot final. The video turned out to cost more than your balance. Resumes when you add credits, cancels itself after 7 days with HELD_EXPIRED. Not charged.

A held job fires a job.held webhook with credits_needed and balance. A pending job with CREDITS_SET_ASIDE is one your balance covers, but not beside the credits your running jobs have set aside. A submit with nothing left to spend is refused with INSUFFICIENT_CREDITS (402).

Progress

While a job is processing, progress shows where it is. Stages run in order: ingestion, transcription, analysis, merging.

"progress": {
  "stage": "analysis",
  "stages_completed": ["ingestion", "transcription"],
  "stages_remaining": ["merging"]
}

A stage the job doesn't need is listed in stages_completed once the job is past it.


GET /v1/jobs

Your jobs, newest first, with cursor pagination.

PropTypeDescription
status"pending" | "processing" | "completed" | "failed" | "cancelled" | "held"Only jobs with this status.
follow_idstring | "any"Only the jobs this follow created, or with "any", every job a follow created.
created_afterstring (ISO 8601)Only jobs created after this time.
created_beforestring (ISO 8601)Only jobs created before this time.
limitinteger (1-100)Jobs per page. Defaults to 20.
cursorstringpagination.next_cursor from the previous page.

next_cursor is null on the last page. A bad cursor or date returns VALIDATION_FAILED (422).

curl "https://api.netraflow.com/v1/jobs?status=completed&limit=5" \
  -H "X-Api-Key: sk_live_your_key_here"
{
  "data": [
    {
      "job_id": "job_abcdefghij123456",
      "status": "completed",
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "platform": "youtube",
      "capabilities": ["transcription", "summary"],
      "video_title": "Never Gonna Give You Up",
      "credits_estimated": 5,
      "credits_charged": 3,
      "follow_id": null,
      "created_at": "2026-04-03T12:00:00.000Z",
      "completed_at": "2026-04-03T12:00:45.000Z"
    }
  ],
  "pagination": {
    "has_more": true,
    "next_cursor": "MjAyNi0wNC0wM1QxMjowMDowMC4wMDBafGpvYl9hYmNkZWZnaGlqMTIzNDU2"
  },
  "meta": { "request_id": "req_xyz789" }
}

List items

A list item is a summary: job_id, status, url (or the upload_key), platform, capabilities, video_title, credits_estimated, credits_charged, follow_id, created_at and completed_at, each as on GET /v1/jobs/:id. Fetch the job for its results.

follow_id is null on a job you submitted, and names the follow that created it otherwise. Follows are set up in the console or over MCP; ?follow_id=any lists everything they caught.


POST /v1/jobs/:id/cancel

Cancel a pending, processing or held job. Returns HTTP 200 with the job as it stands right after the cancel.

curl -X POST https://api.netraflow.com/v1/jobs/job_abcdefghij123456/cancel \
  -H "X-Api-Key: sk_live_your_key_here"
{
  "data": {
    "job_id": "job_abcdefghij123456",
    "status": "cancelled",
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "platform": "youtube",
    "capabilities": ["transcription", "summary"],
    "language": null,
    "video_title": null,
    "video_duration_seconds": null,
    "video_thumbnail_url": null,
    "credits_estimated": 5,
    "credits_charged": null,
    "processing_seconds": null,
    "return_proof": true,
    "follow_id": null,
    "created_at": "2026-04-03T12:00:00.000Z",
    "started_at": "2026-04-03T12:00:01.000Z",
    "completed_at": null
  },
  "meta": { "request_id": "req_xyz789" }
}

credits_charged is still null in the response. A running job is charged for the work it did a few seconds later: read it with GET /v1/jobs/:id, or wait for the job.cancelled webhook, which carries the final charge.

A job in a batch can't be cancelled on its own: cancel the batch with POST /v1/batch/:id/cancel. Cancelling a job that already ended, or is in a batch, returns JOB_NOT_CANCELLABLE (409).


Authentication and scopes

Every request needs an X-Api-Key header. Create keys in the console under API keys (owners and managers).

ScopeGrants
jobs:writePOST to /v1/jobs, /v1/batch, /v1/uploads
jobs:readGET on jobs and batches, plus GET /v1/frames/:key
account:readGET /v1/account, GET /v1/usage

Every key gets all three scopes today, so treat any key as one that can spend credits. A key without a scope an endpoint needs returns INSUFFICIENT_SCOPE (403).


Error codes

Every error has the same shape. Quote meta.request_id when you contact support.

{
  "error": {
    "code": "INVALID_QUERY",
    "message": "queries[0] looks like an attempt to change the model's instructions. Ask about the video instead.",
    "details": { "index": 0 }
  },
  "meta": { "request_id": "req_xyz789" }
}

details comes with three codes only:

PropTypeDescription
VALIDATION_FAILED{ [field]: string[] }Each refused field and its messages, such as { "queries.0": ["String must contain at most 500 character(s)"] }. A rule across fields, such as both url and upload_key, has a message only.
INVALID_QUERY{ index: number }The position of the refused question in queries (or aggregate.queries).
RATE_LIMITED{ limit: number, period_seconds?: number }For the request rate: your plan's burst, and period_seconds 60. For unused uploads: the upload cap.
CodeHTTPMeaning
INVALID_API_KEY401Missing, expired, revoked or invalid API key
INSUFFICIENT_SCOPE403The key lacks the scope this endpoint needs
MODE_NOT_ALLOWED403Not on your plan, such as webhook_url on Free
QUERIES_DISABLED403Questions are switched off for now. Send the job without custom_query and queries, or retry later
INSUFFICIENT_CREDITS402Nothing left to spend: balance empty, or all set aside for running jobs
BUDGET_EXCEEDED402A batch's estimate passes its max_credits
SUBSCRIPTION_PAST_DUE402The subscription payment is overdue
NOT_FOUND404No such endpoint
JOB_NOT_FOUND404No such job in your workspace
JOB_NOT_CANCELLABLE409The job already ended, or is in a batch
BODY_TOO_LARGE413The body is over 10 KB, or an upload over 500 MB
VALIDATION_FAILED422Not JSON, or a field is invalid. See details
INVALID_URL422Not a supported video link, or a refused webhook_url
INVALID_CAPABILITIES422custom_query with no queries, or objects with nothing to track
INVALID_QUERY422A question tries to change the model's instructions. details.index names it
TOO_MANY_QUERIES422Over your plan's questions per job
TOO_MANY_BRANDS422More than 50 brands. A name over 100 characters is VALIDATION_FAILED
INVALID_SURFACE422A surface that isn't available yet. The message lists the ones you can use
RATE_LIMITED429Over your plan's request rate, or too many unused uploads. Comes with Retry-After: 60
CONCURRENT_LIMIT_REACHED429Your plan's jobs-at-once limit is reached. Retry once one finishes
PROCESSING_FAILED500Something went wrong on our side. Retry, and quote the request id to support

A list longer than the every-plan maximum on Limits fails with VALIDATION_FAILED before the plan limit is checked.

Successful responses carry X-RateLimit-Limit (your burst) and X-RateLimit-Remaining. Error responses don't.

Job failure codes

A failed job carries one of these in error.code, inside a 200 from GET /v1/jobs/:id. A failed job is never charged and never retried for you.

CodeMeaning
EXTRACTION_FAILEDThe video couldn't be fetched or read: removed, a post with no video, or a file that isn't a video
PLATFORM_403The platform refused access to the video
PLATFORM_LOGIN_REQUIREDThe video needs a login, such as a private one
PLATFORM_UNAVAILABLENot a video the platform serves, or a live stream or premiere that hasn't ended
PLATFORM_RATE_LIMITEDThe platform is limiting requests. Retry later
UPLOAD_NOT_FOUNDThe upload is gone. Upload the file again
VIDEO_TOO_LONGThe video is longer than your plan allows for this job
BUDGET_EXCEEDEDThe estimate on the real length passed max_credits
TRANSCRIPTION_FAILEDTranscription failed
ANALYSIS_FAILEDAnalysis failed
BRAND_DETECTION_UNAVAILABLEBrand detection is temporarily unavailable. Retry
OBJECT_DETECTION_UNAVAILABLEObject detection is temporarily unavailable. Retry
INVALID_SURFACEA surface you asked for stopped being available. Resubmit with a current one
STORAGE_UNAVAILABLEA temporary storage problem. Retry
TIMEOUTThe job ran out of time. Retry
PROCESSING_INTERRUPTEDProcessing stopped unexpectedly. Retry
HELD_EXPIREDA held job wasn't funded within 7 days. The job is cancelled, not failed
PROCESSING_ERRORAny other failure

BRAND_DETECTION_UNAVAILABLE and OBJECT_DETECTION_UNAVAILABLE fail the job only when that capability is all it asked for; otherwise see When one capability cannot run. A brands job that runs short completes with what it found and meta.brand_detection_degraded: true, and is charged for that work. TIMEOUT always fails the whole job.

CREDITS_SET_ASIDE and CONCURRENT_LIMIT_REACHED on a pending job are not failures: see Job status values.

On this page