API, MCP & CLI

API: Publish & reprice

Use a ListingView Publisher key to push an edited listing live, publish an Etsy draft, and reprice up to 20 live print-on-demand listings under one confirmation, with a below-cost floor.

Updated

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

Publishing changes what shoppers see on Etsy. Through the API that takes a Publisher key, which is always separate from the key that makes drafts. Every live change also goes through a confirmation step: the API shows exactly what will change, then waits for you to confirm.

This guide covers the Publisher key, pushing one listing's edits live, publishing a draft, and repricing live print-on-demand listings in bulk.

New to the API? Start with API: Getting started. To make the drafts you'll publish, see API: Etsy drafts.

What you need

  • A Publisher key. It holds catalog:read and catalog:live_write.

  • Plus plan or higher.

  • For edits before a push, a custom key or a second key. See Edit one listing, then push it live below.

Drafting and publishing never share a key

A key that can create Etsy drafts (etsy_drafts:write) can never publish. A key that can publish (catalog:live_write) can never create Etsy drafts. Settings won't put both on one key.

The split protects you from text hidden in listings. An agent that drafts reads titles, tags and AI copy, and any of it could try to steer the agent. Because a drafting key can't publish, that text can never make anything live.

Create a Publisher key

  1. In ListingView, open Settings → API Keys and click Create key.

  2. Name the key and pick the Publisher access preset.

  3. Copy the key and keep it somewhere safe. It is shown only once.

One ListingView server per app or chat

Give each AI app or chat exactly one ListingView server. A Publisher key gets its own entry, named listingview-publish:

{
  "mcpServers": {
    "listingview-publish": {
      "type": "http",
      "url": "https://api.listingview.io/mcp",
      "headers": {
        "Authorization": "Bearer lv_live_…"
      }
    }
  }
}

If one chat held both a drafting key and a publishing key, text hidden in a listing could steer the agent from drafting into publishing. Keep each key in its own app or chat. See API: Connect AI apps.

Edit one listing, then push it live

A stock Publisher key can push a listing's saved ListingView edits to Etsy. It can't make those edits, because it doesn't hold catalog:write. Pick one of two setups:

  • One custom key with catalog:read, catalog:write and catalog:live_write. In the Create key dialog, open Customize scopes and pick those three. catalog:write may sit beside catalog:live_write; only etsy_drafts:write may not.

  • Two keys. A Catalog Editor key makes the edits, and a Publisher key pushes them.

One push the custom key can't make: creating a listing on Etsy from a listing that exists only in ListingView. A key that holds catalog:write is refused with SCOPE_CONFLICT and the reason create_needs_publisher_key. Use a stock Publisher key for those.

Step 1: Edit the listing in ListingView

PUT /catalog/listings/{listing_id} takes the ListingView listing id:

curl -X PUT https://api.listingview.io/api/v1/catalog/listings/91234 \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "taxonomy_id": 1082, "tags": ["linen apron", "kitchen apron", "gift for cook"] }'

The answer lists updated_fields, here ["tags", "taxonomy_id"]. The edit stays in ListingView. Nothing reaches Etsy yet.

Step 2: Ask to push it

curl -X POST https://api.listingview.io/api/v1/catalog/listings/91234/push \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY"

Nothing changes yet. The answer is 409 CONFIRMATION_REQUIRED, showing what will go live:

{
  "error": {
    "code": "CONFIRMATION_REQUIRED",
    "message": "This action changes your live Etsy shop and needs confirmation. Review the summary, then re-send with X-Confirm: <confirmationId> or POST /api/v1/confirm/<confirmationId>.",
    "details": {
      "confirmation_id": "cnf_…",
      "summary": "…",
      "diff": ["…"],
      "text": "…",
      "expires_at": "…",
      "expires_in_seconds": 300,
      "handler": "…",
      "confirm": { "header": "X-Confirm", "value": "cnf_…", "method": "POST", "path": "/api/v1/confirm/cnf_…" }
    },
    "isRetryable": false,
    "docs_url": "…",
    "request_id": "…"
  }
}

The answer is shortened here.

Step 3: Confirm

curl -X POST https://api.listingview.io/api/v1/confirm/cnf_… \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY"

The change goes live, once. A push that sent new tags answers:

{
  "status": "pushed",
  "action": "push",
  "from_state": "active",
  "catalog_listing_id": 91234,
  "etsy_listing_id": 4012345678,
  "shop_id": 38123456,
  "sync_job_id": "2c6f0e1a-5b7d-4c1e-9a43-0d7e1f2b3c4d",
  "next_poll_after_ms": null,
  "pushed_fields": ["tags"],
  "warnings": [],
  "error": null,
  "next_step": null
}
  • Still syncing. If Etsy takes longer than about 15 seconds, the status is syncing. Read the listing with get_listing after next_poll_after_ms.

  • A draft stays a draft. Pushing edits to an Etsy draft doesn't publish it.

  • What a push sends. Title, description, tags, price, materials, category, who made it and when, renewal, section, shipping profile and return policy. Variants, quantities, SKUs, personalization, attributes and processing times are never sent (fields_not_pushable, naming each field).

  • Print-on-demand prices. On a print-on-demand listing a push never sends the listing price, because Etsy prices each variant. The answer warns you. Reprice those listings with the bulk reprice below.

  • Nothing to push. A listing with no unpushed edits answers no_local_edits.

Publish an Etsy draft

POST /catalog/listings/{listing_id}/publish publishes one Etsy draft, through the same confirmation step. A published draft answers status: "published", from_state: "draft".

  • Push the edits first. A draft with unpushed edits answers local_edits_pending.

  • Already live. A live listing answers already_live; a listing that isn't a draft, not_draft.

Listings that exist only in ListingView

A stock Publisher key can also create the Etsy listing for a listing that exists only in ListingView. push creates it as an Etsy draft (status: "drafted", from_state: "local"), and publish creates it and publishes it (status: "created_and_live"). With more than one connected shop, send shop_id (shop_id_required otherwise). It is never created twice: run publish on the same listing later, under a new confirmation.

The confirmation step

Every live change runs in two steps:

  1. Ask. The first call changes nothing. It answers 409 CONFIRMATION_REQUIRED, and details carries confirmation_id, a summary, the diff and a readable text.

  2. Confirm. Within 5 minutes, send POST /confirm/{confirmation_id} with the same key. Or send the same request again with the header X-Confirm: <confirmation_id>.

  • Single-use, and bound. A confirmation works once, with the same key and for exactly the request it previews. The same request with different content burns it (binding_mismatch).

  • Expired. After 5 minutes you get CONFIRMATION_INVALID with the reason expired. Ask again for a fresh preview. An id from another key or another account also answers expired.

  • Changed underneath. If the listing changed after the preview, the confirm answers CONFIRMATION_INVALID with the reason content_changed and writes nothing. details carries a fresh confirmation: review what changed, then confirm that one.

  • Used. Presenting a confirmation that already ran replays its answer, for 24 hours. One that is running right now answers already_used.

  • Not a human check by itself. The confirmation asks the caller, and the caller can be a script. Show the summary to the person before your agent confirms.

  • Skipping the preview. X-Confirm: auto works only for a key that holds the confirm:auto scope and has auto-confirm switched on. No preset includes confirm:auto. Otherwise it is refused with SCOPE_INSUFFICIENT and the reason auto_confirm_not_allowed, and nothing happens.

Push, publish and bulk reprice are high-cost calls in credits. describe shows each call's cost.

Check live listings before you reprice

