API, MCP & CLI

API: Uploads

Upload designs and the files your digital listings deliver to your ListingView asset library through the API in three steps, or import one image from a public link, then wait until each file is scanned and ready.

Updated

Connect Claude, ChatGPT or Cursor to ListingView to research what's selling on Etsy, update your Etsy shop, manage your listings.

You can add files to your ListingView asset library three ways: upload designs, upload the files your digital listings deliver to buyers, or import one image from a public link. All three need the assets:write scope, which Catalog Editor and Drafting Agent keys include.

The API is upload-only. It never returns your original files or your delivery files, and it can't delete files. A design comes back with a preview, thumbnail_url, which is the only file URL the API gives out.

New to the API? Start with API: Getting started. For a plain-words comparison of uploading and importing, see Using the ListingView API, MCP server & CLI.

Files are scanned before you can use them

Every new file is processed and virus-scanned first. You can use a file in listings, presets or Etsy drafts only once it shows ready: true. Until then, other calls refuse it with ASSET_NOT_READY.

While you wait, finalize tells you where each file is:

  • Waiting for its scan: status: "processing" with reason: "scan_pending". Poll again after next_poll_after_ms.

  • No verdict yet: reason: "scan_unavailable" with retry_safe: true. ListingView keeps retrying the scan for an hour; keep polling.

  • A virus was found: status: "quarantined". The file never becomes ready, and the answer never names the virus.

  • It couldn't be fully scanned: status: "failed" with reason: "scan_incomplete". This happens with an archive that is too large, too deeply nested or password-protected, or a file type ListingView can't fully scan yet; the message says which. A rescan gives the same answer. For an archive, upload the files unzipped, or as a smaller archive.

Upload files

An upload takes three steps. The CLI's listingview assets upload command does all three for you (see API: Command-line tool).

Step 1: Begin

Send each file's name, type, exact size in bytes and SHA-256 hash, up to 20 files per call. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/assets/uploads \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "collection_id": 311,
    "files": [
      { "filename": "spooky-season-front.png", "mime_type": "image/png", "size_bytes": 2841193, "sha256": "<64 hex characters>" },
      { "filename": "spooky-season-back.png", "mime_type": "image/png", "size_bytes": 1934002, "sha256": "<64 hex characters>" }
    ]
  }'
{
  "files": [
    {
      "index": 0,
      "file_id": 88125,
      "filename": "spooky-season-front.png",
      "deduped": false,
      "replayed": false,
      "upload_url": "<upload_url>",
      "required_headers": {
        "Content-Type": "image/png",
        "Content-Length": "2841193",
        "x-amz-checksum-sha256": "nyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyw=",
        "If-None-Match": "*"
      },
      "expires_at": "2026-10-16T09:27:44.000Z"
    },
    {
      "index": 1,
      "file_id": 88123,
      "filename": "spooky-season-back.png",
      "deduped": true,
      "replayed": false,
      "upload_url": null,
      "required_headers": null,
      "expires_at": null
    }
  ],
  "finalize_file_ids": [88125, 88123],
  "warnings": []
}

The answer is shortened here. Each file also carries its asset.

  • Designs or delivery files. purpose says what you're uploading. It defaults to "design". Send "digital_file" for the files a digital listing delivers (see Upload delivery files for digital listings below).

  • Files you already have. A file with the same SHA-256 and size comes back with deduped: true and its existing file_id, like the second file above. It needs no upload and doesn't count against your daily upload budget.

  • File types. Designs can be PNG, JPEG or WebP. Other types (PSD, HEIC, SVG, PDF, GIF) are refused with details.reason: "unsupported_type". Export a PNG instead. The file name must end in the declared type's extension.

  • Repeats. Sending the same idempotency key and body again, within 24 hours, returns the same files with replayed: true. Nothing new is reserved or counted. While the first call with that key is still running, a repeat answers in_flight at once; retry after retry_after.

  • Unfinished uploads. An account can have up to 300 uploads begun but not finished. Past that, begin answers RATE_LIMITED with the reason uploads_in_flight.

Step 2: Upload the bytes

