API, MCP & CLI

API: Etsy drafts

Turn a ListingView preset and your designs into Etsy drafts through the API: stage a batch for a person to review in Saved listings, submit it as they left it, poll the job, and delete and redraft safely.

Updated

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

With a Drafting Agent key, an agent turns a preset and your designs into Etsy drafts. Drafts are hidden from shoppers. The agent can send a batch straight to Etsy, or stage it first so a person can check it in ListingView.

This guide covers both, then the staged loop from start to finish:

  1. Write the listing copy with AI.

  2. Stage the batch. Nothing reaches Etsy.

  3. A person reviews and edits it in ListingView, under Saved listings.

  4. Read the batch as the person left it.

  5. Update it in one call, if anything still needs to change.

  6. Submit it. ListingView makes the Etsy drafts.

  7. Poll the job, then find it later in the job history.

New to the API? Start with API: Getting started. To build the preset first, see API: Presets.

What you need

  • A Drafting Agent key. Creating, staging, editing, submitting and deleting need etsy_drafts:write, which a Drafting Agent key holds. Staging writes nothing to Etsy, but it still needs that scope.

  • Plus plan or higher to create, stage, edit, submit and delete, and to poll one job.

  • Reading after a downgrade. The staged-batch list, one staged batch and the job history stay readable below Plus for a key that holds catalog:read. A key that holds only etsy_drafts:write reads them on Plus or higher.

  • A working preset and ready designs. See API: Presets and API: Uploads.

A Drafting Agent key can never publish. Drafts go live only when someone publishes them. See API: Publish & reprice.

Create Etsy drafts now

POST /etsy-drafts drafts a batch straight to Etsy from one of your presets. Fill each of the preset's upload slots with a ready design's asset id. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-16-ghost-tee" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 38123456,
    "preset_id": 1667,
    "expected_preset_updated_at": "2026-10-14T18:22:05.123Z",
    "items": [{
      "slots": { "A": 90211 },
      "title": "Retro Ghost Tee, Spooky Season Shirt",
      "tags": ["halloween shirt", "ghost tee", "spooky season"],
      "client_item_id": "drop-0412-ghost"
    }]
  }'
{
  "job_id": "edj_48213",
  "status": "pending",
  "replayed": false,
  "shop_id": 38123456,
  "preset_id": 1667,
  "item_count": 1,
  "next_poll_after_ms": 5000,
  "next_step": "Poll get_bulk_job_status with this job_id; drafts appear one by one. Wait next_poll_after_ms between calls."
}
  • The whole job is admitted or refused. Your plan's per-job and per-shop daily draft caps apply. GET /usage shows them.

  • expected_preset_updated_at is the preset's updated_at from your read. If the preset changed since, nothing is drafted (preset_changed).

  • One file, two slots. The same asset may fill two slots, for example a light and a dark slot.

  • The same key returns the same job. Resending the identical request with the same key answers replayed: true and the same job_id, for as long as the job exists. The same key with a different body is refused with idempotency_key_reused.

  • The same item under a new key is refused. A client_item_id you sent to this shop in the last 7 days is refused with 409 DUPLICATE_ITEM while it is still queued or processing, once it became a draft, or when its outcome is unknown. details.duplicates[] names each id and its earlier status. It can go again only if it failed with retry_safe: true, was cancelled before it ran, or was skipped by a job that ended.

  • Test keys answer the plan and write nothing.

Poll the job as in step 7.

1. Write the copy first

Staging refuses inline AI copy with ai_requires_submit. Write the copy first with POST /ai/listing-copy, then stage the batch with the finished copy.

curl -X POST https://api.listingview.io/api/v1/ai/listing-copy \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "design_asset_ids": [88123],
    "current_copy": {
      "title": "Sunset Mountain Sweatshirt",
      "description": "Retro sunset crewneck in soft white ink.",
      "tags": ["sunset sweatshirt", "mountain shirt", "hiking gift"],
      "price": 34.5
    },
    "instructions": "Cozy autumn tone. Mention the night sky.",
    "use_custom_instructions": true
  }'
  • Improve or write fresh. With current_copy, the AI improves that copy. Without it, the AI writes from the artwork alone. current_copy needs a title, description or tags; a price alone is refused.

  • House instructions. use_custom_instructions is on by default. It applies the seller's saved Custom Instructions in ListingView, in addition to your instructions.

  • The answer has title, tags, description, fields, saw_artwork, model, tokens_charged, applied: false, warnings[] and grounding.status (grounded, candidates-only, skipped or unknown). Nothing is saved.

  • The model always sees the design. If it can't, the call is refused with AI_VISION_UNAVAILABLE and nothing is charged.

  • Tags when you improve copy. When you send current_copy with instructions, the AI doesn't top up the tag list on its own, so you may get fewer than 13 tags back. warnings[] doesn't say so; count them. To top up tags, send fields: ["tags"], the tags you already chose in current_copy.tags, and an instruction such as "suggest 10 MORE, none repeating them".

  • Tokens. Every call spends AI tokens, also when the returned title repeats yours. A low balance is refused with AI_TOKENS_INSUFFICIENT before anything is spent. At most 2 AI calls run at once per account; a third answers RATE_LIMITED with the reason ai_concurrency.

  • Scope. AI copy needs catalog:write.

Inline AI copy on a create that drafts now (ai_copy, ai) also needs catalog:write. It writes from the artwork only: send current_copy to POST /ai/listing-copy instead.

2. Stage the batch

Send POST /etsy-drafts with submit: false. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: stage-2026-10-26-batch-01" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 38123456,
    "preset_id": 1667,
    "expected_preset_updated_at": "2026-10-14T18:22:05.123Z",
    "submit": false,
    "items": [
      {
        "slots": { "A": 90501 },
        "title": "Cottagecore Frog Tee, Mushroom Shirt",
        "description": "A soft garment-dyed tee with a hand-drawn frog.",
        "tags": ["frog shirt", "cottagecore tee", "mushroom shirt"],
        "client_item_id": "drop-0526-frog"
      },
      {
        "slots": { "A": 90502 },
        "title": "Moth Club Tee, Dark Academia Shirt",
        "description": "A soft garment-dyed tee with a vintage moth print.",
        "tags": ["moth shirt", "dark academia", "insect tee"],
        "client_item_id": "drop-0526-moth"
      }
    ]
  }'
{
  "replayed": false,
  "shop_id": 38123456,
  "preset_id": 1667,
  "item_count": 2,
  "next_poll_after_ms": null,
  "next_step": "Staged: nothing was sent to Etsy. The batch waits in ListingView → Create listings → Saved listings (review_url) for the person to review and edit. …",
  "draft_id": "stg_48216",
  "revision": 1,
  "review_url": "https://app.listingview.io/dashboard/listing-creation?tab=saved",
  "items": [
    { "index": 0, "client_item_id": "drop-0526-frog" },
    { "index": 1, "client_item_id": "drop-0526-moth" }
  ]
}
  • What staging checks. The shop, the preset, the slots, that every file is ready, the shop settings, the print-on-demand rules and digital deliverables. Nothing reaches Etsy.

  • Checked at submit, not now. Your daily draft caps, a busy shop, and whether every preset photo can still be sent.

  • Batch size. A batch holds up to 100 items, and never more than your plan's per-job cap. A bigger batch is refused with DATA_BUDGET_EXCEEDED and the reason etsy_drafts_job. Stage several batches instead.

  • review_url is a full link to the Saved listings tab in ListingView. Give it to the person who reviews the batch.

  • Test keys answer would_stage and save nothing.

What each item can carry

Items take the usual fields: title, description, tags, price, quantity, sku and client_item_id. They can also carry these. This example shows the first three on one item:

{
  "slots": {
    "A": {
      "asset_id": 90411,
      "placements": [{
        "target": { "product": 1, "print_area": "front" },
        "placement": { "design_width_pct": 62.5, "design_height_pct": 41.2, "x_offset_pct": 50, "y_offset_pct": 12 }
      }]
    }
  },
  "pod_selection": [{ "product": 1, "options": { "color": ["White", "Natural", "Ivory"] } }],
  "photos": { "order": [{ "mockup": 2 }, { "asset_id": 90412 }] },
  "dimensions": { "weight": 6.2, "length": 10, "width": 8, "height": 1, "weight_unit": "oz", "dimensions_unit": "in" },
  "title": "Luna Moth Tee, Cottagecore Shirt",
  "client_item_id": "drop-0526-moth"
}
  • slots fills each design slot: an asset id, as in {"A": 90211}, or an asset id with this listing's own placements. Placements use the same fields and ranges as a preset (see API: Presets).

  • pod_selection sells fewer colors or sizes on this one listing, for example colors a design's ink doesn't suit. It can only remove options, never add them. An item is refused if it leaves an enabled slot with no color (slot_without_color) or a product with no variant (no_variants_left).

  • Removing a color needs photos. Mockups of removed colors are not dropped for you. If pod_selection removes a color on a preset with mockups and the item sends no photos, it is refused with color_prune_photos_required and the mockups as candidates. Drop those mockups with photos.drop, or list the ones to show with photos.order. Removing sizes never needs this.

  • photos sets the photo order on Etsy, lead photo first. order takes {"mockup": <index>} and {"asset_id": <id>} entries; drop takes {"mockup": <index>}. The mockup index is the index the preset's include=mockups read returns. Up to 20 photos.

  • dimensions sets the package weight, length, width, height, weight_unit and dimensions_unit, on print-on-demand and physical presets.

  • digital gives a digital item its own files. See Digital drafts below.

  • Physical photos. images[] stays for physical presets only. Videos always come from the preset.

A refused item names its path, such as items[7].slots.B.placements[0]. Every problem in the batch is listed at once.

Recover a lost stage response

If the answer never arrives, replay the request: send the same body with the same idempotency key (X-Idempotency-Key on REST, idempotency_key on MCP). While the batch is still open, you get the same draft_id back, even days later. The replay answers the batch as it is now, with its current revision. The same key with a different body is refused with CONFLICT and the reason idempotency_key_reused.

Once the batch is submitted or discarded, the key is free again. A replay then stages a new batch, or is refused with DUPLICATE_ITEM under the same 7-day rule as a direct create.

3. A person reviews it in ListingView

The batch appears in ListingView under Saved listings, as a card like any other saved draft: the number of listings and when it was saved. The person opens it, fixes what they want and saves. Their saved edits are what you submit.

  • Wait for the go-ahead. A submit sends the last version the person saved, not edits still open on their screen. Submit only after they say the review is done.

  • They can publish it themselves. Every card has a Publish to Etsy button. If the person uses it, your later submit tells you so (see 6. Submit).

4. Read the batch as it is now

GET /etsy-drafts/staged/{draft_id} returns the batch exactly as it is now, with every item in the same shape you stage with.

{
  "draft_id": "stg_48213",
  "source": "api",
  "status": "staged",
  "submitted_by": null,
  "job_id": null,
  "shop_id": 38123456,
  "preset_id": 1667,
  "preset_missing": false,
  "revision": 3,
  "edited_in_app": true,
  "staging_key_active": true,
  "item_count": 2,
  "items": [
    {
      "index": 0,
      "client_item_id": "drop-0412-ghost",
      "title": "Ghost Frog Tee, Spooky Cottagecore Shirt",
      "tags": ["frog shirt", "cottagecore tee"],
      "slots": { "A": 9001, "B": 9002 },
      "pod_selection": [{ "product": 1, "options": { "color": ["Black", "Navy"] } }]
    },
    {
      "index": 1,
      "client_item_id": null,
      "title": "Moth Club Tee",
      "tags": null,
      "slots": { "A": 9005, "B": 9006 },
      "app_only_fields": ["videos"]
    }
  ],
  "app_only_fields": ["videos"]
}

The answer is shortened here. In this example the person fixed a title, deleted a row and added one.

  • revision goes up with every saved change, by anyone. Send it back as expected_revision when you edit.

  • client_item_id shows only when ListingView can prove which row it belongs to. A row the person added, or one it can't match after their edits, reads null. Address those by index.

  • app_only_fields[] names any edit the API's item fields can't express. You get the names only, never the values. An id that is no longer in your account is listed under unresolvable[], never shown.

  • status, submitted_by and job_id say whether the batch has been sent, and from where. A submitted batch reads items: []: read its items on the job.

  • preset_missing: true means the preset was deleted after you staged.

