> ## Documentation Index
> Fetch the complete documentation index at: https://docs.geekhub.mx/llms.txt
> Use this file to discover all available pages before exploring further.

# GET /v1/videos/{id}

> Poll a video job status

## Path params

<ParamField path="job_id" type="string" required>
  The `id` you received when creating the job (UUID format).
</ParamField>

## Response

While still processing:

```json theme={null}
{
  "id": "e8c96506-72a1-429f-a7ed-4bb0a42541be",
  "object": "video.job",
  "status": "processing",
  "model": "fal/kling-2",
  "prompt": "...",
  "video_url": null,
  "duration": 5,
  "error": null,
  "created": 1782415166,
  "completed": null
}
```

When finished (`completed`):

```json theme={null}
{
  "id": "e8c96506-72a1-429f-a7ed-4bb0a42541be",
  "object": "video.job",
  "status": "completed",
  "model": "fal/kling-2",
  "prompt": "...",
  "video_url": "https://supabase.geekhub.mx/storage/v1/object/public/generated-videos/<org>/<job>/video.mp4",
  "duration": 5,
  "error": null,
  "created": 1782415166,
  "completed": 1782415954
}
```

When it fails:

```json theme={null}
{
  "status": "failed",
  "video_url": null,
  "error": "fal status: ERROR",
  "completed": 1782415954
}
```

## How often to poll

Each provider has different latency. Practical recommendation:

| Model                | Poll every | Typical total time |
| -------------------- | ---------- | ------------------ |
| Kling 2              | 15s        | 2-5 min            |
| Hailuo 02            | 15s        | 2-4 min            |
| Runway Gen-4         | 10s        | 30s-2 min          |
| Luma Ray 2           | 10s        | 1-3 min            |
| Veo 3.1 (with audio) | 30s        | 3-5 min            |
| Veo 3                | 20s        | 2-4 min            |

If you poll faster than the provider processes, you'll just see `status: "processing"` repeated — that's not an error.

## TS pattern

```typescript theme={null}
const job = await fetch("https://api.geekhub.mx/v1/videos/generations", {
  method: "POST",
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
  body: JSON.stringify({ model: "fal/kling-2", prompt: "...", duration: 5 }),
}).then(r => r.json());

const jobId = job.id;

while (true) {
  const status = await fetch(`https://api.geekhub.mx/v1/videos/${jobId}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  }).then(r => r.json());

  if (status.status === "completed") {
    console.log("Done:", status.video_url);
    break;
  }
  if (status.status === "failed") {
    console.error("Failed:", status.error);
    break;
  }
  await new Promise(r => setTimeout(r, 15000)); // 15s
}
```

## Storage

When it finishes, we download the video from the provider and upload it to your Supabase Storage. The `video_url` is **ours**, doesn't expire, and is publicly accessible with a UUID URL (same model as images).

If the upload to Storage fails for some reason, transparent fallback: we return the provider's URL directly (that one does expire in \~1h-24h depending on the provider).