Send one PUT with the file's exact bytes to its upload_url, before expires_at, with exactly the headers in required_headers. Don't send your API key: the upload goes straight to storage.

curl -X PUT "<upload_url>" \
  -H "Content-Type: image/png" \
  -H "Content-Length: 2841193" \
  -H "x-amz-checksum-sha256: nyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyw=" \
  -H "If-None-Match: *" \
  --data-binary @spooky-season-front.png

Treat upload_url as a secret: don't log it or share it. A repeat of begin with the same key returns the same link only while at least a minute of it is left. If it expires before you upload, call begin again with a new idempotency key.

Step 3: Finalize and poll

POST /assets/uploads/finalize takes up to 50 file ids.

curl -X POST https://api.listingview.io/api/v1/assets/uploads/finalize \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_ids": [88125, 88123]}'
{
  "files": [
    { "file_id": 88125, "status": "processing", "ready": false, "reason": null, "retry_safe": true, "next_poll_after_ms": 2000 },
    { "file_id": 88123, "status": "ready", "ready": true, "reason": null, "retry_safe": null, "next_poll_after_ms": null }
  ],
  "all_ready": false,
  "next_poll_after_ms": 2000,
  "next_step": "Call finalize_upload again after next_poll_after_ms with the file ids that aren't ready yet."
}

The answer is shortened here.

Each file reports a status: uploading, processing, ready, quarantined, failed or not_found. Call finalize again with the files that aren't ready yet, no sooner than next_poll_after_ms. Calling it again is safe; it is how you poll.

When a file shows retry_safe: false, it won't become ready, so upload it again. That happens with hash_mismatch or size_mismatch (the bytes aren't what you declared at begin), unsupported_type, scan_incomplete, upload_expired (the bytes arrived after the link ran out) and not_found. Other reasons you may see are not_received, type_mismatch (a design whose bytes aren't PNG, JPEG or WebP), processing_stalled and virus_scan.

Metadata

Uploaded files are kept as you sent them, including their EXIF metadata, the same as uploads in the app. If you don't want to keep location or camera data, remove it before you upload.

Files uploaded through the API in the app

In the ListingView app, a file uploaded through the API can't be replaced, copied as a new version or duplicated (api_file_read_only). The API's own duplicate route still copies it.

Upload delivery files for digital listings

A digital listing delivers files to its buyers. Upload those files with the same three steps, and send "purpose": "digital_file" when you begin.

curl -X POST https://api.listingview.io/api/v1/assets/uploads \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "digital_file",
    "files": [{
      "filename": "printable-planner-2027.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 3612004,
      "sha256": "<64 hex characters>"
    }]
  }'
  • File types. PDF, ZIP, PNG, JPEG, WebP, SVG, GIF, TIFF, TTF, OTF, WOFF, WOFF2, MP3, WAV, MP4, DOCX and XLSX are accepted. PSD, AI, EPS and RAR files are refused, and so are program files. The file name must end in the declared type's extension.

  • The type must match. ListingView reads each file's first bytes when you finalize. If they don't match the type you declared, the file is failed with reason: "unsupported_type", for example: "This file is a ZIP, but it was declared as PDF. Begin the upload again with its real type and a matching file name."

  • Size. Each delivery file can be up to 25 MB. A bigger one is refused with VALIDATION_FAILED, details.reason: "too_large", limit_kind: "deliverable_bytes" and limit_bytes.

  • Headers. A delivery file's required_headers also include Content-Disposition. Send every header exactly as given.

  • Upload time. The upload link lasts between 15 and 60 minutes, longer for bigger files. If the link runs out before the bytes arrive, begin again for that file only.

  • Scanning. Every delivery file is virus-scanned and is ready only once it has been scanned in full (see Files are scanned before you can use them).

  • Downloads. Buyers always get these files as downloads. An SVG or a PDF never opens as a page in the browser.

  • Paused. If ListingView pauses delivery-file uploads, purpose: "digital_file" answers FEATURE_UNAVAILABLE with the reason disabled. Design uploads carry on.