List your batches

GET /etsy-drafts/staged lists your open batches as {batches[], next_cursor}, filtered by shop_id or preset_id, with limit and cursor paging:

{
  "source": "api",
  "draft_id": "stg_48213",
  "shop_id": 38123456,
  "preset_id": 1667,
  "revision": 3,
  "item_count": 12,
  "updated_at": "2026-10-26T15:02:11.000Z",
  "edited_in_app": true,
  "staging_key_active": true
}
  • edited_in_app: true means a person saved changes in ListingView since your last write. Your own edits never set it.

  • Drafts saved in the app are listed with source: "app" for a key that holds catalog:read. The API can read them but can't edit, submit or discard them: those calls answer 403 with the reason app_draft. A key that holds only etsy_drafts:write never sees them. shop_id filters API-staged batches only.

5. Update the batch: send every edit in one PATCH

Collect every change, then send them together in one PATCH /etsy-drafts/staged/{draft_id}. expected_revision and an idempotency key are required.

curl -X PATCH https://api.listingview.io/api/v1/etsy-drafts/staged/stg_48213 \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-27-batch-edit" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_revision": 3,
    "ops": [
      { "op": "remove_items", "index": 5 },
      { "op": "remove_items", "index": 2 },
      {
        "op": "update_item",
        "client_item_id": "drop-0412-ghost",
        "item": {
          "slots": {
            "B": {
              "asset_id": 9010,
              "placements": [{ "target": { "product": 1, "print_area": "front" }, "placement": { "design_width_pct": 70, "x_offset_pct": 50 } }]
            }
          }
        }
      },
      {
        "op": "add_items",
        "items": [{ "slots": { "A": 9011, "B": 9012 }, "title": "Toad Choir Tee", "tags": ["toad shirt"], "client_item_id": "drop-0412-toad" }]
      }
    ]
  }'
  • Operations. add_items (with items[]), remove_items, replace_item and update_item (with item), up to 100 per call. Address an item by client_item_id where it has one, otherwise by index. One op per item per call (item_addressed_twice otherwise).

  • Indexes don't shift. Every index in one PATCH means the batch as you read it at expected_revision. Removing index 5 and index 2 in one call removes the rows that were 5 and 2.

  • update_item keeps what you don't send, including the person's in-app edits. slots merge letter by letter. An item's digital replaces that item's files.

  • App-only edits are protected. A change that would rebuild something the person set in the app that the API can't express is refused with app_only_fields, naming it.

  • All or nothing. Every operation applies, or none do. The answer is the batch as it is now, with the new revision. A change that would leave no items is refused (batch_empty).

  • One PATCH, not many. Your edits and discards share one rate limit with the app's own saves for this account: 30 writes a minute. Past it you get RATE_LIMITED with the reason draft_writes. Small, frequent PATCHes run into it, and so does the person editing in the app.

  • Someone else saved first. A stale expected_revision is refused with CONFLICT and the reason draft_changed. Re-read, re-apply your edits and retry. After 3 conflicts in a row, stop and tell the person.

  • The preset was deleted. Any op other than remove_items answers CONFLICT with the reason preset_missing.

  • Checked again later. Every check runs again in full at submit.

  • Test keys answer would_edit.

6. Submit

POST /etsy-drafts/staged/{draft_id}/submit sends the batch as it is now, with every edit the person saved. It needs a new idempotency key: the key you staged with is refused with idempotency_key_reused.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts/staged/stg_48213/submit \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-27-submit-2" \
  -H "Content-Type: application/json" \
  -d '{ "expected_revision": 4, "drop_ineligible": true }'
{
  "job_id": "edj_48213",
  "status": "pending",
  "replayed": false,
  "item_count": 1,
  "next_poll_after_ms": 2000,
  "draft_id": "stg_48213",
  "edited_in_app": false,
  "dropped": [{
    "index": 1,
    "client_item_id": "drop-0526-moth",
    "reason": "asset_not_ready",
    "message": "It uses assets that aren't ready yet (90812). Wait until list_assets shows them ready."
  }]
}

The answer is shortened here. Poll the job_id like any Etsy-draft job (step 7).

  • Every check runs again on the batch as it is now. An edit made in the app can't slip a file that isn't ready, or another shop's setting, past it.

  • Photos. A preset or item photo must be one of your own ready library files, or a public https:// image. A photo ListingView can't check or send is refused with photo_reselect_required: upload or import it again and pick it again.

  • expected_revision is optional. Leave it out to accept whatever the person saved. Send it to refuse the submit if anything changed since your read (draft_changed).

  • The preset changed. If the preset was edited after you staged, the submit answers preset_changed. Read the batch again, then send accept_preset_changes: true to submit it against the preset as it is now. A deleted preset answers preset_missing.

  • Items that fail. By default a failing item refuses the whole batch with items_ineligible, and details.items names each item with its reasons. With drop_ineligible: true, failing items are left out and listed in dropped[], and the rest go to Etsy. Dropped items also show as failed on the job.

  • A refused submit changes nothing. The batch stays staged exactly as it was.

  • Limits count on the day you submit. A daily-cap refusal leaves the batch staged, with retry_after at the next midnight UTC. A busy shop answers SHOP_BUSY, which is retryable.

  • Sending it twice is safe. The same idempotency key returns the same job. A batch this account's API already submitted answers replayed: true with the same job.

  • Test keys answer would_submit.

Who submitted

If the person published the batch from the app, your submit answers CONFLICT with the reason draft_already_submitted. details.job_id names the job, and details.submitted_by is "app". The batch read shows status: "submitted" and submitted_by. Poll that job, or tell the person it was published from the app.

If staging_key_active is false, the key that staged the batch was revoked or has expired. Don't submit that batch without the person's go-ahead.

7. Poll the job, then find it in the history

GET /etsy-drafts/jobs/{job_id} reports the job and each item. Wait next_poll_after_ms between calls.

{
  "job_id": "edj_48214",
  "status": "running",
  "counts": { "total": 3, "queued": 1, "processing": 0, "drafted": 1, "failed": 1, "skipped": 0, "cancelled": 0, "unknown_outcome": 0 },
  "next_poll_after_ms": 5000,
  "items": [
    {
      "index": 1,
      "client_item_id": "drop-0413-b",
      "status": "failed",
      "retry_safe": true,
      "etsy_listing_id": null,
      "error": {
        "source": "etsy",
        "code": "ITEM_FAILED",
        "message": "price → Price must be greater than or equal to 0.2",
        "field": "price",
        "reason": null,
        "details": null
      }
    }
  ]
}

The answer is shortened here.

  • Per item you get its status, its Etsy listing id and editor link once it is a draft, and pushable once it is ready to publish. A drafted item can read pushable: false with pushable_reason: "catalog_sync_pending" for a short while.

  • Failed items carry Etsy's own wording and retry_safe. Items marked retry_safe: true can go in a new job.

  • unknown_outcome means the item may or may not exist on Etsy, for example when Etsy didn't answer the create in time. ListingView never sends a create twice on its own. Check the shop first, and never resubmit it blindly.

  • What was applied. Each item also echoes what it applied, so you can check it against what you sent: applied_placements, pruned_options and photo_order. For a batch the person edited in the app, only pruned_options is echoed.

  • index is the item's position in the batch as it was submitted.

GET /etsy-drafts/jobs lists your Etsy-draft jobs, newest first, from the last 30 days. Filter by shop_id, preset_id, status or since, and page with cursor.

{
  "job_id": "edj_48213",
  "shop_id": 38123456,
  "preset_id": 1667,
  "status": "completed",
  "created_at": "2026-10-25T14:02:11.000Z",
  "finished_at": "2026-10-25T14:09:47.000Z",
  "items_total": 12,
  "items_drafted": 12,
  "items_failed": 0,
  "source": "api",
  "key_name": "drops-bot"
}
  • Window. A since older than 30 days is refused with too_old, plus details.max_lookback_days and earliest_since. A future since is refused with in_future.

  • Dates. created_at and since are when the job, or its staged batch, was created, not when it was submitted.

  • App jobs. A key that holds catalog:read also sees bulk creates started in the app (source: "app", no key_name), including a staged batch the person published from the app. A key that holds only etsy_drafts:write sees API jobs only.

  • Not listed. Jobs still preparing, or that ended before submit. Read those by job_id.

  • With no jobs, you get {"jobs": [], "next_cursor": null}.

Digital drafts

A digital preset makes digital Etsy drafts, staged or not. Each item can bring its own files, because a digital listing usually delivers its own file:

{
  "title": "2027 Weekly Planner Printable, Minimalist PDF",
  "tags": ["weekly planner", "printable planner", "2027 planner"],
  "digital": {
    "deliverables": [
      { "kind": "file", "asset_id": 88201 },
      { "kind": "collection", "collection_id": 4522 }
    ],
    "design_files": [{ "asset_id": 88202 }]
  },
  "client_item_id": "planner-weekly-2027"
}
  • An item's files replace the preset's. Send every file the listing delivers. Without digital, the item delivers the preset's files.

  • Files and collections only. A link deliverable, the item's own or one the preset carries, is refused with link_deliverables_unsupported.

  • At least one file. Every item needs at least one ready file or collection (digital_files_required).

  • Ready and scanned. Every file, and every file inside a collection, must be ready and scanned clean (collection_file_not_ready). An empty collection is refused (collection_empty).

  • Limits. A listing can deliver up to 20 deliverables, design files included (too_many_deliverables), and up to 200 files through its collections (too_many_collection_files).

  • Old presets. A preset that still carries legacy inline files answers legacy_preset_files. Re-save its deliverables in ListingView.

  • Where a refusal points. A file from the preset is named preset.digital.deliverables[k] or preset.digital.design_files[k]. On a staged submit, an item's own file is named items[i].digitalFiles[k]. A not-ready file at submit is named by its file only.

What the job reports

