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
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:readandcatalog: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
In ListingView, open Settings → API Keys and click Create key.
Name the key and pick the Publisher access preset.
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:writeandcatalog:live_write. In the Create key dialog, open Customize scopes and pick those three.catalog:writemay sit besidecatalog:live_write; onlyetsy_drafts:writemay 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 withget_listingafternext_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:
Ask. The first call changes nothing. It answers
409 CONFIRMATION_REQUIRED, anddetailscarriesconfirmation_id, asummary, thediffand a readabletext.Confirm. Within 5 minutes, send
POST /confirm/{confirmation_id}with the same key. Or send the same request again with the headerX-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_INVALIDwith the reasonexpired. Ask again for a fresh preview. An id from another key or another account also answersexpired.Changed underneath. If the listing changed after the preview, the confirm answers
CONFIRMATION_INVALIDwith the reasoncontent_changedand writes nothing.detailscarries 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: autoworks only for a key that holds theconfirm:autoscope and has auto-confirm switched on. No preset includesconfirm:auto. Otherwise it is refused withSCOPE_INSUFFICIENTand the reasonauto_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, byposition, withproduct,blueprint_id,print_provider_id,colorsandsizes. Itsvariants[]carryoptions,price,sku,quantityandenabled. 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:differssays whether the two disagree.cells[]lists each cell that does, withstored,liveanddifferences[]. 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_idmay 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 samepodbody a single listing update takes, withlisting_idnaming the Etsy listing. Products are addressed byposition.productis 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
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).The confirm. Within 5 minutes, send
POST /confirm/{confirmation_id}. The answer hasjob_id,status,shop_id,listings,changed_cells,skipped_listing_idsandconfirmation_id.The job. ListingView runs one bulk-update job for the whole set. Its id starts
lbj_. Poll it withGET /catalog/bulk/jobs/{job_id}until it finishes. Polling needscatalog: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
describeshows 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_LIMITEDwith the reasonlive_writes_hourlyorlive_writes_daily,details.requestedanddetails.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
413VALIDATION_FAILEDwith the reasontoo_largeanddetails.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 |
|---|---|---|
| A price is below that variant's production cost. | Fix the price, or send |
| A listing can't be repriced without changing how it varies on Etsy. | Edit its variations in the app, then push again. |
| A changed price is outside what Etsy accepts. ListingView never rounds it into range. | Fix the price. |
| Etsy already has every price. | Nothing to do. |
| A color or size the listing's product doesn't sell. The refusal lists the names it offers. | Use one of the offered names. |
| The | Check the position, or drop the guard. |
| The print provider no longer offers that variant. | Leave that variant out. |
| A color price and a size price disagree for one variant. | Add a color-and-size entry for it. |
| A listing appears twice, or a price entry names a listing that isn't in | Fix the request. |
| The listing isn't live, isn't print-on-demand, or ListingView hasn't synced it. | Leave it out, or sync the shop first. |
| A listing isn't in that shop. | Remove it. |
| The stored confirmation would be too big ( | Split the push. |
More than 20 listings | Over the per-confirmation cap ( | Split the push. |
| Fewer live-write units are left than listings. | Wait for the reset, or push fewer listings. |
| Etsy couldn't be read for the preview. | Retry after |
| The shop's Etsy connection needs reconnecting. | Reconnect the shop in ListingView. |
| The confirmation is more than 5 minutes old. | Ask again for a fresh preview. |
| A listing changed in ListingView or on Etsy after the preview. | Review the fresh confirmation in |
| A bulk update from the app is running on this shop. | Retry after |
| After 3 failed writes in a row, ListingView pauses live writes for this account for 15 minutes. | Retry after |
| The account is below the Plus plan. | Upgrade. |
While the job runs
Per listing. Each listing ends
pushed, orfailedwith areasonandretry_safe.updated_fieldsis["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:
statusstaysrunning, andnext_stepsays so. After 24 hours it is cancelled.Stopped. A cancelled job names why in
cancelled_reason:seller,key_revokedoradmin.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_writedescribes the write it would make, withetsy_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_livetakesaction, which is required.action: "push"withlisting_idpushes one listing; withlisting_ids(Etsy ids) andpod_pricesit reprices up to 20.action: "publish"publishes one listing. A combination that matches neither answersVALIDATION_FAILEDwith the reasonno_route, naming what to send. Every call shows what will change and waits for a confirmation, which your app shows you.MCP:
update_listingmakes the edit before a push.get_listingwithetsy_listing_idsandincluderuns the lookup.get_bulk_job_statuswith thelbj_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
API: Getting started: keys, idempotency keys, errors and the retry rules
API: Etsy drafts: stage and submit the drafts you'll publish
API: Presets: the price rules in full
API: Connect AI apps: one ListingView server per app or chat
API: Limits & budgets: live-write limits and the Etsy-call budget
API: Command-line tool: commands and exit codes
Bulk Editor: bulk updates in the app
Using the ListingView API, MCP server & CLI: the seller guide to keys and the two-key setup
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:readandcatalog: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
In ListingView, open Settings → API Keys and click Create key.
Name the key and pick the Publisher access preset.
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:writeandcatalog:live_write. In the Create key dialog, open Customize scopes and pick those three.catalog:writemay sit besidecatalog:live_write; onlyetsy_drafts:writemay 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 withget_listingafternext_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:
Ask. The first call changes nothing. It answers
409 CONFIRMATION_REQUIRED, anddetailscarriesconfirmation_id, asummary, thediffand a readabletext.Confirm. Within 5 minutes, send
POST /confirm/{confirmation_id}with the same key. Or send the same request again with the headerX-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_INVALIDwith the reasonexpired. Ask again for a fresh preview. An id from another key or another account also answersexpired.Changed underneath. If the listing changed after the preview, the confirm answers
CONFIRMATION_INVALIDwith the reasoncontent_changedand writes nothing.detailscarries 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: autoworks only for a key that holds theconfirm:autoscope and has auto-confirm switched on. No preset includesconfirm:auto. Otherwise it is refused withSCOPE_INSUFFICIENTand the reasonauto_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, byposition, withproduct,blueprint_id,print_provider_id,colorsandsizes. Itsvariants[]carryoptions,price,sku,quantityandenabled. 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:differssays whether the two disagree.cells[]lists each cell that does, withstored,liveanddifferences[]. 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_idmay 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 samepodbody a single listing update takes, withlisting_idnaming the Etsy listing. Products are addressed byposition.productis 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
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).The confirm. Within 5 minutes, send
POST /confirm/{confirmation_id}. The answer hasjob_id,status,shop_id,listings,changed_cells,skipped_listing_idsandconfirmation_id.The job. ListingView runs one bulk-update job for the whole set. Its id starts
lbj_. Poll it withGET /catalog/bulk/jobs/{job_id}until it finishes. Polling needscatalog: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
describeshows 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_LIMITEDwith the reasonlive_writes_hourlyorlive_writes_daily,details.requestedanddetails.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
413VALIDATION_FAILEDwith the reasontoo_largeanddetails.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 |
|---|---|---|
| A price is below that variant's production cost. | Fix the price, or send |
| A listing can't be repriced without changing how it varies on Etsy. | Edit its variations in the app, then push again. |
| A changed price is outside what Etsy accepts. ListingView never rounds it into range. | Fix the price. |
| Etsy already has every price. | Nothing to do. |
| A color or size the listing's product doesn't sell. The refusal lists the names it offers. | Use one of the offered names. |
| The | Check the position, or drop the guard. |
| The print provider no longer offers that variant. | Leave that variant out. |
| A color price and a size price disagree for one variant. | Add a color-and-size entry for it. |
| A listing appears twice, or a price entry names a listing that isn't in | Fix the request. |
| The listing isn't live, isn't print-on-demand, or ListingView hasn't synced it. | Leave it out, or sync the shop first. |
| A listing isn't in that shop. | Remove it. |
| The stored confirmation would be too big ( | Split the push. |
More than 20 listings | Over the per-confirmation cap ( | Split the push. |
| Fewer live-write units are left than listings. | Wait for the reset, or push fewer listings. |
| Etsy couldn't be read for the preview. | Retry after |
| The shop's Etsy connection needs reconnecting. | Reconnect the shop in ListingView. |
| The confirmation is more than 5 minutes old. | Ask again for a fresh preview. |
| A listing changed in ListingView or on Etsy after the preview. | Review the fresh confirmation in |
| A bulk update from the app is running on this shop. | Retry after |
| After 3 failed writes in a row, ListingView pauses live writes for this account for 15 minutes. | Retry after |
| The account is below the Plus plan. | Upgrade. |
While the job runs
Per listing. Each listing ends
pushed, orfailedwith areasonandretry_safe.updated_fieldsis["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:
statusstaysrunning, andnext_stepsays so. After 24 hours it is cancelled.Stopped. A cancelled job names why in
cancelled_reason:seller,key_revokedoradmin.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_writedescribes the write it would make, withetsy_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_livetakesaction, which is required.action: "push"withlisting_idpushes one listing; withlisting_ids(Etsy ids) andpod_pricesit reprices up to 20.action: "publish"publishes one listing. A combination that matches neither answersVALIDATION_FAILEDwith the reasonno_route, naming what to send. Every call shows what will change and waits for a confirmation, which your app shows you.MCP:
update_listingmakes the edit before a push.get_listingwithetsy_listing_idsandincluderuns the lookup.get_bulk_job_statuswith thelbj_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
API: Getting started: keys, idempotency keys, errors and the retry rules
API: Etsy drafts: stage and submit the drafts you'll publish
API: Presets: the price rules in full
API: Connect AI apps: one ListingView server per app or chat
API: Limits & budgets: live-write limits and the Etsy-call budget
API: Command-line tool: commands and exit codes
Bulk Editor: bulk updates in the app
Using the ListingView API, MCP server & CLI: the seller guide to keys and the two-key setup
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:readandcatalog: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
In ListingView, open Settings → API Keys and click Create key.
Name the key and pick the Publisher access preset.
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:writeandcatalog:live_write. In the Create key dialog, open Customize scopes and pick those three.catalog:writemay sit besidecatalog:live_write; onlyetsy_drafts:writemay 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 withget_listingafternext_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:
Ask. The first call changes nothing. It answers
409 CONFIRMATION_REQUIRED, anddetailscarriesconfirmation_id, asummary, thediffand a readabletext.Confirm. Within 5 minutes, send
POST /confirm/{confirmation_id}with the same key. Or send the same request again with the headerX-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_INVALIDwith the reasonexpired. Ask again for a fresh preview. An id from another key or another account also answersexpired.Changed underneath. If the listing changed after the preview, the confirm answers
CONFIRMATION_INVALIDwith the reasoncontent_changedand writes nothing.detailscarries 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: autoworks only for a key that holds theconfirm:autoscope and has auto-confirm switched on. No preset includesconfirm:auto. Otherwise it is refused withSCOPE_INSUFFICIENTand the reasonauto_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, byposition, withproduct,blueprint_id,print_provider_id,colorsandsizes. Itsvariants[]carryoptions,price,sku,quantityandenabled. 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:differssays whether the two disagree.cells[]lists each cell that does, withstored,liveanddifferences[]. 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_idmay 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 samepodbody a single listing update takes, withlisting_idnaming the Etsy listing. Products are addressed byposition.productis 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
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).The confirm. Within 5 minutes, send
POST /confirm/{confirmation_id}. The answer hasjob_id,status,shop_id,listings,changed_cells,skipped_listing_idsandconfirmation_id.The job. ListingView runs one bulk-update job for the whole set. Its id starts
lbj_. Poll it withGET /catalog/bulk/jobs/{job_id}until it finishes. Polling needscatalog: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
describeshows 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_LIMITEDwith the reasonlive_writes_hourlyorlive_writes_daily,details.requestedanddetails.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
413VALIDATION_FAILEDwith the reasontoo_largeanddetails.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 |
|---|---|---|
| A price is below that variant's production cost. | Fix the price, or send |
| A listing can't be repriced without changing how it varies on Etsy. | Edit its variations in the app, then push again. |
| A changed price is outside what Etsy accepts. ListingView never rounds it into range. | Fix the price. |
| Etsy already has every price. | Nothing to do. |
| A color or size the listing's product doesn't sell. The refusal lists the names it offers. | Use one of the offered names. |
| The | Check the position, or drop the guard. |
| The print provider no longer offers that variant. | Leave that variant out. |
| A color price and a size price disagree for one variant. | Add a color-and-size entry for it. |
| A listing appears twice, or a price entry names a listing that isn't in | Fix the request. |
| The listing isn't live, isn't print-on-demand, or ListingView hasn't synced it. | Leave it out, or sync the shop first. |
| A listing isn't in that shop. | Remove it. |
| The stored confirmation would be too big ( | Split the push. |
More than 20 listings | Over the per-confirmation cap ( | Split the push. |
| Fewer live-write units are left than listings. | Wait for the reset, or push fewer listings. |
| Etsy couldn't be read for the preview. | Retry after |
| The shop's Etsy connection needs reconnecting. | Reconnect the shop in ListingView. |
| The confirmation is more than 5 minutes old. | Ask again for a fresh preview. |
| A listing changed in ListingView or on Etsy after the preview. | Review the fresh confirmation in |
| A bulk update from the app is running on this shop. | Retry after |
| After 3 failed writes in a row, ListingView pauses live writes for this account for 15 minutes. | Retry after |
| The account is below the Plus plan. | Upgrade. |
While the job runs
Per listing. Each listing ends
pushed, orfailedwith areasonandretry_safe.updated_fieldsis["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:
statusstaysrunning, andnext_stepsays so. After 24 hours it is cancelled.Stopped. A cancelled job names why in
cancelled_reason:seller,key_revokedoradmin.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_writedescribes the write it would make, withetsy_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_livetakesaction, which is required.action: "push"withlisting_idpushes one listing; withlisting_ids(Etsy ids) andpod_pricesit reprices up to 20.action: "publish"publishes one listing. A combination that matches neither answersVALIDATION_FAILEDwith the reasonno_route, naming what to send. Every call shows what will change and waits for a confirmation, which your app shows you.MCP:
update_listingmakes the edit before a push.get_listingwithetsy_listing_idsandincluderuns the lookup.get_bulk_job_statuswith thelbj_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
API: Getting started: keys, idempotency keys, errors and the retry rules
API: Etsy drafts: stage and submit the drafts you'll publish
API: Presets: the price rules in full
API: Connect AI apps: one ListingView server per app or chat
API: Limits & budgets: live-write limits and the Etsy-call budget
API: Command-line tool: commands and exit codes
Bulk Editor: bulk updates in the app
Using the ListingView API, MCP server & CLI: the seller guide to keys and the two-key setup
Still need help?
Can’t find what you’re looking for? Our team is happy to help.
A.I. Summary