To list your delivery files, call GET /api/v1/assets/images?kind=deliverable, or use list_assets with kind: "deliverable" over MCP. To make a digital preset that delivers them, see API: Presets. To draft digital listings from that preset, see API: Etsy drafts.

With a test key, begin and finalize answer a dry run. Begin's plan names the files, the size limit and the allowed types; you get no upload link, and no file is reserved.

Import an image from a link

Use this when your app can't make PUT requests, for example in a chat app. Send one public https:// link per call.

curl -X POST https://api.listingview.io/api/v1/assets/images/import \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://images.example-shop.com/photos/apron-lifestyle.jpg", "collection_id": 311}'
{
  "file_id": 88200,
  "deduplicated": false,
  "source": { "content_type": "image/jpeg", "bytes": 1388201, "redirects": 1, "stored_as": "image/jpeg" },
  "warnings": [],
  "next_poll_after_ms": null,
  "next_step": "Use file_id 88200 wherever an image is needed — e.g. update_listing {listing_id, images:[…, 88200]}."
}

The answer is shortened here. It also carries the file's asset.

  • Public links only. ListingView fetches the image from its own servers, on port 443, following up to 3 redirects, for up to 20 seconds. Links to private or internal networks, to other ports, or with a user name or password are refused with IMAGE_IMPORT_BLOCKED.

  • Real images only. Only real PNG, JPEG, WebP and GIF images, up to 20 MB, are accepted. The type is read from the file's bytes, not its name. A GIF is stored as a PNG, with the warning converted_to_png. Delivery files can't be imported; upload them instead.

  • Metadata is removed. EXIF and GPS metadata are removed from imported images.

  • Scanned like an upload. The image is virus-scanned like an upload. The answer is usually ready at once. If it isn't, check it with GET /api/v1/assets/files, passing its id in ids (up to 50 per call), until it is.

  • Images you already have. An image you already have comes back as that file. Every import uses your daily image-import budget; an image you already have costs less.

  • Idempotency. An idempotency key is optional here.

  • Test keys check the link's syntax and policy only, and fetch nothing.

Budgets

Uploads, delivery files included, count against a daily upload budget and your asset-library storage. Imports count against a daily image-import budget. Your live numbers are in Today's usage on Settings → API Keys and in GET /api/v1/usage. See API: Limits & budgets.

From the CLI or an AI app

  • CLI: listingview assets upload front.png back.png --collection 311 runs all three steps and waits until each file is ready. Add --purpose digital_file for delivery files: listingview assets upload --purpose digital_file printable-planner-2027.pdf. See API: Command-line tool.

  • MCP: the add_assets tool has three modes: begin_upload, finalize_upload and import_url. Pass purpose: "digital_file" to upload delivery files. Most chat apps can't make PUT requests, so use import_url there for images. See API: Connect AI apps.

Every endpoint above, with its full request and response schema, is in the API reference. Error codes such as ASSET_NOT_READY and IMAGE_IMPORT_BLOCKED are on the errors page.

Related features

You can add files to your ListingView asset library three ways: upload designs, upload the files your digital listings deliver to buyers, or import one image from a public link. All three need the assets:write scope, which Catalog Editor and Drafting Agent keys include.

The API is upload-only. It never returns your original files or your delivery files, and it can't delete files. A design comes back with a preview, thumbnail_url, which is the only file URL the API gives out.

New to the API? Start with API: Getting started. For a plain-words comparison of uploading and importing, see Using the ListingView API, MCP server & CLI.

Files are scanned before you can use them

Every new file is processed and virus-scanned first. You can use a file in listings, presets or Etsy drafts only once it shows ready: true. Until then, other calls refuse it with ASSET_NOT_READY.

While you wait, finalize tells you where each file is:

  • Waiting for its scan: status: "processing" with reason: "scan_pending". Poll again after next_poll_after_ms.

  • No verdict yet: reason: "scan_unavailable" with retry_safe: true. ListingView keeps retrying the scan for an hour; keep polling.

  • A virus was found: status: "quarantined". The file never becomes ready, and the answer never names the virus.

  • It couldn't be fully scanned: status: "failed" with reason: "scan_incomplete". This happens with an archive that is too large, too deeply nested or password-protected, or a file type ListingView can't fully scan yet; the message says which. A rescan gives the same answer. For an archive, upload the files unzipped, or as a smaller archive.