A reprice takes each listing's live inventory from Etsy and changes only the prices you confirm. Compare ListingView's stored prices with Etsy's first, with POST /catalog/listings/lookup:

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], "include": ["pod_matrix", "live_state", "live_inventory"] }'
{
  "shop_id": 38123456,
  "partial": false,
  "retry_after": null,
  "etsy_calls": 2,
  "counts": { "requested": 1, "found": 1, "not_found": 0 },
  "results": [{
    "etsy_listing_id": 4012345678,
    "status": "found",
    "catalog_listing_id": 91234,
    "state": "active",
    "live_state": "active",
    "live_state_status": "ok",
    "pod_matrix": { "…": "…" },
    "live_inventory_status": "ok",
    "matrix_vs_live": {
      "differs": true,
      "compared_cells": 4,
      "differing_cell_count": 1,
      "truncated": false,
      "cells": [{
        "position": 1,
        "options": { "color": "Ivory", "size": "S" },
        "stored": { "price": 29, "sku": "CC-IVO-S", "quantity": 999, "enabled": true },
        "live": { "price": 28, "sku": "CC-IVO-S", "quantity": 999, "enabled": true },
        "differences": ["price"]
      }]
    },
    "live_unavailable_reason": null,
    "partial": false
  }]
}

The answer is shortened here. For each listing you get:

  • pod_matrix: each print-on-demand product, by position, with product, blueprint_id, print_provider_id, colors and sizes. Its variants[] carry options, price, sku, quantity and enabled. Prices are decimals in the shop's currency, in the shape a reprice takes.

  • live_inventory: what Etsy has right now, product by product.

  • matrix_vs_live: differs says whether the two disagree. cells[] lists each cell that does, with stored, live and differences[]. It compares SKU, quantity and on/off too, but a reprice only ever changes prices.

The lookup takes up to 100 Etsy ids per call. Up to 25 of them can ask for live_inventory (live_inventory_limit otherwise), and the live parts count against your Etsy-call budget. A part that can't be read is marked on that listing with live_unavailable_reason, the answer has partial: true, and stored data still comes back. The lookup never renews the shop's Etsy connection: while it is renewing, the live parts say token_refresh_pending.

A Publisher key holds catalog:read, so it can run this lookup.

Reprice live print-on-demand listings in bulk

POST /catalog/bulk/push reprices up to 20 live print-on-demand listings in one shop under one confirmation. It changes prices only.

