{"openapi":"3.1.0","info":{"title":"GEN Intelligence API","description":"Asynchronous video analysis: create a job, upload the file, submit it, wait for it to finish, read the result. Every response is JSON.","version":"v1"},"servers":[{"url":"https://gen-intelligence.globalemancipation.ngo","description":"This deployment"}],"security":[{"apiKey":[]}],"tags":[{"name":"Jobs","description":"A job is one video through the pipeline: created, uploaded, submitted, processed, described."},{"name":"Capacity","description":"Whether the describe worker can take a job right now."},{"name":"Schemas","description":"The published contract for the result, as JSON Schema."}],"paths":{"/v1/jobs":{"get":{"tags":["Jobs"],"summary":"List your jobs","description":"Your most recent jobs, newest first.","operationId":"listJobs","parameters":[{"name":"limit","in":"query","description":"How many to return, between 1 and 100.","required":false,"schema":{"type":"integer","format":"int32","default":20,"maximum":100,"minimum":1}}],"responses":{"200":{"description":"The jobs.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Job"}}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}},"post":{"tags":["Jobs"],"summary":"Create a job","description":"Registers a job for one video and mints the URL to upload it to.\n\nThe upload itself goes straight to storage: none of the bytes pass through the API or its rate limits.","operationId":"createJob","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobRequest"}}},"required":true},"responses":{"201":{"description":"The job, awaiting its upload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobResponse"}}}},"400":{"description":"The filename is not acceptable, or `size_bytes` is missing, not positive, or above the cap of 500 MB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/jobs/{jobId}/submit":{"post":{"tags":["Jobs"],"summary":"Submit a job","description":"Tells the pipeline the upload is complete and queues the job.","operationId":"submitJob","parameters":[{"name":"jobId","in":"path","description":"The job's id, from the create response.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The job, now queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"404":{"description":"No such job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The job was already submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"You already have 50 jobs queued or processing. Wait for some to finish, then submit again. Or, from the gateway: This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/GatewayError"}]}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/schemas/result":{"get":{"tags":["Schemas"],"summary":"The result's JSON Schema","description":"The contract for the result response and the document behind `resultUrl`, as JSON Schema draft 2020-12. Validate against this rather than against a copy.","operationId":"getResultSchema","responses":{"200":{"description":"The schema.","content":{"application/schema+json":{"schema":{"type":"object"}}}}},"security":[]}},"/v1/jobs/{jobId}":{"get":{"tags":["Jobs"],"summary":"Read a job","description":"The job's current state.","operationId":"getJob","parameters":[{"name":"jobId","in":"path","description":"The job's id, from the create response.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"404":{"description":"No such job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/jobs/{jobId}/wait":{"get":{"tags":["Jobs"],"summary":"Wait for a job to finish","description":"Blocks for up to `max_wait` seconds, returning as soon as the job reaches a terminal state. Values above the ceiling are quietly shortened to it.\n\nThe ceiling sits below the gateway's 29-second timeout, so a wait never fails at the gateway.","operationId":"waitForJob","parameters":[{"name":"jobId","in":"path","description":"The job's id, from the create response.","required":true,"schema":{"type":"string"}},{"name":"max_wait","in":"query","description":"Seconds to wait, at most 25.","required":false,"schema":{"type":"integer","format":"int32","default":20,"maximum":25,"minimum":0}}],"responses":{"200":{"description":"The job, terminal or not.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"404":{"description":"No such job.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/jobs/{jobId}/video":{"get":{"tags":["Jobs"],"summary":"Play back the uploaded video","description":"A time-limited URL for the file you uploaded, for as long as it is retained.\n\nAnswers `404` once the upload has passed its retention window and been deleted.","operationId":"getVideo","parameters":[{"name":"jobId","in":"path","description":"The job's id, from the create response.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The playback URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Video"}}}},"404":{"description":"No such job, nothing uploaded yet, or the upload has passed its retention window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/jobs/{jobId}/result":{"get":{"tags":["Jobs"],"summary":"Read the result","description":"The description and scene analysis for a job that has `SUCCEEDED`.","operationId":"getResult","parameters":[{"name":"jobId","in":"path","description":"The job's id, from the create response.","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Result"}}}},"404":{"description":"No such job, or no result yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}},"/v1/capacity":{"get":{"tags":["Capacity"],"summary":"Read the worker's availability","description":"The same `capacity` block every job response carries, for callers with no job to read it from.","operationId":"getCapacity","responses":{"200":{"description":"The availability now.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Capacity"}}}},"403":{"description":"The API key is missing, unknown, or not yet propagated to the gateway. Check the `x-api-key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}},"429":{"description":"This API key's request rate is over its limit. Back off and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"}}}}}}}},"components":{"schemas":{"CreateJobRequest":{"type":"object","properties":{"filename":{"type":"string","description":"The file's name, kept for you. Characters outside letters, digits, dot, dash and underscore are replaced.","example":"clip.mp4","maxLength":200,"minLength":1},"size_bytes":{"type":"integer","format":"int64","description":"The file's exact size in bytes. The upload is signed for this many bytes, and the cap is 500 MB.","example":8123456,"exclusiveMinimum":0,"maximum":524288000}},"required":["filename","size_bytes"]},"Capacity":{"type":"object","description":"Whether the describe worker can take a job right now. `etaSeconds`, `estimatedReadyAt` and `message` are null when it is ready.","properties":{"state":{"type":"string","enum":["OFF","STARTING","READY"]},"etaSeconds":{"type":"integer","format":"int64","description":"Seconds until the worker is expected to be ready."},"estimatedReadyAt":{"type":"string","format":"date-time","description":"When the worker is expected to be ready."},"message":{"type":"string","description":"Plain-language status, safe to show verbatim."}},"required":["state"]},"CreateJobResponse":{"type":"object","properties":{"jobId":{"type":"string","description":"The new job's id.","example":"j7k2m9p4q1r8s"},"uploadUrl":{"type":"string","format":"uri","description":"Where to PUT the file. Signed for `application/octet-stream` and the declared size."},"uploadUrlExpiresAt":{"type":"string","format":"date-time","description":"When `uploadUrl` stops working, 15 minutes from now."},"sizeBytes":{"type":"integer","format":"int64","description":"The size you declared."},"capacity":{"$ref":"#/components/schemas/Capacity"}},"required":["jobId","sizeBytes","uploadUrl","uploadUrlExpiresAt"]},"Job":{"type":"object","description":"A job, with the worker's availability at the moment of the response.","properties":{"jobId":{"type":"string","description":"The job's id."},"customerId":{"type":"string","description":"The account the job belongs to."},"status":{"type":"string","description":"Where the job is: `PENDING_UPLOAD` until you submit it, then `QUEUED` and `PROCESSING`, ending in `SUCCEEDED` or `FAILED`.","enum":["PENDING_UPLOAD","QUEUED","PROCESSING","SUCCEEDED","FAILED"]},"filename":{"type":"string","description":"The filename you gave at creation, unchanged. Absent on jobs created before the field existed."},"uploadKey":{"type":"string","description":"Where the upload is stored, once created. An opaque location, not to be parsed."},"resultKey":{"type":"string","description":"Where the result is stored, once succeeded."},"sizeBytes":{"type":"integer","format":"int64","description":"The size declared at creation, in bytes."},"description":{"type":"string","description":"The video's description, once the job has succeeded."},"errorMessage":{"type":"string","description":"Why the job failed, when it did."},"createdAt":{"type":"string","format":"date-time","description":"When the job was created."},"updatedAt":{"type":"string","format":"date-time","description":"When the job last changed state."},"completedAt":{"type":"string","format":"date-time","description":"When the job reached `SUCCEEDED` or `FAILED`."},"metered":{"type":"boolean","description":"Whether this job has been counted as a processed video."},"capacity":{"$ref":"#/components/schemas/Capacity"}},"required":["jobId","status"]},"Video":{"type":"object","properties":{"jobId":{"type":"string"},"videoUrl":{"type":"string","format":"uri","description":"Where to fetch the file from, for as long as the URL lasts."},"videoUrlExpiresAt":{"type":"string","format":"date-time","description":"When `videoUrl` stops working, 30 minutes from now."},"contentType":{"type":"string","description":"What the bytes are, from the filename. `application/octet-stream` when unrecognised."}},"required":["contentType","jobId","videoUrl","videoUrlExpiresAt"]},"Error":{"description":"Every error the API returns has this body.","properties":{"error":{"type":"string","description":"What went wrong, in plain language."},"status":{"type":"integer","format":"int32","description":"The HTTP status, repeated."},"reference":{"type":"string","description":"Present on server-side failures. Quote it to support."}}},"Result":{"type":"object","description":"Response of GET /v1/jobs/{jobId}/result. This document is the whole contract: every field a result can carry is described here, and a field not described here is not returned. Fields will not be renamed or removed without a version bump, though new optional fields may be added. The same document is available from the API at GET /v1/schemas/result.","properties":{"jobId":{"type":"string","description":"The job this result belongs to."},"analysis":{"$ref":"#/components/schemas/Analysis"},"resultUrl":{"type":"string","format":"uri","description":"Time-limited download URL for the full result document (see $defs/resultDocument). Fetch it before resultUrlExpiresAt; call /result again for a fresh URL. Nothing here is stored indefinitely, and a fresh URL cannot recover a result the deployment has already removed - see the retention section of the API guide for this deployment's terms."},"resultUrlExpiresAt":{"type":"string","format":"date-time","description":"Expiry of resultUrl (RFC 3339 timestamp)."}},"required":["jobId","resultUrl","resultUrlExpiresAt"],"title":"GEN Intelligence job result"},"Analysis":{"type":"object","description":"Everything GEN Intelligence found in the video, as one analysis - the products behind it are not a distinction a reader has to make. Two rules hold throughout. Every time is a number of seconds from the start of the video - never a frame number, never a clock string. And no model score is published: where a score decides something the decision is what you get, a verdict or `confident` on a label, because the scores behind them are uncalibrated and a figure would invite a precision the models do not have. An absent field means that analysis did not run or produced nothing; an empty array means it ran and found nothing.","properties":{"schemaVersion":{"type":"integer","description":"Version of this analysis structure. Incremented only for a change that could break a reader; added optional fields do not change it."},"people":{"type":"array","description":"One entry per person tracked in the video, ordered youngest estimated age first so the entry that most warrants attention is read first. Each carries the crops it was read from, which are the only images this document publishes: the frames the analysis was computed over are pipeline bookkeeping, and how many there are is a deployment setting rather than a property of the video. Best-effort; may be empty.","items":{"$ref":"#/components/schemas/Person"}},"description":{"type":"string","description":"A natural-language description of the video, written from the frames the analysis selected. Absent when this deployment runs no description model, so a reader must handle its absence rather than treating it as an error."},"source":{"type":"object","description":"The submitted video's own properties, as the decoder read them.","properties":{"duration":{"type":"number","description":"Length of the video in seconds."},"width":{"type":"integer","description":"Frame width in pixels."},"height":{"type":"integer","description":"Frame height in pixels."},"fps":{"type":"number","description":"Frames per second as the container states it (e.g. 23.976). Here because a reviewer quoting a file needs it; no other field in this document is expressed in frames."}}},"scenes":{"type":"array","description":"Continuous shots, in order.","items":{"$ref":"#/components/schemas/Span"}},"activity":{"type":"array","description":"Stretches of sexual activity, in order. An empty array means the analysis ran and found none.","items":{"$ref":"#/components/schemas/ActivityBlock"}},"nudity":{"type":"object","description":"Nudity across the video.","properties":{"detected":{"type":"boolean","description":"Whether any nudity was found."},"totalSeconds":{"type":"number","description":"Total seconds of the video carrying nudity."},"spans":{"type":"array","description":"When the nudity occurs.","items":{"$ref":"#/components/schemas/Span"}}}},"bodyParts":{"type":"array","description":"Exposure by body region, ordered by how long each is exposed for.","items":{"type":"object","properties":{"label":{"type":"string","description":"The body region in plain language (e.g. \"Chest (exposed)\")."},"spans":{"type":"array","description":"When this region is exposed.","items":{"$ref":"#/components/schemas/Span"}}},"required":["label","spans"]}},"places":{"type":"array","description":"Specific places the video appears to be shot in (e.g. \"Hotel room\"), as the stretches they cover. Overlaps are possible: two places can be reported over the same seconds.","items":{"$ref":"#/components/schemas/LabelledSpan"}},"spaceTypes":{"type":"array","description":"The broader kind of space over the same stretches (e.g. \"Private space\"). Separate from `places` because the specific place says far more about a file than its category does.","items":{"$ref":"#/components/schemas/LabelledSpan"}},"speech":{"type":"object","description":"Speech in the audio track. Absent when the video has no audio or the analysis failed; present with an empty transcript when it ran and heard nothing.","properties":{"transcript":{"type":"string","description":"The full transcript. Empty when nothing was said."},"language":{"type":"string","description":"Detected language as an uppercase code (e.g. \"EN\"). The detector reports a language even for silence, so it means something only alongside a transcript."},"spokenSeconds":{"type":"number","description":"Seconds of the video carrying voice - a measured quantity, not a score."},"captions":{"type":"array","description":"The transcript split into timed utterances, in order.","items":{"type":"object","properties":{"start":{"type":"number","description":"Start of the utterance, in seconds."},"end":{"type":"number","description":"End of the utterance, in seconds."},"text":{"type":"string","description":"What was said."}},"required":["end","start","text"]}}}},"voice":{"$ref":"#/components/schemas/Label","description":"The most likely voice class in the audio (e.g. \"Female\"). A classification of the voice, not of a person, and frequently inconclusive."},"onScreenText":{"type":"array","description":"Text read off the video, in order, at its first sighting. Text sitting in frame is read again in every frame it occupies, so repeats of the same string collapse into the first time it appears. Recognition is imperfect on burnt-in overlays; expect garbling.","items":{"type":"object","properties":{"at":{"type":"number","description":"When the text was first seen, in seconds."},"text":{"type":"string","description":"The text as read."}},"required":["at","text"]}},"watermark":{"type":"string","description":"A stationary text watermark, if one was found - a provenance signal, which is why it is kept apart from onScreenText. Absent when none was found."},"format":{"type":"object","description":"Whole-file verdicts. Each is the analysis engine's own decision; the scores behind them are not published.","properties":{"talkVideo":{"type":"boolean","description":"Whether the video is mostly someone talking to camera."},"pov":{"type":"boolean","description":"Whether the video is shot from a participant's point of view."},"keywords":{"type":"array","description":"Notable words picked out of the speech.","items":{"type":"string"}}}}},"required":["people","schemaVersion"]},"TimeSpan":{"type":"object","description":"A stretch of the video. Both bounds are seconds from the start.","properties":{"start":{"type":"number","description":"Start of the stretch, in seconds."},"end":{"type":"number","description":"End of the stretch, in seconds."}},"required":["end","start"]},"Span":{"type":"object","allOf":[{"$ref":"#/components/schemas/TimeSpan"}],"description":"A stretch of the video, with how strongly the thing was detected across it.","properties":{"intensity":{"type":"number","description":"How strongly the thing was detected across this stretch, 0 to 1. The one score-derived value in this document, and it is here for display only - it is meant to shade a timeline. It is not calibrated for comparison between videos, and not a threshold.","maximum":1,"minimum":0}}},"Image":{"type":"object","description":"One image cut from the video.","properties":{"url":{"type":"string","format":"uri","description":"Time-limited image URL. On a /result response it is minted for that response and expires with its resultUrl, so read /result again for fresh links. Separately from that signature, an image is never removed before the result document that links it, so an estimate read off a crop can be checked against that crop for as long as the estimate itself exists."},"at":{"type":"number","description":"Where in the video the image is from, in seconds. Absent when it could not be timed."}},"required":["url"]},"Crop":{"type":"object","allOf":[{"$ref":"#/components/schemas/Image"}],"description":"One crop of a person's face: an image plus, where available, its own estimated age range.","properties":{"ageRange":{"$ref":"#/components/schemas/AgeRange"}}},"Person":{"type":"object","description":"A person tracked across the video. Identification is per-video only: it says the same face was seen at these moments, and nothing about who they are.","properties":{"id":{"type":"string","description":"Identifies this person within this job only. Not stable across jobs, and carries no meaning of its own."},"ageRange":{"$ref":"#/components/schemas/AgeRange","description":"Estimated age range for this person, from whichever crop reads youngest - which is crops[0], so the frame the estimate came from can always be seen. Absent when no crop could be aged, which is not evidence of an adult."},"age":{"$ref":"#/components/schemas/Label","description":"The analysis engine's own age band for this person (e.g. \"20-29\") - a coarser and separate judgement from ageRange. Present where the engine classified them."},"gender":{"$ref":"#/components/schemas/Label","description":"The analysis engine's apparent-gender classification."},"race":{"$ref":"#/components/schemas/Label","description":"The analysis engine's apparent-race classification. Reported because an investigator describing a victim needs every attribute the analysis offers; it classifies appearance and is frequently inconclusive."},"onScreen":{"type":"array","description":"When this person is on screen. Consecutive sightings are merged into the stretch they cover.","items":{"$ref":"#/components/schemas/Span"}},"crops":{"type":"array","description":"Every crop of this person's face, ordered by estimated age, youngest first. Crops that could not be aged sort last.","items":{"$ref":"#/components/schemas/Crop"}}},"required":["crops","id"]},"AgeRange":{"type":"object","description":"An estimated age range in years. Best-effort: an absent estimate is not evidence of an adult.","properties":{"low":{"type":"integer","description":"Lower bound of the estimated age range, in years.","minimum":0},"high":{"type":"integer","description":"Upper bound of the estimated age range, in years.","minimum":0}},"required":["high","low"]},"Label":{"type":"object","description":"A classification, as the label plus whether it stands clear of its runners-up. The probability is deliberately not published: it is uncalibrated, and `confident` is the only judgement it supports.","properties":{"value":{"type":"string","description":"The most likely label, in plain language."},"confident":{"type":"boolean","description":"False when the top label was close to its runners-up, which happens often. Read a false here as \"inconclusive\" rather than as the label being weakly true."}},"required":["confident","value"]},"ResultDocument":{"type":"object","description":"The JSON document behind resultUrl: the same analysis, stored durably. Its image links are the ones stored with it, and re-fetching it does not renew them - read /result for fresh ones.","properties":{"analysis":{"$ref":"#/components/schemas/Analysis"}},"required":["analysis"]},"LabelledSpan":{"type":"object","allOf":[{"$ref":"#/components/schemas/TimeSpan"}],"description":"A stretch of the video carrying a label.","properties":{"label":{"type":"string","description":"What was found, in plain language."}},"required":["label"]},"ActivityBlock":{"type":"object","allOf":[{"$ref":"#/components/schemas/TimeSpan"}],"description":"A stretch of sexual activity.","properties":{"label":{"type":"string","description":"The activity in reviewer's language (e.g. \"Oral sex\")."},"clinicalDescription":{"type":"string","description":"The same activity in clinical terms, for case notes. Absent where the analysis engine reported an activity this deployment has no clinical wording for."},"confident":{"type":"boolean","description":"Whether the analysis engine reported this as a firm detection rather than a possible one. Present on every block, so a possible one is never mistaken for a firm one."}},"required":["confident","label"]},"GatewayError":{"description":"What the gateway answers with when it refuses a request itself, before the service sees it. Not the service's error body.","properties":{"message":{}},"required":["message"]}},"securitySchemes":{"apiKey":{"type":"apiKey","description":"The API key issued when you subscribed. Send it on every request.","name":"x-api-key","in":"header"}}},"x-gen-guide":[{"id":"access","title":"Getting an API key","paragraphs":["Subscribe through AWS Marketplace. When AWS hands you back to us, the registration page issues your API key and shows it once; it is never stored on our side, so keep it somewhere safe. Send it as the `x-api-key` header on every request.","Lost it? Sign in with your customer ID and the email you verified after subscribing, and a fresh API key is issued. The old one stops working the moment you confirm."]},{"id":"billing","title":"Billing","paragraphs":["One unit per successfully described video, whatever its size. A job that fails costs nothing. Usage is reported through AWS Marketplace and appears on your AWS invoice; there is no separate billing to set up."]},{"id":"capacity","title":"The GPU sleeps when idle","paragraphs":["The worker that describes your video is switched off when nobody has used it for a while. Creating or submitting a job wakes it. While it starts, every job response and `GET /v1/capacity` carry a `capacity` block with the worker's `state`, an `etaSeconds` countdown and a plain-language `message` you can show as it is.","The first job after a quiet spell can wait ten minutes or more before processing starts; later ones run at normal speed. Show the countdown rather than a silent `PROCESSING`."]},{"id":"limits","title":"Rate limits","paragraphs":["The gateway throttles each API key's request rate and answers `429` with a body of `{\"message\":\"Too Many Requests\"}`; back off and retry. Separately, the service caps how many of your jobs may be queued or processing at once, and submitting past the cap answers `429` with the service's own error body."]},{"id":"retention","title":"Retention","paragraphs":["Uploads are kept for a limited window after processing and then deleted; the playback URL answers `404` once that has happened. Results are kept for longer, then deleted too, together with the face crops they link to - so a result and the images behind it never outlive one another. Separately and sooner, a result's download and image links expire, so read the result again for fresh ones."]}],"x-gen-workflow":[{"id":"api-key","title":"Get an API key","paragraphs":["Subscribe through AWS Marketplace and keep the API key the registration page shows you. Every request below carries it as `x-api-key`."]},{"id":"create","title":"Create a job","paragraphs":["Tell the API the file's name and exact size in bytes. The response carries the `jobId` you use from here on and a presigned `uploadUrl`, valid for 15 minutes."],"operationId":"createJob"},{"id":"upload","title":"Upload the file","paragraphs":["PUT the bytes straight to `uploadUrl`. The content type is part of the URL's signature, so send exactly `application/octet-stream`. Nothing in this step goes through the API."],"request":{"method":"PUT","url":"{uploadUrl}","headers":{"Content-Type":"application/octet-stream"},"body":{"kind":"file"}}},{"id":"submit","title":"Submit the job","paragraphs":["Once the upload has finished, hand the job to the pipeline. The job moves to `QUEUED`, then `PROCESSING`."],"operationId":"submitJob"},{"id":"wait","title":"Wait for it to finish","paragraphs":["Long-poll: the request blocks server-side for up to `max_wait` seconds and returns as soon as the job reaches `SUCCEEDED` or `FAILED`. If it is still running when the wait ends, call it again. A plain read of the job works too, if you would rather poll yourself."],"operationId":"waitForJob"},{"id":"result","title":"Read the result","paragraphs":["The description and the scene analysis come back inline, with a time-limited URL to the full result document. Image links inside the result expire on the same schedule; call again for fresh ones."],"operationId":"getResult"}]}