Upload files

An upload takes three steps. The CLI's listingview assets upload command does all three for you (see API: Command-line tool).

Step 1: Begin

Send each file's name, type, exact size in bytes and SHA-256 hash, up to 20 files per call. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/assets/uploads \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "collection_id": 311,
    "files": [
      { "filename": "spooky-season-front.png", "mime_type": "image/png", "size_bytes": 2841193, "sha256": "<64 hex characters>" },
      { "filename": "spooky-season-back.png", "mime_type": "image/png", "size_bytes": 1934002, "sha256": "<64 hex characters>" }
    ]
  }'
{
  "files": [
    {
      "index": 0,
      "file_id": 88125,
      "filename": "spooky-season-front.png",
      "deduped": false,
      "replayed": false,
      "upload_url": "<upload_url>",
      "required_headers": {
        "Content-Type": "image/png",
        "Content-Length": "2841193",
        "x-amz-checksum-sha256": "nyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyw=",
        "If-None-Match": "*"
      },
      "expires_at": "2026-10-16T09:27:44.000Z"
    },
    {
      "index": 1,
      "file_id": 88123,
      "filename": "spooky-season-back.png",
      "deduped": true,
      "replayed": false,
      "upload_url": null,
      "required_headers": null,
      "expires_at": null
    }
  ],
  "finalize_file_ids": [88125, 88123],
  "warnings": []
}

The answer is shortened here. Each file also carries its asset.

  • Designs or delivery files. purpose says what you're uploading. It defaults to "design". Send "digital_file" for the files a digital listing delivers (see Upload delivery files for digital listings below).

  • Files you already have. A file with the same SHA-256 and size comes back with deduped: true and its existing file_id, like the second file above. It needs no upload and doesn't count against your daily upload budget.

  • File types. Designs can be PNG, JPEG or WebP. Other types (PSD, HEIC, SVG, PDF, GIF) are refused with details.reason: "unsupported_type". Export a PNG instead. The file name must end in the declared type's extension.

  • Repeats. Sending the same idempotency key and body again, within 24 hours, returns the same files with replayed: true. Nothing new is reserved or counted. While the first call with that key is still running, a repeat answers in_flight at once; retry after retry_after.

  • Unfinished uploads. An account can have up to 300 uploads begun but not finished. Past that, begin answers RATE_LIMITED with the reason uploads_in_flight.

Step 2: Upload the bytes

Send one PUT with the file's exact bytes to its upload_url, before expires_at, with exactly the headers in required_headers. Don't send your API key: the upload goes straight to storage.

curl -X PUT "<upload_url>" \
  -H "Content-Type: image/png" \
  -H "Content-Length: 2841193" \
  -H "x-amz-checksum-sha256: nyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyw=" \
  -H "If-None-Match: *" \
  --data-binary @spooky-season-front.png

Treat upload_url as a secret: don't log it or share it. A repeat of begin with the same key returns the same link only while at least a minute of it is left. If it expires before you upload, call begin again with a new idempotency key.

Step 3: Finalize and poll

POST /assets/uploads/finalize takes up to 50 file ids.

curl -X POST https://api.listingview.io/api/v1/assets/uploads/finalize \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_ids": [88125, 88123]}'
{
  "files": [
    { "file_id": 88125, "status": "processing", "ready": false, "reason": null, "retry_safe": true, "next_poll_after_ms": 2000 },
    { "file_id": 88123, "status": "ready", "ready": true, "reason": null, "retry_safe": null, "next_poll_after_ms": null }
  ],
  "all_ready": false,
  "next_poll_after_ms": 2000,
  "next_step": "Call finalize_upload again after next_poll_after_ms with the file ids that aren't ready yet."
}

The answer is shortened here.

Each file reports a status: uploading, processing, ready, quarantined, failed or not_found. Call finalize again with the files that aren't ready yet, no sooner than next_poll_after_ms. Calling it again is safe; it is how you poll.

