NetraFlow
API Reference

Jobs

Create, retrieve, list, and cancel video analysis jobs.

POST /v1/jobs

Create a new video analysis job. The API validates the request, estimates credits, and queues the job for processing.

Request body

Provide exactly one source: either url or upload_key.

PropTypeDescription
urlstringVideo URL (YouTube, TikTok, or Instagram). Mutually exclusive with upload_key.
upload_keystringKey returned by POST /v1/uploads/presign, for analyzing your own file. Mutually exclusive with url.
upload_filenamestringOriginal filename for an upload_key job. Becomes the video title. Max 255 chars.
capabilitiesstring[]One or more of: transcription, summary, brands, metadata. At least one required.
queriesstring[]Custom questions about the video. Max 500 chars each, and at most your plan's max_queries_per_job.
brandsstring[]Brand names to target for brand detection. Max 50 items, 100 chars each.
surfacesstring[]Surface slugs to target (e.g. "jersey"). Max 10. Unknown slugs return INVALID_SURFACE (422).
languagestringLanguage hint for transcription (e.g. "en"). Max 10 chars.
webhook_urlstringHTTPS URL for webhook delivery. Starter plan and above.
fpsinteger (1-4)Frame-sampling rate, clamped to your plan's max_fps. Defaults to the plan ceiling.
return_proofbooleanInclude visual evidence: the storyboard filmstrip on brands jobs, and speech_context on speech-derived brand appearances. Defaults to true.
dry_runbooleanReturn a cost preview with HTTP 200 and create nothing. Defaults to false.
max_creditsintegerReject the request with BUDGET_EXCEEDED (402) if the estimate exceeds this cap.

On Free and Starter, brand-detection jobs must be targeted — pass brands, surfaces, or both. An untargeted brands job is rejected with PLAN_REQUIRED (403).

Idempotency

Include an Idempotency-Key header to prevent duplicate job creation. If a matching key is found within the 48-hour deduplication window, the original job is returned with HTTP 200 — no new job is created. After 48 hours the key expires and a new job would be created.

Keys are scoped to your workspace and matched on the key alone: reusing a key with a different body returns the original job, not a new one. dry_run requests are never deduplicated.

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_abc123",
    "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": 17,
    "credits_charged": 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" }
}

Response headers include Location: /v1/jobs/job_abc123, Retry-After: 30, and X-Automated-Processing: true.

Jobs created with queries, brands, or surfaces echo those arrays back in the response.


GET /v1/jobs/:id

Retrieve a job by ID, including results when completed.

curl https://api.netraflow.com/v1/jobs/job_abc123 \
  -H "X-Api-Key: sk_live_your_key_here"
{
  "data": {
    "job_id": "job_abc123",
    "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": 5,
    "credits_charged": 3,
    "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" }
}

Result keys

results carries one key per produced capability — transcription, summary, brands, metadata — plus:

PropTypeDescription
query_resultsobject[]Answers to your queries, each with query, answer, and evidence timestamps.
confidence_summaryobjectBrand detection counts by confidence band: high, medium, low, total.
storyboardobjectFilmstrip manifest on return_proof brands jobs. Its sheets are fetched via GET /v1/frames/:key.
metaobjectProcessing-health flags, such as brand_detection_degraded.

Failed jobs carry error: { code, message } instead of results.

Job status values

PropTypeDescription
pendingstringJob is queued and waiting to be picked up.
processingstringJob is actively being processed. The progress object shows the current stage.
completedstringJob finished successfully. Results are available in the results object.
failedstringJob failed. The error object contains the failure code and message. Failed jobs are not charged.
cancelledstringJob was cancelled via the cancel endpoint.
heldstringNon-terminal. The job passed the submit-time check, ran ingestion, then could not afford its real cost. Resumes automatically on a credit top-up; auto-cancels after 7 days. Nothing is charged.

Progress

The progress object is included only while status is "processing". Pipeline stages run in order: ingestiontranscriptionanalysismerging.


GET /v1/jobs

List jobs with cursor-based pagination.

Query parameters

PropTypeDescription
status"pending" | "processing" | "completed" | "failed" | "cancelled" | "held"Filter by job status.
follow_idstring | "any"Only return jobs created by a Follow. Pass a follow id for one follow, or "any" for every follow-created job in the workspace.
created_afterstring (ISO 8601)Only return jobs created after this timestamp.
created_beforestring (ISO 8601)Only return jobs created before this timestamp.
limitinteger (1-100)Number of jobs per page. Defaults to 20.
cursorstringPagination cursor from a previous response.

Pass next_cursor from the previous response verbatim. Results are ordered newest-first by created_at, then id. next_cursor is null on the last page, and a malformed cursor returns VALIDATION_FAILED (422).

Every job carries follow_idnull for a job you submitted, otherwise the Follow that created it. Together with the filter above, that is the whole read side of Follows: a follow-created job is an ordinary job, so GET /v1/jobs?follow_id=any answers "what did my follows catch this week" with no extra endpoint. Follows themselves are set up in the console; there is no POST /v1/follows.