Each digital item shows deliverables (count and kinds), delivery (pdf, raw_fallback, failed, or null while it isn't settled yet) and delivered_files.

Drafted means delivered. A digital item counts as drafted only when Etsy confirmed every delivery file. Otherwise it is failed, with its etsy_listing_id and a reason:

  • delivery_upload_failed: the Etsy draft exists, but none of its files reached Etsy;

  • delivery_incomplete: the Etsy draft exists, but only some of its files reached Etsy (delivered_files says how many did).

{
  "index": 1,
  "client_item_id": "planner-02",
  "status": "failed",
  "retry_safe": false,
  "etsy_listing_id": 4459812290,
  "error": {
    "source": "listingview",
    "code": "ITEM_FAILED",
    "message": "The Etsy draft was created, but only some of its download files reached Etsy, so a buyer would not receive everything. Delete this draft, then create it again.",
    "reason": "delivery_incomplete"
  },
  "deliverables": { "count": 6, "kinds": ["file"] },
  "delivery": "failed",
  "delivered_files": 5
}

These items are always retry_safe: false. Delete the draft (see Redraft below), then draft it again.

Discard a staged batch

DELETE /etsy-drafts/staged/{draft_id} discards a batch the API staged.

{
  "draft_id": "stg_48213",
  "deleted": true,
  "next_step": "Discarded: the batch is gone from Saved listings and its staged-batch slot is free. Nothing was sent to Etsy; its designs stay in your asset library."
}
  • Nothing on Etsy changes. Your uploaded files stay in your library.

  • Already submitted. A submitted batch answers CONFLICT with the reason draft_already_submitted and its job_id. Poll that job instead.

  • Retrying is safe. A retry with the same idempotency key answers deleted: true again.

  • Test keys answer would_discard.

How long a staged batch stays

  • Until you act on it. A staged batch stays until it is submitted or discarded. It never expires, and there's no timer or reminder.

  • Up to 18 at a time. An account can have up to 18 API-staged batches open. They sit inside the account's 20 open saved drafts, which leaves room for drafts made in the app.

  • When the slots are full, staging answers CONFLICT with the reason draft_slots_full. details.scope says which ran out: api_staged (the 18) or open_drafts (the 20, app and API together). Submit or discard a batch, then stage again.

  • Check the count first. GET /usage shows staged_batches: api_staged and open_drafts, each with used, limit, remaining and exhausted.

  • Paused by ListingView. If ListingView pauses Etsy drafts, staging, editing, submitting and discarding answer SERVICE_UNAVAILABLE with the reason etsy_drafts_disabled until it resumes. Listing and reading still work, a same-key replay still answers, and no staged batch is cancelled. Digital drafts can be paused on their own: a digital preset then answers FEATURE_UNAVAILABLE, while print-on-demand and physical drafts carry on.

  • Plan changes. Below the Plus plan, edit, submit and discard answer PLAN_INSUFFICIENT.

When a key is revoked

  • Revoked by you. The batch stays. Any other key on the account with etsy_drafts:write can edit or submit it, and the list shows staging_key_active: false.

  • Revoked by ListingView support, for example after a leak. That key's staged batches are discarded. If one is still found, an edit or submit discards it then and answers CONFLICT with the reason staging_key_revoked.

Redraft: delete an Etsy draft, then draft again

When drafts went to Etsy wrong, for example with the wrong preset, delete them and draft them again. A Drafting Agent key can do the whole loop.

Look the drafts up by Etsy id

POST /catalog/listings/lookup checks up to 100 of your Etsy listings in one call. It's a read and needs catalog:read.

curl -X POST https://api.listingview.io/api/v1/catalog/listings/lookup \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "shop_id": 38123456, "etsy_listing_ids": [4012345678, 4012345679] }'
{
  "shop_id": 38123456,
  "partial": false,
  "retry_after": null,
  "etsy_calls": 0,
  "counts": { "requested": 2, "found": 1, "not_found": 1 },
  "results": [
    { "etsy_listing_id": 4012345678, "status": "found", "catalog_listing_id": 91234, "shop_id": 38123456, "state": "draft", "partial": false },
    { "etsy_listing_id": 4012345679, "status": "not_found", "catalog_listing_id": null, "shop_id": null, "state": null }
  ]
}
  • Without include you get ListingView's synced state, with no Etsy call. An id that isn't a synced listing of your shop answers not_found and costs nothing.

  • include: ["live_state"] adds the state on Etsy right now, in one Etsy call for the whole list. See API: Publish & reprice for pod_matrix and live_inventory.

  • Live parts count against your Etsy-call budget. A part that can't be read is marked on that listing with live_unavailable_reason, and the answer has partial: true. It never fails the call.

  • The Etsy connection is never renewed inside a lookup. While it is being renewed, live parts read token_refresh_pending; retry after retry_after.

  • With one connected shop you can leave shop_id out.

Delete each draft

DELETE /catalog/listings/{listing_id}?shop_id= deletes one Etsy draft. listing_id is the ListingView catalog id, or etsy: plus the Etsy listing id. An idempotency key is required.

curl -X DELETE "https://api.listingview.io/api/v1/catalog/listings/etsy:4012345679?shop_id=38123456&confirm_shared_delete=true" \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY"
{
  "deleted": true,
  "already_deleted": true,
  "listing_id": 91235,
  "etsy_listing_id": 4012345679,
  "title": "Witchy Vibes Crewneck",
  "deleted_at": "2026-10-26T15:05:40.019Z",
  "shared_confirmed": true,
  "deletes_remaining_today": 37,
  "audit_id": 88124
}
  • What goes. The Etsy draft, its listing in ListingView and its own Printify product.

  • Already gone. A draft that's already deleted answers 200 with already_deleted: true.

  • The seller is told. Each delete puts a notice in ListingView, titled "Etsy draft deleted by your API key". It names the key, the draft and the shop, and ends: "If you didn't expect this, revoke the key in Settings → API Keys."

  • A daily limit. Deletes have their own daily limit per account, shown as etsy_draft_deletes in GET /usage and as deletes_remaining_today in each answer. Past it you get DATA_BUDGET_EXCEEDED with the reason etsy_draft_deletes_daily. Before a redraft, check this and your Etsy-draft caps in /usage, so a redraft never stops halfway.

  • Test keys answer would_delete.

What the delete checks

The delete runs these checks in order and stops at the first that fails:

Check

When it fails

The listing is in your shop

404

This account's Bulk Create made the draft, through the app or the API

CONFLICT draft_not_created_by_listingview. It never says who made it.

No other ListingView account shares the listing

CONFLICT listing_shared_with_other_account, unless you send confirm_shared_delete=true

ListingView isn't pushing or publishing that listing right now

CONFLICT publish_pending, retryable

The shop's Etsy connection is current

SERVICE_UNAVAILABLE token_refresh_pending with retry_after while it renews; SHOP_NOT_CONNECTED when it is gone

Your shop connection allows deletes

SHOP_NOT_CONNECTED with the reason reconnect_required, and details.missing_etsy_scopes when the delete permission is missing. Reconnect the shop.

You have deletes left today

DATA_BUDGET_EXCEEDED etsy_draft_deletes_daily

Etsy still shows it as a draft, read live right before the delete

CONFLICT listing_not_draft with details.state. Nothing is deleted. If Etsy can't be read, nothing is deleted either; retry with the same idempotency key.

  • Shared shops. Up to 8 ListingView accounts can connect the same Etsy shop. A listing is shared when another of them also manages it. Deleting the draft removes it for all of them. Their own Printify products are left alone. The API never sends confirm_shared_delete for you.

  • After the checks. If Etsy itself refuses the delete, you get CONFLICT with the reason etsy_not_deletable. If Etsy deleted the draft but ListingView's cleanup failed, you get SERVICE_UNAVAILABLE with the reason cleanup_pending. Retry it: the retry finishes the cleanup.

The race the API can't close

Etsy has no "delete only if still a draft" call. ListingView reads the live state at most 2 seconds before it deletes, and holds the same lock its API publishing uses, so it never races itself.

One gap remains. If a person activates that same draft on Etsy.com, or publishes it from the ListingView app, in that split second, the now-live listing is deleted. This risk is accepted. Run redrafts only on drafts nobody is publishing by hand at that moment.

Draft again

Stage or create the drafts again as above, with a new idempotency key.

Conflicts and retries

  • A conflict (draft_changed, preset_changed): read again, re-apply, retry. After 3 conflicts in a row, stop and tell the person.

  • A retryable error (isRetryable: true): wait retry_after, with some jitter. Stop after 3 tries and tell the person.

  • Not retryable: never retry. Fix the input, or ask the person.

  • No clear outcome (unknown_outcome, or retry_safe: false): the draft may or may not exist on Etsy. Check it on Etsy, or read the job or the listing, before you do anything else. Never resubmit it blindly.

The full rules are in API: Getting started.

From the CLI or an AI app

  • CLI: listingview etsy-drafts create --shop-id 38123456 --preset-id 1667 --items items.json --stage stages a batch. Running the same command again returns the same batch. The staged batch has its own commands: etsy-drafts staged list, get, update, submit and delete. listingview etsy-drafts jobs status edj_48213 --watch follows a job. An item with an unknown outcome makes a job command exit with code 9: check the shop, and never resubmit it. See API: Command-line tool.

  • MCP: create_etsy_drafts takes operation: create (the default; drafts now), stage, update or submit. update and submit need draft_id; ops and expected_revision go with update, and expected_revision, drop_ineligible and accept_preset_changes with submit. An idempotency key is required on every call.

  • MCP: get_bulk_job_status reads jobs and batches. list: "staged" lists staged batches and draft_id reads one. list: "jobs" lists the job history and job_id reads one job.

  • MCP: delete_preset_or_draft deletes one thing, by kind. kind: "staged_batch" with draft_id discards a batch. kind: "etsy_draft" with listing_id or etsy_listing_id (never both), plus shop_id and confirm_shared_delete, deletes an Etsy draft.

  • MCP: get_listing with etsy_listing_ids and include runs the lookup. enhance_listing_with_ai takes current_copy and use_custom_instructions. See API: Connect AI apps.

Every endpoint above, with its full request and response schema, is in the API reference. Every error code is on the errors page.

Related features

With a Drafting Agent key, an agent turns a preset and your designs into Etsy drafts. Drafts are hidden from shoppers. The agent can send a batch straight to Etsy, or stage it first so a person can check it in ListingView.

This guide covers both, then the staged loop from start to finish:

  1. Write the listing copy with AI.

  2. Stage the batch. Nothing reaches Etsy.

  3. A person reviews and edits it in ListingView, under Saved listings.

  4. Read the batch as the person left it.

  5. Update it in one call, if anything still needs to change.

  6. Submit it. ListingView makes the Etsy drafts.

  7. Poll the job, then find it later in the job history.

New to the API? Start with API: Getting started. To build the preset first, see API: Presets.

What you need

  • A Drafting Agent key. Creating, staging, editing, submitting and deleting need etsy_drafts:write, which a Drafting Agent key holds. Staging writes nothing to Etsy, but it still needs that scope.

  • Plus plan or higher to create, stage, edit, submit and delete, and to poll one job.

  • Reading after a downgrade. The staged-batch list, one staged batch and the job history stay readable below Plus for a key that holds catalog:read. A key that holds only etsy_drafts:write reads them on Plus or higher.

  • A working preset and ready designs. See API: Presets and API: Uploads.

A Drafting Agent key can never publish. Drafts go live only when someone publishes them. See API: Publish & reprice.

Create Etsy drafts now

POST /etsy-drafts drafts a batch straight to Etsy from one of your presets. Fill each of the preset's upload slots with a ready design's asset id. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-16-ghost-tee" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 38123456,
    "preset_id": 1667,
    "expected_preset_updated_at": "2026-10-14T18:22:05.123Z",
    "items": [{
      "slots": { "A": 90211 },
      "title": "Retro Ghost Tee, Spooky Season Shirt",
      "tags": ["halloween shirt", "ghost tee", "spooky season"],
      "client_item_id": "drop-0412-ghost"
    }]
  }'
{
  "job_id": "edj_48213",
  "status": "pending",
  "replayed": false,
  "shop_id": 38123456,
  "preset_id": 1667,
  "item_count": 1,
  "next_poll_after_ms": 5000,
  "next_step": "Poll get_bulk_job_status with this job_id; drafts appear one by one. Wait next_poll_after_ms between calls."
}
  • The whole job is admitted or refused. Your plan's per-job and per-shop daily draft caps apply. GET /usage shows them.

  • expected_preset_updated_at is the preset's updated_at from your read. If the preset changed since, nothing is drafted (preset_changed).

  • One file, two slots. The same asset may fill two slots, for example a light and a dark slot.

  • The same key returns the same job. Resending the identical request with the same key answers replayed: true and the same job_id, for as long as the job exists. The same key with a different body is refused with idempotency_key_reused.

  • The same item under a new key is refused. A client_item_id you sent to this shop in the last 7 days is refused with 409 DUPLICATE_ITEM while it is still queued or processing, once it became a draft, or when its outcome is unknown. details.duplicates[] names each id and its earlier status. It can go again only if it failed with retry_safe: true, was cancelled before it ran, or was skipped by a job that ended.

  • Test keys answer the plan and write nothing.

Poll the job as in step 7.

1. Write the copy first

Staging refuses inline AI copy with ai_requires_submit. Write the copy first with POST /ai/listing-copy, then stage the batch with the finished copy.