When a file shows retry_safe: false, it won't become ready, so upload it again. That happens with hash_mismatch or size_mismatch (the bytes aren't what you declared at begin), unsupported_type, scan_incomplete, upload_expired (the bytes arrived after the link ran out) and not_found. Other reasons you may see are not_received, type_mismatch (a design whose bytes aren't PNG, JPEG or WebP), processing_stalled and virus_scan.

Metadata

Uploaded files are kept as you sent them, including their EXIF metadata, the same as uploads in the app. If you don't want to keep location or camera data, remove it before you upload.

Files uploaded through the API in the app

In the ListingView app, a file uploaded through the API can't be replaced, copied as a new version or duplicated (api_file_read_only). The API's own duplicate route still copies it.

Upload delivery files for digital listings

A digital listing delivers files to its buyers. Upload those files with the same three steps, and send "purpose": "digital_file" when you begin.

curl -X POST https://api.listingview.io/api/v1/assets/uploads \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "digital_file",
    "files": [{
      "filename": "printable-planner-2027.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 3612004,
      "sha256": "<64 hex characters>"
    }]
  }'
  • File types. PDF, ZIP, PNG, JPEG, WebP, SVG, GIF, TIFF, TTF, OTF, WOFF, WOFF2, MP3, WAV, MP4, DOCX and XLSX are accepted. PSD, AI, EPS and RAR files are refused, and so are program files. The file name must end in the declared type's extension.

  • The type must match. ListingView reads each file's first bytes when you finalize. If they don't match the type you declared, the file is failed with reason: "unsupported_type", for example: "This file is a ZIP, but it was declared as PDF. Begin the upload again with its real type and a matching file name."

  • Size. Each delivery file can be up to 25 MB. A bigger one is refused with VALIDATION_FAILED, details.reason: "too_large", limit_kind: "deliverable_bytes" and limit_bytes.

  • Headers. A delivery file's required_headers also include Content-Disposition. Send every header exactly as given.

  • Upload time. The upload link lasts between 15 and 60 minutes, longer for bigger files. If the link runs out before the bytes arrive, begin again for that file only.

  • Scanning. Every delivery file is virus-scanned and is ready only once it has been scanned in full (see Files are scanned before you can use them).

  • Downloads. Buyers always get these files as downloads. An SVG or a PDF never opens as a page in the browser.

  • Paused. If ListingView pauses delivery-file uploads, purpose: "digital_file" answers FEATURE_UNAVAILABLE with the reason disabled. Design uploads carry on.

To list your delivery files, call GET /api/v1/assets/images?kind=deliverable, or use list_assets with kind: "deliverable" over MCP. To make a digital preset that delivers them, see API: Presets. To draft digital listings from that preset, see API: Etsy drafts.

With a test key, begin and finalize answer a dry run. Begin's plan names the files, the size limit and the allowed types; you get no upload link, and no file is reserved.

Import an image from a link

Use this when your app can't make PUT requests, for example in a chat app. Send one public https:// link per call.

curl -X POST https://api.listingview.io/api/v1/assets/images/import \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://images.example-shop.com/photos/apron-lifestyle.jpg", "collection_id": 311}'
{
  "file_id": 88200,
  "deduplicated": false,
  "source": { "content_type": "image/jpeg", "bytes": 1388201, "redirects": 1, "stored_as": "image/jpeg" },
  "warnings": [],
  "next_poll_after_ms": null,
  "next_step": "Use file_id 88200 wherever an image is needed — e.g. update_listing {listing_id, images:[…, 88200]}."
}

The answer is shortened here. It also carries the file's asset.

  • Public links only. ListingView fetches the image from its own servers, on port 443, following up to 3 redirects, for up to 20 seconds. Links to private or internal networks, to other ports, or with a user name or password are refused with IMAGE_IMPORT_BLOCKED.

  • Real images only. Only real PNG, JPEG, WebP and GIF images, up to 20 MB, are accepted. The type is read from the file's bytes, not its name. A GIF is stored as a PNG, with the warning converted_to_png. Delivery files can't be imported; upload them instead.

  • Metadata is removed. EXIF and GPS metadata are removed from imported images.

  • Scanned like an upload. The image is virus-scanned like an upload. The answer is usually ready at once. If it isn't, check it with GET /api/v1/assets/files, passing its id in ids (up to 50 per call), until it is.

  • Images you already have. An image you already have comes back as that file. Every import uses your daily image-import budget; an image you already have costs less.

  • Idempotency. An idempotency key is optional here.

  • Test keys check the link's syntax and policy only, and fetch nothing.

