---
title: Polling and downloading
description: Poll a job until it finishes, then fetch its outputs through short-lived signed URLs.
---

## Poll the job

The API has no webhooks. After you start a job, read it until `status` is `succeeded`, `failed` or `cancelled`:

```js
const finished = ["succeeded", "failed", "cancelled"];
let job = await call(`/v1/jobs/${id}/start`, { method: "POST" });
while (!finished.includes(job.status)) {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  job = await call(`/v1/jobs/${id}`);
}
```

Poll about once a second for a single job. Every poll counts toward the 120 requests a minute your key gets, which is 2 a second across all your jobs. With N jobs running at once, poll each one every N seconds or slower: 4 jobs every 4 seconds, 10 jobs every 10 seconds. That keeps polling at 1 request a second and leaves room for the create, start and download calls.

Give up after a deadline you choose. A conversion attempt runs for at most 10 minutes, and a retry can follow, so 11 minutes covers almost every job. If you stop waiting, [cancel the job](/docs/guides/cancelling) to release its reservation.

## Download the outputs

When the job has succeeded, ask for its download URLs:

```bash
curl https://convt-api-production.up.railway.app/v1/jobs/$JOB/download \
  -H "Authorization: Bearer $CONVT_API_KEY"
```

```json
{
  "outputs": [{ "name": "input.webp", "url": "https://…" }],
  "expires_in": 300
}
```

- `outputs` has one entry per file. Most conversions produce one. Some produce several, such as a PDF converted to PNG, which gives one image per page.
- `name` is the output's file name. It is always based on `input`, not on the name of the file you uploaded: `input.webp`, or `input.png`, `input-2.png`, `input-3.png` for pages. Replace `input` with your own name when you save it.
- `url` is a signed `GET` URL. It needs no `Authorization` header.
- `expires_in` is how many seconds the URLs stay valid, at most 300. Call the route again for fresh URLs.

You can call download as often as you like until the job expires 24 hours after creation. It never charges again.

Calling download before the job succeeds returns `400 not_ready`.

## Save the files

```js Node
import { writeFile } from "node:fs/promises";

const { outputs } = await call(`/v1/jobs/${id}/download`);
for (const output of outputs) {
  const res = await fetch(output.url);
  if (!res.ok) throw new Error(`download failed with ${res.status}`);
  const name = output.name.replace(/^input/, "photo"); // input-2.png -> photo-2.png
  await writeFile(name, Buffer.from(await res.arrayBuffer()));
}
```

```bash cURL
curl -sS "$API/v1/jobs/$JOB/download" -H "$AUTH" |
  jq -r '.outputs[] | "\(.name)\t\(.url)"' |
  while IFS=$'\t' read -r NAME URL; do curl -sS -o "${NAME/#input/photo}" "$URL"; done
```

To hand a file to a browser, return the signed `url` and link to it. A plain link or redirect works from any site. Fetching the URL with JavaScript from your own origin does not, because storage only allows cross-origin reads from convt.app.