curl -X POST https://api.listingview.io/api/v1/ai/listing-copy \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "design_asset_ids": [88123],
    "current_copy": {
      "title": "Sunset Mountain Sweatshirt",
      "description": "Retro sunset crewneck in soft white ink.",
      "tags": ["sunset sweatshirt", "mountain shirt", "hiking gift"],
      "price": 34.5
    },
    "instructions": "Cozy autumn tone. Mention the night sky.",
    "use_custom_instructions": true
  }'
  • Improve or write fresh. With current_copy, the AI improves that copy. Without it, the AI writes from the artwork alone. current_copy needs a title, description or tags; a price alone is refused.

  • House instructions. use_custom_instructions is on by default. It applies the seller's saved Custom Instructions in ListingView, in addition to your instructions.

  • The answer has title, tags, description, fields, saw_artwork, model, tokens_charged, applied: false, warnings[] and grounding.status (grounded, candidates-only, skipped or unknown). Nothing is saved.

  • The model always sees the design. If it can't, the call is refused with AI_VISION_UNAVAILABLE and nothing is charged.

  • Tags when you improve copy. When you send current_copy with instructions, the AI doesn't top up the tag list on its own, so you may get fewer than 13 tags back. warnings[] doesn't say so; count them. To top up tags, send fields: ["tags"], the tags you already chose in current_copy.tags, and an instruction such as "suggest 10 MORE, none repeating them".

  • Tokens. Every call spends AI tokens, also when the returned title repeats yours. A low balance is refused with AI_TOKENS_INSUFFICIENT before anything is spent. At most 2 AI calls run at once per account; a third answers RATE_LIMITED with the reason ai_concurrency.

  • Scope. AI copy needs catalog:write.

Inline AI copy on a create that drafts now (ai_copy, ai) also needs catalog:write. It writes from the artwork only: send current_copy to POST /ai/listing-copy instead.

2. Stage the batch

Send POST /etsy-drafts with submit: false. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: stage-2026-10-26-batch-01" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 38123456,
    "preset_id": 1667,
    "expected_preset_updated_at": "2026-10-14T18:22:05.123Z",
    "submit": false,
    "items": [
      {
        "slots": { "A": 90501 },
        "title": "Cottagecore Frog Tee, Mushroom Shirt",
        "description": "A soft garment-dyed tee with a hand-drawn frog.",
        "tags": ["frog shirt", "cottagecore tee", "mushroom shirt"],
        "client_item_id": "drop-0526-frog"
      },
      {
        "slots": { "A": 90502 },
        "title": "Moth Club Tee, Dark Academia Shirt",
        "description": "A soft garment-dyed tee with a vintage moth print.",
        "tags": ["moth shirt", "dark academia", "insect tee"],
        "client_item_id": "drop-0526-moth"
      }
    ]
  }'
{
  "replayed": false,
  "shop_id": 38123456,
  "preset_id": 1667,
  "item_count": 2,
  "next_poll_after_ms": null,
  "next_step": "Staged: nothing was sent to Etsy. The batch waits in ListingView → Create listings → Saved listings (review_url) for the person to review and edit. …",
  "draft_id": "stg_48216",
  "revision": 1,
  "review_url": "https://app.listingview.io/dashboard/listing-creation?tab=saved",
  "items": [
    { "index": 0, "client_item_id": "drop-0526-frog" },
    { "index": 1, "client_item_id": "drop-0526-moth" }
  ]
}
  • What staging checks. The shop, the preset, the slots, that every file is ready, the shop settings, the print-on-demand rules and digital deliverables. Nothing reaches Etsy.

  • Checked at submit, not now. Your daily draft caps, a busy shop, and whether every preset photo can still be sent.

  • Batch size. A batch holds up to 100 items, and never more than your plan's per-job cap. A bigger batch is refused with DATA_BUDGET_EXCEEDED and the reason etsy_drafts_job. Stage several batches instead.

  • review_url is a full link to the Saved listings tab in ListingView. Give it to the person who reviews the batch.

  • Test keys answer would_stage and save nothing.

What each item can carry

Items take the usual fields: title, description, tags, price, quantity, sku and client_item_id. They can also carry these. This example shows the first three on one item:

{
  "slots": {
    "A": {
      "asset_id": 90411,
      "placements": [{
        "target": { "product": 1, "print_area": "front" },
        "placement": { "design_width_pct": 62.5, "design_height_pct": 41.2, "x_offset_pct": 50, "y_offset_pct": 12 }
      }]
    }
  },
  "pod_selection": [{ "product": 1, "options": { "color": ["White", "Natural", "Ivory"] } }],
  "photos": { "order": [{ "mockup": 2 }, { "asset_id": 90412 }] },
  "dimensions": { "weight": 6.2, "length": 10, "width": 8, "height": 1, "weight_unit": "oz", "dimensions_unit": "in" },
  "title": "Luna Moth Tee, Cottagecore Shirt",
  "client_item_id": "drop-0526-moth"
}
  • slots fills each design slot: an asset id, as in {"A": 90211}, or an asset id with this listing's own placements. Placements use the same fields and ranges as a preset (see API: Presets).

  • pod_selection sells fewer colors or sizes on this one listing, for example colors a design's ink doesn't suit. It can only remove options, never add them. An item is refused if it leaves an enabled slot with no color (slot_without_color) or a product with no variant (no_variants_left).

  • Removing a color needs photos. Mockups of removed colors are not dropped for you. If pod_selection removes a color on a preset with mockups and the item sends no photos, it is refused with color_prune_photos_required and the mockups as candidates. Drop those mockups with photos.drop, or list the ones to show with photos.order. Removing sizes never needs this.

  • photos sets the photo order on Etsy, lead photo first. order takes {"mockup": <index>} and {"asset_id": <id>} entries; drop takes {"mockup": <index>}. The mockup index is the index the preset's include=mockups read returns. Up to 20 photos.

  • dimensions sets the package weight, length, width, height, weight_unit and dimensions_unit, on print-on-demand and physical presets.

  • digital gives a digital item its own files. See Digital drafts below.

  • Physical photos. images[] stays for physical presets only. Videos always come from the preset.

A refused item names its path, such as items[7].slots.B.placements[0]. Every problem in the batch is listed at once.

Recover a lost stage response

If the answer never arrives, replay the request: send the same body with the same idempotency key (X-Idempotency-Key on REST, idempotency_key on MCP). While the batch is still open, you get the same draft_id back, even days later. The replay answers the batch as it is now, with its current revision. The same key with a different body is refused with CONFLICT and the reason idempotency_key_reused.

Once the batch is submitted or discarded, the key is free again. A replay then stages a new batch, or is refused with DUPLICATE_ITEM under the same 7-day rule as a direct create.

3. A person reviews it in ListingView

The batch appears in ListingView under Saved listings, as a card like any other saved draft: the number of listings and when it was saved. The person opens it, fixes what they want and saves. Their saved edits are what you submit.

  • Wait for the go-ahead. A submit sends the last version the person saved, not edits still open on their screen. Submit only after they say the review is done.

  • They can publish it themselves. Every card has a Publish to Etsy button. If the person uses it, your later submit tells you so (see 6. Submit).

4. Read the batch as it is now

GET /etsy-drafts/staged/{draft_id} returns the batch exactly as it is now, with every item in the same shape you stage with.

{
  "draft_id": "stg_48213",
  "source": "api",
  "status": "staged",
  "submitted_by": null,
  "job_id": null,
  "shop_id": 38123456,
  "preset_id": 1667,
  "preset_missing": false,
  "revision": 3,
  "edited_in_app": true,
  "staging_key_active": true,
  "item_count": 2,
  "items": [
    {
      "index": 0,
      "client_item_id": "drop-0412-ghost",
      "title": "Ghost Frog Tee, Spooky Cottagecore Shirt",
      "tags": ["frog shirt", "cottagecore tee"],
      "slots": { "A": 9001, "B": 9002 },
      "pod_selection": [{ "product": 1, "options": { "color": ["Black", "Navy"] } }]
    },
    {
      "index": 1,
      "client_item_id": null,
      "title": "Moth Club Tee",
      "tags": null,
      "slots": { "A": 9005, "B": 9006 },
      "app_only_fields": ["videos"]
    }
  ],
  "app_only_fields": ["videos"]
}

The answer is shortened here. In this example the person fixed a title, deleted a row and added one.

  • revision goes up with every saved change, by anyone. Send it back as expected_revision when you edit.

  • client_item_id shows only when ListingView can prove which row it belongs to. A row the person added, or one it can't match after their edits, reads null. Address those by index.

  • app_only_fields[] names any edit the API's item fields can't express. You get the names only, never the values. An id that is no longer in your account is listed under unresolvable[], never shown.

  • status, submitted_by and job_id say whether the batch has been sent, and from where. A submitted batch reads items: []: read its items on the job.

  • preset_missing: true means the preset was deleted after you staged.

List your batches

GET /etsy-drafts/staged lists your open batches as {batches[], next_cursor}, filtered by shop_id or preset_id, with limit and cursor paging:

{
  "source": "api",
  "draft_id": "stg_48213",
  "shop_id": 38123456,
  "preset_id": 1667,
  "revision": 3,
  "item_count": 12,
  "updated_at": "2026-10-26T15:02:11.000Z",
  "edited_in_app": true,
  "staging_key_active": true
}
  • edited_in_app: true means a person saved changes in ListingView since your last write. Your own edits never set it.

  • Drafts saved in the app are listed with source: "app" for a key that holds catalog:read. The API can read them but can't edit, submit or discard them: those calls answer 403 with the reason app_draft. A key that holds only etsy_drafts:write never sees them. shop_id filters API-staged batches only.

5. Update the batch: send every edit in one PATCH

Collect every change, then send them together in one PATCH /etsy-drafts/staged/{draft_id}. expected_revision and an idempotency key are required.

curl -X PATCH https://api.listingview.io/api/v1/etsy-drafts/staged/stg_48213 \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-27-batch-edit" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_revision": 3,
    "ops": [
      { "op": "remove_items", "index": 5 },
      { "op": "remove_items", "index": 2 },
      {
        "op": "update_item",
        "client_item_id": "drop-0412-ghost",
        "item": {
          "slots": {
            "B": {
              "asset_id": 9010,
              "placements": [{ "target": { "product": 1, "print_area": "front" }, "placement": { "design_width_pct": 70, "x_offset_pct": 50 } }]
            }
          }
        }
      },
      {
        "op": "add_items",
        "items": [{ "slots": { "A": 9011, "B": 9012 }, "title": "Toad Choir Tee", "tags": ["toad shirt"], "client_item_id": "drop-0412-toad" }]
      }
    ]
  }'
  • Operations. add_items (with items[]), remove_items, replace_item and update_item (with item), up to 100 per call. Address an item by client_item_id where it has one, otherwise by index. One op per item per call (item_addressed_twice otherwise).

  • Indexes don't shift. Every index in one PATCH means the batch as you read it at expected_revision. Removing index 5 and index 2 in one call removes the rows that were 5 and 2.

  • update_item keeps what you don't send, including the person's in-app edits. slots merge letter by letter. An item's digital replaces that item's files.

  • App-only edits are protected. A change that would rebuild something the person set in the app that the API can't express is refused with app_only_fields, naming it.

  • All or nothing. Every operation applies, or none do. The answer is the batch as it is now, with the new revision. A change that would leave no items is refused (batch_empty).

  • One PATCH, not many. Your edits and discards share one rate limit with the app's own saves for this account: 30 writes a minute. Past it you get RATE_LIMITED with the reason draft_writes. Small, frequent PATCHes run into it, and so does the person editing in the app.

  • Someone else saved first. A stale expected_revision is refused with CONFLICT and the reason draft_changed. Re-read, re-apply your edits and retry. After 3 conflicts in a row, stop and tell the person.

  • The preset was deleted. Any op other than remove_items answers CONFLICT with the reason preset_missing.

  • Checked again later. Every check runs again in full at submit.

  • Test keys answer would_edit.

6. Submit

POST /etsy-drafts/staged/{draft_id}/submit sends the batch as it is now, with every edit the person saved. It needs a new idempotency key: the key you staged with is refused with idempotency_key_reused.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts/staged/stg_48213/submit \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-27-submit-2" \
  -H "Content-Type: application/json" \
  -d '{ "expected_revision": 4, "drop_ineligible": true }'
{
  "job_id": "edj_48213",
  "status": "pending",
  "replayed": false,
  "item_count": 1,
  "next_poll_after_ms": 2000,
  "draft_id": "stg_48213",
  "edited_in_app": false,
  "dropped": [{
    "index": 1,
    "client_item_id": "drop-0526-moth",
    "reason": "asset_not_ready",
    "message": "It uses assets that aren't ready yet (90812). Wait until list_assets shows them ready."
  }]
}