Budgets

Uploads, delivery files included, count against a daily upload budget and your asset-library storage. Imports count against a daily image-import budget. Your live numbers are in Today's usage on Settings → API Keys and in GET /api/v1/usage. See API: Limits & budgets.

From the CLI or an AI app

  • CLI: listingview assets upload front.png back.png --collection 311 runs all three steps and waits until each file is ready. Add --purpose digital_file for delivery files: listingview assets upload --purpose digital_file printable-planner-2027.pdf. See API: Command-line tool.

  • MCP: the add_assets tool has three modes: begin_upload, finalize_upload and import_url. Pass purpose: "digital_file" to upload delivery files. Most chat apps can't make PUT requests, so use import_url there for images. See API: Connect AI apps.

Every endpoint above, with its full request and response schema, is in the API reference. Error codes such as ASSET_NOT_READY and IMAGE_IMPORT_BLOCKED are on the errors page.

Related features

You can add files to your ListingView asset library three ways: upload designs, upload the files your digital listings deliver to buyers, or import one image from a public link. All three need the assets:write scope, which Catalog Editor and Drafting Agent keys include.

The API is upload-only. It never returns your original files or your delivery files, and it can't delete files. A design comes back with a preview, thumbnail_url, which is the only file URL the API gives out.

New to the API? Start with API: Getting started. For a plain-words comparison of uploading and importing, see Using the ListingView API, MCP server & CLI.

Files are scanned before you can use them

Every new file is processed and virus-scanned first. You can use a file in listings, presets or Etsy drafts only once it shows ready: true. Until then, other calls refuse it with ASSET_NOT_READY.

While you wait, finalize tells you where each file is:

  • Waiting for its scan: status: "processing" with reason: "scan_pending". Poll again after next_poll_after_ms.

  • No verdict yet: reason: "scan_unavailable" with retry_safe: true. ListingView keeps retrying the scan for an hour; keep polling.

  • A virus was found: status: "quarantined". The file never becomes ready, and the answer never names the virus.

  • It couldn't be fully scanned: status: "failed" with reason: "scan_incomplete". This happens with an archive that is too large, too deeply nested or password-protected, or a file type ListingView can't fully scan yet; the message says which. A rescan gives the same answer. For an archive, upload the files unzipped, or as a smaller archive.

Upload files

An upload takes three steps. The CLI's listingview assets upload command does all three for you (see API: Command-line tool).

Step 1: Begin

Send each file's name, type, exact size in bytes and SHA-256 hash, up to 20 files per call. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/assets/uploads \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "collection_id": 311,
    "files": [
      { "filename": "spooky-season-front.png", "mime_type": "image/png", "size_bytes": 2841193, "sha256": "<64 hex characters>" },
      { "filename": "spooky-season-back.png", "mime_type": "image/png", "size_bytes": 1934002, "sha256": "<64 hex characters>" }
    ]
  }'
{
  "files": [
    {
      "index": 0,
      "file_id": 88125,
      "filename": "spooky-season-front.png",
      "deduped": false,
      "replayed": false,
      "upload_url": "<upload_url>",
      "required_headers": {
        "Content-Type": "image/png",
        "Content-Length": "2841193",
        "x-amz-checksum-sha256": "nyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyw=",
        "If-None-Match": "*"
      },
      "expires_at": "2026-10-16T09:27:44.000Z"
    },
    {
      "index": 1,
      "file_id": 88123,
      "filename": "spooky-season-back.png",
      "deduped": true,
      "replayed": false,
      "upload_url": null,
      "required_headers": null,
      "expires_at": null
    }
  ],
  "finalize_file_ids": [88125, 88123],
  "warnings": []
}