curl "https://api.netraflow.com/v1/jobs?status=completed&limit=5" \
  -H "X-Api-Key: sk_live_your_key_here"
{
  "data": [
    {
      "job_id": "job_abc123",
      "status": "completed",
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "platform": "youtube",
      "credits_estimated": 5,
      "follow_id": null,
      "created_at": "2026-04-03T12:00:00.000Z"
    }
  ],
  "pagination": {
    "has_more": true,
    "next_cursor": "eyJpZCI6IjEyMyJ9"
  },
  "meta": { "request_id": "req_xyz789" }
}

POST /v1/jobs/:id/cancel

Cancel a pending, processing, or (unbatched) held job. Jobs belonging to a batch must be cancelled via POST /v1/batch/:id/cancel.

curl -X POST https://api.netraflow.com/v1/jobs/job_abc123/cancel \
  -H "X-Api-Key: sk_live_your_key_here"
{
  "data": {
    "job_id": "job_abc123",
    "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": 2,
    "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" }
}

Returns JOB_NOT_FOUND (404) if the job doesn't exist, or JOB_NOT_CANCELLABLE (409) if the job is already in a terminal state.

Cancelling a running job charges only for the compute already spent, which is typically a fraction of the full price. Cancel before processing starts — or early enough that nothing has been spent — and the job is free (credits_charged stays 0/null). A job that fails is always free.


Authentication and scopes

Every request needs an X-Api-Key header. Keys carry scopes:

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

A key without the required scope returns INSUFFICIENT_SCOPE (403).


Error codes

These are returned synchronously, as error.code with the listed HTTP status.

CodeHTTPDescription
INVALID_API_KEY401Missing, revoked, or invalid API key
INSUFFICIENT_SCOPE403Key lacks the scope this endpoint requires
INSUFFICIENT_CREDITS402Balance is empty at submit time
BUDGET_EXCEEDED402Estimate exceeds the max_credits you set
SUBSCRIPTION_PAST_DUE402Subscription payment is overdue
PLAN_REQUIRED403Your plan requires this job to be targeted
TARGET_REQUIRED403Brand detection needs a brands or surfaces target
MODE_NOT_ALLOWED403Feature not available on your plan (e.g. webhooks on Free)
RATE_LIMITED429Per-plan rate limit exceeded
CONCURRENT_LIMIT_REACHED429Too many jobs processing simultaneously
PLATFORM_RATE_LIMITED429Too many requests to the video platform
INVALID_URL422URL is malformed, unreachable, or not a supported platform
VALIDATION_FAILED422Request body failed schema validation
PLATFORM_UNAVAILABLE422Platform is temporarily unreachable
VIDEO_TOO_LONG422Video exceeds your plan's max duration
TOO_MANY_QUERIES422Exceeds your plan's max_queries_per_job (Free: 3, paid: 10)
TOO_MANY_BRANDS422More than 50 target brands
INVALID_SURFACE422Unknown or unavailable surface slug
JOB_NOT_FOUND404Job does not exist or belongs to another workspace
JOB_NOT_CANCELLABLE409Job is already completed, failed, or cancelled
BODY_TOO_LARGE413Request body exceeds 10 KB

Job failure codes

These appear as error.code inside a 200 response from GET /v1/jobs/:id when a job fails — they are not HTTP statuses.

CodeMeaning
EXTRACTION_FAILEDVideo download or frame extraction failed
PLATFORM_403The platform refused access to the video
PLATFORM_UNAVAILABLEPlatform unreachable during extraction
PLATFORM_RATE_LIMITEDPlatform rate-limited the extraction
VIDEO_TOO_LONGReal duration exceeded your plan cap after ingestion
TRANSCRIPTION_FAILEDTranscription stage failed
ANALYSIS_FAILEDAnalysis stage failed
SURFACE_VERIFICATION_UNAVAILABLESurface targeting could not be verified
TIMEOUTThe job exceeded its overall time budget
STAGE_DEADLINE_EXCEEDEDA single pipeline stage exceeded its deadline
WORKER_STALLEDThe processing worker stopped responding
HELD_EXPIREDA held job was not funded within 7 days
PROCESSING_ERRORUnclassified pipeline failure

INSUFFICIENT_CREDITS (402) is returned at submit time when your balance is empty. The affordability check runs again after ingestion against the video's real duration — but a job you can't cover is not failed or charged there. It moves to the non-terminal held status and fires a job.held webhook carrying credits_needed and balance. Held jobs resume automatically when you add credits, or are cancelled after 7 days with HELD_EXPIRED. Held and failed jobs both cost nothing.


Rate limiting

Requests are rate-limited per plan using a token bucket: a bucket of burst_limit tokens that refills continuously at your plan's sustained rate. An idle client can spend a full burst at once; sustained traffic settles at the per-minute rate.

PlanSustainedBurst
Free10 req/min15
Starter30 req/min45
Pro60 req/min90
Scale300 req/min450

Successful responses carry X-RateLimit-Limit (the burst capacity) and X-RateLimit-Remaining. When rate-limited you get HTTP 429 with Retry-After: 60; the X-RateLimit-* headers are not present on error responses.

On this page