---
title: "Get a download link"
method: POST
path: "/downloads"
tags: ["Downloads"]
---

# Get a download link

`POST /downloads`

## Request

- Base URL: `https://api.propertypixel.app/v1`
- URL: `https://api.propertypixel.app/v1/downloads`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://llflgtovnkrsjqydycyu.supabase.co/auth/v1/oauth/authorize`, token URL `https://llflgtovnkrsjqydycyu.supabase.co/auth/v1/oauth/token`)

Get download links for finished results. Pass job_ids (1–100 completed versions from one project), or project_id to download that project's picks (picks_only defaults to true; false downloads every finished version, up to 100). One version returns a signed link immediately. Several photos start a zip: status is processing, so call get_download with the returned id after poll_after_ms. If any videos are included you get one signed link per file in `files` instead of a zip. Links last 1 hour; trial accounts get watermarked files.

Works with view-only access.

## Headers

- `Idempotency-Key` string: A unique key (for example a UUID) that makes retries safe. A retry with the same key and body returns the stored response; the same key with a different body returns 422. Keys last 24 hours.

## Request body

- object
  - `job_ids` string[]: Completed job IDs from one project (max 100).
  - `project_id` string, uuid: Download from this project instead of listing job_ids.
  - `picks_only` boolean: With project_id: only the picked versions (default true).

## Response `200`

Get a download link

- Download
  - `object` 'download', required
  - `id` string, nullable, required: Zip download ID to poll with get_download while status is processing. Null when the links are ready immediately.
  - `status` 'processing' | 'ready' | 'failed', required
  - `url` string, uri, nullable, required: The file to download: the single version, or the zip once ready. Null while processing, and when files lists per-file links instead.
  - `filename` string, nullable, required
  - `expires_at` string, date-time, nullable, required: When url stops working.
  - `files` object[], required: One signed link per file. Used instead of a zip whenever videos are included (videos are too large to zip server-side).
    - `job_id` string, uuid, required
    - `kind` 'photo' | 'video', required
    - `filename` string, required
    - `url` string, uri, required
    - `expires_at` string, date-time, required
  - `file_count` integer, nullable, required
  - `unavailable_job_ids` string[], required: Requested versions left out because they were deleted, unfinished or failed to package.
  - `poll_after_ms` integer, required: 0 unless status is processing.

## Other responses

- `400`: The request is invalid. Codes: `validation_failed`.
- `401`: Missing, invalid or revoked credential. Codes: `unauthorized`.
- `403`: The credential can't do this. View-only connections get forbidden_scope on writes. Codes: `forbidden_scope`, `forbidden`.
- `404`: The resource doesn't exist in this workspace. Codes: `not_found`.
- `409`: The request conflicts with the current state. Codes: `conflict`, `idempotency_in_progress`.
- `422`: An Idempotency-Key was reused with a different body, or an upload was rejected. Codes: `idempotency_conflict`, `upload_rejected`.
- `429`: Too many requests for this connection. Codes: `rate_limited`.
- `500`: Something failed on our side. Retry later. Codes: `internal`.

---

[API](https://skmtc.dev/propertypixel/apis/propertypixel-api.md) · [All operations](https://skmtc.dev/propertypixel/apis/propertypixel-api/llms.txt) · [OpenAPI document](https://skmtc.dev/propertypixel/apis/propertypixel-api/revisions/8f7baef701cc?raw)