The answer is shortened here. Each file also carries its asset.

  • Designs or delivery files. purpose says what you're uploading. It defaults to "design". Send "digital_file" for the files a digital listing delivers (see Upload delivery files for digital listings below).

  • Files you already have. A file with the same SHA-256 and size comes back with deduped: true and its existing file_id, like the second file above. It needs no upload and doesn't count against your daily upload budget.

  • File types. Designs can be PNG, JPEG or WebP. Other types (PSD, HEIC, SVG, PDF, GIF) are refused with details.reason: "unsupported_type". Export a PNG instead. The file name must end in the declared type's extension.

  • Repeats. Sending the same idempotency key and body again, within 24 hours, returns the same files with replayed: true. Nothing new is reserved or counted. While the first call with that key is still running, a repeat answers in_flight at once; retry after retry_after.

  • Unfinished uploads. An account can have up to 300 uploads begun but not finished. Past that, begin answers RATE_LIMITED with the reason uploads_in_flight.

Step 2: Upload the bytes

Send one PUT with the file's exact bytes to its upload_url, before expires_at, with exactly the headers in required_headers. Don't send your API key: the upload goes straight to storage.

curl -X PUT "<upload_url>" \
  -H "Content-Type: image/png" \
  -H "Content-Length: 2841193" \
  -H "x-amz-checksum-sha256: nyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyyfLJ8snyw=" \
  -H "If-None-Match: *" \
  --data-binary @spooky-season-front.png

Treat upload_url as a secret: don't log it or share it. A repeat of begin with the same key returns the same link only while at least a minute of it is left. If it expires before you upload, call begin again with a new idempotency key.

Step 3: Finalize and poll

POST /assets/uploads/finalize takes up to 50 file ids.

curl -X POST https://api.listingview.io/api/v1/assets/uploads/finalize \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_ids": [88125, 88123]}'
{
  "files": [
    { "file_id": 88125, "status": "processing", "ready": false, "reason": null, "retry_safe": true, "next_poll_after_ms": 2000 },
    { "file_id": 88123, "status": "ready", "ready": true, "reason": null, "retry_safe": null, "next_poll_after_ms": null }
  ],
  "all_ready": false,
  "next_poll_after_ms": 2000,
  "next_step": "Call finalize_upload again after next_poll_after_ms with the file ids that aren't ready yet."
}

The answer is shortened here.

Each file reports a status: uploading, processing, ready, quarantined, failed or not_found. Call finalize again with the files that aren't ready yet, no sooner than next_poll_after_ms. Calling it again is safe; it is how you poll.

When a file shows retry_safe: false, it won't become ready, so upload it again. That happens with hash_mismatch or size_mismatch (the bytes aren't what you declared at begin), unsupported_type, scan_incomplete, upload_expired (the bytes arrived after the link ran out) and not_found. Other reasons you may see are not_received, type_mismatch (a design whose bytes aren't PNG, JPEG or WebP), processing_stalled and virus_scan.

Metadata

Uploaded files are kept as you sent them, including their EXIF metadata, the same as uploads in the app. If you don't want to keep location or camera data, remove it before you upload.

Files uploaded through the API in the app

In the ListingView app, a file uploaded through the API can't be replaced, copied as a new version or duplicated (api_file_read_only). The API's own duplicate route still copies it.

Upload delivery files for digital listings

A digital listing delivers files to its buyers. Upload those files with the same three steps, and send "purpose": "digital_file" when you begin.

curl -X POST https://api.listingview.io/api/v1/assets/uploads \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "purpose": "digital_file",
    "files": [{
      "filename": "printable-planner-2027.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 3612004,
      "sha256": "<64 hex characters>"
    }]
  }'
  • File types. PDF, ZIP, PNG, JPEG, WebP, SVG, GIF, TIFF, TTF, OTF, WOFF, WOFF2, MP3, WAV, MP4, DOCX and XLSX are accepted. PSD, AI, EPS and RAR files are refused, and so are program files. The file name must end in the declared type's extension.

  • The type must match. ListingView reads each file's first bytes when you finalize. If they don't match the type you declared, the file is failed with reason: "unsupported_type", for example: "This file is a ZIP, but it was declared as PDF. Begin the upload again with its real type and a matching file name."

  • Size. Each delivery file can be up to 25 MB. A bigger one is refused with VALIDATION_FAILED, details.reason: "too_large", limit_kind: "deliverable_bytes" and limit_bytes.

  • Headers. A delivery file's required_headers also include Content-Disposition. Send every header exactly as given.

  • Upload time. The upload link lasts between 15 and 60 minutes, longer for bigger files. If the link runs out before the bytes arrive, begin again for that file only.

  • Scanning. Every delivery file is virus-scanned and is ready only once it has been scanned in full (see Files are scanned before you can use them).

  • Downloads. Buyers always get these files as downloads. An SVG or a PDF never opens as a page in the browser.

  • Paused. If ListingView pauses delivery-file uploads, purpose: "digital_file" answers FEATURE_UNAVAILABLE with the reason disabled. Design uploads carry on.