The answer is shortened here. Poll the job_id like any Etsy-draft job (step 7).

  • Every check runs again on the batch as it is now. An edit made in the app can't slip a file that isn't ready, or another shop's setting, past it.

  • Photos. A preset or item photo must be one of your own ready library files, or a public https:// image. A photo ListingView can't check or send is refused with photo_reselect_required: upload or import it again and pick it again.

  • expected_revision is optional. Leave it out to accept whatever the person saved. Send it to refuse the submit if anything changed since your read (draft_changed).

  • The preset changed. If the preset was edited after you staged, the submit answers preset_changed. Read the batch again, then send accept_preset_changes: true to submit it against the preset as it is now. A deleted preset answers preset_missing.

  • Items that fail. By default a failing item refuses the whole batch with items_ineligible, and details.items names each item with its reasons. With drop_ineligible: true, failing items are left out and listed in dropped[], and the rest go to Etsy. Dropped items also show as failed on the job.

  • A refused submit changes nothing. The batch stays staged exactly as it was.

  • Limits count on the day you submit. A daily-cap refusal leaves the batch staged, with retry_after at the next midnight UTC. A busy shop answers SHOP_BUSY, which is retryable.

  • Sending it twice is safe. The same idempotency key returns the same job. A batch this account's API already submitted answers replayed: true with the same job.

  • Test keys answer would_submit.

Who submitted

If the person published the batch from the app, your submit answers CONFLICT with the reason draft_already_submitted. details.job_id names the job, and details.submitted_by is "app". The batch read shows status: "submitted" and submitted_by. Poll that job, or tell the person it was published from the app.

If staging_key_active is false, the key that staged the batch was revoked or has expired. Don't submit that batch without the person's go-ahead.

7. Poll the job, then find it in the history

GET /etsy-drafts/jobs/{job_id} reports the job and each item. Wait next_poll_after_ms between calls.

{
  "job_id": "edj_48214",
  "status": "running",
  "counts": { "total": 3, "queued": 1, "processing": 0, "drafted": 1, "failed": 1, "skipped": 0, "cancelled": 0, "unknown_outcome": 0 },
  "next_poll_after_ms": 5000,
  "items": [
    {
      "index": 1,
      "client_item_id": "drop-0413-b",
      "status": "failed",
      "retry_safe": true,
      "etsy_listing_id": null,
      "error": {
        "source": "etsy",
        "code": "ITEM_FAILED",
        "message": "price → Price must be greater than or equal to 0.2",
        "field": "price",
        "reason": null,
        "details": null
      }
    }
  ]
}

The answer is shortened here.

  • Per item you get its status, its Etsy listing id and editor link once it is a draft, and pushable once it is ready to publish. A drafted item can read pushable: false with pushable_reason: "catalog_sync_pending" for a short while.

  • Failed items carry Etsy's own wording and retry_safe. Items marked retry_safe: true can go in a new job.

  • unknown_outcome means the item may or may not exist on Etsy, for example when Etsy didn't answer the create in time. ListingView never sends a create twice on its own. Check the shop first, and never resubmit it blindly.

  • What was applied. Each item also echoes what it applied, so you can check it against what you sent: applied_placements, pruned_options and photo_order. For a batch the person edited in the app, only pruned_options is echoed.

  • index is the item's position in the batch as it was submitted.

GET /etsy-drafts/jobs lists your Etsy-draft jobs, newest first, from the last 30 days. Filter by shop_id, preset_id, status or since, and page with cursor.

{
  "job_id": "edj_48213",
  "shop_id": 38123456,
  "preset_id": 1667,
  "status": "completed",
  "created_at": "2026-10-25T14:02:11.000Z",
  "finished_at": "2026-10-25T14:09:47.000Z",
  "items_total": 12,
  "items_drafted": 12,
  "items_failed": 0,
  "source": "api",
  "key_name": "drops-bot"
}
  • Window. A since older than 30 days is refused with too_old, plus details.max_lookback_days and earliest_since. A future since is refused with in_future.

  • Dates. created_at and since are when the job, or its staged batch, was created, not when it was submitted.

  • App jobs. A key that holds catalog:read also sees bulk creates started in the app (source: "app", no key_name), including a staged batch the person published from the app. A key that holds only etsy_drafts:write sees API jobs only.

  • Not listed. Jobs still preparing, or that ended before submit. Read those by job_id.

  • With no jobs, you get {"jobs": [], "next_cursor": null}.

Digital drafts

A digital preset makes digital Etsy drafts, staged or not. Each item can bring its own files, because a digital listing usually delivers its own file:

{
  "title": "2027 Weekly Planner Printable, Minimalist PDF",
  "tags": ["weekly planner", "printable planner", "2027 planner"],
  "digital": {
    "deliverables": [
      { "kind": "file", "asset_id": 88201 },
      { "kind": "collection", "collection_id": 4522 }
    ],
    "design_files": [{ "asset_id": 88202 }]
  },
  "client_item_id": "planner-weekly-2027"
}
  • An item's files replace the preset's. Send every file the listing delivers. Without digital, the item delivers the preset's files.

  • Files and collections only. A link deliverable, the item's own or one the preset carries, is refused with link_deliverables_unsupported.

  • At least one file. Every item needs at least one ready file or collection (digital_files_required).

  • Ready and scanned. Every file, and every file inside a collection, must be ready and scanned clean (collection_file_not_ready). An empty collection is refused (collection_empty).

  • Limits. A listing can deliver up to 20 deliverables, design files included (too_many_deliverables), and up to 200 files through its collections (too_many_collection_files).

  • Old presets. A preset that still carries legacy inline files answers legacy_preset_files. Re-save its deliverables in ListingView.

  • Where a refusal points. A file from the preset is named preset.digital.deliverables[k] or preset.digital.design_files[k]. On a staged submit, an item's own file is named items[i].digitalFiles[k]. A not-ready file at submit is named by its file only.

What the job reports

