# Read jobs and clips through the API

Follow a job through its statuses with the Snipri API, read its clips with their scores and download links, and page through every job on the account.

- Page: https://www.snipri.com/docs/read-jobs-and-clips
- By [Cihan Düzgören](https://www.snipri.com/about) · November 2, 2026 · 5 min read
- Section: API, in [the Snipri docs](https://www.snipri.com/docs)

A job moves through the same steps the Studio shows while it runs. Read it to see where it is, and once it has completed, to get its clips and the links to download them.

## A job's status

| Status | What it means |
| --- | --- |
| `awaiting_upload` | Made for a file that has not been started yet: upload the file, then start the job. |
| `queued` | Waiting for its turn to be processed, behind any jobs queued before it. A job whose run failed for a reason that may clear, such as a host that did not answer, comes back here before it runs again. |
| `transcribing` | The speech is being written out, word by word, with the time each word is said. |
| `selecting` | The moments are being chosen and scored. |
| `rendering` | The clips are being framed, captioned and made. |
| `completed` | Done. Its clips are ready; none at all means nothing was worth clipping, and nothing was counted. |
| `failed` | Stopped before it gave clips. Its error says why, and nothing was counted. |

A job takes minutes rather than seconds, so reading it about every half minute is plenty, and a [webhook](https://www.snipri.com/docs/receive-webhooks) saves the reads altogether. [How long a job takes](https://www.snipri.com/docs/how-long-a-job-takes) says what decides the wait.

## Read one job

Request:

```bash
curl https://www.snipri.com/api/v1/jobs/cmhj2b9xk0003l708s4f1z9qa \
  -H "Authorization: Bearer $SNIPRI_API_KEY"
```

Answer:

```json
{
  "job": {
    "id": "cmhj2b9xk0003l708s4f1z9qa",
    "status": "completed",
    "name": "episode-12",
    "origin": "api",
    "source": {
      "type": "url",
      "url": "https://example.com/episode-12.mp4"
    },
    "durationSec": 3542.5,
    "videos": 1,
    "error": null,
    "createdAt": "2026-11-02T18:00:00.000Z",
    "completedAt": "2026-11-02T18:24:10.000Z",
    "clips": [
      {
        "id": "cmhj2kq4e0007l708u2m3c8vd",
        "index": 0,
        "part": 0,
        "title": "Launch week, honestly",
        "hook": "Nobody tells you this about launch week",
        "reason": "A clear question and a complete answer, and the last line lands without anything said before it.",
        "scores": {
          "hook": 8,
          "payoff": 8,
          "standalone": 9,
          "arc": 7,
          "quotability": 7
        },
        "startSec": 312.4,
        "endSec": 349.9,
        "durationSec": 37.5,
        "videoUrl": "https://...",
        "thumbnailUrl": "https://...",
        "urlsExpireAt": "2026-11-02T19:25:10.000Z"
      }
    ]
  }
}
```

| Field | What it holds |
| --- | --- |
| `id` | The job's id, which every other request about it names. |
| `status` | Where the job is, from the list above. |
| `name` | Its name in My clips, or null. |
| `origin` | api for a job made with a key, studio for one started in the Studio. |
| `source` | type url, with the link, or type upload. |
| `durationSec` | The video's length in seconds once it is known, or null. |
| `videos` | What the job holds against your plan while it runs, what it counted once it completed with clips, and 0 for a job that failed, found nothing or was never started. |
| `error` | null, or on a failed job its kind, title and message: the title and sentence the Studio shows. |
| `createdAt, completedAt` | When it was made, and when it completed or null. |
| `clips` | On one job and in a webhook: its clips, in order. |
| `clipCount` | In the list of jobs, in place of clips: how many it gave. |

## Its clips

| Field | What it holds |
| --- | --- |
| `id` | The clip's id. |
| `index` | Its place among the job's clips, from 0. |
| `part` | 0 for the clip as Snipri made it, and 1 and up for the pieces a split in the editor adds. |
| `title` | Its title in My clips. |
| `hook` | The line it opens on, or null on clips made before Snipri wrote one. |
| `reason` | Why the moment was kept, or null on older clips. |
| `scores` | The 5 scores it was kept on, from 1 to 10 each: hook, payoff, standalone, arc and quotability. null on older clips. |
| `startSec, endSec, durationSec` | Where it starts and ends in the video, and how long it runs, in seconds. |
| `videoUrl` | The finished MP4, with its captions burned in and any edit made since in the editor. |
| `thumbnailUrl` | Its cover picture. |
| `urlsExpireAt` | When the two links stop working, an hour after the answer. |

videoUrl and thumbnailUrl stay valid for an hour, and urlsExpireAt says until when. Read the job again for fresh ones whenever you need them.

Clips come in the order My clips shows them. A clip split in the editor is listed as its pieces, which share an index and differ in part. [See why a clip was chosen](https://www.snipri.com/docs/why-a-clip-was-chosen) explains the scores and the reason.

## A job that found nothing

A job can complete with no clips when no moment in the video was strong enough, and then it counts nothing. Its near-misses, the moments that came closest, wait in My clips, where one can be made into a clip. That clip is in the job's clips the next time you read it, and [When a video comes back with no clips](https://www.snipri.com/docs/near-miss-clips) says what making one counts.

## Every job on the account

The list holds 20 jobs a page, or as many as limit asks for, from 1 to 100. Each job carries clipCount in place of its clips, and jobs started in the Studio are listed too, with origin studio.

Request:

```bash
curl "https://www.snipri.com/api/v1/jobs?limit=50" \
  -H "Authorization: Bearer $SNIPRI_API_KEY"
```

Answer:

```json
{
  "jobs": [
    {
      "id": "cmhj2b9xk0003l708s4f1z9qa",
      "status": "completed",
      "name": "episode-12",
      "origin": "api",
      "source": {
        "type": "url",
        "url": "https://example.com/episode-12.mp4"
      },
      "durationSec": 3542.5,
      "videos": 1,
      "error": null,
      "createdAt": "2026-11-02T18:00:00.000Z",
      "completedAt": "2026-11-02T18:24:10.000Z",
      "clipCount": 1
    }
  ],
  "nextCursor": "cmhj2b9xk0003l708s4f1z9qa"
}
```

When there are more jobs, nextCursor is the id to send as cursor for the next page, as in https://www.snipri.com/api/v1/jobs?limit=50&cursor=cmhj2b9xk0003l708s4f1z9qa. On the last page it is null.

A job deleted in My clips is gone from the API too: the list leaves it out, and reading it answers `NOT_FOUND`.

## A job that stops moving

Reading a job is also what fails one that has stopped moving, by the same allowances the Studio uses:

A job whose progress has not moved for 30 minutes is marked as failed. While it waits its turn in the queue it has 3 hours, and while it renders 90 minutes. A failed job is not counted.

When the job was made through the API, failing it this way sends `job.failed` to your webhook, as any other failure does.

Previous guide: [Set clip options in an API request](https://www.snipri.com/docs/api-clip-options)

Next guide: [Receive webhooks when a job ends](https://www.snipri.com/docs/receive-webhooks)

Still stuck? Write to [hello@snipri.com](mailto:hello@snipri.com) with the name of the video and what the screen said.