To list your delivery files, call GET /api/v1/assets/images?kind=deliverable, or use list_assets with kind: "deliverable" over MCP. To make a digital preset that delivers them, see API: Presets. To draft digital listings from that preset, see API: Etsy drafts.

With a test key, begin and finalize answer a dry run. Begin's plan names the files, the size limit and the allowed types; you get no upload link, and no file is reserved.

Import an image from a link

Use this when your app can't make PUT requests, for example in a chat app. Send one public https:// link per call.

curl -X POST https://api.listingview.io/api/v1/assets/images/import \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://images.example-shop.com/photos/apron-lifestyle.jpg", "collection_id": 311}'
{
  "file_id": 88200,
  "deduplicated": false,
  "source": { "content_type": "image/jpeg", "bytes": 1388201, "redirects": 1, "stored_as": "image/jpeg" },
  "warnings": [],
  "next_poll_after_ms": null,
  "next_step": "Use file_id 88200 wherever an image is needed — e.g. update_listing {listing_id, images:[…, 88200]}."
}

The answer is shortened here. It also carries the file's asset.

  • Public links only. ListingView fetches the image from its own servers, on port 443, following up to 3 redirects, for up to 20 seconds. Links to private or internal networks, to other ports, or with a user name or password are refused with IMAGE_IMPORT_BLOCKED.

  • Real images only. Only real PNG, JPEG, WebP and GIF images, up to 20 MB, are accepted. The type is read from the file's bytes, not its name. A GIF is stored as a PNG, with the warning converted_to_png. Delivery files can't be imported; upload them instead.

  • Metadata is removed. EXIF and GPS metadata are removed from imported images.

  • Scanned like an upload. The image is virus-scanned like an upload. The answer is usually ready at once. If it isn't, check it with GET /api/v1/assets/files, passing its id in ids (up to 50 per call), until it is.

  • Images you already have. An image you already have comes back as that file. Every import uses your daily image-import budget; an image you already have costs less.

  • Idempotency. An idempotency key is optional here.

  • Test keys check the link's syntax and policy only, and fetch nothing.

Budgets

Uploads, delivery files included, count against a daily upload budget and your asset-library storage. Imports count against a daily image-import budget. Your live numbers are in Today's usage on Settings → API Keys and in GET /api/v1/usage. See API: Limits & budgets.

From the CLI or an AI app

  • CLI: listingview assets upload front.png back.png --collection 311 runs all three steps and waits until each file is ready. Add --purpose digital_file for delivery files: listingview assets upload --purpose digital_file printable-planner-2027.pdf. See API: Command-line tool.

  • MCP: the add_assets tool has three modes: begin_upload, finalize_upload and import_url. Pass purpose: "digital_file" to upload delivery files. Most chat apps can't make PUT requests, so use import_url there for images. See API: Connect AI apps.

Every endpoint above, with its full request and response schema, is in the API reference. Error codes such as ASSET_NOT_READY and IMAGE_IMPORT_BLOCKED are on the errors page.

Related features

Still need help?

Can’t find what you’re looking for? Our team is happy to help.

A.I. Summary