curl -X POST https://api.listingview.io/api/v1/catalog/bulk/push \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 71000001,
    "listing_ids": [4100000001, 4100000002],
    "pod_prices": [
      {
        "listing_id": 4100000001,
        "pod": {
          "schema_version": "pod.v1",
          "products": [{
            "position": 1,
            "prices": [
              { "options": { "size": "S" }, "price": 26 },
              { "options": { "size": "M" }, "price": 26 },
              { "options": { "size": "2XL" }, "price": 29 }
            ]
          }]
        }
      },
      {
        "listing_id": 4100000002,
        "pod": {
          "schema_version": "pod.v1",
          "products": [{ "position": 1, "prices": [{ "options": { "size": "2XL" }, "price": 29 }] }]
        }
      }
    ]
  }'
  • listing_ids: up to 20 Etsy listing ids (the number in the listing's URL), all in one shop. shop_id may be left out when the account has one connected shop. The single push and publish above take the ListingView id instead.

  • pod_prices: for each listing, the same pod body a single listing update takes, with listing_id naming the Etsy listing. Products are addressed by position. product is an optional guard: if the name at that position differs, the push is refused (pod_product_unknown).

  • Prices are pod.v1 entries, decimals in the shop's currency. A color and a size together price one variant; one of them alone prices a whole row or column. See API: Presets for the full price rules.

  • Without pod_prices, the call pushes the prices ListingView already stores for those listings, prices only, as in {"listing_ids": [4100000001, 4100000002, 4100000003]}.

Prices only

The push reads each listing's live inventory from Etsy and replaces only the prices you confirmed. SKUs, quantities, which variants are on and processing profiles always stay as Etsy has them. Hidden variants keep their live price.

Some listings can't be repriced this way. If changing a price would change how the listing varies on Etsy, the push answers CONFLICT with the reason structure_changed and writes nothing. Each affected listing is named in details.cells[] with issue: "not_writable_as_price_only", and details.reread points at the lookup. This happens when:

  • the price would vary by an option Etsy doesn't price by on that listing;

  • a variant on Etsy carries more than one offering;

  • the listing prices every variant the same, and a hidden variant keeps a different price.

Edit those listings' variations in the ListingView app, then push again. accept_inventory_overwrite is no longer accepted: sending true answers VALIDATION_FAILED with the reason inventory_overwrite_unsupported.

What happens next

  1. The preview. The first call reads each listing live on Etsy and answers CONFIRMATION_REQUIRED. The summary lists every listing and every price that changes, old to new. Listings Etsy already matches are left out. If Etsy can't be read, no confirmation is made (see Refusals).

  2. The confirm. Within 5 minutes, send POST /confirm/{confirmation_id}. The answer has job_id, status, shop_id, listings, changed_cells, skipped_listing_ids and confirmation_id.

  3. The job. ListingView runs one bulk-update job for the whole set. Its id starts lbj_. Poll it with GET /catalog/bulk/jobs/{job_id} until it finishes. Polling needs catalog:read, which a Publisher key holds.

{
  "job_id": "lbj_01JAB4P2D7QX5V8M3N6R9T0WKC",
  "operation": "push",
  "source": "api",
  "key_name": "repricer",
  "status": "completed_with_errors",
  "cancelled_reason": null,
  "counts": { "total": 2, "queued": 0, "processing": 0, "failed": 1, "skipped": 0, "cancelled": 0, "unknown_outcome": 0, "pushed": 1 },
  "items": [
    { "status": "pushed", "listing_id": 4100000001, "updated_fields": ["price"] },
    {
      "status": "failed",
      "retry_safe": true,
      "listing_id": 4100000002,
      "error": {
        "code": "PUSH_FAILED",
        "reason": "api_key_revoked",
        "message": "The API key that started this push was revoked or expired before this listing was written, so it was left unchanged.",
        "retryable": true,
        "field": null
      }
    }
  ]
}

The answer is shortened here.

The below-cost floor: accept_below_cost

The floor is on by default. A price below that variant's production cost is refused with VALIDATION_FAILED and the reason price_below_cost. The refusal names the cells, up to 100, with details.cell_count. The floor is checked on the prices you send and again on every price that actually changes after the live read, including a push without pod_prices. A listing whose Etsy currency differs from its cost currency is refused the same way.

To sell below cost on purpose, send accept_below_cost: true. The floor exists because a confirmation can come from a script. It stops a wrong or injected price from selling at a loss.

Limits

  • 20 listings per confirmation. This cap follows the hourly limit on live writes, and describe shows the current number (bounds.bulkPush.maxListings).

  • One live-write unit per listing. Your account has an hourly and a daily limit on live writes. Only listings that will change use a unit; listings Etsy already matches don't. If fewer units are left than listings, the push answers RATE_LIMITED with the reason live_writes_hourly or live_writes_daily, details.requested and details.remaining, and writes nothing. Units are returned for listings that are refused or never sent.

  • Etsy reads. The preview's live reads are covered by the live-write units, not your Etsy-call budget.

  • Size. A push whose stored request is too big answers 413 VALIDATION_FAILED with the reason too_large and details.limit_kind: "confirmation_payload". Split it into smaller pushes. When the diff is very long, the summary shows each listing's count of changed cells and the first of them.

Refusals

Nothing is written when a push is refused.

Answer

What it means

What to do

price_below_cost

A price is below that variant's production cost.

Fix the price, or send accept_below_cost: true.

structure_changed

A listing can't be repriced without changing how it varies on Etsy.

Edit its variations in the app, then push again.

price_out_of_range

A changed price is outside what Etsy accepts. ListingView never rounds it into range.

Fix the price.

no_local_edits

Etsy already has every price.

Nothing to do.

pod_option_unknown

A color or size the listing's product doesn't sell. The refusal lists the names it offers.

Use one of the offered names.

pod_product_unknown

The product guard doesn't match the product at that position.

Check the position, or drop the guard.

POD_VARIANT_STALE

The print provider no longer offers that variant.

Leave that variant out.

pod_price_ambiguous

A color price and a size price disagree for one variant.

Add a color-and-size entry for it.

duplicate_listing, listing_not_requested

A listing appears twice, or a price entry names a listing that isn't in listing_ids.

Fix the request.

CATALOG_NOT_SYNCED

The listing isn't live, isn't print-on-demand, or ListingView hasn't synced it.

Leave it out, or sync the shop first.

NOT_FOUND

A listing isn't in that shop. details.field names it, such as listing_ids[1].

Remove it.

too_large

The stored confirmation would be too big (limit_kind: "confirmation_payload").

Split the push.

More than 20 listings

Over the per-confirmation cap (VALIDATION_FAILED).

Split the push.

live_writes_hourly, live_writes_daily

Fewer live-write units are left than listings.

Wait for the reset, or push fewer listings.

EXTERNAL_API_ERROR

Etsy couldn't be read for the preview. etsy_unavailable, deadline_exceeded and token_refresh_pending are retryable; etsy_rejected is not.

Retry after retry_after.

SHOP_NOT_CONNECTED reconnect_required

The shop's Etsy connection needs reconnecting.

Reconnect the shop in ListingView.

CONFIRMATION_INVALID expired

The confirmation is more than 5 minutes old.

Ask again for a fresh preview.

content_changed

A listing changed in ListingView or on Etsy after the preview.

Review the fresh confirmation in details.

SHOP_BUSY

A bulk update from the app is running on this shop.

Retry after retry_after. The confirmation stays valid for its 5 minutes.

CHANNEL_CIRCUIT_OPEN

After 3 failed writes in a row, ListingView pauses live writes for this account for 15 minutes.

Retry after retry_after.

PLAN_INSUFFICIENT

The account is below the Plus plan.

Upgrade.

While the job runs

  • Per listing. Each listing ends pushed, or failed with a reason and retry_safe. updated_fields is ["price"], or [] when Etsy already had that price. There's no rollback, because Etsy can't undo several listings at once.

  • Retry only the failures. Once the job is submitted, the confirmation is final. Push the failed listings again under a new confirmation. It re-reads Etsy, so nothing is applied twice.

  • Checked again before every write. Right before each listing's write, ListingView checks the push once more. If a check fails, that listing fails and nothing is sent to Etsy for it:

    • api_bulk_push_disabled: ListingView has paused bulk pushes.

    • api_key_revoked: the key was revoked or expired. Revoking a key also stops its running pushes.

    • content_changed: the listing, or its live price, changed after you confirmed.

    • structure_changed: the listing's variations changed on Etsy after you confirmed.

    • channel_circuit_open: live writes are paused for a few minutes after repeated failures.

  • Unknown outcome. If an earlier attempt at a listing may have reached Etsy, it ends unknown_outcome. Check that listing on Etsy before you confirm a new push.

  • Etsy connection lost. The job pauses until the seller reconnects the shop: status stays running, and next_step says so. After 24 hours it is cancelled.

  • Stopped. A cancelled job names why in cancelled_reason: seller, key_revoked or admin.

  • Plan changes. A downgrade during the job doesn't stop it. The next push answers PLAN_INSUFFICIENT.

What the seller sees

  • When the job finishes, a notice in ListingView: "Your API key '{key}' updated {N} live listings in {shop}." N leaves out listings Etsy already matched. If some failed, it adds: "{F} couldn't be updated. Job Activity shows why." If Etsy never confirmed a write, it adds that those listings may have been changed, and to check them on Etsy.

  • When the job pauses, a notice: "Reconnect {shop} to Etsy so the update from your API key '{key}' can finish."

  • When the job is stopped, a notice: "Your API key '{key}' updated {N} live listings in {shop} before the update was stopped."

  • Job Activity shows the job, labeled "via API" with the key's name.

Rehearse with a test key

With an lv_test_ key, the confirmation round trip runs as usual and nothing is written.

  • Bulk reprice. Etsy isn't read: the preview shows old prices as ?, and its summary says so. After you confirm, the answer is a dry run, {"dry_run": true, "would": {"would_push": {…, "live": "unverified"}}}.

  • Push and publish. After you confirm, would.live_write describes the write it would make, with etsy_writes: 0.

From the CLI or an AI app

  • CLI: the command-line tool doesn't make live changes yet. Use REST or MCP for the routes on this page. See API: Command-line tool.

  • MCP: push_listing_live takes action, which is required. action: "push" with listing_id pushes one listing; with listing_ids (Etsy ids) and pod_prices it reprices up to 20. action: "publish" publishes one listing. A combination that matches neither answers VALIDATION_FAILED with the reason no_route, naming what to send. Every call shows what will change and waits for a confirmation, which your app shows you.

  • MCP: update_listing makes the edit before a push. get_listing with etsy_listing_ids and include runs the lookup. get_bulk_job_status with the lbj_ job id follows the reprice job.

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

Publishing changes what shoppers see on Etsy. Through the API that takes a Publisher key, which is always separate from the key that makes drafts. Every live change also goes through a confirmation step: the API shows exactly what will change, then waits for you to confirm.

This guide covers the Publisher key, pushing one listing's edits live, publishing a draft, and repricing live print-on-demand listings in bulk.

New to the API? Start with API: Getting started. To make the drafts you'll publish, see API: Etsy drafts.

What you need

  • A Publisher key. It holds catalog:read and catalog:live_write.

  • Plus plan or higher.

  • For edits before a push, a custom key or a second key. See Edit one listing, then push it live below.

Drafting and publishing never share a key

A key that can create Etsy drafts (etsy_drafts:write) can never publish. A key that can publish (catalog:live_write) can never create Etsy drafts. Settings won't put both on one key.

The split protects you from text hidden in listings. An agent that drafts reads titles, tags and AI copy, and any of it could try to steer the agent. Because a drafting key can't publish, that text can never make anything live.

Create a Publisher key

  1. In ListingView, open Settings → API Keys and click Create key.

  2. Name the key and pick the Publisher access preset.

  3. Copy the key and keep it somewhere safe. It is shown only once.

One ListingView server per app or chat

Give each AI app or chat exactly one ListingView server. A Publisher key gets its own entry, named listingview-publish:

{
  "mcpServers": {
    "listingview-publish": {
      "type": "http",
      "url": "https://api.listingview.io/mcp",
      "headers": {
        "Authorization": "Bearer lv_live_…"
      }
    }
  }
}

If one chat held both a drafting key and a publishing key, text hidden in a listing could steer the agent from drafting into publishing. Keep each key in its own app or chat. See API: Connect AI apps.

Edit one listing, then push it live

A stock Publisher key can push a listing's saved ListingView edits to Etsy. It can't make those edits, because it doesn't hold catalog:write. Pick one of two setups:

  • One custom key with catalog:read, catalog:write and catalog:live_write. In the Create key dialog, open Customize scopes and pick those three. catalog:write may sit beside catalog:live_write; only etsy_drafts:write may not.

  • Two keys. A Catalog Editor key makes the edits, and a Publisher key pushes them.

One push the custom key can't make: creating a listing on Etsy from a listing that exists only in ListingView. A key that holds catalog:write is refused with SCOPE_CONFLICT and the reason create_needs_publisher_key. Use a stock Publisher key for those.

Step 1: Edit the listing in ListingView

PUT /catalog/listings/{listing_id} takes the ListingView listing id:

curl -X PUT https://api.listingview.io/api/v1/catalog/listings/91234 \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "taxonomy_id": 1082, "tags": ["linen apron", "kitchen apron", "gift for cook"] }'

The answer lists updated_fields, here ["tags", "taxonomy_id"]. The edit stays in ListingView. Nothing reaches Etsy yet.

Step 2: Ask to push it

curl -X POST https://api.listingview.io/api/v1/catalog/listings/91234/push \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY"

Nothing changes yet. The answer is 409 CONFIRMATION_REQUIRED, showing what will go live:

{
  "error": {
    "code": "CONFIRMATION_REQUIRED",
    "message": "This action changes your live Etsy shop and needs confirmation. Review the summary, then re-send with X-Confirm: <confirmationId> or POST /api/v1/confirm/<confirmationId>.",
    "details": {
      "confirmation_id": "cnf_…",
      "summary": "…",
      "diff": ["…"],
      "text": "…",
      "expires_at": "…",
      "expires_in_seconds": 300,
      "handler": "…",
      "confirm": { "header": "X-Confirm", "value": "cnf_…", "method": "POST", "path": "/api/v1/confirm/cnf_…" }
    },
    "isRetryable": false,
    "docs_url": "…",
    "request_id": "…"
  }
}

The answer is shortened here.

Step 3: Confirm

curl -X POST https://api.listingview.io/api/v1/confirm/cnf_… \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY"

The change goes live, once. A push that sent new tags answers:

{
  "status": "pushed",
  "action": "push",
  "from_state": "active",
  "catalog_listing_id": 91234,
  "etsy_listing_id": 4012345678,
  "shop_id": 38123456,
  "sync_job_id": "2c6f0e1a-5b7d-4c1e-9a43-0d7e1f2b3c4d",
  "next_poll_after_ms": null,
  "pushed_fields": ["tags"],
  "warnings": [],
  "error": null,
  "next_step": null
}
  • Still syncing. If Etsy takes longer than about 15 seconds, the status is syncing. Read the listing with get_listing after next_poll_after_ms.

  • A draft stays a draft. Pushing edits to an Etsy draft doesn't publish it.

  • What a push sends. Title, description, tags, price, materials, category, who made it and when, renewal, section, shipping profile and return policy. Variants, quantities, SKUs, personalization, attributes and processing times are never sent (fields_not_pushable, naming each field).

  • Print-on-demand prices. On a print-on-demand listing a push never sends the listing price, because Etsy prices each variant. The answer warns you. Reprice those listings with the bulk reprice below.

  • Nothing to push. A listing with no unpushed edits answers no_local_edits.

Publish an Etsy draft

POST /catalog/listings/{listing_id}/publish publishes one Etsy draft, through the same confirmation step. A published draft answers status: "published", from_state: "draft".

  • Push the edits first. A draft with unpushed edits answers local_edits_pending.

  • Already live. A live listing answers already_live; a listing that isn't a draft, not_draft.

Listings that exist only in ListingView

A stock Publisher key can also create the Etsy listing for a listing that exists only in ListingView. push creates it as an Etsy draft (status: "drafted", from_state: "local"), and publish creates it and publishes it (status: "created_and_live"). With more than one connected shop, send shop_id (shop_id_required otherwise). It is never created twice: run publish on the same listing later, under a new confirmation.

The confirmation step

Every live change runs in two steps:

  1. Ask. The first call changes nothing. It answers 409 CONFIRMATION_REQUIRED, and details carries confirmation_id, a summary, the diff and a readable text.

  2. Confirm. Within 5 minutes, send POST /confirm/{confirmation_id} with the same key. Or send the same request again with the header X-Confirm: <confirmation_id>.

  • Single-use, and bound. A confirmation works once, with the same key and for exactly the request it previews. The same request with different content burns it (binding_mismatch).

  • Expired. After 5 minutes you get CONFIRMATION_INVALID with the reason expired. Ask again for a fresh preview. An id from another key or another account also answers expired.

  • Changed underneath. If the listing changed after the preview, the confirm answers CONFIRMATION_INVALID with the reason content_changed and writes nothing. details carries a fresh confirmation: review what changed, then confirm that one.

  • Used. Presenting a confirmation that already ran replays its answer, for 24 hours. One that is running right now answers already_used.

  • Not a human check by itself. The confirmation asks the caller, and the caller can be a script. Show the summary to the person before your agent confirms.

  • Skipping the preview. X-Confirm: auto works only for a key that holds the confirm:auto scope and has auto-confirm switched on. No preset includes confirm:auto. Otherwise it is refused with SCOPE_INSUFFICIENT and the reason auto_confirm_not_allowed, and nothing happens.

Push, publish and bulk reprice are high-cost calls in credits. describe shows each call's cost.

Check live listings before you reprice

A reprice takes each listing's live inventory from Etsy and changes only the prices you confirm. Compare ListingView's stored prices with Etsy's first, with POST /catalog/listings/lookup:

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], "include": ["pod_matrix", "live_state", "live_inventory"] }'
{
  "shop_id": 38123456,
  "partial": false,
  "retry_after": null,
  "etsy_calls": 2,
  "counts": { "requested": 1, "found": 1, "not_found": 0 },
  "results": [{
    "etsy_listing_id": 4012345678,
    "status": "found",
    "catalog_listing_id": 91234,
    "state": "active",
    "live_state": "active",
    "live_state_status": "ok",
    "pod_matrix": { "…": "…" },
    "live_inventory_status": "ok",
    "matrix_vs_live": {
      "differs": true,
      "compared_cells": 4,
      "differing_cell_count": 1,
      "truncated": false,
      "cells": [{
        "position": 1,
        "options": { "color": "Ivory", "size": "S" },
        "stored": { "price": 29, "sku": "CC-IVO-S", "quantity": 999, "enabled": true },
        "live": { "price": 28, "sku": "CC-IVO-S", "quantity": 999, "enabled": true },
        "differences": ["price"]
      }]
    },
    "live_unavailable_reason": null,
    "partial": false
  }]
}