Each digital item shows deliverables (count and kinds), delivery (pdf, raw_fallback, failed, or null while it isn't settled yet) and delivered_files.

Drafted means delivered. A digital item counts as drafted only when Etsy confirmed every delivery file. Otherwise it is failed, with its etsy_listing_id and a reason:

  • delivery_upload_failed: the Etsy draft exists, but none of its files reached Etsy;

  • delivery_incomplete: the Etsy draft exists, but only some of its files reached Etsy (delivered_files says how many did).

{
  "index": 1,
  "client_item_id": "planner-02",
  "status": "failed",
  "retry_safe": false,
  "etsy_listing_id": 4459812290,
  "error": {
    "source": "listingview",
    "code": "ITEM_FAILED",
    "message": "The Etsy draft was created, but only some of its download files reached Etsy, so a buyer would not receive everything. Delete this draft, then create it again.",
    "reason": "delivery_incomplete"
  },
  "deliverables": { "count": 6, "kinds": ["file"] },
  "delivery": "failed",
  "delivered_files": 5
}

These items are always retry_safe: false. Delete the draft (see Redraft below), then draft it again.

Discard a staged batch

DELETE /etsy-drafts/staged/{draft_id} discards a batch the API staged.

{
  "draft_id": "stg_48213",
  "deleted": true,
  "next_step": "Discarded: the batch is gone from Saved listings and its staged-batch slot is free. Nothing was sent to Etsy; its designs stay in your asset library."
}
  • Nothing on Etsy changes. Your uploaded files stay in your library.

  • Already submitted. A submitted batch answers CONFLICT with the reason draft_already_submitted and its job_id. Poll that job instead.

  • Retrying is safe. A retry with the same idempotency key answers deleted: true again.

  • Test keys answer would_discard.

How long a staged batch stays

  • Until you act on it. A staged batch stays until it is submitted or discarded. It never expires, and there's no timer or reminder.

  • Up to 18 at a time. An account can have up to 18 API-staged batches open. They sit inside the account's 20 open saved drafts, which leaves room for drafts made in the app.

  • When the slots are full, staging answers CONFLICT with the reason draft_slots_full. details.scope says which ran out: api_staged (the 18) or open_drafts (the 20, app and API together). Submit or discard a batch, then stage again.

  • Check the count first. GET /usage shows staged_batches: api_staged and open_drafts, each with used, limit, remaining and exhausted.

  • Paused by ListingView. If ListingView pauses Etsy drafts, staging, editing, submitting and discarding answer SERVICE_UNAVAILABLE with the reason etsy_drafts_disabled until it resumes. Listing and reading still work, a same-key replay still answers, and no staged batch is cancelled. Digital drafts can be paused on their own: a digital preset then answers FEATURE_UNAVAILABLE, while print-on-demand and physical drafts carry on.

  • Plan changes. Below the Plus plan, edit, submit and discard answer PLAN_INSUFFICIENT.

When a key is revoked

  • Revoked by you. The batch stays. Any other key on the account with etsy_drafts:write can edit or submit it, and the list shows staging_key_active: false.

  • Revoked by ListingView support, for example after a leak. That key's staged batches are discarded. If one is still found, an edit or submit discards it then and answers CONFLICT with the reason staging_key_revoked.

Redraft: delete an Etsy draft, then draft again

When drafts went to Etsy wrong, for example with the wrong preset, delete them and draft them again. A Drafting Agent key can do the whole loop.

Look the drafts up by Etsy id

POST /catalog/listings/lookup checks up to 100 of your Etsy listings in one call. It's a read and needs catalog:read.

curl -X POST https://api.listingview.io/api/v1/catalog/listings/lookup \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "shop_id": 38123456, "etsy_listing_ids": [4012345678, 4012345679] }'
{
  "shop_id": 38123456,
  "partial": false,
  "retry_after": null,
  "etsy_calls": 0,
  "counts": { "requested": 2, "found": 1, "not_found": 1 },
  "results": [
    { "etsy_listing_id": 4012345678, "status": "found", "catalog_listing_id": 91234, "shop_id": 38123456, "state": "draft", "partial": false },
    { "etsy_listing_id": 4012345679, "status": "not_found", "catalog_listing_id": null, "shop_id": null, "state": null }
  ]
}
  • Without include you get ListingView's synced state, with no Etsy call. An id that isn't a synced listing of your shop answers not_found and costs nothing.

  • include: ["live_state"] adds the state on Etsy right now, in one Etsy call for the whole list. See API: Publish & reprice for pod_matrix and live_inventory.

  • Live parts count against your Etsy-call budget. A part that can't be read is marked on that listing with live_unavailable_reason, and the answer has partial: true. It never fails the call.

  • The Etsy connection is never renewed inside a lookup. While it is being renewed, live parts read token_refresh_pending; retry after retry_after.

  • With one connected shop you can leave shop_id out.

Delete each draft

DELETE /catalog/listings/{listing_id}?shop_id= deletes one Etsy draft. listing_id is the ListingView catalog id, or etsy: plus the Etsy listing id. An idempotency key is required.

curl -X DELETE "https://api.listingview.io/api/v1/catalog/listings/etsy:4012345679?shop_id=38123456&confirm_shared_delete=true" \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY"
{
  "deleted": true,
  "already_deleted": true,
  "listing_id": 91235,
  "etsy_listing_id": 4012345679,
  "title": "Witchy Vibes Crewneck",
  "deleted_at": "2026-10-26T15:05:40.019Z",
  "shared_confirmed": true,
  "deletes_remaining_today": 37,
  "audit_id": 88124
}
  • What goes. The Etsy draft, its listing in ListingView and its own Printify product.

  • Already gone. A draft that's already deleted answers 200 with already_deleted: true.

  • The seller is told. Each delete puts a notice in ListingView, titled "Etsy draft deleted by your API key". It names the key, the draft and the shop, and ends: "If you didn't expect this, revoke the key in Settings → API Keys."

  • A daily limit. Deletes have their own daily limit per account, shown as etsy_draft_deletes in GET /usage and as deletes_remaining_today in each answer. Past it you get DATA_BUDGET_EXCEEDED with the reason etsy_draft_deletes_daily. Before a redraft, check this and your Etsy-draft caps in /usage, so a redraft never stops halfway.

  • Test keys answer would_delete.

What the delete checks

The delete runs these checks in order and stops at the first that fails:

Check

When it fails

The listing is in your shop

404

This account's Bulk Create made the draft, through the app or the API

CONFLICT draft_not_created_by_listingview. It never says who made it.

No other ListingView account shares the listing

CONFLICT listing_shared_with_other_account, unless you send confirm_shared_delete=true

ListingView isn't pushing or publishing that listing right now

CONFLICT publish_pending, retryable

The shop's Etsy connection is current

SERVICE_UNAVAILABLE token_refresh_pending with retry_after while it renews; SHOP_NOT_CONNECTED when it is gone

Your shop connection allows deletes

SHOP_NOT_CONNECTED with the reason reconnect_required, and details.missing_etsy_scopes when the delete permission is missing. Reconnect the shop.

You have deletes left today

DATA_BUDGET_EXCEEDED etsy_draft_deletes_daily

Etsy still shows it as a draft, read live right before the delete

CONFLICT listing_not_draft with details.state. Nothing is deleted. If Etsy can't be read, nothing is deleted either; retry with the same idempotency key.

  • Shared shops. Up to 8 ListingView accounts can connect the same Etsy shop. A listing is shared when another of them also manages it. Deleting the draft removes it for all of them. Their own Printify products are left alone. The API never sends confirm_shared_delete for you.

  • After the checks. If Etsy itself refuses the delete, you get CONFLICT with the reason etsy_not_deletable. If Etsy deleted the draft but ListingView's cleanup failed, you get SERVICE_UNAVAILABLE with the reason cleanup_pending. Retry it: the retry finishes the cleanup.

The race the API can't close

Etsy has no "delete only if still a draft" call. ListingView reads the live state at most 2 seconds before it deletes, and holds the same lock its API publishing uses, so it never races itself.

One gap remains. If a person activates that same draft on Etsy.com, or publishes it from the ListingView app, in that split second, the now-live listing is deleted. This risk is accepted. Run redrafts only on drafts nobody is publishing by hand at that moment.

Draft again

Stage or create the drafts again as above, with a new idempotency key.

Conflicts and retries

  • A conflict (draft_changed, preset_changed): read again, re-apply, retry. After 3 conflicts in a row, stop and tell the person.

  • A retryable error (isRetryable: true): wait retry_after, with some jitter. Stop after 3 tries and tell the person.

  • Not retryable: never retry. Fix the input, or ask the person.

  • No clear outcome (unknown_outcome, or retry_safe: false): the draft may or may not exist on Etsy. Check it on Etsy, or read the job or the listing, before you do anything else. Never resubmit it blindly.

The full rules are in API: Getting started.

From the CLI or an AI app

  • CLI: listingview etsy-drafts create --shop-id 38123456 --preset-id 1667 --items items.json --stage stages a batch. Running the same command again returns the same batch. The staged batch has its own commands: etsy-drafts staged list, get, update, submit and delete. listingview etsy-drafts jobs status edj_48213 --watch follows a job. An item with an unknown outcome makes a job command exit with code 9: check the shop, and never resubmit it. See API: Command-line tool.

  • MCP: create_etsy_drafts takes operation: create (the default; drafts now), stage, update or submit. update and submit need draft_id; ops and expected_revision go with update, and expected_revision, drop_ineligible and accept_preset_changes with submit. An idempotency key is required on every call.

  • MCP: get_bulk_job_status reads jobs and batches. list: "staged" lists staged batches and draft_id reads one. list: "jobs" lists the job history and job_id reads one job.

  • MCP: delete_preset_or_draft deletes one thing, by kind. kind: "staged_batch" with draft_id discards a batch. kind: "etsy_draft" with listing_id or etsy_listing_id (never both), plus shop_id and confirm_shared_delete, deletes an Etsy draft.

  • MCP: get_listing with etsy_listing_ids and include runs the lookup. enhance_listing_with_ai takes current_copy and use_custom_instructions. See API: Connect AI apps.

Every endpoint above, with its full request and response schema, is in the API reference. Every error code is on the errors page.

Related features

With a Drafting Agent key, an agent turns a preset and your designs into Etsy drafts. Drafts are hidden from shoppers. The agent can send a batch straight to Etsy, or stage it first so a person can check it in ListingView.

This guide covers both, then the staged loop from start to finish:

  1. Write the listing copy with AI.

  2. Stage the batch. Nothing reaches Etsy.

  3. A person reviews and edits it in ListingView, under Saved listings.

  4. Read the batch as the person left it.

  5. Update it in one call, if anything still needs to change.

  6. Submit it. ListingView makes the Etsy drafts.

  7. Poll the job, then find it later in the job history.

New to the API? Start with API: Getting started. To build the preset first, see API: Presets.

What you need

  • A Drafting Agent key. Creating, staging, editing, submitting and deleting need etsy_drafts:write, which a Drafting Agent key holds. Staging writes nothing to Etsy, but it still needs that scope.

  • Plus plan or higher to create, stage, edit, submit and delete, and to poll one job.

  • Reading after a downgrade. The staged-batch list, one staged batch and the job history stay readable below Plus for a key that holds catalog:read. A key that holds only etsy_drafts:write reads them on Plus or higher.

  • A working preset and ready designs. See API: Presets and API: Uploads.

A Drafting Agent key can never publish. Drafts go live only when someone publishes them. See API: Publish & reprice.

Create Etsy drafts now

POST /etsy-drafts drafts a batch straight to Etsy from one of your presets. Fill each of the preset's upload slots with a ready design's asset id. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-16-ghost-tee" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 38123456,
    "preset_id": 1667,
    "expected_preset_updated_at": "2026-10-14T18:22:05.123Z",
    "items": [{
      "slots": { "A": 90211 },
      "title": "Retro Ghost Tee, Spooky Season Shirt",
      "tags": ["halloween shirt", "ghost tee", "spooky season"],
      "client_item_id": "drop-0412-ghost"
    }]
  }'
{
  "job_id": "edj_48213",
  "status": "pending",
  "replayed": false,
  "shop_id": 38123456,
  "preset_id": 1667,
  "item_count": 1,
  "next_poll_after_ms": 5000,
  "next_step": "Poll get_bulk_job_status with this job_id; drafts appear one by one. Wait next_poll_after_ms between calls."
}
  • The whole job is admitted or refused. Your plan's per-job and per-shop daily draft caps apply. GET /usage shows them.

  • expected_preset_updated_at is the preset's updated_at from your read. If the preset changed since, nothing is drafted (preset_changed).

  • One file, two slots. The same asset may fill two slots, for example a light and a dark slot.

  • The same key returns the same job. Resending the identical request with the same key answers replayed: true and the same job_id, for as long as the job exists. The same key with a different body is refused with idempotency_key_reused.

  • The same item under a new key is refused. A client_item_id you sent to this shop in the last 7 days is refused with 409 DUPLICATE_ITEM while it is still queued or processing, once it became a draft, or when its outcome is unknown. details.duplicates[] names each id and its earlier status. It can go again only if it failed with retry_safe: true, was cancelled before it ran, or was skipped by a job that ended.

  • Test keys answer the plan and write nothing.

Poll the job as in step 7.

1. Write the copy first

Staging refuses inline AI copy with ai_requires_submit. Write the copy first with POST /ai/listing-copy, then stage the batch with the finished copy.

curl -X POST https://api.listingview.io/api/v1/ai/listing-copy \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "design_asset_ids": [88123],
    "current_copy": {
      "title": "Sunset Mountain Sweatshirt",
      "description": "Retro sunset crewneck in soft white ink.",
      "tags": ["sunset sweatshirt", "mountain shirt", "hiking gift"],
      "price": 34.5
    },
    "instructions": "Cozy autumn tone. Mention the night sky.",
    "use_custom_instructions": true
  }'
  • Improve or write fresh. With current_copy, the AI improves that copy. Without it, the AI writes from the artwork alone. current_copy needs a title, description or tags; a price alone is refused.

  • House instructions. use_custom_instructions is on by default. It applies the seller's saved Custom Instructions in ListingView, in addition to your instructions.

  • The answer has title, tags, description, fields, saw_artwork, model, tokens_charged, applied: false, warnings[] and grounding.status (grounded, candidates-only, skipped or unknown). Nothing is saved.

  • The model always sees the design. If it can't, the call is refused with AI_VISION_UNAVAILABLE and nothing is charged.

  • Tags when you improve copy. When you send current_copy with instructions, the AI doesn't top up the tag list on its own, so you may get fewer than 13 tags back. warnings[] doesn't say so; count them. To top up tags, send fields: ["tags"], the tags you already chose in current_copy.tags, and an instruction such as "suggest 10 MORE, none repeating them".

  • Tokens. Every call spends AI tokens, also when the returned title repeats yours. A low balance is refused with AI_TOKENS_INSUFFICIENT before anything is spent. At most 2 AI calls run at once per account; a third answers RATE_LIMITED with the reason ai_concurrency.

  • Scope. AI copy needs catalog:write.

Inline AI copy on a create that drafts now (ai_copy, ai) also needs catalog:write. It writes from the artwork only: send current_copy to POST /ai/listing-copy instead.

2. Stage the batch

Send POST /etsy-drafts with submit: false. An idempotency key is required.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: stage-2026-10-26-batch-01" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 38123456,
    "preset_id": 1667,
    "expected_preset_updated_at": "2026-10-14T18:22:05.123Z",
    "submit": false,
    "items": [
      {
        "slots": { "A": 90501 },
        "title": "Cottagecore Frog Tee, Mushroom Shirt",
        "description": "A soft garment-dyed tee with a hand-drawn frog.",
        "tags": ["frog shirt", "cottagecore tee", "mushroom shirt"],
        "client_item_id": "drop-0526-frog"
      },
      {
        "slots": { "A": 90502 },
        "title": "Moth Club Tee, Dark Academia Shirt",
        "description": "A soft garment-dyed tee with a vintage moth print.",
        "tags": ["moth shirt", "dark academia", "insect tee"],
        "client_item_id": "drop-0526-moth"
      }
    ]
  }'
{
  "replayed": false,
  "shop_id": 38123456,
  "preset_id": 1667,
  "item_count": 2,
  "next_poll_after_ms": null,
  "next_step": "Staged: nothing was sent to Etsy. The batch waits in ListingView → Create listings → Saved listings (review_url) for the person to review and edit. …",
  "draft_id": "stg_48216",
  "revision": 1,
  "review_url": "https://app.listingview.io/dashboard/listing-creation?tab=saved",
  "items": [
    { "index": 0, "client_item_id": "drop-0526-frog" },
    { "index": 1, "client_item_id": "drop-0526-moth" }
  ]
}
  • What staging checks. The shop, the preset, the slots, that every file is ready, the shop settings, the print-on-demand rules and digital deliverables. Nothing reaches Etsy.

  • Checked at submit, not now. Your daily draft caps, a busy shop, and whether every preset photo can still be sent.

  • Batch size. A batch holds up to 100 items, and never more than your plan's per-job cap. A bigger batch is refused with DATA_BUDGET_EXCEEDED and the reason etsy_drafts_job. Stage several batches instead.

  • review_url is a full link to the Saved listings tab in ListingView. Give it to the person who reviews the batch.

  • Test keys answer would_stage and save nothing.

What each item can carry

Items take the usual fields: title, description, tags, price, quantity, sku and client_item_id. They can also carry these. This example shows the first three on one item:

{
  "slots": {
    "A": {
      "asset_id": 90411,
      "placements": [{
        "target": { "product": 1, "print_area": "front" },
        "placement": { "design_width_pct": 62.5, "design_height_pct": 41.2, "x_offset_pct": 50, "y_offset_pct": 12 }
      }]
    }
  },
  "pod_selection": [{ "product": 1, "options": { "color": ["White", "Natural", "Ivory"] } }],
  "photos": { "order": [{ "mockup": 2 }, { "asset_id": 90412 }] },
  "dimensions": { "weight": 6.2, "length": 10, "width": 8, "height": 1, "weight_unit": "oz", "dimensions_unit": "in" },
  "title": "Luna Moth Tee, Cottagecore Shirt",
  "client_item_id": "drop-0526-moth"
}
  • slots fills each design slot: an asset id, as in {"A": 90211}, or an asset id with this listing's own placements. Placements use the same fields and ranges as a preset (see API: Presets).

  • pod_selection sells fewer colors or sizes on this one listing, for example colors a design's ink doesn't suit. It can only remove options, never add them. An item is refused if it leaves an enabled slot with no color (slot_without_color) or a product with no variant (no_variants_left).

  • Removing a color needs photos. Mockups of removed colors are not dropped for you. If pod_selection removes a color on a preset with mockups and the item sends no photos, it is refused with color_prune_photos_required and the mockups as candidates. Drop those mockups with photos.drop, or list the ones to show with photos.order. Removing sizes never needs this.

  • photos sets the photo order on Etsy, lead photo first. order takes {"mockup": <index>} and {"asset_id": <id>} entries; drop takes {"mockup": <index>}. The mockup index is the index the preset's include=mockups read returns. Up to 20 photos.

  • dimensions sets the package weight, length, width, height, weight_unit and dimensions_unit, on print-on-demand and physical presets.

  • digital gives a digital item its own files. See Digital drafts below.

  • Physical photos. images[] stays for physical presets only. Videos always come from the preset.

A refused item names its path, such as items[7].slots.B.placements[0]. Every problem in the batch is listed at once.

Recover a lost stage response

If the answer never arrives, replay the request: send the same body with the same idempotency key (X-Idempotency-Key on REST, idempotency_key on MCP). While the batch is still open, you get the same draft_id back, even days later. The replay answers the batch as it is now, with its current revision. The same key with a different body is refused with CONFLICT and the reason idempotency_key_reused.

Once the batch is submitted or discarded, the key is free again. A replay then stages a new batch, or is refused with DUPLICATE_ITEM under the same 7-day rule as a direct create.

3. A person reviews it in ListingView

The batch appears in ListingView under Saved listings, as a card like any other saved draft: the number of listings and when it was saved. The person opens it, fixes what they want and saves. Their saved edits are what you submit.

  • Wait for the go-ahead. A submit sends the last version the person saved, not edits still open on their screen. Submit only after they say the review is done.

  • They can publish it themselves. Every card has a Publish to Etsy button. If the person uses it, your later submit tells you so (see 6. Submit).

4. Read the batch as it is now

GET /etsy-drafts/staged/{draft_id} returns the batch exactly as it is now, with every item in the same shape you stage with.

{
  "draft_id": "stg_48213",
  "source": "api",
  "status": "staged",
  "submitted_by": null,
  "job_id": null,
  "shop_id": 38123456,
  "preset_id": 1667,
  "preset_missing": false,
  "revision": 3,
  "edited_in_app": true,
  "staging_key_active": true,
  "item_count": 2,
  "items": [
    {
      "index": 0,
      "client_item_id": "drop-0412-ghost",
      "title": "Ghost Frog Tee, Spooky Cottagecore Shirt",
      "tags": ["frog shirt", "cottagecore tee"],
      "slots": { "A": 9001, "B": 9002 },
      "pod_selection": [{ "product": 1, "options": { "color": ["Black", "Navy"] } }]
    },
    {
      "index": 1,
      "client_item_id": null,
      "title": "Moth Club Tee",
      "tags": null,
      "slots": { "A": 9005, "B": 9006 },
      "app_only_fields": ["videos"]
    }
  ],
  "app_only_fields": ["videos"]
}

The answer is shortened here. In this example the person fixed a title, deleted a row and added one.

  • revision goes up with every saved change, by anyone. Send it back as expected_revision when you edit.

  • client_item_id shows only when ListingView can prove which row it belongs to. A row the person added, or one it can't match after their edits, reads null. Address those by index.

  • app_only_fields[] names any edit the API's item fields can't express. You get the names only, never the values. An id that is no longer in your account is listed under unresolvable[], never shown.

  • status, submitted_by and job_id say whether the batch has been sent, and from where. A submitted batch reads items: []: read its items on the job.

  • preset_missing: true means the preset was deleted after you staged.

List your batches

GET /etsy-drafts/staged lists your open batches as {batches[], next_cursor}, filtered by shop_id or preset_id, with limit and cursor paging:

{
  "source": "api",
  "draft_id": "stg_48213",
  "shop_id": 38123456,
  "preset_id": 1667,
  "revision": 3,
  "item_count": 12,
  "updated_at": "2026-10-26T15:02:11.000Z",
  "edited_in_app": true,
  "staging_key_active": true
}
  • edited_in_app: true means a person saved changes in ListingView since your last write. Your own edits never set it.

  • Drafts saved in the app are listed with source: "app" for a key that holds catalog:read. The API can read them but can't edit, submit or discard them: those calls answer 403 with the reason app_draft. A key that holds only etsy_drafts:write never sees them. shop_id filters API-staged batches only.

5. Update the batch: send every edit in one PATCH

Collect every change, then send them together in one PATCH /etsy-drafts/staged/{draft_id}. expected_revision and an idempotency key are required.

curl -X PATCH https://api.listingview.io/api/v1/etsy-drafts/staged/stg_48213 \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-27-batch-edit" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_revision": 3,
    "ops": [
      { "op": "remove_items", "index": 5 },
      { "op": "remove_items", "index": 2 },
      {
        "op": "update_item",
        "client_item_id": "drop-0412-ghost",
        "item": {
          "slots": {
            "B": {
              "asset_id": 9010,
              "placements": [{ "target": { "product": 1, "print_area": "front" }, "placement": { "design_width_pct": 70, "x_offset_pct": 50 } }]
            }
          }
        }
      },
      {
        "op": "add_items",
        "items": [{ "slots": { "A": 9011, "B": 9012 }, "title": "Toad Choir Tee", "tags": ["toad shirt"], "client_item_id": "drop-0412-toad" }]
      }
    ]
  }'
  • Operations. add_items (with items[]), remove_items, replace_item and update_item (with item), up to 100 per call. Address an item by client_item_id where it has one, otherwise by index. One op per item per call (item_addressed_twice otherwise).

  • Indexes don't shift. Every index in one PATCH means the batch as you read it at expected_revision. Removing index 5 and index 2 in one call removes the rows that were 5 and 2.

  • update_item keeps what you don't send, including the person's in-app edits. slots merge letter by letter. An item's digital replaces that item's files.

  • App-only edits are protected. A change that would rebuild something the person set in the app that the API can't express is refused with app_only_fields, naming it.

  • All or nothing. Every operation applies, or none do. The answer is the batch as it is now, with the new revision. A change that would leave no items is refused (batch_empty).

  • One PATCH, not many. Your edits and discards share one rate limit with the app's own saves for this account: 30 writes a minute. Past it you get RATE_LIMITED with the reason draft_writes. Small, frequent PATCHes run into it, and so does the person editing in the app.

  • Someone else saved first. A stale expected_revision is refused with CONFLICT and the reason draft_changed. Re-read, re-apply your edits and retry. After 3 conflicts in a row, stop and tell the person.

  • The preset was deleted. Any op other than remove_items answers CONFLICT with the reason preset_missing.

  • Checked again later. Every check runs again in full at submit.

  • Test keys answer would_edit.

6. Submit

POST /etsy-drafts/staged/{draft_id}/submit sends the batch as it is now, with every edit the person saved. It needs a new idempotency key: the key you staged with is refused with idempotency_key_reused.

curl -X POST https://api.listingview.io/api/v1/etsy-drafts/staged/stg_48213/submit \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: drops-2026-10-27-submit-2" \
  -H "Content-Type: application/json" \
  -d '{ "expected_revision": 4, "drop_ineligible": true }'
{
  "job_id": "edj_48213",
  "status": "pending",
  "replayed": false,
  "item_count": 1,
  "next_poll_after_ms": 2000,
  "draft_id": "stg_48213",
  "edited_in_app": false,
  "dropped": [{
    "index": 1,
    "client_item_id": "drop-0526-moth",
    "reason": "asset_not_ready",
    "message": "It uses assets that aren't ready yet (90812). Wait until list_assets shows them ready."
  }]
}

The answer is shortened here. Poll the job_id like any Etsy-draft job (step 7).

  • Every check runs again on the batch as it is now. An edit made in the app can't slip a file that isn't ready, or another shop's setting, past it.

  • Photos. A preset or item photo must be one of your own ready library files, or a public https:// image. A photo ListingView can't check or send is refused with photo_reselect_required: upload or import it again and pick it again.

  • expected_revision is optional. Leave it out to accept whatever the person saved. Send it to refuse the submit if anything changed since your read (draft_changed).

  • The preset changed. If the preset was edited after you staged, the submit answers preset_changed. Read the batch again, then send accept_preset_changes: true to submit it against the preset as it is now. A deleted preset answers preset_missing.

  • Items that fail. By default a failing item refuses the whole batch with items_ineligible, and details.items names each item with its reasons. With drop_ineligible: true, failing items are left out and listed in dropped[], and the rest go to Etsy. Dropped items also show as failed on the job.

  • A refused submit changes nothing. The batch stays staged exactly as it was.

  • Limits count on the day you submit. A daily-cap refusal leaves the batch staged, with retry_after at the next midnight UTC. A busy shop answers SHOP_BUSY, which is retryable.

  • Sending it twice is safe. The same idempotency key returns the same job. A batch this account's API already submitted answers replayed: true with the same job.

  • Test keys answer would_submit.

Who submitted

If the person published the batch from the app, your submit answers CONFLICT with the reason draft_already_submitted. details.job_id names the job, and details.submitted_by is "app". The batch read shows status: "submitted" and submitted_by. Poll that job, or tell the person it was published from the app.

If staging_key_active is false, the key that staged the batch was revoked or has expired. Don't submit that batch without the person's go-ahead.

7. Poll the job, then find it in the history

GET /etsy-drafts/jobs/{job_id} reports the job and each item. Wait next_poll_after_ms between calls.

{
  "job_id": "edj_48214",
  "status": "running",
  "counts": { "total": 3, "queued": 1, "processing": 0, "drafted": 1, "failed": 1, "skipped": 0, "cancelled": 0, "unknown_outcome": 0 },
  "next_poll_after_ms": 5000,
  "items": [
    {
      "index": 1,
      "client_item_id": "drop-0413-b",
      "status": "failed",
      "retry_safe": true,
      "etsy_listing_id": null,
      "error": {
        "source": "etsy",
        "code": "ITEM_FAILED",
        "message": "price → Price must be greater than or equal to 0.2",
        "field": "price",
        "reason": null,
        "details": null
      }
    }
  ]
}

The answer is shortened here.

  • Per item you get its status, its Etsy listing id and editor link once it is a draft, and pushable once it is ready to publish. A drafted item can read pushable: false with pushable_reason: "catalog_sync_pending" for a short while.

  • Failed items carry Etsy's own wording and retry_safe. Items marked retry_safe: true can go in a new job.

  • unknown_outcome means the item may or may not exist on Etsy, for example when Etsy didn't answer the create in time. ListingView never sends a create twice on its own. Check the shop first, and never resubmit it blindly.

  • What was applied. Each item also echoes what it applied, so you can check it against what you sent: applied_placements, pruned_options and photo_order. For a batch the person edited in the app, only pruned_options is echoed.

  • index is the item's position in the batch as it was submitted.

GET /etsy-drafts/jobs lists your Etsy-draft jobs, newest first, from the last 30 days. Filter by shop_id, preset_id, status or since, and page with cursor.

{
  "job_id": "edj_48213",
  "shop_id": 38123456,
  "preset_id": 1667,
  "status": "completed",
  "created_at": "2026-10-25T14:02:11.000Z",
  "finished_at": "2026-10-25T14:09:47.000Z",
  "items_total": 12,
  "items_drafted": 12,
  "items_failed": 0,
  "source": "api",
  "key_name": "drops-bot"
}
  • Window. A since older than 30 days is refused with too_old, plus details.max_lookback_days and earliest_since. A future since is refused with in_future.

  • Dates. created_at and since are when the job, or its staged batch, was created, not when it was submitted.

  • App jobs. A key that holds catalog:read also sees bulk creates started in the app (source: "app", no key_name), including a staged batch the person published from the app. A key that holds only etsy_drafts:write sees API jobs only.

  • Not listed. Jobs still preparing, or that ended before submit. Read those by job_id.

  • With no jobs, you get {"jobs": [], "next_cursor": null}.