The answer is shortened here. For each listing you get:

  • pod_matrix: each print-on-demand product, by position, with product, blueprint_id, print_provider_id, colors and sizes. Its variants[] carry options, price, sku, quantity and enabled. Prices are decimals in the shop's currency, in the shape a reprice takes.

  • live_inventory: what Etsy has right now, product by product.

  • matrix_vs_live: differs says whether the two disagree. cells[] lists each cell that does, with stored, live and differences[]. It compares SKU, quantity and on/off too, but a reprice only ever changes prices.

The lookup takes up to 100 Etsy ids per call. Up to 25 of them can ask for live_inventory (live_inventory_limit otherwise), and the live parts count against your Etsy-call budget. A part that can't be read is marked on that listing with live_unavailable_reason, the answer has partial: true, and stored data still comes back. The lookup never renews the shop's Etsy connection: while it is renewing, the live parts say token_refresh_pending.

A Publisher key holds catalog:read, so it can run this lookup.

Reprice live print-on-demand listings in bulk

POST /catalog/bulk/push reprices up to 20 live print-on-demand listings in one shop under one confirmation. It changes prices only.

curl -X POST https://api.listingview.io/api/v1/catalog/bulk/push \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 71000001,
    "listing_ids": [4100000001, 4100000002],
    "pod_prices": [
      {
        "listing_id": 4100000001,
        "pod": {
          "schema_version": "pod.v1",
          "products": [{
            "position": 1,
            "prices": [
              { "options": { "size": "S" }, "price": 26 },
              { "options": { "size": "M" }, "price": 26 },
              { "options": { "size": "2XL" }, "price": 29 }
            ]
          }]
        }
      },
      {
        "listing_id": 4100000002,
        "pod": {
          "schema_version": "pod.v1",
          "products": [{ "position": 1, "prices": [{ "options": { "size": "2XL" }, "price": 29 }] }]
        }
      }
    ]
  }'
  • listing_ids: up to 20 Etsy listing ids (the number in the listing's URL), all in one shop. shop_id may be left out when the account has one connected shop. The single push and publish above take the ListingView id instead.

  • pod_prices: for each listing, the same pod body a single listing update takes, with listing_id naming the Etsy listing. Products are addressed by position. product is an optional guard: if the name at that position differs, the push is refused (pod_product_unknown).

  • Prices are pod.v1 entries, decimals in the shop's currency. A color and a size together price one variant; one of them alone prices a whole row or column. See API: Presets for the full price rules.

  • Without pod_prices, the call pushes the prices ListingView already stores for those listings, prices only, as in {"listing_ids": [4100000001, 4100000002, 4100000003]}.

Prices only

The push reads each listing's live inventory from Etsy and replaces only the prices you confirmed. SKUs, quantities, which variants are on and processing profiles always stay as Etsy has them. Hidden variants keep their live price.

Some listings can't be repriced this way. If changing a price would change how the listing varies on Etsy, the push answers CONFLICT with the reason structure_changed and writes nothing. Each affected listing is named in details.cells[] with issue: "not_writable_as_price_only", and details.reread points at the lookup. This happens when:

  • the price would vary by an option Etsy doesn't price by on that listing;

  • a variant on Etsy carries more than one offering;

  • the listing prices every variant the same, and a hidden variant keeps a different price.

Edit those listings' variations in the ListingView app, then push again. accept_inventory_overwrite is no longer accepted: sending true answers VALIDATION_FAILED with the reason inventory_overwrite_unsupported.

What happens next

  1. The preview. The first call reads each listing live on Etsy and answers CONFIRMATION_REQUIRED. The summary lists every listing and every price that changes, old to new. Listings Etsy already matches are left out. If Etsy can't be read, no confirmation is made (see Refusals).

  2. The confirm. Within 5 minutes, send POST /confirm/{confirmation_id}. The answer has job_id, status, shop_id, listings, changed_cells, skipped_listing_ids and confirmation_id.

  3. The job. ListingView runs one bulk-update job for the whole set. Its id starts lbj_. Poll it with GET /catalog/bulk/jobs/{job_id} until it finishes. Polling needs catalog:read, which a Publisher key holds.

{
  "job_id": "lbj_01JAB4P2D7QX5V8M3N6R9T0WKC",
  "operation": "push",
  "source": "api",
  "key_name": "repricer",
  "status": "completed_with_errors",
  "cancelled_reason": null,
  "counts": { "total": 2, "queued": 0, "processing": 0, "failed": 1, "skipped": 0, "cancelled": 0, "unknown_outcome": 0, "pushed": 1 },
  "items": [
    { "status": "pushed", "listing_id": 4100000001, "updated_fields": ["price"] },
    {
      "status": "failed",
      "retry_safe": true,
      "listing_id": 4100000002,
      "error": {
        "code": "PUSH_FAILED",
        "reason": "api_key_revoked",
        "message": "The API key that started this push was revoked or expired before this listing was written, so it was left unchanged.",
        "retryable": true,
        "field": null
      }
    }
  ]
}

The answer is shortened here.

The below-cost floor: accept_below_cost

The floor is on by default. A price below that variant's production cost is refused with VALIDATION_FAILED and the reason price_below_cost. The refusal names the cells, up to 100, with details.cell_count. The floor is checked on the prices you send and again on every price that actually changes after the live read, including a push without pod_prices. A listing whose Etsy currency differs from its cost currency is refused the same way.

To sell below cost on purpose, send accept_below_cost: true. The floor exists because a confirmation can come from a script. It stops a wrong or injected price from selling at a loss.

Limits

  • 20 listings per confirmation. This cap follows the hourly limit on live writes, and describe shows the current number (bounds.bulkPush.maxListings).

  • One live-write unit per listing. Your account has an hourly and a daily limit on live writes. Only listings that will change use a unit; listings Etsy already matches don't. If fewer units are left than listings, the push answers RATE_LIMITED with the reason live_writes_hourly or live_writes_daily, details.requested and details.remaining, and writes nothing. Units are returned for listings that are refused or never sent.

  • Etsy reads. The preview's live reads are covered by the live-write units, not your Etsy-call budget.

  • Size. A push whose stored request is too big answers 413 VALIDATION_FAILED with the reason too_large and details.limit_kind: "confirmation_payload". Split it into smaller pushes. When the diff is very long, the summary shows each listing's count of changed cells and the first of them.

Refusals

Nothing is written when a push is refused.

Answer

What it means

What to do

price_below_cost

A price is below that variant's production cost.

Fix the price, or send accept_below_cost: true.

structure_changed

A listing can't be repriced without changing how it varies on Etsy.

Edit its variations in the app, then push again.

price_out_of_range

A changed price is outside what Etsy accepts. ListingView never rounds it into range.

Fix the price.

no_local_edits

Etsy already has every price.

Nothing to do.

pod_option_unknown

A color or size the listing's product doesn't sell. The refusal lists the names it offers.

Use one of the offered names.

pod_product_unknown

The product guard doesn't match the product at that position.

Check the position, or drop the guard.

POD_VARIANT_STALE

The print provider no longer offers that variant.

Leave that variant out.

pod_price_ambiguous

A color price and a size price disagree for one variant.

Add a color-and-size entry for it.

duplicate_listing, listing_not_requested

A listing appears twice, or a price entry names a listing that isn't in listing_ids.

Fix the request.

CATALOG_NOT_SYNCED

The listing isn't live, isn't print-on-demand, or ListingView hasn't synced it.

Leave it out, or sync the shop first.

NOT_FOUND

A listing isn't in that shop. details.field names it, such as listing_ids[1].

Remove it.

too_large

The stored confirmation would be too big (limit_kind: "confirmation_payload").

Split the push.

More than 20 listings

Over the per-confirmation cap (VALIDATION_FAILED).

Split the push.

live_writes_hourly, live_writes_daily

Fewer live-write units are left than listings.

Wait for the reset, or push fewer listings.

EXTERNAL_API_ERROR

Etsy couldn't be read for the preview. etsy_unavailable, deadline_exceeded and token_refresh_pending are retryable; etsy_rejected is not.

Retry after retry_after.

SHOP_NOT_CONNECTED reconnect_required

The shop's Etsy connection needs reconnecting.

Reconnect the shop in ListingView.

CONFIRMATION_INVALID expired

The confirmation is more than 5 minutes old.

Ask again for a fresh preview.

content_changed

A listing changed in ListingView or on Etsy after the preview.

Review the fresh confirmation in details.

SHOP_BUSY

A bulk update from the app is running on this shop.

Retry after retry_after. The confirmation stays valid for its 5 minutes.

CHANNEL_CIRCUIT_OPEN

After 3 failed writes in a row, ListingView pauses live writes for this account for 15 minutes.

Retry after retry_after.

PLAN_INSUFFICIENT

The account is below the Plus plan.

Upgrade.

While the job runs

  • Per listing. Each listing ends pushed, or failed with a reason and retry_safe. updated_fields is ["price"], or [] when Etsy already had that price. There's no rollback, because Etsy can't undo several listings at once.

  • Retry only the failures. Once the job is submitted, the confirmation is final. Push the failed listings again under a new confirmation. It re-reads Etsy, so nothing is applied twice.

  • Checked again before every write. Right before each listing's write, ListingView checks the push once more. If a check fails, that listing fails and nothing is sent to Etsy for it:

    • api_bulk_push_disabled: ListingView has paused bulk pushes.

    • api_key_revoked: the key was revoked or expired. Revoking a key also stops its running pushes.

    • content_changed: the listing, or its live price, changed after you confirmed.

    • structure_changed: the listing's variations changed on Etsy after you confirmed.

    • channel_circuit_open: live writes are paused for a few minutes after repeated failures.

  • Unknown outcome. If an earlier attempt at a listing may have reached Etsy, it ends unknown_outcome. Check that listing on Etsy before you confirm a new push.

  • Etsy connection lost. The job pauses until the seller reconnects the shop: status stays running, and next_step says so. After 24 hours it is cancelled.

  • Stopped. A cancelled job names why in cancelled_reason: seller, key_revoked or admin.

  • Plan changes. A downgrade during the job doesn't stop it. The next push answers PLAN_INSUFFICIENT.

What the seller sees

  • When the job finishes, a notice in ListingView: "Your API key '{key}' updated {N} live listings in {shop}." N leaves out listings Etsy already matched. If some failed, it adds: "{F} couldn't be updated. Job Activity shows why." If Etsy never confirmed a write, it adds that those listings may have been changed, and to check them on Etsy.

  • When the job pauses, a notice: "Reconnect {shop} to Etsy so the update from your API key '{key}' can finish."

  • When the job is stopped, a notice: "Your API key '{key}' updated {N} live listings in {shop} before the update was stopped."

  • Job Activity shows the job, labeled "via API" with the key's name.

Rehearse with a test key

With an lv_test_ key, the confirmation round trip runs as usual and nothing is written.

  • Bulk reprice. Etsy isn't read: the preview shows old prices as ?, and its summary says so. After you confirm, the answer is a dry run, {"dry_run": true, "would": {"would_push": {…, "live": "unverified"}}}.

  • Push and publish. After you confirm, would.live_write describes the write it would make, with etsy_writes: 0.

From the CLI or an AI app

  • CLI: the command-line tool doesn't make live changes yet. Use REST or MCP for the routes on this page. See API: Command-line tool.

  • MCP: push_listing_live takes action, which is required. action: "push" with listing_id pushes one listing; with listing_ids (Etsy ids) and pod_prices it reprices up to 20. action: "publish" publishes one listing. A combination that matches neither answers VALIDATION_FAILED with the reason no_route, naming what to send. Every call shows what will change and waits for a confirmation, which your app shows you.

  • MCP: update_listing makes the edit before a push. get_listing with etsy_listing_ids and include runs the lookup. get_bulk_job_status with the lbj_ job id follows the reprice job.

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

Publishing changes what shoppers see on Etsy. Through the API that takes a Publisher key, which is always separate from the key that makes drafts. Every live change also goes through a confirmation step: the API shows exactly what will change, then waits for you to confirm.

This guide covers the Publisher key, pushing one listing's edits live, publishing a draft, and repricing live print-on-demand listings in bulk.

New to the API? Start with API: Getting started. To make the drafts you'll publish, see API: Etsy drafts.

What you need

  • A Publisher key. It holds catalog:read and catalog:live_write.

  • Plus plan or higher.

  • For edits before a push, a custom key or a second key. See Edit one listing, then push it live below.

Drafting and publishing never share a key

A key that can create Etsy drafts (etsy_drafts:write) can never publish. A key that can publish (catalog:live_write) can never create Etsy drafts. Settings won't put both on one key.

The split protects you from text hidden in listings. An agent that drafts reads titles, tags and AI copy, and any of it could try to steer the agent. Because a drafting key can't publish, that text can never make anything live.

Create a Publisher key

  1. In ListingView, open Settings → API Keys and click Create key.

  2. Name the key and pick the Publisher access preset.

  3. Copy the key and keep it somewhere safe. It is shown only once.

One ListingView server per app or chat

Give each AI app or chat exactly one ListingView server. A Publisher key gets its own entry, named listingview-publish:

{
  "mcpServers": {
    "listingview-publish": {
      "type": "http",
      "url": "https://api.listingview.io/mcp",
      "headers": {
        "Authorization": "Bearer lv_live_…"
      }
    }
  }
}

If one chat held both a drafting key and a publishing key, text hidden in a listing could steer the agent from drafting into publishing. Keep each key in its own app or chat. See API: Connect AI apps.

Edit one listing, then push it live

A stock Publisher key can push a listing's saved ListingView edits to Etsy. It can't make those edits, because it doesn't hold catalog:write. Pick one of two setups:

  • One custom key with catalog:read, catalog:write and catalog:live_write. In the Create key dialog, open Customize scopes and pick those three. catalog:write may sit beside catalog:live_write; only etsy_drafts:write may not.

  • Two keys. A Catalog Editor key makes the edits, and a Publisher key pushes them.

One push the custom key can't make: creating a listing on Etsy from a listing that exists only in ListingView. A key that holds catalog:write is refused with SCOPE_CONFLICT and the reason create_needs_publisher_key. Use a stock Publisher key for those.

Step 1: Edit the listing in ListingView

PUT /catalog/listings/{listing_id} takes the ListingView listing id:

curl -X PUT https://api.listingview.io/api/v1/catalog/listings/91234 \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "taxonomy_id": 1082, "tags": ["linen apron", "kitchen apron", "gift for cook"] }'

The answer lists updated_fields, here ["tags", "taxonomy_id"]. The edit stays in ListingView. Nothing reaches Etsy yet.

Step 2: Ask to push it

curl -X POST https://api.listingview.io/api/v1/catalog/listings/91234/push \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY"

Nothing changes yet. The answer is 409 CONFIRMATION_REQUIRED, showing what will go live:

{
  "error": {
    "code": "CONFIRMATION_REQUIRED",
    "message": "This action changes your live Etsy shop and needs confirmation. Review the summary, then re-send with X-Confirm: <confirmationId> or POST /api/v1/confirm/<confirmationId>.",
    "details": {
      "confirmation_id": "cnf_…",
      "summary": "…",
      "diff": ["…"],
      "text": "…",
      "expires_at": "…",
      "expires_in_seconds": 300,
      "handler": "…",
      "confirm": { "header": "X-Confirm", "value": "cnf_…", "method": "POST", "path": "/api/v1/confirm/cnf_…" }
    },
    "isRetryable": false,
    "docs_url": "…",
    "request_id": "…"
  }
}

The answer is shortened here.

Step 3: Confirm

curl -X POST https://api.listingview.io/api/v1/confirm/cnf_… \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY"

The change goes live, once. A push that sent new tags answers:

{
  "status": "pushed",
  "action": "push",
  "from_state": "active",
  "catalog_listing_id": 91234,
  "etsy_listing_id": 4012345678,
  "shop_id": 38123456,
  "sync_job_id": "2c6f0e1a-5b7d-4c1e-9a43-0d7e1f2b3c4d",
  "next_poll_after_ms": null,
  "pushed_fields": ["tags"],
  "warnings": [],
  "error": null,
  "next_step": null
}
  • Still syncing. If Etsy takes longer than about 15 seconds, the status is syncing. Read the listing with get_listing after next_poll_after_ms.

  • A draft stays a draft. Pushing edits to an Etsy draft doesn't publish it.

  • What a push sends. Title, description, tags, price, materials, category, who made it and when, renewal, section, shipping profile and return policy. Variants, quantities, SKUs, personalization, attributes and processing times are never sent (fields_not_pushable, naming each field).

  • Print-on-demand prices. On a print-on-demand listing a push never sends the listing price, because Etsy prices each variant. The answer warns you. Reprice those listings with the bulk reprice below.

  • Nothing to push. A listing with no unpushed edits answers no_local_edits.

Publish an Etsy draft

POST /catalog/listings/{listing_id}/publish publishes one Etsy draft, through the same confirmation step. A published draft answers status: "published", from_state: "draft".

  • Push the edits first. A draft with unpushed edits answers local_edits_pending.

  • Already live. A live listing answers already_live; a listing that isn't a draft, not_draft.

Listings that exist only in ListingView

A stock Publisher key can also create the Etsy listing for a listing that exists only in ListingView. push creates it as an Etsy draft (status: "drafted", from_state: "local"), and publish creates it and publishes it (status: "created_and_live"). With more than one connected shop, send shop_id (shop_id_required otherwise). It is never created twice: run publish on the same listing later, under a new confirmation.

The confirmation step

Every live change runs in two steps:

  1. Ask. The first call changes nothing. It answers 409 CONFIRMATION_REQUIRED, and details carries confirmation_id, a summary, the diff and a readable text.

  2. Confirm. Within 5 minutes, send POST /confirm/{confirmation_id} with the same key. Or send the same request again with the header X-Confirm: <confirmation_id>.

  • Single-use, and bound. A confirmation works once, with the same key and for exactly the request it previews. The same request with different content burns it (binding_mismatch).

  • Expired. After 5 minutes you get CONFIRMATION_INVALID with the reason expired. Ask again for a fresh preview. An id from another key or another account also answers expired.

  • Changed underneath. If the listing changed after the preview, the confirm answers CONFIRMATION_INVALID with the reason content_changed and writes nothing. details carries a fresh confirmation: review what changed, then confirm that one.

  • Used. Presenting a confirmation that already ran replays its answer, for 24 hours. One that is running right now answers already_used.

  • Not a human check by itself. The confirmation asks the caller, and the caller can be a script. Show the summary to the person before your agent confirms.

  • Skipping the preview. X-Confirm: auto works only for a key that holds the confirm:auto scope and has auto-confirm switched on. No preset includes confirm:auto. Otherwise it is refused with SCOPE_INSUFFICIENT and the reason auto_confirm_not_allowed, and nothing happens.

Push, publish and bulk reprice are high-cost calls in credits. describe shows each call's cost.

Check live listings before you reprice

A reprice takes each listing's live inventory from Etsy and changes only the prices you confirm. Compare ListingView's stored prices with Etsy's first, with POST /catalog/listings/lookup:

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], "include": ["pod_matrix", "live_state", "live_inventory"] }'
{
  "shop_id": 38123456,
  "partial": false,
  "retry_after": null,
  "etsy_calls": 2,
  "counts": { "requested": 1, "found": 1, "not_found": 0 },
  "results": [{
    "etsy_listing_id": 4012345678,
    "status": "found",
    "catalog_listing_id": 91234,
    "state": "active",
    "live_state": "active",
    "live_state_status": "ok",
    "pod_matrix": { "…": "…" },
    "live_inventory_status": "ok",
    "matrix_vs_live": {
      "differs": true,
      "compared_cells": 4,
      "differing_cell_count": 1,
      "truncated": false,
      "cells": [{
        "position": 1,
        "options": { "color": "Ivory", "size": "S" },
        "stored": { "price": 29, "sku": "CC-IVO-S", "quantity": 999, "enabled": true },
        "live": { "price": 28, "sku": "CC-IVO-S", "quantity": 999, "enabled": true },
        "differences": ["price"]
      }]
    },
    "live_unavailable_reason": null,
    "partial": false
  }]
}

The answer is shortened here. For each listing you get:

  • pod_matrix: each print-on-demand product, by position, with product, blueprint_id, print_provider_id, colors and sizes. Its variants[] carry options, price, sku, quantity and enabled. Prices are decimals in the shop's currency, in the shape a reprice takes.

  • live_inventory: what Etsy has right now, product by product.

  • matrix_vs_live: differs says whether the two disagree. cells[] lists each cell that does, with stored, live and differences[]. It compares SKU, quantity and on/off too, but a reprice only ever changes prices.

The lookup takes up to 100 Etsy ids per call. Up to 25 of them can ask for live_inventory (live_inventory_limit otherwise), and the live parts count against your Etsy-call budget. A part that can't be read is marked on that listing with live_unavailable_reason, the answer has partial: true, and stored data still comes back. The lookup never renews the shop's Etsy connection: while it is renewing, the live parts say token_refresh_pending.

A Publisher key holds catalog:read, so it can run this lookup.

Reprice live print-on-demand listings in bulk

POST /catalog/bulk/push reprices up to 20 live print-on-demand listings in one shop under one confirmation. It changes prices only.

curl -X POST https://api.listingview.io/api/v1/catalog/bulk/push \
  -H "Authorization: Bearer $LISTINGVIEW_API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_id": 71000001,
    "listing_ids": [4100000001, 4100000002],
    "pod_prices": [
      {
        "listing_id": 4100000001,
        "pod": {
          "schema_version": "pod.v1",
          "products": [{
            "position": 1,
            "prices": [
              { "options": { "size": "S" }, "price": 26 },
              { "options": { "size": "M" }, "price": 26 },
              { "options": { "size": "2XL" }, "price": 29 }
            ]
          }]
        }
      },
      {
        "listing_id": 4100000002,
        "pod": {
          "schema_version": "pod.v1",
          "products": [{ "position": 1, "prices": [{ "options": { "size": "2XL" }, "price": 29 }] }]
        }
      }
    ]
  }'
  • listing_ids: up to 20 Etsy listing ids (the number in the listing's URL), all in one shop. shop_id may be left out when the account has one connected shop. The single push and publish above take the ListingView id instead.

  • pod_prices: for each listing, the same pod body a single listing update takes, with listing_id naming the Etsy listing. Products are addressed by position. product is an optional guard: if the name at that position differs, the push is refused (pod_product_unknown).

  • Prices are pod.v1 entries, decimals in the shop's currency. A color and a size together price one variant; one of them alone prices a whole row or column. See API: Presets for the full price rules.

  • Without pod_prices, the call pushes the prices ListingView already stores for those listings, prices only, as in {"listing_ids": [4100000001, 4100000002, 4100000003]}.

Prices only

The push reads each listing's live inventory from Etsy and replaces only the prices you confirmed. SKUs, quantities, which variants are on and processing profiles always stay as Etsy has them. Hidden variants keep their live price.

Some listings can't be repriced this way. If changing a price would change how the listing varies on Etsy, the push answers CONFLICT with the reason structure_changed and writes nothing. Each affected listing is named in details.cells[] with issue: "not_writable_as_price_only", and details.reread points at the lookup. This happens when:

  • the price would vary by an option Etsy doesn't price by on that listing;

  • a variant on Etsy carries more than one offering;

  • the listing prices every variant the same, and a hidden variant keeps a different price.

Edit those listings' variations in the ListingView app, then push again. accept_inventory_overwrite is no longer accepted: sending true answers VALIDATION_FAILED with the reason inventory_overwrite_unsupported.

What happens next

  1. The preview. The first call reads each listing live on Etsy and answers CONFIRMATION_REQUIRED. The summary lists every listing and every price that changes, old to new. Listings Etsy already matches are left out. If Etsy can't be read, no confirmation is made (see Refusals).

  2. The confirm. Within 5 minutes, send POST /confirm/{confirmation_id}. The answer has job_id, status, shop_id, listings, changed_cells, skipped_listing_ids and confirmation_id.

  3. The job. ListingView runs one bulk-update job for the whole set. Its id starts lbj_. Poll it with GET /catalog/bulk/jobs/{job_id} until it finishes. Polling needs catalog:read, which a Publisher key holds.

{
  "job_id": "lbj_01JAB4P2D7QX5V8M3N6R9T0WKC",
  "operation": "push",
  "source": "api",
  "key_name": "repricer",
  "status": "completed_with_errors",
  "cancelled_reason": null,
  "counts": { "total": 2, "queued": 0, "processing": 0, "failed": 1, "skipped": 0, "cancelled": 0, "unknown_outcome": 0, "pushed": 1 },
  "items": [
    { "status": "pushed", "listing_id": 4100000001, "updated_fields": ["price"] },
    {
      "status": "failed",
      "retry_safe": true,
      "listing_id": 4100000002,
      "error": {
        "code": "PUSH_FAILED",
        "reason": "api_key_revoked",
        "message": "The API key that started this push was revoked or expired before this listing was written, so it was left unchanged.",
        "retryable": true,
        "field": null
      }
    }
  ]
}

The answer is shortened here.

The below-cost floor: accept_below_cost

The floor is on by default. A price below that variant's production cost is refused with VALIDATION_FAILED and the reason price_below_cost. The refusal names the cells, up to 100, with details.cell_count. The floor is checked on the prices you send and again on every price that actually changes after the live read, including a push without pod_prices. A listing whose Etsy currency differs from its cost currency is refused the same way.

To sell below cost on purpose, send accept_below_cost: true. The floor exists because a confirmation can come from a script. It stops a wrong or injected price from selling at a loss.

Limits

  • 20 listings per confirmation. This cap follows the hourly limit on live writes, and describe shows the current number (bounds.bulkPush.maxListings).

  • One live-write unit per listing. Your account has an hourly and a daily limit on live writes. Only listings that will change use a unit; listings Etsy already matches don't. If fewer units are left than listings, the push answers RATE_LIMITED with the reason live_writes_hourly or live_writes_daily, details.requested and details.remaining, and writes nothing. Units are returned for listings that are refused or never sent.

  • Etsy reads. The preview's live reads are covered by the live-write units, not your Etsy-call budget.

  • Size. A push whose stored request is too big answers 413 VALIDATION_FAILED with the reason too_large and details.limit_kind: "confirmation_payload". Split it into smaller pushes. When the diff is very long, the summary shows each listing's count of changed cells and the first of them.

Refusals

Nothing is written when a push is refused.

Answer

What it means

What to do

price_below_cost

A price is below that variant's production cost.

Fix the price, or send accept_below_cost: true.

structure_changed

A listing can't be repriced without changing how it varies on Etsy.

Edit its variations in the app, then push again.

price_out_of_range

A changed price is outside what Etsy accepts. ListingView never rounds it into range.

Fix the price.

no_local_edits

Etsy already has every price.

Nothing to do.

pod_option_unknown

A color or size the listing's product doesn't sell. The refusal lists the names it offers.

Use one of the offered names.

pod_product_unknown

The product guard doesn't match the product at that position.

Check the position, or drop the guard.

POD_VARIANT_STALE

The print provider no longer offers that variant.

Leave that variant out.

pod_price_ambiguous

A color price and a size price disagree for one variant.

Add a color-and-size entry for it.

duplicate_listing, listing_not_requested

A listing appears twice, or a price entry names a listing that isn't in listing_ids.

Fix the request.

CATALOG_NOT_SYNCED

The listing isn't live, isn't print-on-demand, or ListingView hasn't synced it.

Leave it out, or sync the shop first.

NOT_FOUND

A listing isn't in that shop. details.field names it, such as listing_ids[1].

Remove it.

too_large

The stored confirmation would be too big (limit_kind: "confirmation_payload").

Split the push.

More than 20 listings

Over the per-confirmation cap (VALIDATION_FAILED).

Split the push.

live_writes_hourly, live_writes_daily

Fewer live-write units are left than listings.

Wait for the reset, or push fewer listings.

EXTERNAL_API_ERROR

Etsy couldn't be read for the preview. etsy_unavailable, deadline_exceeded and token_refresh_pending are retryable; etsy_rejected is not.

Retry after retry_after.

SHOP_NOT_CONNECTED reconnect_required

The shop's Etsy connection needs reconnecting.

Reconnect the shop in ListingView.

CONFIRMATION_INVALID expired

The confirmation is more than 5 minutes old.

Ask again for a fresh preview.

content_changed

A listing changed in ListingView or on Etsy after the preview.

Review the fresh confirmation in details.

SHOP_BUSY

A bulk update from the app is running on this shop.

Retry after retry_after. The confirmation stays valid for its 5 minutes.

CHANNEL_CIRCUIT_OPEN

After 3 failed writes in a row, ListingView pauses live writes for this account for 15 minutes.

Retry after retry_after.

PLAN_INSUFFICIENT

The account is below the Plus plan.

Upgrade.

While the job runs

  • Per listing. Each listing ends pushed, or failed with a reason and retry_safe. updated_fields is ["price"], or [] when Etsy already had that price. There's no rollback, because Etsy can't undo several listings at once.

  • Retry only the failures. Once the job is submitted, the confirmation is final. Push the failed listings again under a new confirmation. It re-reads Etsy, so nothing is applied twice.

  • Checked again before every write. Right before each listing's write, ListingView checks the push once more. If a check fails, that listing fails and nothing is sent to Etsy for it:

    • api_bulk_push_disabled: ListingView has paused bulk pushes.

    • api_key_revoked: the key was revoked or expired. Revoking a key also stops its running pushes.

    • content_changed: the listing, or its live price, changed after you confirmed.

    • structure_changed: the listing's variations changed on Etsy after you confirmed.

    • channel_circuit_open: live writes are paused for a few minutes after repeated failures.

  • Unknown outcome. If an earlier attempt at a listing may have reached Etsy, it ends unknown_outcome. Check that listing on Etsy before you confirm a new push.

  • Etsy connection lost. The job pauses until the seller reconnects the shop: status stays running, and next_step says so. After 24 hours it is cancelled.

  • Stopped. A cancelled job names why in cancelled_reason: seller, key_revoked or admin.

  • Plan changes. A downgrade during the job doesn't stop it. The next push answers PLAN_INSUFFICIENT.

What the seller sees

  • When the job finishes, a notice in ListingView: "Your API key '{key}' updated {N} live listings in {shop}." N leaves out listings Etsy already matched. If some failed, it adds: "{F} couldn't be updated. Job Activity shows why." If Etsy never confirmed a write, it adds that those listings may have been changed, and to check them on Etsy.

  • When the job pauses, a notice: "Reconnect {shop} to Etsy so the update from your API key '{key}' can finish."

  • When the job is stopped, a notice: "Your API key '{key}' updated {N} live listings in {shop} before the update was stopped."

  • Job Activity shows the job, labeled "via API" with the key's name.

Rehearse with a test key

With an lv_test_ key, the confirmation round trip runs as usual and nothing is written.

  • Bulk reprice. Etsy isn't read: the preview shows old prices as ?, and its summary says so. After you confirm, the answer is a dry run, {"dry_run": true, "would": {"would_push": {…, "live": "unverified"}}}.

  • Push and publish. After you confirm, would.live_write describes the write it would make, with etsy_writes: 0.

From the CLI or an AI app

  • CLI: the command-line tool doesn't make live changes yet. Use REST or MCP for the routes on this page. See API: Command-line tool.

  • MCP: push_listing_live takes action, which is required. action: "push" with listing_id pushes one listing; with listing_ids (Etsy ids) and pod_prices it reprices up to 20. action: "publish" publishes one listing. A combination that matches neither answers VALIDATION_FAILED with the reason no_route, naming what to send. Every call shows what will change and waits for a confirmation, which your app shows you.

  • MCP: update_listing makes the edit before a push. get_listing with etsy_listing_ids and include runs the lookup. get_bulk_job_status with the lbj_ job id follows the reprice job.

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