Digital drafts

A digital preset makes digital Etsy drafts, staged or not. Each item can bring its own files, because a digital listing usually delivers its own file:

{
  "title": "2027 Weekly Planner Printable, Minimalist PDF",
  "tags": ["weekly planner", "printable planner", "2027 planner"],
  "digital": {
    "deliverables": [
      { "kind": "file", "asset_id": 88201 },
      { "kind": "collection", "collection_id": 4522 }
    ],
    "design_files": [{ "asset_id": 88202 }]
  },
  "client_item_id": "planner-weekly-2027"
}
  • An item's files replace the preset's. Send every file the listing delivers. Without digital, the item delivers the preset's files.

  • Files and collections only. A link deliverable, the item's own or one the preset carries, is refused with link_deliverables_unsupported.

  • At least one file. Every item needs at least one ready file or collection (digital_files_required).

  • Ready and scanned. Every file, and every file inside a collection, must be ready and scanned clean (collection_file_not_ready). An empty collection is refused (collection_empty).

  • Limits. A listing can deliver up to 20 deliverables, design files included (too_many_deliverables), and up to 200 files through its collections (too_many_collection_files).

  • Old presets. A preset that still carries legacy inline files answers legacy_preset_files. Re-save its deliverables in ListingView.

  • Where a refusal points. A file from the preset is named preset.digital.deliverables[k] or preset.digital.design_files[k]. On a staged submit, an item's own file is named items[i].digitalFiles[k]. A not-ready file at submit is named by its file only.

What the job reports

Each digital item shows deliverables (count and kinds), delivery (pdf, raw_fallback, failed, or null while it isn't settled yet) and delivered_files.

Drafted means delivered. A digital item counts as drafted only when Etsy confirmed every delivery file. Otherwise it is failed, with its etsy_listing_id and a reason:

  • delivery_upload_failed: the Etsy draft exists, but none of its files reached Etsy;

  • delivery_incomplete: the Etsy draft exists, but only some of its files reached Etsy (delivered_files says how many did).

{
  "index": 1,
  "client_item_id": "planner-02",
  "status": "failed",
  "retry_safe": false,
  "etsy_listing_id": 4459812290,
  "error": {
    "source": "listingview",
    "code": "ITEM_FAILED",
    "message": "The Etsy draft was created, but only some of its download files reached Etsy, so a buyer would not receive everything. Delete this draft, then create it again.",
    "reason": "delivery_incomplete"
  },
  "deliverables": { "count": 6, "kinds": ["file"] },
  "delivery": "failed",
  "delivered_files": 5
}

These items are always retry_safe: false. Delete the draft (see Redraft below), then draft it again.

Discard a staged batch

DELETE /etsy-drafts/staged/{draft_id} discards a batch the API staged.

{
  "draft_id": "stg_48213",
  "deleted": true,
  "next_step": "Discarded: the batch is gone from Saved listings and its staged-batch slot is free. Nothing was sent to Etsy; its designs stay in your asset library."
}
  • Nothing on Etsy changes. Your uploaded files stay in your library.

  • Already submitted. A submitted batch answers CONFLICT with the reason draft_already_submitted and its job_id. Poll that job instead.

  • Retrying is safe. A retry with the same idempotency key answers deleted: true again.

  • Test keys answer would_discard.

How long a staged batch stays

  • Until you act on it. A staged batch stays until it is submitted or discarded. It never expires, and there's no timer or reminder.

  • Up to 18 at a time. An account can have up to 18 API-staged batches open. They sit inside the account's 20 open saved drafts, which leaves room for drafts made in the app.

  • When the slots are full, staging answers CONFLICT with the reason draft_slots_full. details.scope says which ran out: api_staged (the 18) or open_drafts (the 20, app and API together). Submit or discard a batch, then stage again.

  • Check the count first. GET /usage shows staged_batches: api_staged and open_drafts, each with used, limit, remaining and exhausted.

  • Paused by ListingView. If ListingView pauses Etsy drafts, staging, editing, submitting and discarding answer SERVICE_UNAVAILABLE with the reason etsy_drafts_disabled until it resumes. Listing and reading still work, a same-key replay still answers, and no staged batch is cancelled. Digital drafts can be paused on their own: a digital preset then answers FEATURE_UNAVAILABLE, while print-on-demand and physical drafts carry on.

  • Plan changes. Below the Plus plan, edit, submit and discard answer PLAN_INSUFFICIENT.

When a key is revoked

  • Revoked by you. The batch stays. Any other key on the account with etsy_drafts:write can edit or submit it, and the list shows staging_key_active: false.

  • Revoked by ListingView support, for example after a leak. That key's staged batches are discarded. If one is still found, an edit or submit discards it then and answers CONFLICT with the reason staging_key_revoked.

Redraft: delete an Etsy draft, then draft again

When drafts went to Etsy wrong, for example with the wrong preset, delete them and draft them again. A Drafting Agent key can do the whole loop.

Look the drafts up by Etsy id

POST /catalog/listings/lookup checks up to 100 of your Etsy listings in one call. It's a read and needs catalog:read.

curl -X POST https://api.listingview.io/api/v1/catalog/listings/lookup \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "shop_id": 38123456, "etsy_listing_ids": [4012345678, 4012345679] }'
{
  "shop_id": 38123456,
  "partial": false,
  "retry_after": null,
  "etsy_calls": 0,
  "counts": { "requested": 2, "found": 1, "not_found": 1 },
  "results": [
    { "etsy_listing_id": 4012345678, "status": "found", "catalog_listing_id": 91234, "shop_id": 38123456, "state": "draft", "partial": false },
    { "etsy_listing_id": 4012345679, "status": "not_found", "catalog_listing_id": null, "shop_id": null, "state": null }
  ]
}
  • Without include you get ListingView's synced state, with no Etsy call. An id that isn't a synced listing of your shop answers not_found and costs nothing.

  • include: ["live_state"] adds the state on Etsy right now, in one Etsy call for the whole list. See API: Publish & reprice for pod_matrix and live_inventory.

  • Live parts count against your Etsy-call budget. A part that can't be read is marked on that listing with live_unavailable_reason, and the answer has partial: true. It never fails the call.

  • The Etsy connection is never renewed inside a lookup. While it is being renewed, live parts read token_refresh_pending; retry after retry_after.

  • With one connected shop you can leave shop_id out.

Delete each draft

DELETE /catalog/listings/{listing_id}?shop_id= deletes one Etsy draft. listing_id is the ListingView catalog id, or etsy: plus the Etsy listing id. An idempotency key is required.

curl -X DELETE "https://api.listingview.io/api/v1/catalog/listings/etsy:4012345679?shop_id=38123456&confirm_shared_delete=true" \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY"
{
  "deleted": true,
  "already_deleted": true,
  "listing_id": 91235,
  "etsy_listing_id": 4012345679,
  "title": "Witchy Vibes Crewneck",
  "deleted_at": "2026-10-26T15:05:40.019Z",
  "shared_confirmed": true,
  "deletes_remaining_today": 37,
  "audit_id": 88124
}
  • What goes. The Etsy draft, its listing in ListingView and its own Printify product.

  • Already gone. A draft that's already deleted answers 200 with already_deleted: true.

  • The seller is told. Each delete puts a notice in ListingView, titled "Etsy draft deleted by your API key". It names the key, the draft and the shop, and ends: "If you didn't expect this, revoke the key in Settings → API Keys."

  • A daily limit. Deletes have their own daily limit per account, shown as etsy_draft_deletes in GET /usage and as deletes_remaining_today in each answer. Past it you get DATA_BUDGET_EXCEEDED with the reason etsy_draft_deletes_daily. Before a redraft, check this and your Etsy-draft caps in /usage, so a redraft never stops halfway.

  • Test keys answer would_delete.

What the delete checks

The delete runs these checks in order and stops at the first that fails:

Check

When it fails

The listing is in your shop

404

This account's Bulk Create made the draft, through the app or the API

CONFLICT draft_not_created_by_listingview. It never says who made it.

No other ListingView account shares the listing

CONFLICT listing_shared_with_other_account, unless you send confirm_shared_delete=true

ListingView isn't pushing or publishing that listing right now

CONFLICT publish_pending, retryable

The shop's Etsy connection is current

SERVICE_UNAVAILABLE token_refresh_pending with retry_after while it renews; SHOP_NOT_CONNECTED when it is gone

Your shop connection allows deletes

SHOP_NOT_CONNECTED with the reason reconnect_required, and details.missing_etsy_scopes when the delete permission is missing. Reconnect the shop.

You have deletes left today

DATA_BUDGET_EXCEEDED etsy_draft_deletes_daily

Etsy still shows it as a draft, read live right before the delete

CONFLICT listing_not_draft with details.state. Nothing is deleted. If Etsy can't be read, nothing is deleted either; retry with the same idempotency key.

  • Shared shops. Up to 8 ListingView accounts can connect the same Etsy shop. A listing is shared when another of them also manages it. Deleting the draft removes it for all of them. Their own Printify products are left alone. The API never sends confirm_shared_delete for you.

  • After the checks. If Etsy itself refuses the delete, you get CONFLICT with the reason etsy_not_deletable. If Etsy deleted the draft but ListingView's cleanup failed, you get SERVICE_UNAVAILABLE with the reason cleanup_pending. Retry it: the retry finishes the cleanup.

The race the API can't close

Etsy has no "delete only if still a draft" call. ListingView reads the live state at most 2 seconds before it deletes, and holds the same lock its API publishing uses, so it never races itself.

One gap remains. If a person activates that same draft on Etsy.com, or publishes it from the ListingView app, in that split second, the now-live listing is deleted. This risk is accepted. Run redrafts only on drafts nobody is publishing by hand at that moment.

Draft again

Stage or create the drafts again as above, with a new idempotency key.

Conflicts and retries

  • A conflict (draft_changed, preset_changed): read again, re-apply, retry. After 3 conflicts in a row, stop and tell the person.

  • A retryable error (isRetryable: true): wait retry_after, with some jitter. Stop after 3 tries and tell the person.

  • Not retryable: never retry. Fix the input, or ask the person.

  • No clear outcome (unknown_outcome, or retry_safe: false): the draft may or may not exist on Etsy. Check it on Etsy, or read the job or the listing, before you do anything else. Never resubmit it blindly.

The full rules are in API: Getting started.

From the CLI or an AI app

  • CLI: listingview etsy-drafts create --shop-id 38123456 --preset-id 1667 --items items.json --stage stages a batch. Running the same command again returns the same batch. The staged batch has its own commands: etsy-drafts staged list, get, update, submit and delete. listingview etsy-drafts jobs status edj_48213 --watch follows a job. An item with an unknown outcome makes a job command exit with code 9: check the shop, and never resubmit it. See API: Command-line tool.

  • MCP: create_etsy_drafts takes operation: create (the default; drafts now), stage, update or submit. update and submit need draft_id; ops and expected_revision go with update, and expected_revision, drop_ineligible and accept_preset_changes with submit. An idempotency key is required on every call.

  • MCP: get_bulk_job_status reads jobs and batches. list: "staged" lists staged batches and draft_id reads one. list: "jobs" lists the job history and job_id reads one job.

  • MCP: delete_preset_or_draft deletes one thing, by kind. kind: "staged_batch" with draft_id discards a batch. kind: "etsy_draft" with listing_id or etsy_listing_id (never both), plus shop_id and confirm_shared_delete, deletes an Etsy draft.

  • MCP: get_listing with etsy_listing_ids and include runs the lookup. enhance_listing_with_ai takes current_copy and use_custom_instructions. See API: Connect AI apps.

Every endpoint above, with its full request and response schema, is in the API reference. Every error code is 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